From e8ac3a8f0ae7a85fe6bd3b83bf08265e9ccca4b8 Mon Sep 17 00:00:00 2001 From: Michael Mainguy Date: Sat, 29 Nov 2025 05:01:28 -0600 Subject: [PATCH] Refactor main.ts to meet coding standards (<100 lines) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Extract cleanup logic to src/core/cleanup.ts - Extract XR setup to src/core/xrSetup.ts - Extract scene/physics/audio setup to src/core/sceneSetup.ts - Remove unused GameState enum and _gameState field - main.ts reduced from 192 to 91 lines - All methods now under 20 lines 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- src/core/cleanup.ts | 76 +++++++++++++++++++++ src/core/sceneSetup.ts | 74 +++++++++++++++++++++ src/core/xrSetup.ts | 53 +++++++++++++++ src/main.ts | 148 +++++++---------------------------------- 4 files changed, 227 insertions(+), 124 deletions(-) create mode 100644 src/core/cleanup.ts create mode 100644 src/core/sceneSetup.ts create mode 100644 src/core/xrSetup.ts diff --git a/src/core/cleanup.ts b/src/core/cleanup.ts new file mode 100644 index 0000000..823940d --- /dev/null +++ b/src/core/cleanup.ts @@ -0,0 +1,76 @@ +import { Engine } from "@babylonjs/core"; +import { DefaultScene } from "./defaultScene"; +import { RockFactory } from "../environment/asteroids/rockFactory"; +import debugLog from './debug'; +import Level from "../levels/level"; + +export interface CleanupContext { + getEngine(): Engine; + getCurrentLevel(): Level | null; + setCurrentLevel(level: Level | null): void; + resetState(): void; +} + +/** + * Gracefully shutdown the game, disposing all resources + */ +export async function cleanupAndExit( + context: CleanupContext, + canvas: HTMLCanvasElement +): Promise { + debugLog('[Main] cleanupAndExit() called - starting graceful shutdown'); + try { + context.getEngine().stopRenderLoop(); + disposeCurrentLevel(context); + RockFactory.reset(); + await exitXRSession(); + disposeSceneResources(); + disablePhysics(); + context.resetState(); + clearCanvas(canvas); + } catch (error) { + console.error('[Main] Cleanup failed:', error); + window.location.reload(); + } +} + +function disposeCurrentLevel(context: CleanupContext): void { + const level = context.getCurrentLevel(); + if (level) { + level.dispose(); + context.setCurrentLevel(null); + } +} + +async function exitXRSession(): Promise { + if (DefaultScene.XR?.baseExperience.state === 2) { + try { + await DefaultScene.XR.baseExperience.exitXRAsync(); + } catch (error) { + debugLog('[Main] Error exiting XR:', error); + } + } + DefaultScene.XR = null; +} + +function disposeSceneResources(): void { + if (!DefaultScene.MainScene) return; + DefaultScene.MainScene.meshes.slice().forEach(m => { + if (!m.isDisposed()) m.dispose(); + }); + DefaultScene.MainScene.materials.slice().forEach(m => m.dispose()); +} + +function disablePhysics(): void { + if (DefaultScene.MainScene?.isPhysicsEnabled()) { + DefaultScene.MainScene.disablePhysicsEngine(); + } +} + +function clearCanvas(canvas: HTMLCanvasElement): void { + 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); + } +} diff --git a/src/core/sceneSetup.ts b/src/core/sceneSetup.ts new file mode 100644 index 0000000..3ea3509 --- /dev/null +++ b/src/core/sceneSetup.ts @@ -0,0 +1,74 @@ +import { + AudioEngineV2, + Color3, + CreateAudioEngineAsync, + Engine, + HavokPlugin, + Scene, + Vector3 +} from "@babylonjs/core"; +import HavokPhysics from "@babylonjs/havok"; +import { DefaultScene } from "./defaultScene"; +import { ProgressReporter } from "./xrSetup"; + +export interface SceneSetupResult { + engine: Engine; + audioEngine: AudioEngineV2; +} + +/** + * Setup the BabylonJS engine, scene, physics, and audio + */ +export async function setupScene( + canvas: HTMLCanvasElement, + reporter: ProgressReporter +): Promise { + reporter.reportProgress(5, 'Creating rendering engine...'); + const engine = createEngine(canvas); + + reporter.reportProgress(10, 'Creating scene...'); + createMainScene(engine); + + reporter.reportProgress(15, 'Loading physics engine...'); + await setupPhysics(); + reporter.reportProgress(20, 'Physics engine ready'); + + reporter.reportProgress(22, 'Initializing spatial audio...'); + const audioEngine = await createAudioEngine(); + reporter.reportProgress(30, 'Audio engine ready'); + + engine.runRenderLoop(() => DefaultScene.MainScene.render()); + + return { engine, audioEngine }; +} + +function createEngine(canvas: HTMLCanvasElement): Engine { + const engine = new Engine(canvas, true); + engine.setHardwareScalingLevel(1 / window.devicePixelRatio); + window.onresize = () => engine.resize(); + return engine; +} + +function createMainScene(engine: Engine): void { + DefaultScene.MainScene = new Scene(engine); + DefaultScene.MainScene.ambientColor = new Color3(.2, .2, .2); + DefaultScene.MainScene.clearColor = new Color3(0, 0, 0).toColor4(); +} + +async function setupPhysics(): Promise { + const havok = await HavokPhysics(); + const havokPlugin = new HavokPlugin(true, havok); + DefaultScene.MainScene.enablePhysics(new Vector3(0, 0, 0), havokPlugin); + DefaultScene.MainScene.getPhysicsEngine()!.setTimeStep(1/60); + DefaultScene.MainScene.getPhysicsEngine()!.setSubTimeStep(5); + DefaultScene.MainScene.collisionsEnabled = true; +} + +async function createAudioEngine(): Promise { + return await CreateAudioEngineAsync({ + volume: 1.0, + listenerAutoUpdate: true, + listenerEnabled: true, + resumeOnInteraction: true + }); +} diff --git a/src/core/xrSetup.ts b/src/core/xrSetup.ts new file mode 100644 index 0000000..2cacef9 --- /dev/null +++ b/src/core/xrSetup.ts @@ -0,0 +1,53 @@ +import { WebXRDefaultExperience, WebXRFeaturesManager } from "@babylonjs/core"; +import { DefaultScene } from "./defaultScene"; +import { InputControlManager } from "../ship/input/inputControlManager"; +import debugLog from './debug'; + +export interface ProgressReporter { + reportProgress(percent: number, message: string): void; +} + +/** + * Initialize WebXR experience if available + */ +export async function initializeXR(reporter: ProgressReporter): Promise { + reporter.reportProgress(35, 'Checking VR support...'); + + if (!navigator.xr) { + DefaultScene.XR = null; + reporter.reportProgress(40, 'Desktop mode'); + return; + } + + try { + await createXRExperience(); + registerXRStateHandler(); + reporter.reportProgress(40, 'VR support enabled'); + } catch (error) { + debugLog("WebXR initialization failed:", error); + DefaultScene.XR = null; + reporter.reportProgress(40, 'Desktop mode'); + } +} + +async function createXRExperience(): Promise { + DefaultScene.XR = await WebXRDefaultExperience.CreateAsync(DefaultScene.MainScene, { + disableTeleportation: true, + disableNearInteraction: true, + disableHandTracking: true, + disableDefaultUI: true + }); + debugLog(WebXRFeaturesManager.GetAvailableFeatures()); +} + +function registerXRStateHandler(): void { + 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); + } + } + }); +} diff --git a/src/main.ts b/src/main.ts index 5bd2f8d..41b79e4 100644 --- a/src/main.ts +++ b/src/main.ts @@ -1,43 +1,25 @@ -import { - AudioEngineV2, - Color3, - CreateAudioEngineAsync, - Engine, - HavokPlugin, - Scene, - Vector3, - WebXRDefaultExperience, - WebXRFeaturesManager -} from "@babylonjs/core"; +import { AudioEngineV2, Engine } from "@babylonjs/core"; import '@babylonjs/loaders'; -import HavokPhysics from "@babylonjs/havok"; import { DefaultScene } from "./core/defaultScene"; import Level from "./levels/level"; -import { RockFactory } from "./environment/asteroids/rockFactory"; import debugLog from './core/debug'; -import { InputControlManager } from './ship/input/inputControlManager'; import { initializeAnalytics } from './analytics/initAnalytics'; import { createLevelSelectedHandler, LevelSelectedContext } from './core/handlers/levelSelectedHandler'; import { initializeApp, setupErrorHandler } from './core/appInitializer'; +import { cleanupAndExit, CleanupContext } from './core/cleanup'; +import { initializeXR } from './core/xrSetup'; +import { setupScene } from './core/sceneSetup'; -// Initialize analytics +// Initialize analytics and error handler initializeAnalytics(); - -// Setup error handler setupErrorHandler(); const canvas = document.querySelector('#gameCanvas') as HTMLCanvasElement; -enum GameState { - PLAY, - DEMO -} - -export class Main implements LevelSelectedContext { +export class Main implements LevelSelectedContext, CleanupContext { private _currentLevel: Level | null = null; - private _gameState: GameState = GameState.DEMO; private _engine: Engine; private _audioEngine: AudioEngineV2; private _initialized: boolean = false; @@ -47,17 +29,14 @@ export class Main implements LevelSelectedContext { constructor(progressCallback?: (percent: number, message: string) => void) { this._progressCallback = progressCallback || null; - - // Register event handlers window.addEventListener('levelSelected', createLevelSelectedHandler(this) as EventListener); - window.addEventListener('DOMContentLoaded', () => { const levelSelect = document.querySelector('#levelSelect'); if (levelSelect) levelSelect.classList.add('ready'); }); } - // LevelSelectedContext interface implementation + // LevelSelectedContext interface isStarted(): boolean { return this._started; } setStarted(value: boolean): void { this._started = value; } isInitialized(): boolean { return this._initialized; } @@ -66,124 +45,45 @@ export class Main implements LevelSelectedContext { 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; + setProgressCallback(cb: (percent: number, message: string) => void): void { + this._progressCallback = cb; + } + + // CleanupContext interface + getCurrentLevel(): Level | null { return this._currentLevel; } + resetState(): void { + this._initialized = false; + this._assetsLoaded = false; + this._started = false; } public async initializeEngine(): Promise { if (this._initialized) return; debugLog('[Main] Starting engine initialization'); this.reportProgress(0, 'Initializing 3D engine...'); - await this.setupScene(); + const result = await setupScene(canvas, this); + this._engine = result.engine; + this._audioEngine = result.audioEngine; this.reportProgress(30, '3D engine ready'); - await this.initializeXR(); + await initializeXR(this); this._initialized = true; this.reportProgress(100, 'All systems ready!'); } - private reportProgress(percent: number, message: string): void { + public reportProgress(percent: number, message: string): void { if (this._progressCallback) this._progressCallback(percent, message); } public async cleanupAndExit(): Promise { - debugLog('[Main] cleanupAndExit() called - starting graceful shutdown'); - try { - this._engine.stopRenderLoop(); - if (this._currentLevel) { - this._currentLevel.dispose(); - this._currentLevel = null; - } - RockFactory.reset(); - if (DefaultScene.XR?.baseExperience.state === 2) { - try { await DefaultScene.XR.baseExperience.exitXRAsync(); } - catch (error) { debugLog('[Main] Error exiting XR:', error); } - } - if (DefaultScene.MainScene) { - DefaultScene.MainScene.meshes.slice().forEach(m => { if (!m.isDisposed()) m.dispose(); }); - DefaultScene.MainScene.materials.slice().forEach(m => m.dispose()); - } - if (DefaultScene.MainScene?.isPhysicsEnabled()) { - DefaultScene.MainScene.disablePhysicsEngine(); - } - DefaultScene.XR = null; - this._initialized = false; - this._assetsLoaded = false; - this._started = false; - - 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); } - } catch (error) { - console.error('[Main] Cleanup failed:', error); - window.location.reload(); - } + await cleanupAndExit(this, canvas); } public async play(): Promise { - this._gameState = GameState.PLAY; if (this._currentLevel) await this._currentLevel.play(); } public async initializeXR(): Promise { - this.reportProgress(35, 'Checking VR support...'); - if (navigator.xr) { - try { - DefaultScene.XR = await WebXRDefaultExperience.CreateAsync(DefaultScene.MainScene, { - disableTeleportation: true, - disableNearInteraction: true, - disableHandTracking: true, - disableDefaultUI: true - }); - debugLog(WebXRFeaturesManager.GetAvailableFeatures()); - - 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); - } - }); - this.reportProgress(40, 'VR support enabled'); - } catch (error) { - debugLog("WebXR initialization failed:", error); - DefaultScene.XR = null; - this.reportProgress(40, 'Desktop mode'); - } - } else { - DefaultScene.XR = null; - this.reportProgress(40, 'Desktop mode'); - } - } - - private async setupScene(): Promise { - this.reportProgress(5, 'Creating rendering engine...'); - this._engine = new Engine(canvas, true); - this._engine.setHardwareScalingLevel(1 / window.devicePixelRatio); - window.onresize = () => this._engine.resize(); - - this.reportProgress(10, 'Creating scene...'); - DefaultScene.MainScene = new Scene(this._engine); - DefaultScene.MainScene.ambientColor = new Color3(.2, .2, .2); - DefaultScene.MainScene.clearColor = new Color3(0, 0, 0).toColor4(); - - this.reportProgress(15, 'Loading physics engine...'); - await this.setupPhysics(); - this.reportProgress(20, 'Physics engine ready'); - - this.reportProgress(22, 'Initializing spatial audio...'); - this._audioEngine = await CreateAudioEngineAsync({ - volume: 1.0, listenerAutoUpdate: true, listenerEnabled: true, resumeOnInteraction: true - }); - this.reportProgress(30, 'Audio engine ready'); - - this._engine.runRenderLoop(() => DefaultScene.MainScene.render()); - } - - private async setupPhysics(): Promise { - const havok = await HavokPhysics(); - const havokPlugin = new HavokPlugin(true, havok); - DefaultScene.MainScene.enablePhysics(new Vector3(0, 0, 0), havokPlugin); - DefaultScene.MainScene.getPhysicsEngine()!.setTimeStep(1/60); - DefaultScene.MainScene.getPhysicsEngine()!.setSubTimeStep(5); - DefaultScene.MainScene.collisionsEnabled = true; + await initializeXR(this); } }