Refactor main.ts to meet coding standards (<100 lines)
All checks were successful
Build / build (push) Successful in 1m7s
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:
parent
71ec1f162c
commit
e8ac3a8f0a
76
src/core/cleanup.ts
Normal file
76
src/core/cleanup.ts
Normal 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
74
src/core/sceneSetup.ts
Normal 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
53
src/core/xrSetup.ts
Normal 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);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
148
src/main.ts
148
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<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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Loading…
Reference in New Issue
Block a user