diff --git a/CLOUD_STORAGE_PLAN.md b/CLOUD_STORAGE_PLAN.md new file mode 100644 index 0000000..c5e87a1 --- /dev/null +++ b/CLOUD_STORAGE_PLAN.md @@ -0,0 +1,357 @@ +# Cloud Leaderboard Implementation Plan + +## Overview + +Implement a global cloud-based leaderboard using **Supabase** with existing **Auth0** authentication. No backend server required - uses direct client-to-database communication with Row Level Security. + +## Architecture + +``` +┌─────────────┐ ┌─────────┐ ┌───────────┐ +│ Browser │────▶│ Auth0 │────▶│ JWT Token │ +│ (Game) │ └─────────┘ └─────┬─────┘ +│ │ │ +│ Supabase │◀──────────────────────────┘ +│ JS Client │ +│ │ ┌───────────────────────────┐ +│ │────▶│ Supabase (Postgres + RLS)│ +└─────────────┘ │ - leaderboard table │ + │ - Row Level Security │ + └───────────────────────────┘ +``` + +## Implementation Steps + +### Phase 1: Supabase Project Setup (Manual - Dashboard) + +1. Create free Supabase project at https://supabase.com +2. Get project credentials: + - `SUPABASE_URL` (e.g., `https://xxx.supabase.co`) + - `SUPABASE_ANON_KEY` (public, safe for client) +3. Configure Third-Party Auth for Auth0: + - Navigate to Authentication → Third-Party Auth + - Add Auth0 integration with your tenant ID +4. Create `leaderboard` table: + +```sql +CREATE TABLE leaderboard ( + id UUID DEFAULT gen_random_uuid() PRIMARY KEY, + user_id TEXT NOT NULL, -- Auth0 sub claim + player_name TEXT NOT NULL, + level_id TEXT NOT NULL, + level_name TEXT NOT NULL, + completed BOOLEAN NOT NULL, + end_reason TEXT NOT NULL, -- 'victory' | 'death' | 'stranded' + + -- Stats + game_time_seconds NUMERIC NOT NULL, + asteroids_destroyed INTEGER NOT NULL, + total_asteroids INTEGER NOT NULL, + accuracy NUMERIC NOT NULL, + hull_damage_taken NUMERIC NOT NULL, + fuel_consumed NUMERIC NOT NULL, + + -- Scoring + final_score INTEGER NOT NULL, + star_rating INTEGER NOT NULL, -- 0-12 + + created_at TIMESTAMPTZ DEFAULT NOW() +); + +-- Index for leaderboard queries +CREATE INDEX idx_leaderboard_score ON leaderboard(final_score DESC); +CREATE INDEX idx_leaderboard_user ON leaderboard(user_id); +``` + +5. Enable Row Level Security: + +```sql +-- Enable RLS +ALTER TABLE leaderboard ENABLE ROW LEVEL SECURITY; + +-- Anyone can read leaderboard (global leaderboard) +CREATE POLICY "Anyone can read leaderboard" ON leaderboard + FOR SELECT USING (true); + +-- Authenticated users can insert their own scores +CREATE POLICY "Users can insert own scores" ON leaderboard + FOR INSERT WITH CHECK ( + auth.jwt() ->> 'sub' = user_id + ); +``` + +### Phase 2: Auth0 Action Configuration (Manual - Auth0 Dashboard) + +Create a Post-Login Action to add required claims to JWT: + +```javascript +// Auth0 Action: Add Supabase Claims +exports.onExecutePostLogin = async (event, api) => { + const namespace = 'https://supabase.com/'; + + // Add the 'authenticated' role claim for Supabase RLS + api.accessToken.setCustomClaim(`${namespace}role`, 'authenticated'); + + // Supabase expects 'sub' claim which Auth0 already provides +}; +``` + +### Phase 3: Install Supabase Client + +```bash +npm install @supabase/supabase-js +``` + +### Phase 4: Create Supabase Service + +**New file: `src/services/supabaseService.ts`** + +```typescript +import { createClient, SupabaseClient } from '@supabase/supabase-js'; +import { AuthService } from './authService'; + +const SUPABASE_URL = import.meta.env.VITE_SUPABASE_URL; +const SUPABASE_ANON_KEY = import.meta.env.VITE_SUPABASE_ANON_KEY; + +export class SupabaseService { + private static instance: SupabaseService; + private client: SupabaseClient; + + private constructor() { + this.client = createClient(SUPABASE_URL, SUPABASE_ANON_KEY); + } + + static getInstance(): SupabaseService { + if (!SupabaseService.instance) { + SupabaseService.instance = new SupabaseService(); + } + return SupabaseService.instance; + } + + // Update client with Auth0 token for authenticated requests + async setAuthToken(): Promise { + const authService = AuthService.getInstance(); + const token = await authService.getAccessToken(); + if (token) { + this.client = createClient(SUPABASE_URL, SUPABASE_ANON_KEY, { + global: { + headers: { + Authorization: `Bearer ${token}` + } + } + }); + } + } + + getClient(): SupabaseClient { + return this.client; + } +} +``` + +### Phase 5: Create Cloud Leaderboard Service + +**New file: `src/services/cloudLeaderboardService.ts`** + +```typescript +import { SupabaseService } from './supabaseService'; +import { AuthService } from './authService'; +import type { GameResult } from './gameResultsService'; + +export interface CloudLeaderboardEntry { + id: string; + user_id: string; + player_name: string; + level_id: string; + level_name: string; + completed: boolean; + end_reason: string; + game_time_seconds: number; + asteroids_destroyed: number; + total_asteroids: number; + accuracy: number; + hull_damage_taken: number; + fuel_consumed: number; + final_score: number; + star_rating: number; + created_at: string; +} + +export class CloudLeaderboardService { + private static instance: CloudLeaderboardService; + + static getInstance(): CloudLeaderboardService { + if (!CloudLeaderboardService.instance) { + CloudLeaderboardService.instance = new CloudLeaderboardService(); + } + return CloudLeaderboardService.instance; + } + + // Submit a score to cloud leaderboard + async submitScore(result: GameResult): Promise { + const supabase = SupabaseService.getInstance(); + await supabase.setAuthToken(); + + const authService = AuthService.getInstance(); + const user = authService.getUser(); + + if (!user?.sub) { + console.warn('Cannot submit score: user not authenticated'); + return false; + } + + const entry = { + user_id: user.sub, + player_name: result.playerName, + level_id: result.levelId, + level_name: result.levelName, + completed: result.completed, + end_reason: result.endReason, + game_time_seconds: result.gameTimeSeconds, + asteroids_destroyed: result.asteroidsDestroyed, + total_asteroids: result.totalAsteroids, + accuracy: result.accuracy, + hull_damage_taken: result.hullDamageTaken, + fuel_consumed: result.fuelConsumed, + final_score: result.finalScore, + star_rating: result.starRating + }; + + const { error } = await supabase.getClient() + .from('leaderboard') + .insert(entry); + + if (error) { + console.error('Failed to submit score:', error); + return false; + } + + return true; + } + + // Fetch global leaderboard (top scores) + async getGlobalLeaderboard(limit = 20): Promise { + const supabase = SupabaseService.getInstance(); + + const { data, error } = await supabase.getClient() + .from('leaderboard') + .select('*') + .order('final_score', { ascending: false }) + .limit(limit); + + if (error) { + console.error('Failed to fetch leaderboard:', error); + return []; + } + + return data || []; + } + + // Get user's personal best scores + async getUserScores(userId: string, limit = 10): Promise { + const supabase = SupabaseService.getInstance(); + + const { data, error } = await supabase.getClient() + .from('leaderboard') + .select('*') + .eq('user_id', userId) + .order('final_score', { ascending: false }) + .limit(limit); + + if (error) { + console.error('Failed to fetch user scores:', error); + return []; + } + + return data || []; + } +} +``` + +### Phase 6: Integrate with Existing Code + +**Modify: `src/services/gameResultsService.ts`** + +Add cloud submission after local save: + +```typescript +import { CloudLeaderboardService } from './cloudLeaderboardService'; + +// In saveResult() method, after localStorage save: +async saveResult(result: GameResult): Promise { + // Existing localStorage save + const results = this.getAllResults(); + results.push(result); + localStorage.setItem(STORAGE_KEY, JSON.stringify(results)); + + // NEW: Submit to cloud leaderboard + try { + const cloudService = CloudLeaderboardService.getInstance(); + await cloudService.submitScore(result); + } catch (error) { + console.warn('Cloud leaderboard submission failed:', error); + // Don't block on cloud failure - local save succeeded + } +} +``` + +**Modify: `src/components/leaderboard/Leaderboard.svelte`** + +Add toggle between local and cloud leaderboard: + +```svelte + + + +``` + +### Phase 7: Environment Variables + +**Create/update: `.env`** + +``` +VITE_SUPABASE_URL=https://your-project.supabase.co +VITE_SUPABASE_ANON_KEY=your-anon-key +``` + +## Files to Modify + +| File | Action | Purpose | +|------|--------|---------| +| `src/services/supabaseService.ts` | CREATE | Supabase client singleton | +| `src/services/cloudLeaderboardService.ts` | CREATE | Cloud leaderboard API | +| `src/services/gameResultsService.ts` | MODIFY | Add cloud submission | +| `src/components/leaderboard/Leaderboard.svelte` | MODIFY | Add cloud data source | +| `.env` | CREATE/MODIFY | Add Supabase credentials | +| `package.json` | MODIFY | Add @supabase/supabase-js | + +## External Setup Required (Manual) + +1. **Supabase Dashboard**: Create project, table, RLS policies, Auth0 integration +2. **Auth0 Dashboard**: Create Post-Login Action for role claim + +## Cost Analysis + +- **Supabase Free Tier**: 500MB database, 10k MAU, 1GB file storage +- **Your usage** (<100 players): Well within free tier +- **Cost**: $0/month + +## Considerations + +- **Offline/failure handling**: Falls back to localStorage if cloud fails +- **Inactivity pause**: Free tier projects pause after 7 days inactivity (easy to unpause) +- **No anti-cheat**: Scores submitted directly from client (per your requirements) diff --git a/public/levels/asteroid-mania.json b/public/levels/asteroid-mania.json new file mode 100644 index 0000000..0385484 --- /dev/null +++ b/public/levels/asteroid-mania.json @@ -0,0 +1,289 @@ +{ + "version": "1.2", + "difficulty": "rookie", + "timestamp": "2025-11-11T23:44:24.810Z", + "metadata": { + "author": "System", + "description": "Asteroid Mania!", + "estimatedTime": "5-8 minutes", + "type": "default" + }, + "ship": { + "position": [ + 0, + 1, + 0 + ], + "rotation": [ + 0, + 0, + 0 + ], + "linearVelocity": [ + 0, + 0, + 0 + ], + "angularVelocity": [ + 0, + 0, + 0 + ] + }, + "startBase": { + "position": [ + 0, + 0, + 0 + ], + "baseGlbPath": "base.glb" + }, + "sun": { + "position": [ + 0, + 0, + 400 + ], + "diameter": 50, + "intensity": 1000000 + }, + "planets": [], + "asteroids": [ + { + "id": "asteroid-0", + "position": [ + 242.60734209985543, + -114.56996058926651, + 5.575229357062 + ], + "scale": 2, + "linearVelocity": [ + -170.167175139332553, + 80.177863609194048, + -0.39450965492725215 + ], + "angularVelocity": [ + -0.834980024785148, + 0.9648009938830251, + 0.8185653748494373 + ] + }, + { + "id": "asteroid-1", + "position": [ + 145.90971366777896, + 42.273817290099984, + -244.80503221456152 + ], + "scale": 6, + "linearVelocity": [ + -14.737555578618144, + -42.168846343154079, + 240.72643991613985 + ], + "angularVelocity": [ + 0.575649251710729, + -2.8551046445434349, + -0.9477761112717422 + ] + }, + { + "id": "asteroid-2", + "position": [ + 195.05992969157123, + -311.0584087077698, + -22.40662780090249 + ], + "scale": 4, + "linearVelocity": [ + -160.81570103491442, + 9.660316715266058, + 160.9316276535952197 + ], + "angularVelocity": [ + 0.8587973467645904, + 0.25620436829463733, + -0.7705721105608303 + ] + }, + { + "id": "asteroid-3", + "position": [ + -0.9357515100775112, + 85.76554222686204, + 249.4670613777975 + ], + "scale": 17.34408913479813, + "linearVelocity": [ + 0.07109432360434195, + -6.440116659897093, + -18.953420645560346 + ], + "angularVelocity": [ + 0.19650221972006143, + 0.4226089665809898, + -0.9419176203015098 + ] + }, + { + "id": "asteroid-4", + "position": [ + -254.14456477364413, + 54.65967750105119, + 82.65652287437858 + ], + "scale": 4, + "linearVelocity": [ + 22.372081486064396, + -400.723605553550473, + -7.2761676675924445 + ], + "angularVelocity": [ + -0.22039903827783025, + 0.03062354927084643, + 0.3628209366655213 + ] + }, + { + "id": "asteroid-5", + "position": [ + -257.7249224576784, + -112.97325792551102, + -92.25372143357285 + ], + "scale": 12, + "linearVelocity": [ + 17.764361846647077, + 7.855903788127005, + 6.358828139777149 + ], + "angularVelocity": [ + -0.27982741337355455, + 0.2465507084870353, + -0.8489416083688623 + ] + }, + { + "id": "asteroid-6", + "position": [ + -61.74000302102928, + 103.75532261403117, + -224.6843746923246 + ], + "scale": 14.438006716048399, + "linearVelocity": [ + 4.573571795825104, + -7.611901885044768, + 16.644154013167135 + ], + "angularVelocity": [ + -0.41949593751738457, + -0.5881266007071146, + 0.2671577602439994 + ] + }, + { + "id": "asteroid-7", + "position": [ + 16.846663100767792, + 72.36836836065181, + -271.36235273889974 + ], + "scale": 10, + "linearVelocity": [ + 220.2776861733199087, + -345.412726361379603, + -20.580688530433683 + ], + "angularVelocity": [ + -0.5793176374486806, + 0.8207961833131412, + -0.034658037798875885 + ] + }, + { + "id": "asteroid-8", + "position": [ + 129.11110725214024, + 91.10691458736655, + 205.0668479159754 + ], + "scale": 10, + "linearVelocity": [ + -10.330594112594069, + -7.209743461671342, + 160.4080567261488 + ], + "angularVelocity": [ + -2.572098306083443, + 0.6581860817605101, + -0.7141435682550208 + ] + }, + { + "id": "asteroid-9", + "position": [ + -300.953057070289603, + 225.21952155696817, + 139.05608152400566 + ], + "scale": 14.151176153817078, + "linearVelocity": [ + 1.9861965590557589, + -314.387724003424648, + -8.922954201633985 + ], + "angularVelocity": [ + 0.7016416714654072, + -4.8069811132136699, + -0.16093262088047533 + ] + }, + { + "id": "asteroid-10", + "position": [ + 300.953057070289603, + 225.21952155696817, + 139.05608152400566 + ], + "scale": 12, + "linearVelocity": [ + 100.9861965590557589, + -314.387724003424648, + -240.922954201633985 + ], + "angularVelocity": [ + 0.7016416714654072, + -4.8069811132136699, + -0.16093262088047533 + ] + },{ + "id": "asteroid-11", + "position": [ + 300.953057070289603, + -225.21952155696817, + 69.05608152400566 + ], + "scale": 30, + "linearVelocity": [ + 100.9861965590557589, + -214.387724003424648, + 140.922954201633985 + ], + "angularVelocity": [ + 0.7016416714654072, + -4.8069811132136699, + -0.16093262088047533 + ] + } + + ], + "difficultyConfig": { + "rockCount": 10, + "forceMultiplier": 1, + "rockSizeMin": 8, + "rockSizeMax": 20, + "distanceMin": 225, + "distanceMax": 300 + } +} \ No newline at end of file diff --git a/public/levels/directory.json b/public/levels/directory.json index d9aef16..0564e4e 100644 --- a/public/levels/directory.json +++ b/public/levels/directory.json @@ -1,5 +1,5 @@ { - "version": "1.0.6", + "version": "1.0.11", "levels": [ { "id": "rookie-training", @@ -21,21 +21,21 @@ "defaultLocked": false }, { - "id": "rescue-mission", - "name": "Rescue Mission", - "description": "Rescue operation in moderate asteroid field", - "version": "1.0", - "levelPath": "rescue-mission.json", + "id": "asteroid-mania", + "name": "Asteroid Mania!!!", + "description": "Still low stakes, just more asteroids", + "version": "1.1", + "levelPath": "asteroid-mania.json", "difficulty": "pilot", "estimatedTime": "5-8 minutes", "missionBrief": [ "More asteroids and increased difficulty", "Manage your fuel and ammunition carefully", "Complete the mission and return to base", - "Use your radar to track asteroids", - "Watch your shield strength" + "Watch your hull integrity!", + "Some of the asteroids are a little more distant" ], - "unlockRequirements": ["rookie-training"], + "unlockRequirements": [], "tags": ["medium"], "defaultLocked": true }, diff --git a/public/levels/rescue-mission.json b/public/levels/rescue-mission.json index ab97ef5..3dfa8e6 100644 --- a/public/levels/rescue-mission.json +++ b/public/levels/rescue-mission.json @@ -1,6 +1,6 @@ { "version": "1.0", - "difficulty": "pilot", + "difficulty": "rookie", "timestamp": "2025-11-11T23:44:24.810Z", "metadata": { "author": "System", diff --git a/src/components/game/LevelSelect.svelte b/src/components/game/LevelSelect.svelte index 937a955..fb6f332 100644 --- a/src/components/game/LevelSelect.svelte +++ b/src/components/game/LevelSelect.svelte @@ -4,10 +4,10 @@ import LevelCard from './LevelCard.svelte'; import ProgressBar from './ProgressBar.svelte'; - // Get default levels in order + // Get default levels in order (must match directory.json) const DEFAULT_LEVEL_ORDER = [ 'rookie-training', - 'rescue-mission', + 'asteroid-mania', 'deep-space-patrol', 'enemy-territory', 'the-gauntlet', diff --git a/src/core/router.ts b/src/core/router.ts deleted file mode 100644 index 067d00d..0000000 --- a/src/core/router.ts +++ /dev/null @@ -1,96 +0,0 @@ -/** - * Simple hash-based client-side router - */ -export class Router { - private routes: Map void> = new Map(); - private currentRoute: string = ''; - private started: boolean = false; - - constructor() { - // Listen for hash changes - window.addEventListener('hashchange', () => this.handleRoute()); - } - - /** - * Start the router (call after registering all routes) - */ - public start(): void { - if (!this.started) { - this.started = true; - this.handleRoute(); - } - } - - /** - * Register a route handler - */ - public on(path: string, handler: () => void | Promise): void { - this.routes.set(path, handler); - } - - /** - * Navigate to a route programmatically - */ - public navigate(path: string): void { - window.location.hash = path; - } - - /** - * Get current route path (without #) - */ - public getCurrentRoute(): string { - return this.currentRoute; - } - - /** - * Handle route changes - */ - private async handleRoute(): Promise { - // Get hash without the # - let hash = window.location.hash.slice(1) || '/'; - - // Normalize route - if (!hash.startsWith('/')) { - hash = '/' + hash; - } - - this.currentRoute = hash; - - // Find and execute route handler - const handler = this.routes.get(hash); - if (handler) { - await handler(); - } else { - // Default to root if route not found - const defaultHandler = this.routes.get('/'); - if (defaultHandler) { - await defaultHandler(); - } - } - } -} - -// Global router instance -export const router = new Router(); - -/** - * Helper to show/hide views - */ -export function showView(viewId: string): void { - console.log('[Router] showView() called with viewId:', viewId); - - // Hide all views - const views = document.querySelectorAll('[data-view]'); - console.log('[Router] Found views:', views.length); - views.forEach(view => { - (view as HTMLElement).style.display = 'none'; - }); - - // Show requested view - const targetView = document.querySelector(`[data-view="${viewId}"]`); - console.log('[Router] Target view found:', !!targetView); - if (targetView) { - (targetView as HTMLElement).style.display = 'block'; - console.log('[Router] View display set to block'); - } -} diff --git a/src/environment/asteroids/rockFactory.ts b/src/environment/asteroids/rockFactory.ts index ab55889..bad9de9 100644 --- a/src/environment/asteroids/rockFactory.ts +++ b/src/environment/asteroids/rockFactory.ts @@ -123,11 +123,15 @@ export class RockFactory { // PhysicsAggregate will automatically compute sphere size from mesh bounding info // The mesh scaling is already applied, so Babylon will create correctly sized physics shape const agg = new PhysicsAggregate(rock, PhysicsShapeType.SPHERE, { - mass: 10000, - restitution: .5 + mass: 200, + friction: 0, + restitution: .8 // Don't pass radius - let Babylon compute from scaled mesh bounds }, DefaultScene.MainScene); const body = agg.body; + body.setAngularDamping(0); + + // Only apply orbit constraint if enabled for this level and orbit center exists if (useOrbitConstraint && this._orbitCenter) { diff --git a/src/levels/storage/levelRegistry.ts b/src/levels/storage/levelRegistry.ts index 932a37c..4a1d029 100644 --- a/src/levels/storage/levelRegistry.ts +++ b/src/levels/storage/levelRegistry.ts @@ -36,11 +36,10 @@ export interface LevelRegistryEntry { } const CUSTOM_LEVELS_KEY = 'space-game-custom-levels'; -const CACHE_NAME = 'space-game-levels-v1'; -const CACHED_VERSION_KEY = 'space-game-levels-cached-version'; /** * Singleton registry for managing both default and custom levels + * Always fetches fresh from network - no caching */ export class LevelRegistry { private static instance: LevelRegistry | null = null; @@ -63,121 +62,49 @@ export class LevelRegistry { * Initialize the registry by loading directory and levels */ public async initialize(): Promise { - console.log('[LevelRegistry] initialize() called, initialized =', this.initialized); - if (this.initialized) { - console.log('[LevelRegistry] Already initialized, skipping'); return; } try { - console.log('[LevelRegistry] Loading directory manifest...'); - // Load directory manifest await this.loadDirectory(); - console.log('[LevelRegistry] Directory loaded, entries:', this.directoryManifest?.levels.length); - - console.log('[LevelRegistry] Loading custom levels from localStorage...'); - // Load custom levels from localStorage this.loadCustomLevels(); - console.log('[LevelRegistry] Custom levels loaded:', this.customLevels.size); - this.initialized = true; - console.log('[LevelRegistry] Initialization complete!'); + console.log('[LevelRegistry] Initialized with', this.defaultLevels.size, 'default levels'); } catch (error) { - console.error('[LevelRegistry] Failed to initialize level registry:', error); + console.error('[LevelRegistry] Failed to initialize:', error); throw error; } } /** - * Load the directory.json manifest + * Check if running in development mode (for cache-busting HTTP requests) + */ + private isDevMode(): boolean { + return window.location.hostname === 'localhost' || + window.location.hostname.includes('dev.') || + window.location.port !== ''; + } + + /** + * Load the directory.json manifest (always fresh from network) */ private async loadDirectory(): Promise { - console.log('[LevelRegistry] ======================================'); - console.log('[LevelRegistry] loadDirectory() ENTERED at', Date.now()); - console.log('[LevelRegistry] ======================================'); - try { - console.log('[LevelRegistry] Attempting to fetch /levels/directory.json'); - console.log('[LevelRegistry] window.location.origin:', window.location.origin); - console.log('[LevelRegistry] Full URL will be:', window.location.origin + '/levels/directory.json'); - - // First, fetch from network to get the latest version - console.log('[LevelRegistry] About to call fetch() - Timestamp:', Date.now()); - console.log('[LevelRegistry] Fetching from network to check version...'); - - // Add cache-busting for development or when debugging - const isDev = window.location.hostname === 'localhost' || - window.location.hostname.includes('dev.') || - window.location.port !== ''; - const cacheBuster = isDev ? `?v=${Date.now()}` : ''; - console.log('[LevelRegistry] Cache busting:', isDev ? 'ENABLED (dev mode)' : 'DISABLED (production)'); - - const fetchStartTime = Date.now(); + // Add cache-busting in dev mode to avoid browser HTTP cache + const cacheBuster = this.isDevMode() ? `?v=${Date.now()}` : ''; const response = await fetch(`/levels/directory.json${cacheBuster}`); - const fetchEndTime = Date.now(); - - console.log('[LevelRegistry] fetch() returned! Time taken:', fetchEndTime - fetchStartTime, 'ms'); - console.log('[LevelRegistry] Fetch response status:', response.status, response.ok); - console.log('[LevelRegistry] Fetch response type:', response.type); - console.log('[LevelRegistry] Fetch response headers:', { - contentType: response.headers.get('content-type'), - contentLength: response.headers.get('content-length') - }); if (!response.ok) { - // If network fails, try to use cached version as fallback - console.warn('[LevelRegistry] Network fetch failed, trying cache...'); - const cached = await this.getCachedResource('/levels/directory.json'); - if (cached) { - console.log('[LevelRegistry] Using cached directory as fallback'); - this.directoryManifest = cached; - this.populateDefaultLevelEntries(); - return; - } throw new Error(`Failed to fetch directory: ${response.status}`); } - console.log('[LevelRegistry] About to parse response.json()'); - const parseStartTime = Date.now(); - const networkManifest = await response.json(); - const parseEndTime = Date.now(); - console.log('[LevelRegistry] JSON parsed successfully! Time taken:', parseEndTime - parseStartTime, 'ms'); - console.log('[LevelRegistry] Directory JSON parsed:', networkManifest); - console.log('[LevelRegistry] Number of levels in manifest:', networkManifest?.levels?.length || 0); + this.directoryManifest = await response.json(); + console.log('[LevelRegistry] Loaded directory with', this.directoryManifest?.levels?.length || 0, 'levels'); - // Check if version changed - const cachedVersion = localStorage.getItem(CACHED_VERSION_KEY); - const currentVersion = networkManifest.version; - - if (cachedVersion && cachedVersion !== currentVersion) { - console.log('[LevelRegistry] Version changed from', cachedVersion, 'to', currentVersion, '- invalidating cache'); - await this.invalidateCache(); - } else { - console.log('[LevelRegistry] Version unchanged or first load:', currentVersion); - } - - // Update cached version - localStorage.setItem(CACHED_VERSION_KEY, currentVersion); - - // Store the manifest - this.directoryManifest = networkManifest; - - // Cache the directory - await this.cacheResource('/levels/directory.json', this.directoryManifest); - - console.log('[LevelRegistry] About to populate default level entries'); this.populateDefaultLevelEntries(); - console.log('[LevelRegistry] Default level entries populated successfully'); - console.log('[LevelRegistry] ======================================'); - console.log('[LevelRegistry] loadDirectory() COMPLETED at', Date.now()); - console.log('[LevelRegistry] ======================================'); } catch (error) { - console.error('[LevelRegistry] !!!!! EXCEPTION in loadDirectory() !!!!!'); console.error('[LevelRegistry] Failed to load directory:', error); - console.error('[LevelRegistry] Error type:', error?.constructor?.name); - console.error('[LevelRegistry] Error message:', error?.message); - console.error('[LevelRegistry] Error stack:', error?.stack); throw new Error('Unable to load level directory. Please check your connection.'); } } @@ -187,27 +114,12 @@ export class LevelRegistry { */ private populateDefaultLevelEntries(): void { if (!this.directoryManifest) { - console.error('[LevelRegistry] ❌ Cannot populate - directoryManifest is null'); return; } - console.log('[LevelRegistry] ======================================'); - console.log('[LevelRegistry] Populating default level entries...'); - console.log('[LevelRegistry] Directory manifest levels:', this.directoryManifest.levels.length); - this.defaultLevels.clear(); for (const entry of this.directoryManifest.levels) { - console.log(`[LevelRegistry] Storing level: ${entry.id}`, { - name: entry.name, - levelPath: entry.levelPath, - hasMissionBrief: !!entry.missionBrief, - missionBriefItems: entry.missionBrief?.length || 0, - hasLevelPath: !!entry.levelPath, - estimatedTime: entry.estimatedTime, - difficulty: entry.difficulty - }); - this.defaultLevels.set(entry.id, { directoryEntry: entry, config: null, // Lazy load @@ -215,9 +127,7 @@ export class LevelRegistry { }); } - console.log('[LevelRegistry] Populated entries. Total count:', this.defaultLevels.size); console.log('[LevelRegistry] Level IDs:', Array.from(this.defaultLevels.keys())); - console.log('[LevelRegistry] ======================================'); } /** @@ -241,7 +151,7 @@ export class LevelRegistry { name: config.metadata?.description || id, description: config.metadata?.description || '', version: config.version || '1.0', - levelPath: '', // Not applicable for custom + levelPath: '', difficulty: config.difficulty, missionBrief: [], defaultLocked: false @@ -275,91 +185,30 @@ export class LevelRegistry { } /** - * Load a default level's config from JSON + * Load a default level's config from JSON (always fresh from network) */ private async loadDefaultLevel(levelId: string): Promise { - console.log('[LevelRegistry] ======================================'); - console.log('[LevelRegistry] loadDefaultLevel() called for:', levelId); - console.log('[LevelRegistry] Timestamp:', Date.now()); - console.log('[LevelRegistry] ======================================'); - const entry = this.defaultLevels.get(levelId); if (!entry || entry.config) { - console.log('[LevelRegistry] Early return - entry:', !!entry, ', config loaded:', !!entry?.config); - return; // Already loaded or doesn't exist + return; } + const levelPath = `/levels/${entry.directoryEntry.levelPath}`; + try { - const levelPath = `/levels/${entry.directoryEntry.levelPath}`; - console.log('[LevelRegistry] Constructed levelPath:', levelPath); - console.log('[LevelRegistry] Full URL will be:', window.location.origin + levelPath); - - // Check if cache busting is enabled (dev mode) - const isDev = window.location.hostname === 'localhost' || - window.location.hostname.includes('dev.') || - window.location.port !== ''; - - // In dev mode, skip cache and always fetch fresh - let cached = null; - if (!isDev) { - console.log('[LevelRegistry] Checking cache for:', levelPath); - cached = await this.getCachedResource(levelPath); - } else { - console.log('[LevelRegistry] Skipping cache check (dev mode)'); - } - - if (cached) { - console.log('[LevelRegistry] Found in cache! Using cached config'); - entry.config = cached; - entry.loadedAt = new Date(); - return; - } - console.log('[LevelRegistry] Not in cache, fetching from network'); - - // Fetch from network with cache-busting in dev mode - const cacheBuster = isDev ? `?v=${Date.now()}` : ''; - console.log('[LevelRegistry] About to fetch level JSON - Timestamp:', Date.now()); - console.log('[LevelRegistry] Cache busting:', isDev ? 'ENABLED' : 'DISABLED'); - const fetchStartTime = Date.now(); + // Add cache-busting in dev mode + const cacheBuster = this.isDevMode() ? `?v=${Date.now()}` : ''; const response = await fetch(`${levelPath}${cacheBuster}`); - const fetchEndTime = Date.now(); - - console.log('[LevelRegistry] Level fetch() returned! Time taken:', fetchEndTime - fetchStartTime, 'ms'); - console.log('[LevelRegistry] Response status:', response.status, response.ok); if (!response.ok) { - console.error('[LevelRegistry] Fetch failed with status:', response.status); throw new Error(`Failed to fetch level: ${response.status}`); } - console.log('[LevelRegistry] Parsing level JSON...'); - const parseStartTime = Date.now(); - const config: LevelConfig = await response.json(); - const parseEndTime = Date.now(); - console.log('[LevelRegistry] Level JSON parsed! Time taken:', parseEndTime - parseStartTime, 'ms'); - console.log('[LevelRegistry] Level config loaded:', { - version: config.version, - difficulty: config.difficulty, - asteroidCount: config.asteroids?.length || 0 - }); - - // Cache the level - console.log('[LevelRegistry] Caching level config...'); - await this.cacheResource(levelPath, config); - console.log('[LevelRegistry] Level cached successfully'); - - entry.config = config; + entry.config = await response.json(); entry.loadedAt = new Date(); - - console.log('[LevelRegistry] ======================================'); - console.log('[LevelRegistry] loadDefaultLevel() COMPLETED for:', levelId); - console.log('[LevelRegistry] ======================================'); + console.log('[LevelRegistry] Loaded level:', levelId); } catch (error) { - console.error('[LevelRegistry] !!!!! EXCEPTION in loadDefaultLevel() !!!!!'); - console.error(`[LevelRegistry] Failed to load default level ${levelId}:`, error); - console.error('[LevelRegistry] Error type:', error?.constructor?.name); - console.error('[LevelRegistry] Error message:', error?.message); - console.error('[LevelRegistry] Error stack:', error?.stack); + console.error(`[LevelRegistry] Failed to load level ${levelId}:`, error); throw error; } } @@ -370,12 +219,10 @@ export class LevelRegistry { public getAllLevels(): Map { const all = new Map(); - // Add defaults for (const [id, entry] of this.defaultLevels) { all.set(id, entry); } - // Add customs for (const [id, entry] of this.customLevels) { all.set(id, entry); } @@ -401,7 +248,6 @@ export class LevelRegistry { * Save a custom level */ public saveCustomLevel(levelId: string, config: LevelConfig): void { - // Ensure metadata exists if (!config.metadata) { config.metadata = { author: 'Player', @@ -409,12 +255,10 @@ export class LevelRegistry { }; } - // Remove 'default' type if present if (config.metadata.type === 'default') { delete config.metadata.type; } - // Add/update in memory this.customLevels.set(levelId, { directoryEntry: { id: levelId, @@ -431,7 +275,6 @@ export class LevelRegistry { loadedAt: new Date() }); - // Persist to localStorage this.saveCustomLevelsToStorage(); } @@ -455,10 +298,8 @@ export class LevelRegistry { return false; } - // Deep clone the config const clonedConfig: LevelConfig = JSON.parse(JSON.stringify(config)); - // Update metadata clonedConfig.metadata = { ...clonedConfig.metadata, type: undefined, @@ -487,72 +328,14 @@ export class LevelRegistry { } /** - * Get a resource from cache + * Force refresh all default levels from network */ - private async getCachedResource(path: string): Promise { - if (!('caches' in window)) { - return null; - } - - try { - const cache = await caches.open(CACHE_NAME); - const response = await cache.match(path); - - if (response) { - return await response.json(); - } - } catch (error) { - console.warn('Cache read failed:', error); - } - - return null; - } - - /** - * Cache a resource - */ - private async cacheResource(path: string, data: any): Promise { - if (!('caches' in window)) { - return; - } - - try { - const cache = await caches.open(CACHE_NAME); - const response = new Response(JSON.stringify(data), { - headers: {'Content-Type': 'application/json'} - }); - await cache.put(path, response); - } catch (error) { - console.warn('Cache write failed:', error); - } - } - - /** - * Invalidate the entire cache (called when version changes) - */ - private async invalidateCache(): Promise { - console.log('[LevelRegistry] Invalidating cache...'); - if ('caches' in window) { - await caches.delete(CACHE_NAME); - } - - // Clear loaded configs + public async refreshDefaultLevels(): Promise { + // Clear in-memory configs for (const entry of this.defaultLevels.values()) { entry.config = null; entry.loadedAt = undefined; } - console.log('[LevelRegistry] Cache invalidated'); - } - - /** - * Force refresh all default levels from network - */ - public async refreshDefaultLevels(): Promise { - // Clear cache - await this.invalidateCache(); - - // Clear cached version to force re-check - localStorage.removeItem(CACHED_VERSION_KEY); // Reload directory await this.loadDirectory(); @@ -608,36 +391,17 @@ export class LevelRegistry { } /** - * Clear all caches and force reload from network - * Useful for development or when data needs to be refreshed + * Reset registry state (for testing or force reload) */ - public async clearAllCaches(): Promise { - console.log('[LevelRegistry] Clearing all caches...'); - - // Clear Cache API - if ('caches' in window) { - const cacheKeys = await caches.keys(); - for (const key of cacheKeys) { - await caches.delete(key); - console.log('[LevelRegistry] Deleted cache:', key); - } - } - - // Clear localStorage cache version - localStorage.removeItem(CACHED_VERSION_KEY); - console.log('[LevelRegistry] Cleared localStorage cache version'); - - // Clear loaded configs + public reset(): void { for (const entry of this.defaultLevels.values()) { entry.config = null; entry.loadedAt = undefined; } - console.log('[LevelRegistry] Cleared loaded configs'); - // Reset initialization flag to force reload this.initialized = false; this.directoryManifest = null; - console.log('[LevelRegistry] All caches cleared. Call initialize() to reload.'); + console.log('[LevelRegistry] Reset complete. Call initialize() to reload.'); } } diff --git a/src/levels/ui/levelSelector.ts b/src/levels/ui/levelSelector.ts deleted file mode 100644 index b051fa7..0000000 --- a/src/levels/ui/levelSelector.ts +++ /dev/null @@ -1,416 +0,0 @@ -import {LevelConfig} from "../config/levelConfig"; -import {ProgressionManager} from "../../game/progression"; -import {GameConfig} from "../../core/gameConfig"; -import {AuthService} from "../../services/authService"; -import debugLog from '../../core/debug'; -import {LevelRegistry} from "../storage/levelRegistry"; -import {LevelVersionManager} from "../versioning/levelVersionManager"; -import {LevelStatsManager} from "../stats/levelStats"; - -const SELECTED_LEVEL_KEY = 'space-game-selected-level'; - -// Default level IDs in display order (matches directory.json) -const DEFAULT_LEVEL_ORDER = [ - 'rookie-training', - 'rescue-mission', - 'deep-space-patrol', - 'enemy-territory', - 'the-gauntlet', - 'final-challenge' -]; - -/** - * Populate the level selection screen with levels from registry - * Shows all 6 default levels in a 3x2 carousel with locked/unlocked states - */ -export async function populateLevelSelector(): Promise { - console.log('[LevelSelector] populateLevelSelector() called'); - const container = document.getElementById('levelCardsContainer'); - if (!container) { - console.warn('[LevelSelector] Level cards container not found'); - return false; - } - console.log('[LevelSelector] Container found:', container); - - const registry = LevelRegistry.getInstance(); - const versionManager = LevelVersionManager.getInstance(); - const statsManager = LevelStatsManager.getInstance(); - - // Initialize registry - try { - console.log('[LevelSelector] Initializing registry...'); - await registry.initialize(); - console.log('[LevelSelector] Registry initialized'); - } catch (error) { - console.error('[LevelSelector] Registry initialization error:', error); - container.innerHTML = ` -
-

