Refactor main.ts to meet coding standards (<100 lines)
All checks were successful
Build / build (push) Successful in 1m7s

- 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 <noreply@anthropic.com>
This commit is contained in:
Michael Mainguy 2025-11-29 05:01:28 -06:00
parent 71ec1f162c
commit e8ac3a8f0a
4 changed files with 227 additions and 124 deletions

76
src/core/cleanup.ts Normal file
View File

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

74
src/core/sceneSetup.ts Normal file
View File

@ -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<SceneSetupResult> {
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<void> {
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<AudioEngineV2> {
return await CreateAudioEngineAsync({
volume: 1.0,
listenerAutoUpdate: true,
listenerEnabled: true,
resumeOnInteraction: true
});
}

53
src/core/xrSetup.ts Normal file
View File

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

View File

@ -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<void> {
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<void> {
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<void> {
this._gameState = GameState.PLAY;
if (this._currentLevel) await this._currentLevel.play();
}
public async initializeXR(): Promise<void> {
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<void> {
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<void> {
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);
}
}