import { AudioEngineV2, Color3, CreateAudioEngineAsync, Engine, FreeCamera, HavokPlugin, ParticleHelper, Scene, Vector3, WebGPUEngine, WebXRDefaultExperience, WebXRFeaturesManager } from "@babylonjs/core"; import '@babylonjs/loaders'; import HavokPhysics from "@babylonjs/havok"; import {DefaultScene} from "./core/defaultScene"; import {Level1} from "./levels/level1"; import {TestLevel} from "./levels/testLevel"; import Demo from "./game/demo"; import Level from "./levels/level"; import setLoadingMessage from "./utils/setLoadingMessage"; import {RockFactory} from "./environment/asteroids/rockFactory"; import {ControllerDebug} from "./utils/controllerDebug"; import {LevelConfig} from "./levels/config/levelConfig"; import {LegacyMigration} from "./levels/migration/legacyMigration"; import {LevelRegistry} from "./levels/storage/levelRegistry"; import debugLog from './core/debug'; import {ReplaySelectionScreen} from "./replay/ReplaySelectionScreen"; import {ReplayManager} from "./replay/ReplayManager"; import {AuthService} from "./services/authService"; import {updateUserProfile} from "./ui/screens/loginScreen"; import {Preloader} from "./ui/screens/preloader"; import {DiscordWidget} from "./ui/widgets/discordWidget"; // Svelte App import { mount } from 'svelte'; import App from './components/layouts/App.svelte'; import { BrowserAgent } from '@newrelic/browser-agent/loaders/browser-agent' import { AnalyticsService } from './analytics/analyticsService'; import { NewRelicAdapter } from './analytics/adapters/newRelicAdapter'; import { InputControlManager } from './ship/input/inputControlManager'; // Populate using values from NerdGraph const options = { init: {distributed_tracing:{enabled:true},performance:{capture_measures:true},browser_consent_mode:{enabled:false},privacy:{cookies_enabled:true},ajax:{deny_list:["bam.nr-data.net"]}}, loader_config: {accountID:"7354964",trustKey:"7354964",agentID:"601599788",licenseKey:"NRJS-5673c7fa13b17021446",applicationID:"601599788"}, info: {beacon:"bam.nr-data.net",errorBeacon:"bam.nr-data.net",licenseKey:"NRJS-5673c7fa13b17021446",applicationID:"601599788",sa:1} } const nrba = new BrowserAgent(options) // Initialize analytics service with New Relic adapter const analytics = AnalyticsService.initialize({ enabled: true, includeSessionMetadata: true, debug: true // Set to true for development debugging }); // Configure New Relic adapter with batching const newRelicAdapter = new NewRelicAdapter(nrba, { batchSize: 10, // Flush after 10 events flushInterval: 30000, // Flush every 30 seconds debug: true // Set to true to see batching in action }); analytics.addAdapter(newRelicAdapter); // Track initial session start analytics.track('session_start', { platform: navigator.xr ? 'vr' : (/mobile|android|iphone|ipad/i.test(navigator.userAgent) ? 'mobile' : 'desktop'), userAgent: navigator.userAgent, screenWidth: window.screen.width, screenHeight: window.screen.height }); // Remaining code // Set to true to run minimal controller debug test const DEBUG_CONTROLLERS = false; const webGpu = false; const canvas = (document.querySelector('#gameCanvas') as HTMLCanvasElement); enum GameState { PLAY, DEMO } export class Main { private _currentLevel: Level; private _gameState: GameState = GameState.DEMO; private _engine: Engine | WebGPUEngine; private _audioEngine: AudioEngineV2; private _replayManager: ReplayManager | null = null; private _initialized: boolean = false; private _assetsLoaded: boolean = false; private _progressCallback: ((percent: number, message: string) => void) | null = null; constructor(progressCallback?: (percent: number, message: string) => void) { this._progressCallback = progressCallback || null; // Listen for level selection event window.addEventListener('levelSelected', async (e: CustomEvent) => { this._started = true; const {levelName, config} = e.detail as {levelName: string, config: LevelConfig}; debugLog(`[Main] Starting level: ${levelName}`); // Hide all UI elements const mainDiv = document.querySelector('#mainDiv'); const levelSelect = document.querySelector('#levelSelect') as HTMLElement; const appHeader = document.querySelector('#appHeader') as HTMLElement; if (levelSelect) { levelSelect.style.display = 'none'; } if (appHeader) { appHeader.style.display = 'none'; } // Hide Discord widget during gameplay const discord = (window as any).__discordWidget as DiscordWidget; if (discord) { debugLog('[Main] Hiding Discord widget for gameplay'); discord.hide(); } // Show preloader for initialization const preloader = new Preloader(); this._progressCallback = (percent, message) => { preloader.updateProgress(percent, message); }; try { // Initialize engine if this is first time if (!this._initialized) { debugLog('[Main] First level selected - initializing engine'); preloader.updateProgress(0, 'Initializing game engine...'); await this.initializeEngine(); } // Load assets if this is the first level being played if (!this._assetsLoaded) { preloader.updateProgress(40, 'Loading 3D models and textures...'); debugLog('[Main] Loading assets for first time'); // Load visual assets (meshes, particles) ParticleHelper.BaseAssetsUrl = window.location.href; await RockFactory.init(); this._assetsLoaded = true; debugLog('[Main] Assets loaded successfully'); preloader.updateProgress(60, 'Assets loaded'); } preloader.updateProgress(70, 'Preparing VR session...'); // Initialize WebXR for this level await this.initialize(); // If XR is available, enter XR immediately (while we have user activation) let xrSession = null; if (DefaultScene.XR) { try { preloader.updateProgress(75, 'Entering VR...'); // Stop render loop BEFORE entering XR to prevent showing wrong camera orientation // The ship model is rotated 180 degrees, so the XR camera would briefly face backwards // We'll resume rendering after the camera is properly parented to the ship this._engine.stopRenderLoop(); debugLog('Render loop stopped before entering XR'); xrSession = await DefaultScene.XR.baseExperience.enterXRAsync('immersive-vr', 'local-floor'); debugLog('XR session started successfully (render loop paused until camera is ready)'); } catch (error) { debugLog('Failed to enter XR, will fall back to flat mode:', error); DefaultScene.XR = null; // Disable XR for this session // Resume render loop for flat mode this._engine.runRenderLoop(() => { DefaultScene.MainScene.render(); }); } } // Unlock audio engine on user interaction if (this._audioEngine) { await this._audioEngine.unlockAsync(); } // Now load audio assets (after unlock) preloader.updateProgress(80, 'Loading audio...'); await RockFactory.initAudio(this._audioEngine); // Attach audio listener to camera for spatial audio const camera = DefaultScene.XR?.baseExperience?.camera || DefaultScene.MainScene.activeCamera; if (camera && this._audioEngine.listener) { this._audioEngine.listener.attach(camera); debugLog('[Main] Audio listener attached to camera for spatial audio'); } else { debugLog('[Main] WARNING: Could not attach audio listener - camera or listener not available'); } preloader.updateProgress(90, 'Creating level...'); // Create and initialize level from config this._currentLevel = new Level1(config, this._audioEngine, false, levelName); // Wait for level to be ready this._currentLevel.getReadyObservable().add(async () => { preloader.updateProgress(95, 'Starting game...'); // Get ship and set up replay observable const level1 = this._currentLevel as Level1; const ship = (level1 as any)._ship; // Listen for replay requests from the ship if (ship) { // Note: Level info for progression/results is now set in Level1.initialize() ship.onReplayRequestObservable.add(() => { debugLog('Replay requested - reloading page'); window.location.reload(); }); } // If we entered XR before level creation, manually setup camera parenting // (This is needed because onInitialXRPoseSetObservable won't fire if we're already in XR) console.log('[Main] ========== CHECKING XR STATE =========='); console.log('[Main] DefaultScene.XR exists:', !!DefaultScene.XR); console.log('[Main] xrSession exists:', !!xrSession); if (DefaultScene.XR) { console.log('[Main] XR base experience state:', DefaultScene.XR.baseExperience.state); } if (DefaultScene.XR && xrSession && DefaultScene.XR.baseExperience.state === 2) { // WebXRState.IN_XR = 2 console.log('[Main] ========== XR ALREADY ACTIVE - MANUAL SETUP =========='); if (ship && ship.transformNode) { console.log('[Main] Ship and transformNode exist - parenting camera'); debugLog('Manually parenting XR camera to ship transformNode'); DefaultScene.XR.baseExperience.camera.parent = ship.transformNode; DefaultScene.XR.baseExperience.camera.position = new Vector3(0, 1.5, 0); // Rotate camera 180 degrees around Y to compensate for inverted ship GLB model DefaultScene.XR.baseExperience.camera.rotationQuaternion = null; DefaultScene.XR.baseExperience.camera.rotation = new Vector3(0, Math.PI, 0); console.log('[Main] Camera parented and rotated 180° to face forward'); // NOW resume the render loop - camera is properly positioned this._engine.runRenderLoop(() => { DefaultScene.MainScene.render(); }); debugLog('Render loop resumed after camera setup'); console.log('[Main] ========== ABOUT TO SHOW MISSION BRIEF =========='); console.log('[Main] level1 object:', level1); console.log('[Main] level1._missionBrief:', (level1 as any)._missionBrief); // Show mission brief (since onInitialXRPoseSetObservable won't fire) await level1.showMissionBrief(); console.log('[Main] ========== MISSION BRIEF SHOW() RETURNED =========='); console.log('[Main] Mission brief will call startGameplay() when trigger is pulled'); // NOTE: Don't start timer/recording here anymore - mission brief will do it // when the user clicks the START button } else { console.error('[Main] !!!!! SHIP OR TRANSFORM NODE NOT FOUND !!!!!'); console.log('[Main] ship exists:', !!ship); console.log('[Main] ship.transformNode exists:', ship ? !!ship.transformNode : 'N/A'); debugLog('WARNING: Could not parent XR camera - ship or transformNode not found'); // Resume render loop anyway to avoid black screen this._engine.runRenderLoop(() => { DefaultScene.MainScene.render(); }); } } else { console.log('[Main] XR not active yet - will use onInitialXRPoseSetObservable instead'); // Resume render loop for non-XR path (flat mode or XR entry via observable) this._engine.runRenderLoop(() => { DefaultScene.MainScene.render(); }); } // Hide preloader preloader.updateProgress(100, 'Ready!'); setTimeout(() => { preloader.hide(); }, 500); // Hide UI (no longer remove from DOM - let Svelte routing handle it) console.log('[Main] ========== HIDING UI FOR GAMEPLAY =========='); console.log('[Main] mainDiv exists:', !!mainDiv); console.log('[Main] Timestamp:', Date.now()); // Note: With route-based loading, the app will be hidden by PlayLevel component // This code path is only used when dispatching levelSelected event (legacy support) // Start the game (XR session already active, or flat mode) console.log('[Main] About to call this.play()'); await this.play(); console.log('[Main] this.play() completed'); }); // Now initialize the level (after observable is registered) await this._currentLevel.initialize(); } catch (error) { console.error('[Main] Level initialization failed:', error); preloader.updateProgress(0, 'Failed to load level. Please refresh and try again.'); } }); // Listen for test level button click window.addEventListener('DOMContentLoaded', () => { const levelSelect = document.querySelector('#levelSelect'); levelSelect.classList.add('ready'); debugLog('[Main] DOMContentLoaded fired, looking for test button...'); const testLevelBtn = document.querySelector('#testLevelBtn'); debugLog('[Main] Test button found:', !!testLevelBtn); if (testLevelBtn) { testLevelBtn.addEventListener('click', async () => { debugLog('[Main] ========== TEST LEVEL BUTTON CLICKED =========='); // Hide all UI elements const mainDiv = document.querySelector('#mainDiv'); const levelSelect = document.querySelector('#levelSelect') as HTMLElement; const appHeader = document.querySelector('#appHeader') as HTMLElement; debugLog('[Main] mainDiv exists:', !!mainDiv); debugLog('[Main] levelSelect exists:', !!levelSelect); if (levelSelect) { levelSelect.style.display = 'none'; debugLog('[Main] levelSelect hidden'); } if (appHeader) { appHeader.style.display = 'none'; } setLoadingMessage("Initializing Test Scene..."); // Unlock audio engine on user interaction if (this._audioEngine) { debugLog('[Main] Unlocking audio engine...'); await this._audioEngine.unlockAsync(); debugLog('[Main] Audio engine unlocked'); } // Now load audio assets (after unlock) setLoadingMessage("Loading audio assets..."); await RockFactory.initAudio(this._audioEngine); // Attach audio listener to camera for spatial audio const camera = DefaultScene.XR?.baseExperience?.camera || DefaultScene.MainScene.activeCamera; if (camera && this._audioEngine.listener) { this._audioEngine.listener.attach(camera); debugLog('[Main] Audio listener attached to camera for spatial audio (test level)'); } else { debugLog('[Main] WARNING: Could not attach audio listener - camera or listener not available (test level)'); } // Create test level debugLog('[Main] Creating TestLevel...'); this._currentLevel = new TestLevel(this._audioEngine); debugLog('[Main] TestLevel created:', !!this._currentLevel); // Wait for level to be ready debugLog('[Main] Registering ready observable...'); this._currentLevel.getReadyObservable().add(async () => { debugLog('[Main] ========== TEST LEVEL READY OBSERVABLE FIRED =========='); setLoadingMessage("Test Scene Ready! Entering VR..."); // Hide UI for gameplay (no longer remove from DOM) // Test level doesn't use routing, so we need to hide the app element const appElement = document.getElementById('app'); if (appElement) { appElement.style.display = 'none'; debugLog('[Main] App UI hidden for test level'); } debugLog('[Main] About to call this.play()...'); await this.play(); }); debugLog('[Main] Ready observable registered'); // Now initialize the level (after observable is registered) debugLog('[Main] Calling TestLevel.initialize()...'); await this._currentLevel.initialize(); debugLog('[Main] TestLevel.initialize() completed'); }); debugLog('[Main] Click listener added to test button'); } else { console.warn('[Main] Test level button not found in DOM'); } // View Replays button handler const viewReplaysBtn = document.querySelector('#viewReplaysBtn'); debugLog('[Main] View Replays button found:', !!viewReplaysBtn); if (viewReplaysBtn) { viewReplaysBtn.addEventListener('click', async () => { debugLog('[Main] ========== VIEW REPLAYS BUTTON CLICKED =========='); // Initialize engine and physics if not already done if (!this._started) { this._started = true; await this.initialize(); } // Hide main menu const levelSelect = document.querySelector('#levelSelect') as HTMLElement; const appHeader = document.querySelector('#appHeader') as HTMLElement; if (levelSelect) { levelSelect.style.display = 'none'; } if (appHeader) { appHeader.style.display = 'none'; } // Show replay selection screen const selectionScreen = new ReplaySelectionScreen( async (recordingId: string) => { // Play callback - start replay debugLog(`[Main] Starting replay for recording: ${recordingId}`); selectionScreen.dispose(); // Create replay manager if not exists if (!this._replayManager) { this._replayManager = new ReplayManager( this._engine as Engine, () => { // On exit callback - return to main menu debugLog('[Main] Exiting replay, returning to menu'); if (levelSelect) { levelSelect.style.display = 'block'; } const appHeader = document.querySelector('#appHeader') as HTMLElement; if (appHeader) { appHeader.style.display = 'block'; } } ); } // Start replay if (this._replayManager) { await this._replayManager.startReplay(recordingId); } }, () => { // Cancel callback - return to main menu debugLog('[Main] Replay selection cancelled'); selectionScreen.dispose(); if (levelSelect) { levelSelect.style.display = 'block'; } const appHeader = document.querySelector('#appHeader') as HTMLElement; if (appHeader) { appHeader.style.display = 'block'; } } ); await selectionScreen.initialize(); }); debugLog('[Main] Click listener added to view replays button'); } else { console.warn('[Main] View Replays button not found in DOM'); } }); } private _started = false; /** * Public method to initialize the game engine * Call this to preload all assets before showing the level selector */ public async initializeEngine(): Promise { if (this._initialized) { debugLog('[Main] Engine already initialized, skipping'); return; } debugLog('[Main] Starting engine initialization'); // Progress: 0-30% - Scene setup this.reportProgress(0, 'Initializing 3D engine...'); await this.setupScene(); this.reportProgress(30, '3D engine ready'); // Progress: 30-100% - WebXR, physics, assets await this.initialize(); this._initialized = true; this.reportProgress(100, 'All systems ready!'); debugLog('[Main] Engine initialization complete'); } /** * Report loading progress to callback */ private reportProgress(percent: number, message: string): void { if (this._progressCallback) { this._progressCallback(percent, message); } } /** * Check if engine is initialized */ public isInitialized(): boolean { return this._initialized; } /** * Get the audio engine (for external use) */ public getAudioEngine(): AudioEngineV2 { return this._audioEngine; } /** * Cleanup and exit XR gracefully, returning to main menu */ public async cleanupAndExit(): Promise { debugLog('[Main] cleanupAndExit() called - starting graceful shutdown'); try { // 1. Stop render loop first (before disposing anything) debugLog('[Main] Stopping render loop...'); this._engine.stopRenderLoop(); // 2. Dispose current level and all its resources (includes ship, weapons, etc.) if (this._currentLevel) { debugLog('[Main] Disposing level...'); this._currentLevel.dispose(); this._currentLevel = null; } // 2.5. Reset RockFactory static state (asteroid mesh, explosion manager, etc.) RockFactory.reset(); // 3. Exit XR session if active (after disposing level to avoid state issues) if (DefaultScene.XR && DefaultScene.XR.baseExperience.state === 2) { // WebXRState.IN_XR = 2 debugLog('[Main] Exiting XR session...'); try { await DefaultScene.XR.baseExperience.exitXRAsync(); debugLog('[Main] XR session exited successfully'); } catch (error) { debugLog('[Main] Error exiting XR session:', error); } } // 4. Clear remaining scene objects (anything not disposed by level) if (DefaultScene.MainScene) { debugLog('[Main] Disposing remaining scene meshes and materials...'); // Clone arrays to avoid modification during iteration const meshes = DefaultScene.MainScene.meshes.slice(); const materials = DefaultScene.MainScene.materials.slice(); meshes.forEach(mesh => { if (!mesh.isDisposed()) { try { mesh.dispose(); } catch (error) { debugLog('[Main] Error disposing mesh:', error); } } }); materials.forEach(material => { try { material.dispose(); } catch (error) { debugLog('[Main] Error disposing material:', error); } }); } // 5. Disable physics engine (properly disposes AND clears scene reference) if (DefaultScene.MainScene && DefaultScene.MainScene.isPhysicsEnabled()) { debugLog('[Main] Disabling physics engine...'); DefaultScene.MainScene.disablePhysicsEngine(); } // 6. Clear XR reference (will be recreated on next game start) DefaultScene.XR = null; // 7. Reset initialization flags so game can be restarted this._initialized = false; this._assetsLoaded = false; this._started = false; // 8. Clear the canvas so it doesn't show the last frame debugLog('[Main] Clearing canvas...'); const canvas = document.getElementById('gameCanvas') as HTMLCanvasElement; if (canvas) { const gl = canvas.getContext('webgl2') || canvas.getContext('webgl'); if (gl) { gl.clearColor(0, 0, 0, 1); gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT); } } // 9. Keep render loop stopped until next game starts // No need to render an empty scene - saves resources debugLog('[Main] Render loop stopped - will restart when game starts'); // 10. Show Discord widget (UI will be shown by Svelte router) const discord = (window as any).__discordWidget as DiscordWidget; if (discord) { debugLog('[Main] Showing Discord widget'); discord.show(); } debugLog('[Main] Cleanup complete - ready for new game'); } catch (error) { console.error('[Main] Error during cleanup:', error); // If cleanup fails, fall back to page reload debugLog('[Main] Cleanup failed, falling back to page reload'); window.location.reload(); } } public async play() { debugLog('[Main] play() called'); debugLog('[Main] Current level exists:', !!this._currentLevel); this._gameState = GameState.PLAY; if (this._currentLevel) { debugLog('[Main] Calling level.play()...'); await this._currentLevel.play(); debugLog('[Main] level.play() completed'); } else { console.error('[Main] ERROR: No current level to play!'); } } public demo() { this._gameState = GameState.DEMO; } private async initialize() { // Try to initialize WebXR if available (30-40%) this.reportProgress(35, 'Checking VR support...'); if (navigator.xr) { try { DefaultScene.XR = await WebXRDefaultExperience.CreateAsync(DefaultScene.MainScene, { // Don't disable pointer selection - we need it for status screen buttons // Will detach it during gameplay and attach when status screen is shown disableTeleportation: true, disableNearInteraction: true, disableHandTracking: true, disableDefaultUI: true }); debugLog(WebXRFeaturesManager.GetAvailableFeatures()); debugLog("WebXR initialized successfully"); // Register pointer selection feature with InputControlManager if (DefaultScene.XR) { const pointerFeature = DefaultScene.XR.baseExperience.featuresManager.getEnabledFeature( "xr-controller-pointer-selection" ); if (pointerFeature) { // Store for backward compatibility (can be removed later if not needed) (DefaultScene.XR as any).pointerSelectionFeature = pointerFeature; // Register with InputControlManager const inputManager = InputControlManager.getInstance(); inputManager.registerPointerFeature(pointerFeature); debugLog("Pointer selection feature registered with InputControlManager"); // Configure scene-wide picking predicate to only allow UI meshes /*DefaultScene.MainScene.pointerMovePredicate = (mesh) => { // Only allow picking meshes with metadata.uiPickable = true return mesh.metadata?.uiPickable === true; };*/ debugLog("Scene picking predicate configured for VR UI only"); } // Hide Discord widget when entering VR, show when exiting DefaultScene.XR.baseExperience.onStateChangedObservable.add((state) => { const discord = (window as any).__discordWidget as DiscordWidget; if (discord) { if (state === 2) { // WebXRState.IN_XR debugLog('[Main] Entering VR - hiding Discord widget'); discord.hide(); } else if (state === 0) { // WebXRState.NOT_IN_XR debugLog('[Main] Exiting VR - showing Discord widget'); discord.show(); } } }); } this.reportProgress(40, 'VR support enabled'); } catch (error) { debugLog("WebXR initialization failed, falling back to flat mode:", error); DefaultScene.XR = null; this.reportProgress(40, 'Desktop mode (VR not available)'); } } else { debugLog("WebXR not available, using flat camera mode"); DefaultScene.XR = null; this.reportProgress(40, 'Desktop mode'); } DefaultScene.MainScene.onAfterRenderObservable.add(() => { // Reserved for photo domes if needed }); } private async setupScene() { // 0-10%: Engine initialization this.reportProgress(5, 'Creating rendering engine...'); if (webGpu) { this._engine = new WebGPUEngine(canvas); debugLog("Webgpu enabled"); await (this._engine as WebGPUEngine).initAsync(); } else { debugLog("Standard WebGL enabled"); this._engine = new Engine(canvas, true); } this._engine.setHardwareScalingLevel(1 / window.devicePixelRatio); window.onresize = () => { this._engine.resize(); } this.reportProgress(10, 'Creating scenes...'); DefaultScene.DemoScene = new Scene(this._engine); DefaultScene.MainScene = new Scene(this._engine); DefaultScene.MainScene.ambientColor = new Color3(.2,.2,.2); DefaultScene.MainScene.clearColor = new Color3(0, 0, 0).toColor4(); // 10-20%: Physics this.reportProgress(15, 'Loading physics engine...'); await this.setupPhysics(); this.reportProgress(20, 'Physics engine ready'); // 20-30%: Audio this.reportProgress(22, 'Initializing spatial audio...'); this._audioEngine = await CreateAudioEngineAsync({ volume: 1.0, listenerAutoUpdate: true, listenerEnabled: true, resumeOnInteraction: true }); debugLog('Audio engine created with spatial audio enabled'); this.reportProgress(30, 'Audio engine ready'); // Assets (meshes, textures) will be loaded when user selects a level // This makes initial load faster // Start render loop this._engine.runRenderLoop(() => { DefaultScene.MainScene.render(); }); } private async setupPhysics() { //DefaultScene.MainScene.useRightHandedSystem = true; const havok = await HavokPhysics(); const havokPlugin = new HavokPlugin(true, havok); //DefaultScene.MainScene.ambientColor = new Color3(.1, .1, .1); //const light = new HemisphericLight("mainlight", new Vector3(-1, -1, 0), DefaultScene.MainScene); //light.diffuse = new Color3(.4, .4, .3); //light.groundColor = new Color3(.2, .2, .1); //light.intensity = .5; //light.specular = new Color3(0,0,0); DefaultScene.MainScene.enablePhysics(new Vector3(0, 0, 0), havokPlugin); DefaultScene.MainScene.getPhysicsEngine().setTimeStep(1/60); DefaultScene.MainScene.getPhysicsEngine().setSubTimeStep(5); DefaultScene.MainScene.collisionsEnabled = true; } } // Initialize registry and mount Svelte app async function initializeApp() { console.log('[Main] ========================================'); console.log('[Main] initializeApp() STARTED at', new Date().toISOString()); console.log('[Main] ========================================'); // Check for legacy data migration const needsMigration = LegacyMigration.needsMigration(); console.log('[Main] Needs migration check:', needsMigration); if (needsMigration) { debugLog('[Main] Legacy data detected - showing migration modal'); return new Promise((resolve) => { LegacyMigration.showMigrationModal(async (result) => { debugLog('[Main] Migration completed:', result); // Initialize the new registry system try { console.log('[Main] About to call LevelRegistry.getInstance().initialize() [AFTER MIGRATION]'); await LevelRegistry.getInstance().initialize(); console.log('[Main] LevelRegistry.initialize() completed successfully [AFTER MIGRATION]'); debugLog('[Main] LevelRegistry initialized after migration'); // Mount Svelte app console.log('[Main] Mounting Svelte app [AFTER MIGRATION]'); const appElement = document.getElementById('app'); if (appElement) { mount(App, { target: appElement }); console.log('[Main] Svelte app mounted successfully [AFTER MIGRATION]'); // Create Main instance lazily only if it doesn't exist if (!DEBUG_CONTROLLERS && !(window as any).__mainInstance) { debugLog('[Main] Creating Main instance (not initialized) [AFTER MIGRATION]'); const main = new Main(); (window as any).__mainInstance = main; // Initialize demo mode without engine (just for UI purposes) const demo = new Demo(main); } } else { console.error('[Main] Failed to mount Svelte app - #app element not found [AFTER MIGRATION]'); } resolve(); } catch (error) { console.error('[Main] Failed to initialize LevelRegistry after migration:', error); resolve(); } }); }); } else { console.log('[Main] No migration needed - proceeding to initialize registry'); // Initialize the new registry system try { console.log('[Main] About to call LevelRegistry.getInstance().initialize()'); console.log('[Main] Timestamp before initialize:', Date.now()); await LevelRegistry.getInstance().initialize(); console.log('[Main] Timestamp after initialize:', Date.now()); console.log('[Main] LevelRegistry.initialize() completed successfully'); debugLog('[Main] LevelRegistry initialized'); // Expose registry to window for debugging (dev mode) const isDev = window.location.hostname === 'localhost' || window.location.hostname.includes('dev.') || window.location.port !== ''; 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.reset(); location.reload()'); } } catch (error) { console.error('[Main] !!!!! EXCEPTION in LevelRegistry initialization !!!!!'); console.error('[Main] Failed to initialize LevelRegistry:', error); console.error('[Main] Error stack:', error?.stack); } } // Mount Svelte app console.log('[Main] Mounting Svelte app'); const appElement = document.getElementById('app'); if (appElement) { mount(App, { target: appElement }); console.log('[Main] Svelte app mounted successfully'); // Create Main instance lazily only if it doesn't exist if (!DEBUG_CONTROLLERS && !(window as any).__mainInstance) { debugLog('[Main] 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); } } else { console.error('[Main] Failed to mount Svelte app - #app element not found'); } console.log('[Main] initializeApp() FINISHED at', new Date().toISOString()); } // Start the app console.log('[Main] ========================================'); console.log('[Main] main.ts MODULE LOADED at', new Date().toISOString()); console.log('[Main] About to call initializeApp()'); console.log('[Main] ========================================'); initializeApp(); // Suppress non-critical BabylonJS shader loading errors during development // Note: After Vite config fix to pre-bundle shaders, these errors should no longer occur // Keeping this handler for backwards compatibility with older cached builds window.addEventListener('unhandledrejection', (event) => { const error = event.reason; if (error && error.message) { // Only suppress specific shader-related errors, not asset loading errors if (error.message.includes('rgbdDecode.fragment') || error.message.includes('procedural.vertex') || (error.message.includes('Failed to fetch dynamically imported module') && (error.message.includes('rgbdDecode') || error.message.includes('procedural')))) { debugLog('[Main] Suppressed shader loading error (should be fixed by Vite pre-bundling):', error.message); event.preventDefault(); // Prevent error from appearing in console } } }); // DO NOT start router here - it will be started after registry initialization below if (DEBUG_CONTROLLERS) { debugLog('🔍 DEBUG MODE: Running minimal controller test'); // Hide the UI elements const mainDiv = document.querySelector('#mainDiv'); if (mainDiv) { (mainDiv as HTMLElement).style.display = 'none'; } new ControllerDebug(); }