Failed to Load Levels

-

Could not load level directory. Check your connection and try again.

- -
- `; - return false; - } - - const gameConfig = GameConfig.getInstance(); - const progressionEnabled = gameConfig.progressionEnabled; - const progression = ProgressionManager.getInstance(); - - // Update version manager with directory - const directory = registry.getDirectory(); - if (directory) { - versionManager.updateManifestVersions(directory); - } - - const defaultLevels = registry.getDefaultLevels(); - const customLevels = registry.getCustomLevels(); - - console.log('[LevelSelector] Default levels:', defaultLevels.size); - console.log('[LevelSelector] Custom levels:', customLevels.size); - console.log('[LevelSelector] Default level IDs:', Array.from(defaultLevels.keys())); - - if (defaultLevels.size === 0 && customLevels.size === 0) { - console.warn('[LevelSelector] No levels found!'); - container.innerHTML = ` -
-

No Levels Found

-

No levels available. Please check your installation.

- Create Custom Level -
- `; - return false; - } - - let html = ''; - - // Show progression stats only if progression is enabled - if (progressionEnabled) { - const completedCount = progression.getCompletedCount(); - const totalCount = progression.getTotalDefaultLevels(); - const completionPercent = progression.getCompletionPercentage(); - const nextLevel = progression.getNextLevel(); - - html += ` -
-

