diff --git a/src/levels/level1.ts b/src/levels/level1.ts index f1aa7e7..970c4b5 100644 --- a/src/levels/level1.ts +++ b/src/levels/level1.ts @@ -16,6 +16,8 @@ import {BackgroundStars} from "../environment/background/backgroundStars"; import debugLog from '../core/debug'; import {PhysicsRecorder} from "../replay/recording/physicsRecorder"; import {getAnalytics} from "../analytics"; +import {MissionBrief} from "../ui/hud/missionBrief"; +import {LevelRegistry, LevelDirectoryEntry} from "./storage/levelRegistry"; export class Level1 implements Level { private _ship: Ship; @@ -25,19 +27,25 @@ export class Level1 implements Level { private _landingAggregate: PhysicsAggregate | null; private _endBase: AbstractMesh; private _levelConfig: LevelConfig; + private _levelId: string | null = null; private _audioEngine: AudioEngineV2; private _deserializer: LevelDeserializer; private _backgroundStars: BackgroundStars; private _physicsRecorder: PhysicsRecorder; private _isReplayMode: boolean; private _backgroundMusic: StaticSound; + private _missionBrief: MissionBrief; + private _gameStarted: boolean = false; + private _missionBriefShown: boolean = false; - constructor(levelConfig: LevelConfig, audioEngine: AudioEngineV2, isReplayMode: boolean = false) { + constructor(levelConfig: LevelConfig, audioEngine: AudioEngineV2, isReplayMode: boolean = false, levelId?: string) { this._levelConfig = levelConfig; + this._levelId = levelId || null; this._audioEngine = audioEngine; this._isReplayMode = isReplayMode; this._deserializer = new LevelDeserializer(levelConfig); this._ship = new Ship(audioEngine, isReplayMode); + this._missionBrief = new MissionBrief(); // Only set up XR observables in game mode (not replay mode) if (!isReplayMode && DefaultScene.XR) { @@ -63,20 +71,15 @@ export class Level1 implements Level { debugLog('Analytics tracking failed:', error); } - // Start game timer when XR pose is set - this._ship.gameStats.startTimer(); - debugLog('Game timer started'); - - // Start physics recording when gameplay begins - if (this._physicsRecorder) { - this._physicsRecorder.startRingBuffer(); - debugLog('Physics recorder started'); - } - + // 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 + debugLog('[Level1] Showing mission brief on XR entry'); + this.showMissionBrief(); }); } // Don't call initialize here - let Main call it after registering the observable @@ -86,6 +89,113 @@ export class Level1 implements Level { return this._onReadyObservable; } + /** + * Show mission brief with directory entry data + * Public so it can be called from main.ts when XR is already active + */ + public async showMissionBrief(): Promise { + // Prevent showing twice + if (this._missionBriefShown) { + console.log('[Level1] Mission brief already shown, skipping'); + return; + } + + this._missionBriefShown = true; + console.log('[Level1] showMissionBrief() called'); + + let directoryEntry: LevelDirectoryEntry | null = null; + + // Try to get directory entry if we have a level ID + if (this._levelId) { + try { + const registry = LevelRegistry.getInstance(); + console.log('[Level1] ======================================'); + console.log('[Level1] Getting all levels from registry...'); + const allLevels = registry.getAllLevels(); + console.log('[Level1] Total levels in registry:', allLevels.size); + console.log('[Level1] Looking for level ID:', this._levelId); + + const registryEntry = allLevels.get(this._levelId); + console.log('[Level1] Registry entry found:', !!registryEntry); + + if (registryEntry) { + directoryEntry = registryEntry.directoryEntry; + console.log('[Level1] Directory entry data:', { + id: directoryEntry?.id, + name: directoryEntry?.name, + description: directoryEntry?.description, + levelPath: directoryEntry?.levelPath, + missionBriefCount: directoryEntry?.missionBrief?.length || 0, + estimatedTime: directoryEntry?.estimatedTime, + difficulty: directoryEntry?.difficulty + }); + + if (directoryEntry?.missionBrief) { + console.log('[Level1] Mission brief objectives:'); + directoryEntry.missionBrief.forEach((item, i) => { + console.log(` ${i + 1}. ${item}`); + }); + } else { + console.warn('[Level1] ⚠️ No missionBrief found in directory entry!'); + } + + if (!directoryEntry?.levelPath) { + console.warn('[Level1] ⚠️ No levelPath found in directory entry!'); + } + } else { + console.error('[Level1] ❌ No registry entry found for level ID:', this._levelId); + console.log('[Level1] Available level IDs:', Array.from(allLevels.keys())); + } + console.log('[Level1] ======================================'); + + debugLog('[Level1] Retrieved directory entry for level:', this._levelId, directoryEntry); + } catch (error) { + console.error('[Level1] ❌ Exception while getting directory entry:', error); + debugLog('[Level1] Failed to get directory entry:', error); + } + } else { + console.warn('[Level1] ⚠️ No level ID available, using config-only mission brief'); + debugLog('[Level1] No level ID available, using config-only mission brief'); + } + + console.log('[Level1] About to show mission brief. Has directoryEntry:', !!directoryEntry); + + // Disable ship controls while mission brief is showing + debugLog('[Level1] Disabling ship controls for mission brief'); + this._ship.disableControls(); + + // Show mission brief with trigger observable + this._missionBrief.show(this._levelConfig, directoryEntry, this._ship.onMissionBriefTriggerObservable, () => { + debugLog('[Level1] Mission brief dismissed - enabling controls and starting game'); + this._ship.enableControls(); + this.startGameplay(); + }); + } + + /** + * Start gameplay - called when mission brief start button is clicked + * or immediately if not in XR mode + */ + private startGameplay(): void { + if (this._gameStarted) { + debugLog('[Level1] startGameplay called but game already started'); + return; + } + + this._gameStarted = true; + debugLog('[Level1] Starting gameplay'); + + // Start game timer + this._ship.gameStats.startTimer(); + debugLog('Game timer started'); + + // Start physics recording + if (this._physicsRecorder) { + this._physicsRecorder.startRingBuffer(); + debugLog('Physics recorder started'); + } + } + public async play() { if (this._isReplayMode) { throw new Error("Cannot call play() in replay mode"); @@ -109,9 +219,9 @@ export class Level1 implements Level { debugLog('Started playing background music'); } - // If XR is available and session is active, check for controllers + // If XR is available and session is active, mission brief will handle starting gameplay if (DefaultScene.XR && DefaultScene.XR.baseExperience.state === WebXRState.IN_XR) { - // XR session already active, just check for controllers + // XR session already active, mission brief is showing or has been dismissed debugLog('XR session already active, checking for controllers. Count:', DefaultScene.XR.input.controllers.length); DefaultScene.XR.input.controllers.forEach((controller, index) => { debugLog(`Controller ${index} - handedness: ${controller.inputSource.handedness}`); @@ -126,6 +236,9 @@ export class Level1 implements Level { debugLog(` Late controller ${index} - handedness: ${controller.inputSource.handedness}`); }); }, 2000); + + // Note: Mission brief will call startGameplay() when start button is clicked + debugLog('XR mode: Mission brief will control game start'); } else if (DefaultScene.XR) { // XR available but not entered yet, try to enter try { @@ -136,27 +249,17 @@ export class Level1 implements Level { debugLog(`Controller ${index} - handedness: ${controller.inputSource.handedness}`); this._ship.addController(controller); }); + // Mission brief will show and handle starting gameplay + debugLog('XR mode entered: Mission brief will control game start'); } catch (error) { debugLog('Failed to enter XR from play(), falling back to flat mode:', error); - // Start flat mode - this._ship.gameStats.startTimer(); - debugLog('Game timer started (flat mode)'); - - if (this._physicsRecorder) { - this._physicsRecorder.startRingBuffer(); - debugLog('Physics recorder started (flat mode)'); - } + // Start flat mode immediately + this.startGameplay(); } } else { // Flat camera mode - start game timer and physics recording immediately debugLog('Playing in flat camera mode (no XR)'); - this._ship.gameStats.startTimer(); - debugLog('Game timer started'); - - if (this._physicsRecorder) { - this._physicsRecorder.startRingBuffer(); - debugLog('Physics recorder started'); - } + this.startGameplay(); } } @@ -171,6 +274,9 @@ export class Level1 implements Level { if (this._physicsRecorder) { this._physicsRecorder.dispose(); } + if (this._missionBrief) { + this._missionBrief.dispose(); + } } public async initialize() { @@ -244,6 +350,11 @@ export class Level1 implements Level { debugLog('Background music loaded successfully'); } + // Initialize mission brief (will be shown when entering XR) + setLoadingMessage("Initializing mission brief..."); + this._missionBrief.initialize(); + debugLog('Mission brief initialized'); + this._initialized = true; // Notify that initialization is complete diff --git a/src/levels/storage/levelRegistry.ts b/src/levels/storage/levelRegistry.ts index 129177d..932a37c 100644 --- a/src/levels/storage/levelRegistry.ts +++ b/src/levels/storage/levelRegistry.ts @@ -93,13 +93,37 @@ export class LevelRegistry { * Load the directory.json manifest */ private async loadDirectory(): Promise { + console.log('[LevelRegistry] ======================================'); + console.log('[LevelRegistry] loadDirectory() ENTERED at', Date.now()); + console.log('[LevelRegistry] ======================================'); + try { console.log('[LevelRegistry] Attempting to fetch /levels/directory.json'); + console.log('[LevelRegistry] window.location.origin:', window.location.origin); + console.log('[LevelRegistry] Full URL will be:', window.location.origin + '/levels/directory.json'); // First, fetch from network to get the latest version + console.log('[LevelRegistry] About to call fetch() - Timestamp:', Date.now()); console.log('[LevelRegistry] Fetching from network to check version...'); - const response = await fetch('/levels/directory.json'); + + // Add cache-busting for development or when debugging + const isDev = window.location.hostname === 'localhost' || + window.location.hostname.includes('dev.') || + window.location.port !== ''; + const cacheBuster = isDev ? `?v=${Date.now()}` : ''; + console.log('[LevelRegistry] Cache busting:', isDev ? 'ENABLED (dev mode)' : 'DISABLED (production)'); + + const fetchStartTime = Date.now(); + const response = await fetch(`/levels/directory.json${cacheBuster}`); + const fetchEndTime = Date.now(); + + console.log('[LevelRegistry] fetch() returned! Time taken:', fetchEndTime - fetchStartTime, 'ms'); console.log('[LevelRegistry] Fetch response status:', response.status, response.ok); + console.log('[LevelRegistry] Fetch response type:', response.type); + console.log('[LevelRegistry] Fetch response headers:', { + contentType: response.headers.get('content-type'), + contentLength: response.headers.get('content-length') + }); if (!response.ok) { // If network fails, try to use cached version as fallback @@ -114,8 +138,13 @@ export class LevelRegistry { throw new Error(`Failed to fetch directory: ${response.status}`); } + console.log('[LevelRegistry] About to parse response.json()'); + const parseStartTime = Date.now(); const networkManifest = await response.json(); + const parseEndTime = Date.now(); + console.log('[LevelRegistry] JSON parsed successfully! Time taken:', parseEndTime - parseStartTime, 'ms'); console.log('[LevelRegistry] Directory JSON parsed:', networkManifest); + console.log('[LevelRegistry] Number of levels in manifest:', networkManifest?.levels?.length || 0); // Check if version changed const cachedVersion = localStorage.getItem(CACHED_VERSION_KEY); @@ -137,9 +166,18 @@ export class LevelRegistry { // Cache the directory await this.cacheResource('/levels/directory.json', this.directoryManifest); + console.log('[LevelRegistry] About to populate default level entries'); this.populateDefaultLevelEntries(); + console.log('[LevelRegistry] Default level entries populated successfully'); + console.log('[LevelRegistry] ======================================'); + console.log('[LevelRegistry] loadDirectory() COMPLETED at', Date.now()); + console.log('[LevelRegistry] ======================================'); } catch (error) { + console.error('[LevelRegistry] !!!!! EXCEPTION in loadDirectory() !!!!!'); console.error('[LevelRegistry] Failed to load directory:', error); + console.error('[LevelRegistry] Error type:', error?.constructor?.name); + console.error('[LevelRegistry] Error message:', error?.message); + console.error('[LevelRegistry] Error stack:', error?.stack); throw new Error('Unable to load level directory. Please check your connection.'); } } @@ -149,18 +187,37 @@ export class LevelRegistry { */ private populateDefaultLevelEntries(): void { if (!this.directoryManifest) { + console.error('[LevelRegistry] ❌ Cannot populate - directoryManifest is null'); return; } + console.log('[LevelRegistry] ======================================'); + console.log('[LevelRegistry] Populating default level entries...'); + console.log('[LevelRegistry] Directory manifest levels:', this.directoryManifest.levels.length); + this.defaultLevels.clear(); for (const entry of this.directoryManifest.levels) { + console.log(`[LevelRegistry] Storing level: ${entry.id}`, { + name: entry.name, + levelPath: entry.levelPath, + hasMissionBrief: !!entry.missionBrief, + missionBriefItems: entry.missionBrief?.length || 0, + hasLevelPath: !!entry.levelPath, + estimatedTime: entry.estimatedTime, + difficulty: entry.difficulty + }); + this.defaultLevels.set(entry.id, { directoryEntry: entry, config: null, // Lazy load isDefault: true }); } + + console.log('[LevelRegistry] Populated entries. Total count:', this.defaultLevels.size); + console.log('[LevelRegistry] Level IDs:', Array.from(this.defaultLevels.keys())); + console.log('[LevelRegistry] ======================================'); } /** @@ -221,37 +278,88 @@ export class LevelRegistry { * Load a default level's config from JSON */ private async loadDefaultLevel(levelId: string): Promise { + console.log('[LevelRegistry] ======================================'); + console.log('[LevelRegistry] loadDefaultLevel() called for:', levelId); + console.log('[LevelRegistry] Timestamp:', Date.now()); + console.log('[LevelRegistry] ======================================'); + const entry = this.defaultLevels.get(levelId); if (!entry || entry.config) { + console.log('[LevelRegistry] Early return - entry:', !!entry, ', config loaded:', !!entry?.config); return; // Already loaded or doesn't exist } try { const levelPath = `/levels/${entry.directoryEntry.levelPath}`; + console.log('[LevelRegistry] Constructed levelPath:', levelPath); + console.log('[LevelRegistry] Full URL will be:', window.location.origin + levelPath); + + // Check if cache busting is enabled (dev mode) + const isDev = window.location.hostname === 'localhost' || + window.location.hostname.includes('dev.') || + window.location.port !== ''; + + // In dev mode, skip cache and always fetch fresh + let cached = null; + if (!isDev) { + console.log('[LevelRegistry] Checking cache for:', levelPath); + cached = await this.getCachedResource(levelPath); + } else { + console.log('[LevelRegistry] Skipping cache check (dev mode)'); + } - // Try cache first - const cached = await this.getCachedResource(levelPath); if (cached) { + console.log('[LevelRegistry] Found in cache! Using cached config'); entry.config = cached; entry.loadedAt = new Date(); return; } + console.log('[LevelRegistry] Not in cache, fetching from network'); + + // Fetch from network with cache-busting in dev mode + const cacheBuster = isDev ? `?v=${Date.now()}` : ''; + console.log('[LevelRegistry] About to fetch level JSON - Timestamp:', Date.now()); + console.log('[LevelRegistry] Cache busting:', isDev ? 'ENABLED' : 'DISABLED'); + const fetchStartTime = Date.now(); + const response = await fetch(`${levelPath}${cacheBuster}`); + const fetchEndTime = Date.now(); + + console.log('[LevelRegistry] Level fetch() returned! Time taken:', fetchEndTime - fetchStartTime, 'ms'); + console.log('[LevelRegistry] Response status:', response.status, response.ok); - // Fetch from network - const response = await fetch(levelPath); if (!response.ok) { + console.error('[LevelRegistry] Fetch failed with status:', response.status); throw new Error(`Failed to fetch level: ${response.status}`); } + console.log('[LevelRegistry] Parsing level JSON...'); + const parseStartTime = Date.now(); const config: LevelConfig = await response.json(); + const parseEndTime = Date.now(); + console.log('[LevelRegistry] Level JSON parsed! Time taken:', parseEndTime - parseStartTime, 'ms'); + console.log('[LevelRegistry] Level config loaded:', { + version: config.version, + difficulty: config.difficulty, + asteroidCount: config.asteroids?.length || 0 + }); // Cache the level + console.log('[LevelRegistry] Caching level config...'); await this.cacheResource(levelPath, config); + console.log('[LevelRegistry] Level cached successfully'); entry.config = config; entry.loadedAt = new Date(); + + console.log('[LevelRegistry] ======================================'); + console.log('[LevelRegistry] loadDefaultLevel() COMPLETED for:', levelId); + console.log('[LevelRegistry] ======================================'); } catch (error) { - console.error(`Failed to load default level ${levelId}:`, error); + console.error('[LevelRegistry] !!!!! EXCEPTION in loadDefaultLevel() !!!!!'); + console.error(`[LevelRegistry] Failed to load default level ${levelId}:`, error); + console.error('[LevelRegistry] Error type:', error?.constructor?.name); + console.error('[LevelRegistry] Error message:', error?.message); + console.error('[LevelRegistry] Error stack:', error?.stack); throw error; } } @@ -498,4 +606,38 @@ export class LevelRegistry { public isInitialized(): boolean { return this.initialized; } + + /** + * Clear all caches and force reload from network + * Useful for development or when data needs to be refreshed + */ + public async clearAllCaches(): Promise { + console.log('[LevelRegistry] Clearing all caches...'); + + // Clear Cache API + if ('caches' in window) { + const cacheKeys = await caches.keys(); + for (const key of cacheKeys) { + await caches.delete(key); + console.log('[LevelRegistry] Deleted cache:', key); + } + } + + // Clear localStorage cache version + localStorage.removeItem(CACHED_VERSION_KEY); + console.log('[LevelRegistry] Cleared localStorage cache version'); + + // Clear loaded configs + for (const entry of this.defaultLevels.values()) { + entry.config = null; + entry.loadedAt = undefined; + } + console.log('[LevelRegistry] Cleared loaded configs'); + + // Reset initialization flag to force reload + this.initialized = false; + this.directoryManifest = null; + + console.log('[LevelRegistry] All caches cleared. Call initialize() to reload.'); + } } diff --git a/src/levels/ui/levelSelector.ts b/src/levels/ui/levelSelector.ts index f451341..b051fa7 100644 --- a/src/levels/ui/levelSelector.ts +++ b/src/levels/ui/levelSelector.ts @@ -403,7 +403,7 @@ export async function selectLevel(levelId: string): Promise { // Dispatch custom event that Main class will listen for const event = new CustomEvent('levelSelected', { - detail: {levelId, config} + detail: {levelName: levelId, config} }); window.dispatchEvent(event); } diff --git a/src/main.ts b/src/main.ts index 4cddab0..c95c69f 100644 --- a/src/main.ts +++ b/src/main.ts @@ -187,7 +187,7 @@ export class Main { preloader.updateProgress(90, 'Creating level...'); // Create and initialize level from config - this._currentLevel = new Level1(config, this._audioEngine); + this._currentLevel = new Level1(config, this._audioEngine, false, levelName); // Wait for level to be ready this._currentLevel.getReadyObservable().add(async () => { @@ -220,14 +220,13 @@ export class Main { DefaultScene.XR.baseExperience.camera.parent = ship.transformNode; DefaultScene.XR.baseExperience.camera.position = new Vector3(0, 1.5, 0); - // Also start timer and recording here (since onInitialXRPoseSetObservable won't fire) - ship.gameStats.startTimer(); - debugLog('Game timer started (manual)'); + console.log('[Main] XR already active - showing mission brief'); + // Show mission brief (since onInitialXRPoseSetObservable won't fire) + await level1.showMissionBrief(); + console.log('[Main] Mission brief shown, mission brief will call startGameplay() on button click'); - if ((level1 as any)._physicsRecorder) { - (level1 as any)._physicsRecorder.startRingBuffer(); - debugLog('Physics recorder started (manual)'); - } + // NOTE: Don't start timer/recording here anymore - mission brief will do it + // when the user clicks the START button } else { debugLog('WARNING: Could not parent XR camera - ship or transformNode not found'); } @@ -664,7 +663,7 @@ router.on('/', async () => { } // Discord widget initialization with enhanced error logging - if (!(window as any).__discordWidget) { + /*if (!(window as any).__discordWidget) { debugLog('[Router] Initializing Discord widget'); const discord = new DiscordWidget(); @@ -687,7 +686,7 @@ router.on('/', async () => { console.error('[Router] GraphQL response error:', error.response); } }); - } + }*/ } debugLog('[Router] Home route handler complete'); @@ -717,15 +716,24 @@ router.on('/settings', () => { // Initialize registry and start router // This must happen BEFORE router.start() so levels are available async function initializeApp() { + console.log('[Main] ========================================'); + console.log('[Main] initializeApp() STARTED at', new Date().toISOString()); + console.log('[Main] ========================================'); + // Check for legacy data migration - if (LegacyMigration.needsMigration()) { + const needsMigration = LegacyMigration.needsMigration(); + console.log('[Main] Needs migration check:', needsMigration); + + if (needsMigration) { debugLog('[Main] Legacy data detected - showing migration modal'); return new Promise((resolve) => { LegacyMigration.showMigrationModal(async (result) => { debugLog('[Main] Migration completed:', result); // Initialize the new registry system try { + console.log('[Main] About to call LevelRegistry.getInstance().initialize() [AFTER MIGRATION]'); await LevelRegistry.getInstance().initialize(); + console.log('[Main] LevelRegistry.initialize() completed successfully [AFTER MIGRATION]'); debugLog('[Main] LevelRegistry initialized after migration'); router.start(); resolve(); @@ -737,19 +745,45 @@ async function initializeApp() { }); }); } else { + console.log('[Main] No migration needed - proceeding to initialize registry'); // Initialize the new registry system try { + console.log('[Main] About to call LevelRegistry.getInstance().initialize()'); + console.log('[Main] Timestamp before initialize:', Date.now()); await LevelRegistry.getInstance().initialize(); + console.log('[Main] Timestamp after initialize:', Date.now()); + console.log('[Main] LevelRegistry.initialize() completed successfully'); debugLog('[Main] LevelRegistry initialized'); + + // Expose registry to window for debugging (dev mode) + const isDev = window.location.hostname === 'localhost' || + window.location.hostname.includes('dev.') || + window.location.port !== ''; + if (isDev) { + (window as any).__levelRegistry = LevelRegistry.getInstance(); + console.log('[Main] LevelRegistry exposed to window.__levelRegistry for debugging'); + console.log('[Main] To clear caches: window.__levelRegistry.clearAllCaches().then(() => location.reload())'); + } + + console.log('[Main] About to call router.start()'); router.start(); + console.log('[Main] router.start() completed'); } catch (error) { + console.error('[Main] !!!!! EXCEPTION in LevelRegistry initialization !!!!!'); console.error('[Main] Failed to initialize LevelRegistry:', error); + console.error('[Main] Error stack:', error?.stack); router.start(); // Start anyway to show error state } } + + console.log('[Main] initializeApp() FINISHED at', new Date().toISOString()); } // Start the app +console.log('[Main] ========================================'); +console.log('[Main] main.ts MODULE LOADED at', new Date().toISOString()); +console.log('[Main] About to call initializeApp()'); +console.log('[Main] ========================================'); initializeApp(); // Suppress non-critical BabylonJS shader loading errors during development diff --git a/src/ship/input/controllerInput.ts b/src/ship/input/controllerInput.ts index e4c7cf2..4762c30 100644 --- a/src/ship/input/controllerInput.ts +++ b/src/ship/input/controllerInput.ts @@ -226,7 +226,8 @@ export class ControllerInput { } if (!this._enabled && controllerEvent.type === "button" && - !(controllerEvent.component.id === "x-button" && controllerEvent.hand === "left")) { + !(controllerEvent.component.id === "x-button" && controllerEvent.hand === "left") && + controllerEvent.component.type !== "trigger") { return; } diff --git a/src/ship/ship.ts b/src/ship/ship.ts index c6654a9..f5418d0 100644 --- a/src/ship/ship.ts +++ b/src/ship/ship.ts @@ -59,9 +59,15 @@ export class Ship { // Observable for replay requests public onReplayRequestObservable: Observable = new Observable(); + // Observable for mission brief trigger dismissal + private _onMissionBriefTriggerObservable: Observable = new Observable(); + // Auto-show status screen flag private _statusScreenAutoShown: boolean = false; + // Controls enabled state + private _controlsEnabled: boolean = true; + constructor(audioEngine?: AudioEngineV2, isReplayMode: boolean = false) { this._audioEngine = audioEngine; this._isReplayMode = isReplayMode; @@ -83,6 +89,10 @@ export class Ship { return this._isInLandingZone; } + public get onMissionBriefTriggerObservable(): Observable { + return this._onMissionBriefTriggerObservable; + } + public get velocity(): Vector3 { if (this._ship?.physicsBody) { return this._ship.physicsBody.getLinearVelocity(); @@ -564,6 +574,13 @@ export class Ship { * Handle shooting from any input source */ private handleShoot(): void { + // If controls are disabled, fire mission brief trigger observable instead of shooting + if (!this._controlsEnabled) { + debugLog('[Ship] Controls disabled - firing mission brief trigger observable'); + this._onMissionBriefTriggerObservable.notifyObservers(); + return; + } + if (this._audio) { this._audio.playWeaponSound(); } @@ -611,6 +628,34 @@ export class Ship { } } + /** + * Disable ship controls (for mission brief, etc.) + */ + public disableControls(): void { + debugLog('[Ship] Disabling controls'); + this._controlsEnabled = false; + if (this._controllerInput) { + this._controllerInput.setEnabled(false); + } + if (this._keyboardInput) { + this._keyboardInput.setEnabled(false); + } + } + + /** + * Enable ship controls + */ + public enableControls(): void { + debugLog('[Ship] Enabling controls'); + this._controlsEnabled = true; + if (this._controllerInput) { + this._controllerInput.setEnabled(true); + } + if (this._keyboardInput) { + this._keyboardInput.setEnabled(true); + } + } + /** * Dispose of ship resources */ diff --git a/src/ui/hud/missionBrief.ts b/src/ui/hud/missionBrief.ts new file mode 100644 index 0000000..9215aef --- /dev/null +++ b/src/ui/hud/missionBrief.ts @@ -0,0 +1,257 @@ +import { + AdvancedDynamicTexture, + Control, + Rectangle, + StackPanel, + TextBlock +} from "@babylonjs/gui"; +import { DefaultScene } from "../../core/defaultScene"; +import {Mesh, MeshBuilder, Vector3, Observable, Observer} from "@babylonjs/core"; +import debugLog from '../../core/debug'; +import { LevelConfig } from "../../levels/config/levelConfig"; +import { LevelDirectoryEntry } from "../../levels/storage/levelRegistry"; + +/** + * Mission brief display for VR + * Shows mission objectives and start button on cockpit screen + */ +export class MissionBrief { + private _advancedTexture: AdvancedDynamicTexture | null = null; + private _container: Rectangle | null = null; + private _isVisible: boolean = false; + private _onStartCallback: (() => void) | null = null; + private _triggerObserver: Observer | null = null; + + /** + * Initialize the mission brief as a fullscreen overlay + */ + public initialize(): void { + const scene = DefaultScene.MainScene; + + console.log('[MissionBrief] Initializing as fullscreen overlay'); + const mesh = MeshBuilder.CreatePlane('brief', {size: 2}); + const ship = scene.getNodeById('Ship'); + mesh.parent = ship; + mesh.position = new Vector3(0,1,2.8); + // Create fullscreen advanced texture (not attached to mesh) + this._advancedTexture = AdvancedDynamicTexture.CreateForMesh(mesh); + + + console.log('[MissionBrief] Fullscreen UI created'); + + // Create main container - centered overlay + this._container = new Rectangle("missionBriefContainer"); + this._container.width = "800px"; + this._container.height = "600px"; + this._container.thickness = 4; + this._container.color = "#00ff00"; + this._container.background = "rgba(0, 0, 0, 0.95)"; + this._container.cornerRadius = 20; + this._container.horizontalAlignment = Control.HORIZONTAL_ALIGNMENT_CENTER; + this._container.verticalAlignment = Control.VERTICAL_ALIGNMENT_CENTER; + this._advancedTexture.addControl(this._container); + + // Initially hidden + this._container.isVisible = false; + + console.log('[MissionBrief] Fullscreen overlay initialized'); + } + + /** + * Show mission brief with level information + * @param levelConfig - Level configuration containing mission details + * @param directoryEntry - Optional directory entry with mission brief details + * @param triggerObservable - Observable that fires when trigger is pulled + * @param onStart - Callback when start button is pressed + */ + public show(levelConfig: LevelConfig, directoryEntry: LevelDirectoryEntry | null, triggerObservable: Observable, onStart: () => void): void { + if (!this._container || !this._advancedTexture) { + debugLog('[MissionBrief] Cannot show - not initialized'); + return; + } + + debugLog('[MissionBrief] Showing with config:', { + difficulty: levelConfig.difficulty, + description: levelConfig.metadata?.description, + asteroidCount: levelConfig.asteroids?.length, + hasDirectoryEntry: !!directoryEntry, + missionBriefItems: directoryEntry?.missionBrief?.length || 0 + }); + + this._onStartCallback = onStart; + + // Listen for trigger pulls to dismiss the mission brief + this._triggerObserver = triggerObservable.add(() => { + debugLog('[MissionBrief] Trigger pulled - dismissing mission brief'); + this.hide(); + if (this._onStartCallback) { + this._onStartCallback(); + } + // Remove observer after first trigger + if (this._triggerObserver) { + triggerObservable.remove(this._triggerObserver); + this._triggerObserver = null; + } + }); + + // Clear previous content + this._container.children.forEach(child => child.dispose()); + this._container.clearControls(); + + // Create content panel + const contentPanel = new StackPanel("missionContent"); + contentPanel.width = "750px"; + contentPanel.verticalAlignment = Control.VERTICAL_ALIGNMENT_CENTER; + contentPanel.horizontalAlignment = Control.HORIZONTAL_ALIGNMENT_CENTER; + contentPanel.paddingTop = "20px"; + contentPanel.paddingBottom = "20px"; + this._container.addControl(contentPanel); + + // Title + const title = new TextBlock("missionTitle"); + title.text = "MISSION BRIEF"; + title.color = "#00ff00"; + title.fontSize = 48; + title.fontWeight = "bold"; + title.height = "70px"; + title.textVerticalAlignment = Control.VERTICAL_ALIGNMENT_CENTER; + contentPanel.addControl(title); + + // Spacer + const spacer1 = new Rectangle("spacer1"); + spacer1.height = "30px"; + spacer1.thickness = 0; + contentPanel.addControl(spacer1); + + // Divider line + const divider = new Rectangle("divider"); + divider.height = "3px"; + divider.width = "700px"; + divider.background = "#00ff00"; + divider.thickness = 0; + contentPanel.addControl(divider); + + // Spacer + const spacer2 = new Rectangle("spacer2"); + spacer2.height = "40px"; + spacer2.thickness = 0; + contentPanel.addControl(spacer2); + + // Mission description + const description = this.getMissionDescription(levelConfig, directoryEntry); + const descriptionText = new TextBlock("missionDescription"); + descriptionText.text = description; + descriptionText.color = "#ffffff"; + descriptionText.fontSize = 20; + descriptionText.textWrapping = true; + descriptionText.height = "150px"; + descriptionText.textHorizontalAlignment = Control.HORIZONTAL_ALIGNMENT_CENTER; + descriptionText.textVerticalAlignment = Control.VERTICAL_ALIGNMENT_TOP; + contentPanel.addControl(descriptionText); + + // Objectives + const objectives = this.getObjectives(levelConfig, directoryEntry); + const objectivesText = new TextBlock("objectives"); + objectivesText.text = objectives; + objectivesText.color = "#ffaa00"; + objectivesText.fontSize = 18; + objectivesText.textWrapping = true; + objectivesText.height = "200px"; + objectivesText.textHorizontalAlignment = Control.HORIZONTAL_ALIGNMENT_LEFT; + objectivesText.textVerticalAlignment = Control.VERTICAL_ALIGNMENT_TOP; + objectivesText.paddingLeft = "20px"; + contentPanel.addControl(objectivesText); + + // Spacer before button + const spacer3 = new Rectangle("spacer3"); + spacer3.height = "40px"; + spacer3.thickness = 0; + contentPanel.addControl(spacer3); + + const startText = new TextBlock("startTExt"); + startText.text = 'Pull trigger to start'; + startText.color = "#00aa00"; + startText.fontSize = 48; + startText.textWrapping = true; + startText.height = "80px"; + startText.textHorizontalAlignment = Control.HORIZONTAL_ALIGNMENT_CENTER; + startText.textVerticalAlignment = Control.VERTICAL_ALIGNMENT_TOP; + startText.paddingLeft = "20px"; + contentPanel.addControl(startText); + + // Show the container + this._container.isVisible = true; + this._isVisible = true; + + debugLog('[MissionBrief] Mission brief displayed'); + } + + /** + * Hide the mission brief + */ + public hide(): void { + if (this._container) { + this._container.isVisible = false; + this._isVisible = false; + debugLog('[MissionBrief] Mission brief hidden'); + } + } + + /** + * Check if mission brief is currently visible + */ + public get isVisible(): boolean { + return this._isVisible; + } + + /** + * Get mission description text based on level config and directory entry + */ + private getMissionDescription(levelConfig: LevelConfig, directoryEntry: LevelDirectoryEntry | null): string { + const difficulty = levelConfig.difficulty.toUpperCase(); + const name = directoryEntry?.name || levelConfig.metadata?.description || "Mission"; + const description = directoryEntry?.description || "Clear the asteroid field"; + const estimatedTime = directoryEntry?.estimatedTime || "Unknown"; + + return `${name}\n` + + `Difficulty: ${difficulty}\n` + + `Estimated Time: ${estimatedTime}\n\n` + + `${description}`; + } + + /** + * Get objectives text based on level config and directory entry + */ + private getObjectives(levelConfig: LevelConfig, directoryEntry: LevelDirectoryEntry | null): string { + const asteroidCount = levelConfig.asteroids?.length || 0; + + // Use mission brief from directory if available + if (directoryEntry?.missionBrief && directoryEntry.missionBrief.length > 0) { + const objectives = directoryEntry.missionBrief + .map(item => `• ${item}`) + .join('\n'); + return `OBJECTIVES:\n${objectives}`; + } + + // Fallback to default objectives + return `OBJECTIVES:\n` + + `• Destroy all ${asteroidCount} asteroids\n` + + `• Manage fuel and ammunition\n` + + `• Return to base safely`; + } + + /** + * Clean up resources + */ + public dispose(): void { + if (this._advancedTexture) { + this._advancedTexture.dispose(); + this._advancedTexture = null; + } + this._container = null; + this._onStartCallback = null; + this._triggerObserver = null; + this._isVisible = false; + debugLog('[MissionBrief] Disposed'); + } +} diff --git a/src/ui/widgets/discordWidget.ts b/src/ui/widgets/discordWidget.ts index 28bedfe..5ceca20 100644 --- a/src/ui/widgets/discordWidget.ts +++ b/src/ui/widgets/discordWidget.ts @@ -25,6 +25,9 @@ export class DiscordWidget { */ async initialize(options: DiscordWidgetOptions): Promise { try { + // Suppress WidgetBot console errors (CSP and CORS issues from their side) + this.suppressWidgetBotErrors(); + // Load the Crate script if not already loaded if (!this.scriptLoaded) { console.log('[DiscordWidget] Loading Crate script...'); @@ -83,6 +86,7 @@ export class DiscordWidget { script.src = 'https://cdn.jsdelivr.net/npm/@widgetbot/crate@3'; script.async = true; script.defer = true; + script.crossOrigin = 'anonymous'; script.onload = () => { console.log('[DiscordWidget] Script loaded successfully'); @@ -115,6 +119,46 @@ export class DiscordWidget { }); } + /** + * Suppress WidgetBot console errors (CSP/CORS issues from their infrastructure) + */ + private suppressWidgetBotErrors(): void { + // Filter console.error to suppress known WidgetBot issues + const originalError = console.error; + console.error = (...args: any[]) => { + const message = args.join(' '); + + // Skip known WidgetBot infrastructure errors + if ( + message.includes('widgetbot') || + message.includes('stonks.widgetbot.io') || + message.includes('e.widgetbot.io') || + message.includes('Content Security Policy') || + message.includes('[embed-api]') || + message.includes('[mobx]') || + message.includes('GraphQL') && message.includes('widgetbot') + ) { + return; // Suppress these errors + } + + // Pass through all other errors + originalError.apply(console, args); + }; + + // Filter console.log for WidgetBot verbose logging + const originalLog = console.log; + console.log = (...args: any[]) => { + const message = args.join(' '); + + // Skip WidgetBot internal logging + if (message.includes('[embed-api]')) { + return; // Suppress verbose embed-api logs + } + + originalLog.apply(console, args); + }; + } + /** * Setup event listeners for widget events */ @@ -132,29 +176,10 @@ export class DiscordWidget { console.log('[DiscordWidget] Chat visibility:', visible); }); - // Listen for any errors from the widget - this.crate.on('error', (error: any) => { - console.error('[DiscordWidget] Widget error event:', error); + // Suppress widget internal errors - they're from WidgetBot's infrastructure + this.crate.on('error', () => { + // Silently ignore - these are CSP/CORS issues on WidgetBot's side }); - - // Monitor window errors that might be related to Discord widget - const originalErrorHandler = window.onerror; - window.onerror = (message, source, lineno, colno, error) => { - if (source?.includes('widgetbot') || message?.toString().includes('GraphQL')) { - console.error('[DiscordWidget] Window error (possibly related):', { - message, - source, - lineno, - colno, - error - }); - } - // Call original handler if it existed - if (originalErrorHandler) { - return originalErrorHandler(message, source, lineno, colno, error); - } - return false; - }; } /**