From c0b9f772eeb571f9a33d8549e0624e1464f75d7d Mon Sep 17 00:00:00 2001 From: Michael Mainguy Date: Sat, 29 Nov 2025 04:26:48 -0600 Subject: [PATCH] Refactor main.ts: extract handlers and remove dead code MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Extract analytics init to src/analytics/initAnalytics.ts - Extract level selection handler to src/core/handlers/levelSelectedHandler.ts - Extract replay handler to src/core/handlers/viewReplaysHandler.ts - Extract app initialization to src/core/appInitializer.ts - Remove unused DemoScene and demo.ts - Remove dead code: DEBUG_CONTROLLERS, webGpu, TestLevel handler - Add BabylonJS shader pre-bundling to fix Vite dev server issues - Reduce main.ts from 885 lines to 211 lines 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- CLAUDE.md | 2 - src/analytics/initAnalytics.ts | 60 ++ src/core/appInitializer.ts | 121 ++++ src/core/defaultScene.ts | 1 - src/core/handlers/levelSelectedHandler.ts | 194 +++++ src/core/handlers/viewReplaysHandler.ts | 90 +++ src/game/demo.ts | 18 - src/main.ts | 830 ++-------------------- vite.config.ts | 28 +- 9 files changed, 567 insertions(+), 777 deletions(-) create mode 100644 src/analytics/initAnalytics.ts create mode 100644 src/core/appInitializer.ts create mode 100644 src/core/handlers/levelSelectedHandler.ts create mode 100644 src/core/handlers/viewReplaysHandler.ts delete mode 100644 src/game/demo.ts diff --git a/CLAUDE.md b/CLAUDE.md index 65226ad..947380e 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -29,7 +29,6 @@ npm run speech ### Scene Management Pattern The project uses a singleton pattern for scene access via `DefaultScene`: - `DefaultScene.MainScene` - Primary game scene -- `DefaultScene.DemoScene` - Demo/attract mode scene - `DefaultScene.XR` - WebXR experience instance All game objects reference these static properties rather than passing scene instances. @@ -124,7 +123,6 @@ src/ createSun.ts - Sun mesh generation createPlanets.ts - Procedural planet generation planetTextures.ts - Planet texture library - demo.ts - Attract mode implementation public/ systems/ - Particle system definitions diff --git a/src/analytics/initAnalytics.ts b/src/analytics/initAnalytics.ts new file mode 100644 index 0000000..95c8792 --- /dev/null +++ b/src/analytics/initAnalytics.ts @@ -0,0 +1,60 @@ +import { BrowserAgent } from '@newrelic/browser-agent/loaders/browser-agent'; +import { AnalyticsService } from './analyticsService'; +import { NewRelicAdapter } from './adapters/newRelicAdapter'; + +// New Relic configuration +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 + } +}; + +/** + * Initialize analytics with New Relic adapter + * @returns The configured AnalyticsService instance + */ +export function initializeAnalytics(): AnalyticsService { + const nrba = new BrowserAgent(options); + + const analytics = AnalyticsService.initialize({ + enabled: true, + includeSessionMetadata: true, + debug: true + }); + + const newRelicAdapter = new NewRelicAdapter(nrba, { + batchSize: 10, + flushInterval: 30000, + debug: true + }); + + 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 + }); + + return analytics; +} diff --git a/src/core/appInitializer.ts b/src/core/appInitializer.ts new file mode 100644 index 0000000..97bf1f8 --- /dev/null +++ b/src/core/appInitializer.ts @@ -0,0 +1,121 @@ +import { mount } from 'svelte'; +import App from '../components/layouts/App.svelte'; +import { LegacyMigration } from '../levels/migration/legacyMigration'; +import { LevelRegistry } from '../levels/storage/levelRegistry'; +import debugLog from './debug'; + +// Type for Main class - imported dynamically to avoid circular dependency +type MainClass = new (progressCallback?: (percent: number, message: string) => void) => any; + +/** + * Initialize the application + * - Check for legacy data migration + * - Initialize level registry + * - Mount Svelte app + * - Create Main instance + */ +export async function initializeApp(MainConstructor: MainClass): Promise { + 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 and create Main + mountAppAndCreateMain(MainConstructor); + 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 as Error)?.stack); + } + } + + // Mount Svelte app and create Main + mountAppAndCreateMain(MainConstructor); + + console.log('[Main] initializeApp() FINISHED at', new Date().toISOString()); +} + +/** + * Mount the Svelte app and create Main instance + */ +function mountAppAndCreateMain(MainConstructor: MainClass): void { + 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 (!(window as any).__mainInstance) { + debugLog('[Main] Creating Main instance (not initialized)'); + const main = new MainConstructor(); + (window as any).__mainInstance = main; + } + } else { + console.error('[Main] Failed to mount Svelte app - #app element not found'); + } +} + +/** + * Set up global error handler for shader loading errors + * Suppress non-critical BabylonJS shader loading errors during development + */ +export function setupErrorHandler(): void { + 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(); + } + } + }); +} diff --git a/src/core/defaultScene.ts b/src/core/defaultScene.ts index 75b563d..8136b4c 100644 --- a/src/core/defaultScene.ts +++ b/src/core/defaultScene.ts @@ -2,6 +2,5 @@ import {Scene, WebXRDefaultExperience} from "@babylonjs/core"; export class DefaultScene { public static MainScene: Scene; - public static DemoScene: Scene; public static XR: WebXRDefaultExperience; } \ No newline at end of file diff --git a/src/core/handlers/levelSelectedHandler.ts b/src/core/handlers/levelSelectedHandler.ts new file mode 100644 index 0000000..4d0c181 --- /dev/null +++ b/src/core/handlers/levelSelectedHandler.ts @@ -0,0 +1,194 @@ +import { AudioEngineV2, Engine, ParticleHelper } from "@babylonjs/core"; +import { DefaultScene } from "../defaultScene"; +import { Level1 } from "../../levels/level1"; +import Level from "../../levels/level"; +import { RockFactory } from "../../environment/asteroids/rockFactory"; +import { LevelConfig } from "../../levels/config/levelConfig"; +import { Preloader } from "../../ui/screens/preloader"; +import { DiscordWidget } from "../../ui/widgets/discordWidget"; +import debugLog from '../debug'; + +/** + * Interface for Main class methods needed by the level selected handler + */ +export interface LevelSelectedContext { + isStarted(): boolean; + setStarted(value: boolean): void; + isInitialized(): boolean; + areAssetsLoaded(): boolean; + setAssetsLoaded(value: boolean): void; + initializeEngine(): Promise; + initializeXR(): Promise; + getAudioEngine(): AudioEngineV2; + getEngine(): Engine; + setCurrentLevel(level: Level): void; + setProgressCallback(callback: (percent: number, message: string) => void): void; + play(): Promise; +} + +/** + * Creates the levelSelected event handler + * @param context - Main instance implementing LevelSelectedContext + * @returns Event handler function + */ +export function createLevelSelectedHandler(context: LevelSelectedContext): (e: CustomEvent) => Promise { + return async (e: CustomEvent) => { + context.setStarted(true); + const { levelName, config } = e.detail as { levelName: string, config: LevelConfig }; + + debugLog(`[Main] Starting level: ${levelName}`); + + // Hide all UI elements + 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(); + context.setProgressCallback((percent, message) => { + preloader.updateProgress(percent, message); + }); + + try { + // Initialize engine if this is first time + if (!context.isInitialized()) { + debugLog('[Main] First level selected - initializing engine'); + preloader.updateProgress(0, 'Initializing game engine...'); + await context.initializeEngine(); + } + + // Load assets if this is the first level being played + if (!context.areAssetsLoaded()) { + 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(); + context.setAssetsLoaded(true); + + debugLog('[Main] Assets loaded successfully'); + preloader.updateProgress(60, 'Assets loaded'); + } + + preloader.updateProgress(70, 'Preparing VR session...'); + + // Initialize WebXR for this level + await context.initializeXR(); + + // If XR is available, enter XR immediately (while we have user activation) + let xrSession = null; + const engine = context.getEngine(); + if (DefaultScene.XR) { + try { + preloader.updateProgress(75, 'Entering VR...'); + 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; + engine.runRenderLoop(() => { + DefaultScene.MainScene.render(); + }); + } + } + + // Unlock audio engine on user interaction + const audioEngine = context.getAudioEngine(); + if (audioEngine) { + await audioEngine.unlockAsync(); + } + + // Now load audio assets (after unlock) + preloader.updateProgress(80, 'Loading audio...'); + await RockFactory.initAudio(audioEngine); + + // Attach audio listener to camera for spatial audio + const camera = DefaultScene.XR?.baseExperience?.camera || DefaultScene.MainScene.activeCamera; + if (camera && audioEngine.listener) { + 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 + const currentLevel = new Level1(config, audioEngine, false, levelName); + context.setCurrentLevel(currentLevel); + + // Wait for level to be ready + currentLevel.getReadyObservable().add(async () => { + preloader.updateProgress(95, 'Starting game...'); + + // Get ship and set up replay observable + const level1 = currentLevel as Level1; + const ship = (level1 as any)._ship; + + // Listen for replay requests from the ship + if (ship) { + ship.onReplayRequestObservable.add(() => { + debugLog('Replay requested - reloading page'); + window.location.reload(); + }); + } + + // If we entered XR before level creation, manually setup camera parenting + 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) { + debugLog('[Main] XR already active - using consolidated setupXRCamera()'); + level1.setupXRCamera(); + await level1.showMissionBrief(); + debugLog('[Main] XR setup and mission brief complete'); + } else { + console.log('[Main] XR not active yet - will use onInitialXRPoseSetObservable instead'); + 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] Timestamp:', Date.now()); + + // Start the game + console.log('[Main] About to call context.play()'); + await context.play(); + console.log('[Main] context.play() completed'); + }); + + // Now initialize the level (after observable is registered) + await currentLevel.initialize(); + + } catch (error) { + console.error('[Main] Level initialization failed:', error); + preloader.updateProgress(0, 'Failed to load level. Please refresh and try again.'); + } + }; +} diff --git a/src/core/handlers/viewReplaysHandler.ts b/src/core/handlers/viewReplaysHandler.ts new file mode 100644 index 0000000..88bee28 --- /dev/null +++ b/src/core/handlers/viewReplaysHandler.ts @@ -0,0 +1,90 @@ +import { Engine } from "@babylonjs/core"; +import { ReplaySelectionScreen } from "../../replay/ReplaySelectionScreen"; +import { ReplayManager } from "../../replay/ReplayManager"; +import debugLog from '../debug'; + +/** + * Interface for Main class methods needed by the view replays handler + */ +export interface ViewReplaysContext { + isStarted(): boolean; + setStarted(value: boolean): void; + initializeXR(): Promise; + getEngine(): Engine; + getReplayManager(): ReplayManager | null; + setReplayManager(manager: ReplayManager): void; +} + +/** + * Creates the view replays button click handler + * @param context - Main instance implementing ViewReplaysContext + * @returns Click handler function + */ +export function createViewReplaysHandler(context: ViewReplaysContext): () => Promise { + return async () => { + debugLog('[Main] ========== VIEW REPLAYS BUTTON CLICKED =========='); + + // Initialize engine and physics if not already done + if (!context.isStarted()) { + context.setStarted(true); + await context.initializeXR(); + } + + // 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 + let replayManager = context.getReplayManager(); + if (!replayManager) { + replayManager = new ReplayManager( + context.getEngine() 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'; + } + } + ); + context.setReplayManager(replayManager); + } + + // Start replay + await 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(); + }; +} diff --git a/src/game/demo.ts b/src/game/demo.ts deleted file mode 100644 index a854855..0000000 --- a/src/game/demo.ts +++ /dev/null @@ -1,18 +0,0 @@ -import {DefaultScene} from "../core/defaultScene"; -import {ArcRotateCamera, Vector3} from "@babylonjs/core"; -import {Main} from "../main"; - -export default class Demo { - private _main: Main; - constructor(main: Main) { - this._main = main; - this.initialize(); - } - private async initialize() { - if (!DefaultScene.DemoScene) { - return; - } - const scene = DefaultScene.DemoScene; - const _camera = new ArcRotateCamera("camera", -Math.PI / 2, Math.PI / 2, 5, new Vector3(0, 0, 0), scene); - } -} \ No newline at end of file diff --git a/src/main.ts b/src/main.ts index 944d6df..ec9c8aa 100644 --- a/src/main.ts +++ b/src/main.ts @@ -4,881 +4,207 @@ import { CreateAudioEngineAsync, Engine, 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 { DefaultScene } from "./core/defaultScene"; 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 { RockFactory } from "./environment/asteroids/rockFactory"; +import { DiscordWidget } from "./ui/widgets/discordWidget"; import debugLog from './core/debug'; -import {ReplaySelectionScreen} from "./replay/ReplaySelectionScreen"; -import {ReplayManager} from "./replay/ReplayManager"; -import {Preloader} from "./ui/screens/preloader"; -import {DiscordWidget} from "./ui/widgets/discordWidget"; - -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 { ReplayManager } from "./replay/ReplayManager"; 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) +import { initializeAnalytics } from './analytics/initAnalytics'; +import { createLevelSelectedHandler, LevelSelectedContext } from './core/handlers/levelSelectedHandler'; +import { createViewReplaysHandler, ViewReplaysContext } from './core/handlers/viewReplaysHandler'; +import { initializeApp, setupErrorHandler } from './core/appInitializer'; -// Initialize analytics service with New Relic adapter -const analytics = AnalyticsService.initialize({ - enabled: true, - includeSessionMetadata: true, - debug: true // Set to true for development debugging -}); +// Initialize analytics +initializeAnalytics(); -// 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 -}); +// Setup error handler +setupErrorHandler(); -analytics.addAdapter(newRelicAdapter); +const canvas = document.querySelector('#gameCanvas') as HTMLCanvasElement; -// 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; + +export class Main implements LevelSelectedContext, ViewReplaysContext { + private _currentLevel: Level | null = null; private _gameState: GameState = GameState.DEMO; - private _engine: Engine | WebGPUEngine; + private _engine: Engine; private _audioEngine: AudioEngineV2; private _replayManager: ReplayManager | null = null; private _initialized: boolean = false; private _assetsLoaded: boolean = false; + private _started: 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}`); + // Register event handlers + window.addEventListener('levelSelected', createLevelSelectedHandler(this) as EventListener); - // 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...'); - - // FIX: Don't stop render loop - it may prevent XR observables from firing properly - // The brief camera orientation flash is acceptable for now - // 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 - debugLog('[Main] XR already active - using consolidated setupXRCamera()'); - - // Use consolidated XR camera setup from Level1 - level1.setupXRCamera(); - - // Show mission brief (since onInitialXRPoseSetObservable won't fire when already in XR) - await level1.showMissionBrief(); - - debugLog('[Main] XR setup and mission brief complete'); - } 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 (levelSelect) levelSelect.classList.add('ready'); - 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'); + viewReplaysBtn.addEventListener('click', createViewReplaysHandler(this)); } }); } - private _started = false; - /** - * Public method to initialize the game engine - * Call this to preload all assets before showing the level selector - */ + // LevelSelectedContext interface implementation + isStarted(): boolean { return this._started; } + setStarted(value: boolean): void { this._started = value; } + isInitialized(): boolean { return this._initialized; } + areAssetsLoaded(): boolean { return this._assetsLoaded; } + setAssetsLoaded(value: boolean): void { this._assetsLoaded = value; } + getAudioEngine(): AudioEngineV2 { return this._audioEngine; } + getEngine(): Engine { return this._engine; } + setCurrentLevel(level: Level): void { this._currentLevel = level; } + setProgressCallback(callback: (percent: number, message: string) => void): void { + this._progressCallback = callback; + } + + // ViewReplaysContext interface implementation + getReplayManager(): ReplayManager | null { return this._replayManager; } + setReplayManager(manager: ReplayManager): void { this._replayManager = manager; } + public async initializeEngine(): Promise { - if (this._initialized) { - debugLog('[Main] Engine already initialized, skipping'); - return; - } - + if (this._initialized) 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(); - + await this.initializeXR(); 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); - } + 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); - } + if (DefaultScene.XR?.baseExperience.state === 2) { + try { await DefaultScene.XR.baseExperience.exitXRAsync(); } + catch (error) { debugLog('[Main] Error exiting XR:', 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); - } - }); + DefaultScene.MainScene.meshes.slice().forEach(m => { if (!m.isDisposed()) m.dispose(); }); + DefaultScene.MainScene.materials.slice().forEach(m => m.dispose()); } - - // 5. Disable physics engine (properly disposes AND clears scene reference) - if (DefaultScene.MainScene && DefaultScene.MainScene.isPhysicsEnabled()) { - debugLog('[Main] Disabling physics engine...'); + if (DefaultScene.MainScene?.isPhysicsEnabled()) { 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); - } - } + 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'); - + if (discord) discord.show(); } 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'); + console.error('[Main] Cleanup failed:', error); window.location.reload(); } } - public async play() { - debugLog('[Main] play() called'); - debugLog('[Main] Current level exists:', !!this._currentLevel); + public async play(): Promise { this._gameState = GameState.PLAY; + if (this._currentLevel) await this._currentLevel.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%) + public async initializeXR(): Promise { 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"); - // FIX: Pointer selection feature must be registered AFTER XR session starts - // The feature is not available during initialize() - it only becomes enabled - // when the XR session is active. Moving registration to onStateChangedObservable. - if (DefaultScene.XR) { - // Handle XR state changes - register pointer feature when entering VR - DefaultScene.XR.baseExperience.onStateChangedObservable.add((state) => { - if (state === 2) { // WebXRState.IN_XR - debugLog('[Main] Entering VR - registering pointer selection feature'); - - // Register pointer selection feature NOW that XR session is active - 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"); - } else { - debugLog('[Main] WARNING: Pointer selection feature not available'); - } - - // Hide Discord widget when entering VR - const discord = (window as any).__discordWidget as DiscordWidget; - if (discord) { - debugLog('[Main] Hiding Discord widget'); - discord.hide(); - } - } else if (state === 0) { // WebXRState.NOT_IN_XR - debugLog('[Main] Exiting VR - showing Discord widget'); - const discord = (window as any).__discordWidget as DiscordWidget; - if (discord) { - discord.show(); - } - } - }); - } + DefaultScene.XR.baseExperience.onStateChangedObservable.add((state) => { + if (state === 2) { + const pointerFeature = DefaultScene.XR!.baseExperience.featuresManager.getEnabledFeature("xr-controller-pointer-selection"); + if (pointerFeature) InputControlManager.getInstance().registerPointerFeature(pointerFeature); + ((window as any).__discordWidget as DiscordWidget)?.hide(); + } else if (state === 0) { + ((window as any).__discordWidget as DiscordWidget)?.show(); + } + }); this.reportProgress(40, 'VR support enabled'); } catch (error) { - debugLog("WebXR initialization failed, falling back to flat mode:", error); + debugLog("WebXR initialization failed:", error); DefaultScene.XR = null; - this.reportProgress(40, 'Desktop mode (VR not available)'); + this.reportProgress(40, 'Desktop mode'); } } 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 + private async setupScene(): Promise { 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 = new Engine(canvas, true); this._engine.setHardwareScalingLevel(1 / window.devicePixelRatio); - window.onresize = () => { - this._engine.resize(); - } + window.onresize = () => this._engine.resize(); - this.reportProgress(10, 'Creating scenes...'); - DefaultScene.DemoScene = new Scene(this._engine); + this.reportProgress(10, 'Creating scene...'); DefaultScene.MainScene = new Scene(this._engine); - - DefaultScene.MainScene.ambientColor = new Color3(.2,.2,.2); + 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 + 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(); - }); + this._engine.runRenderLoop(() => DefaultScene.MainScene.render()); } - private async setupPhysics() { - //DefaultScene.MainScene.useRightHandedSystem = true; + private async setupPhysics(): Promise { 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.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(); -} - - - +initializeApp(Main); diff --git a/vite.config.ts b/vite.config.ts index ae398b5..bbc4e13 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -11,8 +11,6 @@ export default defineConfig({ rollupOptions: { output: { manualChunks: { - 'babylon': ['@babylonjs/core'], - 'babylon-procedural': ['@babylonjs/procedural-textures'], 'babylon-inspector': ['@babylonjs/inspector'], } } @@ -25,13 +23,35 @@ export default defineConfig({ } }, // Include BabylonJS modules - force pre-bundle to prevent dynamic import issues + // Shaders must be explicitly included to avoid dynamic import failures through CloudFlare proxy include: [ '@babylonjs/core', + // Core shaders + '@babylonjs/core/Shaders/default.vertex', + '@babylonjs/core/Shaders/default.fragment', + '@babylonjs/core/Shaders/rgbdDecode.fragment', + '@babylonjs/core/Shaders/procedural.vertex', + // PBR shaders + '@babylonjs/core/Shaders/pbr.vertex', + '@babylonjs/core/Shaders/pbr.fragment', + '@babylonjs/core/Shaders/pbrDebug.fragment', + // Particle shaders + '@babylonjs/core/Shaders/particles.vertex', + '@babylonjs/core/Shaders/particles.fragment', + '@babylonjs/core/Shaders/gpuRenderParticles.vertex', + '@babylonjs/core/Shaders/gpuRenderParticles.fragment', + // Other common shaders + '@babylonjs/core/Shaders/standard.fragment', + '@babylonjs/core/Shaders/postprocess.vertex', + '@babylonjs/core/Shaders/pass.fragment', + '@babylonjs/core/Shaders/shadowMap.vertex', + '@babylonjs/core/Shaders/shadowMap.fragment', + '@babylonjs/core/Shaders/depth.vertex', + '@babylonjs/core/Shaders/depth.fragment', '@babylonjs/loaders', '@babylonjs/havok', '@babylonjs/materials', - '@babylonjs/procedural-textures', - '@babylonjs/procedural-textures/fireProceduralTexture' + '@babylonjs/procedural-textures' ], // Prevent cache invalidation issues with CloudFlare proxy force: false,