import {DefaultScene} from "./defaultScene"; import type {AudioEngineV2} from "@babylonjs/core"; import { AbstractMesh, Color3, DistanceConstraint, MeshBuilder, Observable, PhysicsAggregate, PhysicsMotionType, PhysicsShapeType, StandardMaterial, Vector3 } from "@babylonjs/core"; import {Ship} from "./ship"; import Level from "./level"; import {Scoreboard} from "./scoreboard"; import setLoadingMessage from "./setLoadingMessage"; import {LevelConfig} from "./levelConfig"; import {LevelDeserializer} from "./levelDeserializer"; import {BackgroundStars} from "./backgroundStars"; import debugLog from './debug'; export class Level1 implements Level { private _ship: Ship; private _onReadyObservable: Observable = new Observable(); private _initialized: boolean = false; private _startBase: AbstractMesh; private _endBase: AbstractMesh; private _scoreboard: Scoreboard; private _levelConfig: LevelConfig; private _audioEngine: AudioEngineV2; private _deserializer: LevelDeserializer; private _backgroundStars: BackgroundStars; constructor(levelConfig: LevelConfig, audioEngine: AudioEngineV2) { this._levelConfig = levelConfig; this._audioEngine = audioEngine; this._deserializer = new LevelDeserializer(levelConfig); this._ship = new Ship(audioEngine); this._scoreboard = new Scoreboard(); const xr = DefaultScene.XR; debugLog('Level1 constructor - Setting up XR observables'); debugLog('XR input exists:', !!xr.input); debugLog('onControllerAddedObservable exists:', !!xr.input?.onControllerAddedObservable); xr.baseExperience.onInitialXRPoseSetObservable.add(() => { xr.baseExperience.camera.parent = this._ship.transformNode; xr.baseExperience.camera.position = new Vector3(0, 0, 0); const observer = xr.input.onControllerAddedObservable.add((controller) => { debugLog('🎮 onControllerAddedObservable FIRED for:', controller.inputSource.handedness); this._ship.addController(controller); }); }); this.initialize(); } getReadyObservable(): Observable { return this._onReadyObservable; } private scored: Set = new Set(); public async play() { // Create background music using AudioEngineV2 const background = await this._audioEngine.createSoundAsync("background", "/song1.mp3", { loop: true, volume: 0.5 }); background.play(); // Enter XR mode const xr = await DefaultScene.XR.baseExperience.enterXRAsync('immersive-vr', 'local-floor'); // Check for controllers that are already connected after entering XR debugLog('Checking for controllers after entering XR. Count:', DefaultScene.XR.input.controllers.length); DefaultScene.XR.input.controllers.forEach((controller, index) => { debugLog(`Controller ${index} - handedness: ${controller.inputSource.handedness}`); this._ship.addController(controller); }); // Wait and check again after a delay (controllers might connect later) debugLog('Waiting 2 seconds to check for controllers again...'); setTimeout(() => { debugLog('After 2 second delay - controller count:', DefaultScene.XR.input.controllers.length); DefaultScene.XR.input.controllers.forEach((controller, index) => { debugLog(` Late controller ${index} - handedness: ${controller.inputSource.handedness}`); }); }, 2000); } public dispose() { this._startBase.dispose(); this._endBase.dispose(); if (this._backgroundStars) { this._backgroundStars.dispose(); } } public async initialize() { debugLog('Initializing level from config:', this._levelConfig.difficulty); if (this._initialized) { return; } setLoadingMessage("Loading level from configuration..."); // Use deserializer to create all entities from config const entities = await this._deserializer.deserialize(this._scoreboard.onScoreObservable); this._startBase = entities.startBase; // sun and planets are already created by deserializer // Initialize scoreboard with total asteroid count this._scoreboard.setRemainingCount(entities.asteroids.length); debugLog(`Initialized scoreboard with ${entities.asteroids.length} asteroids`); // Position ship from config const shipConfig = this._deserializer.getShipConfig(); this._ship.position = new Vector3(shipConfig.position[0], shipConfig.position[1], shipConfig.position[2]); // Add distance constraints to asteroids (if physics enabled) setLoadingMessage("Configuring physics constraints..."); const asteroidMeshes = entities.asteroids; if (this._startBase.physicsBody) { for (let i = 0; i < asteroidMeshes.length; i++) { const asteroidMesh = asteroidMeshes[i]; if (asteroidMesh.physicsBody) { // Calculate distance from start base const dist = Vector3.Distance(asteroidMesh.position, this._startBase.position); const constraint = new DistanceConstraint(dist, DefaultScene.MainScene); // constraint.isCollisionsEnabled = true; this._startBase.physicsBody.addConstraint(asteroidMesh.physicsBody, constraint); } } } // Create background starfield setLoadingMessage("Creating starfield..."); this._backgroundStars = new BackgroundStars(DefaultScene.MainScene, { count: 5000, radius: 5000, minBrightness: 0.3, maxBrightness: 1.0, pointSize: 2 }); // Set up camera follow for stars (keeps stars at infinite distance) DefaultScene.MainScene.onBeforeRenderObservable.add(() => { if (this._backgroundStars && DefaultScene.XR.baseExperience.camera) { this._backgroundStars.followCamera(DefaultScene.XR.baseExperience.camera.position); } }); this._initialized = true; // Notify that initialization is complete this._onReadyObservable.notifyObservers(this); } private createTarget(i: number) { const target = MeshBuilder.CreateTorus("target" + i, {diameter: 10, tessellation: 72}, DefaultScene.MainScene); const targetLOD = MeshBuilder.CreateTorus("target" + i, { diameter: 50, tessellation: 10 }, DefaultScene.MainScene); targetLOD.parent = target; target.addLODLevel(300, targetLOD); const material = new StandardMaterial("material", DefaultScene.MainScene); material.diffuseColor = new Color3(1, 0, 0); material.alpha = .9; target.material = material; target.position = Vector3.Random(-1000, 1000); target.rotation = Vector3.Random(0, Math.PI * 2); const disc = MeshBuilder.CreateDisc("disc-" + i, {radius: 2, tessellation: 72}, DefaultScene.MainScene); const discMaterial = new StandardMaterial("material", DefaultScene.MainScene); discMaterial.ambientColor = new Color3(.1, 1, .1); discMaterial.alpha = .2; target.addLODLevel(200, null); disc.material = discMaterial; disc.parent = target; disc.rotation.x = -Math.PI / 2; const agg = new PhysicsAggregate(disc, PhysicsShapeType.MESH, {mass: 0}, DefaultScene.MainScene); agg.body.setMotionType(PhysicsMotionType.STATIC); agg.shape.isTrigger = true; } }