diff --git a/src/levels/level1.ts b/src/levels/level1.ts index 417e059..dc57ba7 100644 --- a/src/levels/level1.ts +++ b/src/levels/level1.ts @@ -4,6 +4,7 @@ import { AbstractMesh, Observable, PhysicsAggregate, + TransformNode, Vector3, WebXRState } from "@babylonjs/core"; @@ -57,43 +58,12 @@ export class Level1 implements Level { debugLog('onControllerAddedObservable exists:', !!xr.input?.onControllerAddedObservable); xr.baseExperience.onInitialXRPoseSetObservable.add(() => { - xr.baseExperience.camera.parent = this._ship.transformNode; - xr.baseExperience.camera.position = new Vector3(0, 1.5, 0); - // Rotate camera 180 degrees around Y to compensate for inverted ship GLB model - xr.baseExperience.camera.rotationQuaternion = null; - xr.baseExperience.camera.rotation = new Vector3(0, 0, 0); + debugLog('[Level1] onInitialXRPoseSetObservable fired'); - // Resume render loop if it was stopped (ensures camera is properly set before first visible frame) - const engine = DefaultScene.MainScene.getEngine(); - engine.runRenderLoop(() => { - DefaultScene.MainScene.render(); - }); - debugLog('[Level1] Render loop resumed after XR camera setup'); + // Use consolidated XR camera setup + this.setupXRCamera(); - // Disable keyboard input in VR mode to prevent interference - if (this._ship.keyboardInput) { - this._ship.keyboardInput.setEnabled(false); - debugLog('[Level1] Keyboard input disabled for VR mode'); - } - - // Track WebXR session start - try { - const analytics = getAnalytics(); - analytics.track('webxr_session_start', { - deviceName: navigator.userAgent, - isImmersive: true - }); - } catch (error) { - debugLog('Analytics tracking failed:', error); - } - - // Add controllers - const observer = xr.input.onControllerAddedObservable.add((controller) => { - debugLog('🎮 onControllerAddedObservable FIRED for:', controller.inputSource.handedness); - this._ship.addController(controller); - }); - - // Show mission brief instead of starting immediately + // Show mission brief after camera setup debugLog('[Level1] Showing mission brief on XR entry'); this.showMissionBrief(); }); @@ -105,6 +75,83 @@ export class Level1 implements Level { return this._onReadyObservable; } + /** + * Setup XR camera, pointer selection, and controllers + * Consolidated function called from both onInitialXRPoseSetObservable and main.ts + * when XR is already active before level creation + */ + public setupXRCamera(): void { + const xr = DefaultScene.XR; + if (!xr) { + debugLog('[Level1] setupXRCamera: No XR experience available'); + return; + } + + if (!this._ship?.transformNode) { + console.error('[Level1] setupXRCamera: Ship or transformNode not available'); + return; + } + + debugLog('[Level1] ========== setupXRCamera START =========='); + + // Create intermediate TransformNode for camera rotation + // WebXR camera only uses rotationQuaternion (not .rotation), and XR frame updates overwrite it + // By rotating an intermediate node, we can orient the camera without fighting XR frame updates + const cameraRig = new TransformNode("xrCameraRig", DefaultScene.MainScene); + cameraRig.parent = this._ship.transformNode; + cameraRig.rotation = new Vector3(0, 0, 0); // Rotate 180° to face forward + debugLog('[Level1] Created cameraRig TransformNode, rotated 180°'); + + // Parent XR camera to the rig + xr.baseExperience.camera.parent = cameraRig; + xr.baseExperience.camera.position = new Vector3(0, .8, 0); + debugLog('[Level1] XR camera parented to cameraRig at position (0, 1.2, 0)'); + + // Ensure render loop is running + const engine = DefaultScene.MainScene.getEngine(); + engine.runRenderLoop(() => { + DefaultScene.MainScene.render(); + }); + debugLog('[Level1] Render loop started/resumed'); + + // Disable keyboard input in VR mode to prevent interference + if (this._ship.keyboardInput) { + this._ship.keyboardInput.setEnabled(false); + debugLog('[Level1] Keyboard input disabled for VR mode'); + } + + // Register pointer selection feature + const pointerFeature = xr.baseExperience.featuresManager.getEnabledFeature( + "xr-controller-pointer-selection" + ); + if (pointerFeature) { + const inputManager = InputControlManager.getInstance(); + inputManager.registerPointerFeature(pointerFeature); + debugLog('[Level1] Pointer selection feature registered'); + } else { + debugLog('[Level1] WARNING: Pointer selection feature not available'); + } + + // Track WebXR session start + try { + const analytics = getAnalytics(); + analytics.track('webxr_session_start', { + deviceName: navigator.userAgent, + isImmersive: true + }); + } catch (error) { + debugLog('[Level1] Analytics tracking failed:', error); + } + + // Setup controller observer + xr.input.onControllerAddedObservable.add((controller) => { + debugLog('[Level1] 🎮 Controller added:', controller.inputSource.handedness); + this._ship.addController(controller); + }); + + debugLog('[Level1] ========== setupXRCamera COMPLETE =========='); + } + /** * Show mission brief with directory entry data * Public so it can be called from main.ts when XR is already active @@ -380,7 +427,7 @@ export class Level1 implements Level { // Load background music before marking as ready if (this._audioEngine) { setLoadingMessage("Loading background music..."); - this._backgroundMusic = await this._audioEngine.createSoundAsync("background", "/song1.mp3", { + this._backgroundMusic = await this._audioEngine.createSoundAsync("background", "/assets/themes/default/audio/song1.mp3", { loop: true, volume: 0.5 }); diff --git a/src/main.ts b/src/main.ts index de14f46..c2e98c4 100644 --- a/src/main.ts +++ b/src/main.ts @@ -162,11 +162,10 @@ export class Main { 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'); + // FIX: Don't stop render loop - it may prevent XR observables from firing properly + // The brief camera orientation flash is acceptable for now + // 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)'); @@ -231,46 +230,15 @@ export class Main { } if (DefaultScene.XR && xrSession && DefaultScene.XR.baseExperience.state === 2) { // WebXRState.IN_XR = 2 - console.log('[Main] ========== XR ALREADY ACTIVE - MANUAL SETUP =========='); + debugLog('[Main] XR already active - using consolidated setupXRCamera()'); - 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'); + // Use consolidated XR camera setup from Level1 + level1.setupXRCamera(); - // NOW resume the render loop - camera is properly positioned - this._engine.runRenderLoop(() => { - DefaultScene.MainScene.render(); - }); - debugLog('Render loop resumed after camera setup'); + // Show mission brief (since onInitialXRPoseSetObservable won't fire when already in XR) + await level1.showMissionBrief(); - 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(); - }); - } + debugLog('[Main] XR setup and mission brief complete'); } 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) @@ -651,37 +619,41 @@ export class Main { debugLog(WebXRFeaturesManager.GetAvailableFeatures()); debugLog("WebXR initialized successfully"); - // Register pointer selection feature with InputControlManager + // FIX: Pointer selection feature must be registered AFTER XR session starts + // The feature is not available during initialize() - it only becomes enabled + // when the XR session is active. Moving registration to onStateChangedObservable. 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 + // Handle XR state changes - register pointer feature when entering VR 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'); + if (state === 2) { // WebXRState.IN_XR + debugLog('[Main] Entering VR - registering pointer selection feature'); + + // Register pointer selection feature NOW that XR session is active + 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"); + } else { + debugLog('[Main] WARNING: Pointer selection feature not available'); + } + + // Hide Discord widget when entering VR + const discord = (window as any).__discordWidget as DiscordWidget; + if (discord) { + debugLog('[Main] Hiding Discord widget'); discord.hide(); - } else if (state === 0) { // WebXRState.NOT_IN_XR - debugLog('[Main] Exiting VR - showing Discord widget'); + } + } else if (state === 0) { // WebXRState.NOT_IN_XR + debugLog('[Main] Exiting VR - showing Discord widget'); + const discord = (window as any).__discordWidget as DiscordWidget; + if (discord) { discord.show(); } } diff --git a/src/ship/shipAudio.ts b/src/ship/shipAudio.ts index f3720d3..34b2f7d 100644 --- a/src/ship/shipAudio.ts +++ b/src/ship/shipAudio.ts @@ -24,7 +24,7 @@ export class ShipAudio { this._primaryThrustSound = await this._audioEngine.createSoundAsync( "thrust", - "/thrust5.mp3", + "/assets/themes/default/audio/thrust5.mp3", { loop: true, volume: 0.2, @@ -33,7 +33,7 @@ export class ShipAudio { this._secondaryThrustSound = await this._audioEngine.createSoundAsync( "thrust2", - "/thrust5.mp3", + "/assets/themes/default/audio/thrust5.mp3", { loop: true, volume: 0.5, @@ -42,7 +42,7 @@ export class ShipAudio { this._weaponSound = await this._audioEngine.createSoundAsync( "shot", - "/shot.mp3", + "/assets/themes/default/audio/shot.mp3", { loop: false, volume: 0.5,