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:
parent
5e67b796ba
commit
c0b9f772ee
@ -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
|
||||
|
||||
60
src/analytics/initAnalytics.ts
Normal file
60
src/analytics/initAnalytics.ts
Normal 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
121
src/core/appInitializer.ts
Normal 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();
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
@ -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;
|
||||
}
|
||||
194
src/core/handlers/levelSelectedHandler.ts
Normal file
194
src/core/handlers/levelSelectedHandler.ts
Normal 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.');
|
||||
}
|
||||
};
|
||||
}
|
||||
90
src/core/handlers/viewReplaysHandler.ts
Normal file
90
src/core/handlers/viewReplaysHandler.ts
Normal 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();
|
||||
};
|
||||
}
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
830
src/main.ts
830
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<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);
|
||||
}
|
||||
}
|
||||
});
|
||||
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<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();
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
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<void> {
|
||||
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<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);
|
||||
|
||||
@ -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,
|
||||
|
||||
Loading…
Reference in New Issue
Block a user