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