Refactor main.ts: extract handlers and remove dead code

- 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 <noreply@anthropic.com>
This commit is contained in:
Michael Mainguy 2025-11-29 04:26:48 -06:00
parent 5e67b796ba
commit c0b9f772ee
9 changed files with 567 additions and 777 deletions

View File

@ -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

View File

@ -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;
}

121
src/core/appInitializer.ts Normal file
View File

@ -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<void> {
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<void>((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();
}
}
});
}

View File

@ -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;
}

View File

@ -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<void>;
initializeXR(): Promise<void>;
getAudioEngine(): AudioEngineV2;
getEngine(): Engine;
setCurrentLevel(level: Level): void;
setProgressCallback(callback: (percent: number, message: string) => void): void;
play(): Promise<void>;
}
/**
* Creates the levelSelected event handler
* @param context - Main instance implementing LevelSelectedContext
* @returns Event handler function
*/
export function createLevelSelectedHandler(context: LevelSelectedContext): (e: CustomEvent) => Promise<void> {
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.');
}
};
}

View File

@ -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<void>;
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<void> {
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();
};
}

View File

@ -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);
}
}

View File

@ -4,10 +4,8 @@ import {
CreateAudioEngineAsync,
Engine,
HavokPlugin,
ParticleHelper,
Scene,
Vector3,
WebGPUEngine,
WebXRDefaultExperience,
WebXRFeaturesManager
} from "@babylonjs/core";
@ -15,870 +13,198 @@ 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 {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 debugLog from './core/debug';
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<void> {
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<void> {
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);
DefaultScene.MainScene.meshes.slice().forEach(m => { if (!m.isDisposed()) m.dispose(); });
DefaultScene.MainScene.materials.slice().forEach(m => m.dispose());
}
}
});
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...');
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<void> {
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<void> {
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();
}
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);
DefaultScene.XR = null;
this.reportProgress(40, 'Desktop mode (VR not available)');
}
} else {
debugLog("WebXR not available, using flat camera mode");
debugLog("WebXR initialization failed:", error);
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");
DefaultScene.XR = null;
this.reportProgress(40, 'Desktop mode');
}
}
private async setupScene(): Promise<void> {
this.reportProgress(5, 'Creating rendering engine...');
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.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<void> {
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<void>((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);

View File

@ -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,