Progress

-
- ${completedCount} of ${totalCount} default levels completed (${completionPercent.toFixed(0)}%) -
-
-
-
- ${nextLevel ? `
Next: ${nextLevel}
` : ''} -
- `; - } - - // Check if user is authenticated - const authService = AuthService.getInstance(); - const isAuthenticated = await authService.isAuthenticated(); - const isTutorial = (levelId: string) => levelId === DEFAULT_LEVEL_ORDER[0]; - - debugLog('[LevelSelector] Authenticated:', isAuthenticated); - debugLog('[LevelSelector] Progression enabled:', progressionEnabled); - debugLog('[LevelSelector] Default levels count:', defaultLevels.size); - - // Show all default levels in order (3x2 grid) - if (defaultLevels.size > 0) { - for (const levelId of DEFAULT_LEVEL_ORDER) { - const entry = defaultLevels.get(levelId); - - if (!entry) { - // Level doesn't exist - show empty slot - html += ` -
-
-

Missing Level

-
🔒
-
-
Level not found
-

This level has not been created yet.

- -
- `; - continue; - } - - const dirEntry = entry.directoryEntry; - const levelName = dirEntry.name; - const description = dirEntry.description; - const estimatedTime = dirEntry.estimatedTime || ''; - const difficulty = dirEntry.difficulty || 'unknown'; - - // Check for version updates - const hasUpdate = versionManager.hasUpdate(levelId); - - // Get stats - const stats = statsManager.getStats(levelId); - const completionRate = stats?.completionRate || 0; - const bestTime = stats?.bestTimeSeconds; - - // Check progression - const isCompleted = progressionEnabled && progression.isLevelComplete(levelName); - - // Check if level is unlocked - let isUnlocked = false; - const isTut = isTutorial(levelId); - - if (isTut) { - isUnlocked = true; // Tutorial always unlocked - } else if (!isAuthenticated) { - isUnlocked = false; // Non-tutorial levels require authentication - } else { - isUnlocked = !progressionEnabled || progression.isLevelUnlocked(levelName); - } - - const isCurrentNext = progressionEnabled && progression.getNextLevel() === levelName; - - // Determine card state - let cardClasses = 'level-card'; - let statusIcons = ''; - let buttonText = 'Play Level'; - let buttonDisabled = ''; - let lockReason = ''; - let metaTags = ''; - - // Version update badge - if (hasUpdate) { - statusIcons += '
UPDATED
'; - } - - if (isCompleted) { - cardClasses += ' level-card-completed'; - statusIcons += '
'; - buttonText = 'Replay'; - } else if (isCurrentNext && isUnlocked) { - cardClasses += ' level-card-current'; - statusIcons += '
START HERE
'; - } else if (!isUnlocked) { - cardClasses += ' level-card-locked'; - statusIcons += '
🔒
'; - - // Determine why it's locked - if (!isAuthenticated && !isTutorial(levelId)) { - buttonText = 'Sign In Required'; - lockReason = '
Sign in to unlock
'; - } else if (progressionEnabled) { - const levelIndex = DEFAULT_LEVEL_ORDER.indexOf(levelId); - if (levelIndex > 0) { - const prevId = DEFAULT_LEVEL_ORDER[levelIndex - 1]; - const prevEntry = defaultLevels.get(prevId); - const prevName = prevEntry?.directoryEntry.name || 'previous level'; - lockReason = `
Complete "${prevName}" to unlock
`; - } - buttonText = 'Locked'; - } else { - buttonText = 'Locked'; - } - buttonDisabled = ' disabled'; - } - - // Show stats if available - if (stats && stats.totalAttempts > 0) { - metaTags = '
'; - if (bestTime) { - metaTags += `⏱️ ${LevelStatsManager.formatTime(bestTime)}`; - } - if (stats.totalCompletions > 0) { - metaTags += `✓ ${stats.totalCompletions}`; - } - metaTags += `${LevelStatsManager.formatCompletionRate(completionRate)}`; - metaTags += '
'; - } - - html += ` -
-
-

${levelName}

-
${statusIcons}
-
-
- Difficulty: ${difficulty}${estimatedTime ? ` • ${estimatedTime}` : ''} -
-

${description}

- ${metaTags} - ${lockReason} -
- - ${entry.isDefault && isUnlocked ? `` : ''} -
-
- `; - } - } - - // Show custom levels section if any exist - if (customLevels.size > 0) { - html += ` -
-

Custom Levels

-
- `; - - for (const [levelId, entry] of customLevels.entries()) { - const config = entry.config; - if (!config) continue; - - const description = config.metadata?.description || `${config.asteroids.length} asteroids`; - const author = config.metadata?.author ? ` by ${config.metadata.author}` : ''; - const difficulty = config.difficulty || 'custom'; - - // Get stats - const stats = statsManager.getStats(levelId); - const bestTime = stats?.bestTimeSeconds; - let metaTags = ''; - - if (stats && stats.totalAttempts > 0) { - metaTags = '
'; - if (bestTime) { - metaTags += `⏱️ ${LevelStatsManager.formatTime(bestTime)}`; - } - if (stats.totalCompletions > 0) { - metaTags += `✓ ${stats.totalCompletions}`; - } - metaTags += '
'; - } - - html += ` -
-
-

${levelId}

-
CUSTOM
-
-
- ${difficulty}${author} -
-

${description}

- ${metaTags} -
- - -
-
- `; - } - } - - console.log('[LevelSelector] Setting container innerHTML, html length:', html.length); - container.innerHTML = html; - console.log('[LevelSelector] Container innerHTML set, now attaching event listeners'); - - // Attach event listeners to all level buttons - const playButtons = container.querySelectorAll('.level-button:not([disabled])'); - playButtons.forEach(button => { - button.addEventListener('click', (e) => { - const target = e.target as HTMLButtonElement; - const levelId = target.getAttribute('data-level-id'); - if (levelId) { - selectLevel(levelId); - } - }); - }); - - // Attach copy button listeners - const copyButtons = container.querySelectorAll('[data-copy-level]'); - copyButtons.forEach(button => { - button.addEventListener('click', async (e) => { - const target = e.target as HTMLButtonElement; - const levelId = target.getAttribute('data-copy-level'); - if (levelId) { - await copyLevelToCustom(levelId); - } - }); - }); - - // Attach delete button listeners - const deleteButtons = container.querySelectorAll('[data-delete-level]'); - deleteButtons.forEach(button => { - button.addEventListener('click', (e) => { - const target = e.target as HTMLButtonElement; - const levelId = target.getAttribute('data-delete-level'); - if (levelId) { - deleteCustomLevel(levelId); - } - }); - }); - - console.log('[LevelSelector] Event listeners attached, returning true'); - - // Make the level selector visible by adding 'ready' class - const levelSelectDiv = document.getElementById('levelSelect'); - if (levelSelectDiv) { - levelSelectDiv.classList.add('ready'); - console.log('[LevelSelector] Added "ready" class to #levelSelect'); - } - - return true; -} - -/** - * Copy a default level to custom levels - */ -async function copyLevelToCustom(levelId: string): Promise { - const registry = LevelRegistry.getInstance(); - const customName = prompt(`Enter a name for your copy of this level:`, `${levelId}-copy`); - - if (!customName || customName.trim() === '') { - return; - } - - const success = await registry.copyDefaultToCustom(levelId, customName); - - if (success) { - alert(`Level copied as "${customName}"!`); - await populateLevelSelector(); // Refresh UI - } else { - alert('Failed to copy level. Please try again.'); - } -} - -/** - * Delete a custom level - */ -function deleteCustomLevel(levelId: string): void { - if (!confirm(`Are you sure you want to delete "${levelId}"?`)) { - return; - } - - const registry = LevelRegistry.getInstance(); - const success = registry.deleteCustomLevel(levelId); - - if (success) { - populateLevelSelector(); // Refresh UI - } -} - -/** - * Select a level and dispatch event to start it - */ -export async function selectLevel(levelId: string): Promise { - debugLog(`[LevelSelector] Level selected: ${levelId}`); - - const registry = LevelRegistry.getInstance(); - const config = await registry.getLevel(levelId); - - if (!config) { - console.error(`Level not found: ${levelId}`); - return; - } - - // Save selected level - localStorage.setItem(SELECTED_LEVEL_KEY, levelId); - - // Dispatch custom event that Main class will listen for - const event = new CustomEvent('levelSelected', { - detail: {levelName: levelId, config} - }); - window.dispatchEvent(event); -} - -/** - * Get the last selected level ID - */ -export function getSelectedLevel(): string | null { - return localStorage.getItem(SELECTED_LEVEL_KEY); -} diff --git a/src/levels/versioning/levelVersionManager.ts b/src/levels/versioning/levelVersionManager.ts deleted file mode 100644 index fff1892..0000000 --- a/src/levels/versioning/levelVersionManager.ts +++ /dev/null @@ -1,262 +0,0 @@ -import {LevelDirectory, LevelDirectoryEntry} from "../storage/levelRegistry"; - -/** - * Tracked version information for a level - */ -export interface LevelVersionInfo { - levelId: string; - loadedVersion: string; - loadedAt: Date; - manifestVersion?: string; // Latest version from directory -} - -/** - * Version comparison result - */ -export interface VersionComparison { - levelId: string; - currentVersion: string; - latestVersion: string; - isOutdated: boolean; - changelog?: string; -} - -const VERSION_STORAGE_KEY = 'space-game-level-versions'; - -/** - * Manages level version tracking and update detection - */ -export class LevelVersionManager { - private static instance: LevelVersionManager | null = null; - - private versionMap: Map = new Map(); - - private constructor() { - this.loadVersions(); - } - - public static getInstance(): LevelVersionManager { - if (!LevelVersionManager.instance) { - LevelVersionManager.instance = new LevelVersionManager(); - } - return LevelVersionManager.instance; - } - - /** - * Load version tracking from localStorage - */ - private loadVersions(): void { - const stored = localStorage.getItem(VERSION_STORAGE_KEY); - if (!stored) { - return; - } - - try { - const versionsArray: [string, LevelVersionInfo][] = JSON.parse(stored); - - for (const [id, info] of versionsArray) { - // Parse date string back to Date object - if (info.loadedAt && typeof info.loadedAt === 'string') { - info.loadedAt = new Date(info.loadedAt); - } - this.versionMap.set(id, info); - } - } catch (error) { - console.error('Failed to load level versions:', error); - } - } - - /** - * Save version tracking to localStorage - */ - private saveVersions(): void { - const versionsArray = Array.from(this.versionMap.entries()); - localStorage.setItem(VERSION_STORAGE_KEY, JSON.stringify(versionsArray)); - } - - /** - * Record that a level was loaded with a specific version - */ - public recordLevelLoaded(levelId: string, version: string): void { - const info: LevelVersionInfo = { - levelId, - loadedVersion: version, - loadedAt: new Date() - }; - - this.versionMap.set(levelId, info); - this.saveVersions(); - } - - /** - * Update manifest versions from directory - */ - public updateManifestVersions(directory: LevelDirectory): void { - for (const entry of directory.levels) { - const existing = this.versionMap.get(entry.id); - if (existing) { - existing.manifestVersion = entry.version; - } else { - // First time seeing this level - this.versionMap.set(entry.id, { - levelId: entry.id, - loadedVersion: '', // Not yet loaded - loadedAt: new Date(), - manifestVersion: entry.version - }); - } - } - - this.saveVersions(); - } - - /** - * Check if a level has an update available - */ - public hasUpdate(levelId: string): boolean { - const info = this.versionMap.get(levelId); - if (!info || !info.manifestVersion || !info.loadedVersion) { - return false; - } - - return this.compareVersions(info.loadedVersion, info.manifestVersion) < 0; - } - - /** - * Get version comparison for a level - */ - public getVersionComparison(levelId: string): VersionComparison | null { - const info = this.versionMap.get(levelId); - if (!info || !info.manifestVersion) { - return null; - } - - const currentVersion = info.loadedVersion || '0.0'; - const latestVersion = info.manifestVersion; - const isOutdated = this.compareVersions(currentVersion, latestVersion) < 0; - - return { - levelId, - currentVersion, - latestVersion, - isOutdated - }; - } - - /** - * Get all levels with available updates - */ - public getUpdatableLevels(): VersionComparison[] { - const updatable: VersionComparison[] = []; - - for (const [levelId, info] of this.versionMap) { - if (info.manifestVersion && info.loadedVersion) { - const comparison = this.getVersionComparison(levelId); - if (comparison && comparison.isOutdated) { - updatable.push(comparison); - } - } - } - - return updatable; - } - - /** - * Get version info for a level - */ - public getVersionInfo(levelId: string): LevelVersionInfo | undefined { - return this.versionMap.get(levelId); - } - - /** - * Mark a level as updated (user accepted the new version) - */ - public markAsUpdated(levelId: string, newVersion: string): void { - const info = this.versionMap.get(levelId); - if (info) { - info.loadedVersion = newVersion; - info.loadedAt = new Date(); - this.saveVersions(); - } - } - - /** - * Compare two semantic version strings - * Returns: -1 if v1 < v2, 0 if equal, 1 if v1 > v2 - */ - private compareVersions(v1: string, v2: string): number { - const parts1 = v1.split('.').map(Number); - const parts2 = v2.split('.').map(Number); - - const maxLength = Math.max(parts1.length, parts2.length); - - for (let i = 0; i < maxLength; i++) { - const part1 = parts1[i] || 0; - const part2 = parts2[i] || 0; - - if (part1 < part2) return -1; - if (part1 > part2) return 1; - } - - return 0; - } - - /** - * Clear all version tracking (for testing/reset) - */ - public clearAll(): void { - this.versionMap.clear(); - localStorage.removeItem(VERSION_STORAGE_KEY); - } - - /** - * Get summary of version statuses - */ - public getVersionSummary(): { - total: number; - tracked: number; - updatable: number; - upToDate: number; - } { - let tracked = 0; - let updatable = 0; - let upToDate = 0; - - for (const info of this.versionMap.values()) { - if (info.loadedVersion) { - tracked++; - - if (info.manifestVersion) { - if (this.compareVersions(info.loadedVersion, info.manifestVersion) < 0) { - updatable++; - } else { - upToDate++; - } - } - } - } - - return { - total: this.versionMap.size, - tracked, - updatable, - upToDate - }; - } - - /** - * Build changelog text for version updates - */ - public static buildChangelog(directoryEntry: LevelDirectoryEntry): string { - // In the future, this could fetch from a changelog file or API - // For now, generate a simple message - return `Level updated to version ${directoryEntry.version}. Check for improvements and changes!`; - } - - /** - * Check if this is the first time loading any levels - */ - public isFirstRun(): boolean { - return this.versionMap.size === 0; - } -} diff --git a/src/main.ts b/src/main.ts index 7f0ad35..4e348c7 100644 --- a/src/main.ts +++ b/src/main.ts @@ -23,8 +23,6 @@ import Level from "./levels/level"; import setLoadingMessage from "./utils/setLoadingMessage"; import {RockFactory} from "./environment/asteroids/rockFactory"; import {ControllerDebug} from "./utils/controllerDebug"; -import {router, showView} from "./core/router"; -import {populateLevelSelector} from "./levels/ui/levelSelector"; import {LevelConfig} from "./levels/config/levelConfig"; import {LegacyMigration} from "./levels/migration/legacyMigration"; import {LevelRegistry} from "./levels/storage/levelRegistry"; @@ -747,128 +745,7 @@ export class Main { } } -// Setup router -router.on('/', async () => { - debugLog('[Router] Home route triggered'); - - // Always show game view - showView('game'); - debugLog('[Router] Game view shown'); - - // Initialize auth service (but don't block on it) - try { - const authService = AuthService.getInstance(); - debugLog('[Router] Initializing auth service...'); - await authService.initialize(); - debugLog('[Router] Auth service initialized'); - - // Check if user is authenticated - const isAuthenticated = await authService.isAuthenticated(); - const user = authService.getUser(); - debugLog('[Router] Auth check - authenticated:', isAuthenticated, 'user:', user); - - if (isAuthenticated && user) { - // User is authenticated - update profile display - debugLog('User authenticated:', user?.email || user?.name || 'Unknown'); - updateUserProfile(user.name || user.email || 'Player'); - } else { - // User not authenticated - show login/signup button - debugLog('User not authenticated, showing login button'); - updateUserProfile(null); // This will show login button instead - } - } catch (error) { - // Auth failed, but allow game to continue - debugLog('Auth initialization failed, continuing without auth:', error); - updateUserProfile(null); - } - - // Show the app header - const appHeader = document.getElementById('appHeader'); - if (appHeader) { - appHeader.style.display = 'block'; - } - - // Just show the level selector - don't initialize anything yet! - if (!DEBUG_CONTROLLERS) { - debugLog('[Router] Populating level selector (no engine initialization yet)'); - await populateLevelSelector(); - - // Create Main instance lazily only if it doesn't exist - // But don't initialize it yet - that will happen on level selection - if (!(window as any).__mainInstance) { - debugLog('[Router] Creating Main instance (not initialized)'); - const main = new Main(); - (window as any).__mainInstance = main; - - // Initialize demo mode without engine (just for UI purposes) - const demo = new Demo(main); - } - - // Discord widget initialization with enhanced error logging - /*if (!(window as any).__discordWidget) { - debugLog('[Router] Initializing Discord widget'); - const discord = new DiscordWidget(); - - // Initialize with your server and channel IDs - discord.initialize({ - server: '1112846185913401475', // Replace with your Discord server ID - channel: '1437561367908581406', // Replace with your Discord channel ID - color: '#667eea', - glyph: ['💬', '✖️'], - notifications: true - }).then(() => { - debugLog('[Router] Discord widget ready'); - (window as any).__discordWidget = discord; - }).catch(error => { - console.error('[Router] Failed to initialize Discord widget:', error); - console.error('[Router] Error type:', error?.constructor?.name); - console.error('[Router] Error message:', error?.message); - console.error('[Router] Error stack:', error?.stack); - if (error?.response) { - console.error('[Router] GraphQL response error:', error.response); - } - }); - }*/ - } - - debugLog('[Router] Home route handler complete'); -}); - -router.on('/editor', () => { - showView('editor'); - // Dynamically import and initialize editor - if (!(window as any).__editorInitialized) { - import('./levels/generation/levelEditor').then(() => { - (window as any).__editorInitialized = true; - }); - } -}); - -router.on('/settings', () => { - showView('settings'); - // Dynamically import and initialize settings - if (!(window as any).__settingsInitialized) { - import('./ui/screens/settingsScreen').then((module) => { - module.initializeSettingsScreen(); - (window as any).__settingsInitialized = true; - }); - } -}); - -router.on('/controls', () => { - showView('controls'); - // Dynamically import and initialize controls screen - if (!(window as any).__controlsInitialized) { - import('./ui/screens/controlsScreen').then((module) => { - const controlsScreen = new module.ControlsScreen(); - controlsScreen.initialize(); - (window as any).__controlsInitialized = true; - }); - } -}); - -// Initialize registry and start router -// This must happen BEFORE router.start() so levels are available +// Initialize registry and mount Svelte app async function initializeApp() { console.log('[Main] ========================================'); console.log('[Main] initializeApp() STARTED at', new Date().toISOString()); @@ -889,8 +766,6 @@ async function initializeApp() { await LevelRegistry.getInstance().initialize(); console.log('[Main] LevelRegistry.initialize() completed successfully [AFTER MIGRATION]'); debugLog('[Main] LevelRegistry initialized after migration'); - // NOTE: Old router disabled - now using svelte-routing - // router.start(); // Mount Svelte app console.log('[Main] Mounting Svelte app [AFTER MIGRATION]'); @@ -917,8 +792,6 @@ async function initializeApp() { resolve(); } catch (error) { console.error('[Main] Failed to initialize LevelRegistry after migration:', error); - // NOTE: Old router disabled - now using svelte-routing - // router.start(); // Start anyway to show error state resolve(); } }); @@ -941,19 +814,12 @@ async function initializeApp() { if (isDev) { (window as any).__levelRegistry = LevelRegistry.getInstance(); console.log('[Main] LevelRegistry exposed to window.__levelRegistry for debugging'); - console.log('[Main] To clear caches: window.__levelRegistry.clearAllCaches().then(() => location.reload())'); + console.log('[Main] To clear caches: window.__levelRegistry.reset(); location.reload()'); } - - // NOTE: Old router disabled - now using svelte-routing - // console.log('[Main] About to call router.start()'); - // router.start(); - // console.log('[Main] router.start() completed'); } catch (error) { console.error('[Main] !!!!! EXCEPTION in LevelRegistry initialization !!!!!'); console.error('[Main] Failed to initialize LevelRegistry:', error); console.error('[Main] Error stack:', error?.stack); - // NOTE: Old router disabled - now using svelte-routing - // router.start(); // Start anyway to show error state } } diff --git a/src/ship/weaponSystem.ts b/src/ship/weaponSystem.ts index e66628e..030f675 100644 --- a/src/ship/weaponSystem.ts +++ b/src/ship/weaponSystem.ts @@ -104,7 +104,8 @@ export class WeaponSystem { ammoAggregate.body.setCollisionCallbackEnabled(true); // Set projectile velocity (already includes ship velocity) - ammoAggregate.body.setLinearVelocity(velocityVector); + // Clone to capture current direction - prevents curving if source vector updates + ammoAggregate.body.setLinearVelocity(velocityVector.clone()); // Consume ammo if (this._shipStatus) {