Add ENTER XR button to preloader with level info display
- Replace automatic XR entry with user-triggered ENTER XR button - Display level name, difficulty, and mission brief during loading - Add VR availability check with "VR not available" error for desktop - Add deep link protection - redirect locked levels to level select - Extract XR entry logic to xrEntryHandler.ts for code organization - Refactor levelSelectedHandler.ts from 206 to 150 lines 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
parent
4ce5f5f2de
commit
1528f54472
@ -817,6 +817,74 @@ body {
|
||||
color: var(--color-text-disabled);
|
||||
}
|
||||
|
||||
.preloader-level-info {
|
||||
margin-bottom: var(--space-xl);
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.preloader-level-name {
|
||||
font-size: var(--font-size-2xl);
|
||||
color: var(--color-text-primary);
|
||||
margin-bottom: var(--space-sm);
|
||||
}
|
||||
|
||||
.preloader-difficulty {
|
||||
display: inline-block;
|
||||
padding: var(--space-xs) var(--space-md);
|
||||
border-radius: var(--radius-sm);
|
||||
font-size: var(--font-size-sm);
|
||||
font-weight: bold;
|
||||
text-transform: uppercase;
|
||||
background: var(--color-bg-overlay);
|
||||
color: var(--color-text-muted);
|
||||
}
|
||||
|
||||
.preloader-difficulty.difficulty-easy {
|
||||
background: rgba(76, 175, 80, 0.2);
|
||||
color: #4caf50;
|
||||
}
|
||||
|
||||
.preloader-difficulty.difficulty-medium {
|
||||
background: rgba(255, 193, 7, 0.2);
|
||||
color: #ffc107;
|
||||
}
|
||||
|
||||
.preloader-difficulty.difficulty-hard {
|
||||
background: rgba(244, 67, 54, 0.2);
|
||||
color: #f44336;
|
||||
}
|
||||
|
||||
.preloader-mission-brief {
|
||||
list-style: none;
|
||||
padding: 0;
|
||||
margin: var(--space-md) 0 0 0;
|
||||
text-align: left;
|
||||
max-width: 400px;
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
}
|
||||
|
||||
.preloader-mission-brief li {
|
||||
padding: var(--space-xs) 0;
|
||||
color: var(--color-text-muted);
|
||||
font-size: var(--font-size-sm);
|
||||
}
|
||||
|
||||
.preloader-mission-brief li::before {
|
||||
content: "▸ ";
|
||||
color: var(--color-primary);
|
||||
}
|
||||
|
||||
.preloader-error {
|
||||
background: rgba(244, 67, 54, 0.1);
|
||||
border: 1px solid rgba(244, 67, 54, 0.3);
|
||||
border-radius: var(--radius-md);
|
||||
padding: var(--space-lg);
|
||||
margin-bottom: var(--space-xl);
|
||||
color: #f44336;
|
||||
font-size: var(--font-size-base);
|
||||
}
|
||||
|
||||
/* ============================================================================
|
||||
Test Buttons & Links
|
||||
========================================================================= */
|
||||
|
||||
@ -4,6 +4,7 @@
|
||||
import { Main } from '../../main';
|
||||
import type { LevelConfig } from '../../levels/config/levelConfig';
|
||||
import { LevelRegistry } from '../../levels/storage/levelRegistry';
|
||||
import { progressionStore } from '../../stores/progression';
|
||||
import log from '../../core/logger';
|
||||
import { DefaultScene } from '../../core/defaultScene';
|
||||
|
||||
@ -85,23 +86,29 @@
|
||||
throw new Error('Main instance not found');
|
||||
}
|
||||
|
||||
// Get level config from registry
|
||||
// Get full level entry from registry
|
||||
const registry = LevelRegistry.getInstance();
|
||||
const levelEntry = await registry.getLevel(levelName);
|
||||
const levelEntry = registry.getLevelEntry(levelName);
|
||||
|
||||
if (!levelEntry) {
|
||||
throw new Error(`Level "${levelName}" not found`);
|
||||
}
|
||||
|
||||
// Check if level is unlocked (deep link protection)
|
||||
const isDefault = levelEntry.levelType === 'official';
|
||||
if (!progressionStore.isLevelUnlocked(levelEntry.name, isDefault)) {
|
||||
log.warn('[PlayLevel] Level locked, redirecting to level select');
|
||||
navigate('/', { replace: true });
|
||||
return;
|
||||
}
|
||||
|
||||
log.debug('[PlayLevel] Level config loaded:', levelEntry);
|
||||
|
||||
// Dispatch the levelSelected event (existing system expects this)
|
||||
// We'll refactor this later to call Main methods directly
|
||||
// Note: registry.getLevel() returns LevelConfig directly, not a wrapper
|
||||
// Dispatch the levelSelected event
|
||||
const event = new CustomEvent('levelSelected', {
|
||||
detail: {
|
||||
levelName: levelName,
|
||||
config: levelEntry
|
||||
config: levelEntry.config
|
||||
}
|
||||
});
|
||||
window.dispatchEvent(event);
|
||||
|
||||
@ -1,15 +1,14 @@
|
||||
import { AudioEngineV2, Engine, FreeCamera, ParticleHelper, Vector3 } from "@babylonjs/core";
|
||||
import { AudioEngineV2, Engine, ParticleHelper } from "@babylonjs/core";
|
||||
import { DefaultScene } from "../defaultScene";
|
||||
import { Level1 } from "../../levels/level1";
|
||||
import Level from "../../levels/level";
|
||||
import { RockFactory } from "../../environment/asteroids/rockFactory";
|
||||
import { LevelConfig } from "../../levels/config/levelConfig";
|
||||
import { Preloader } from "../../ui/screens/preloader";
|
||||
import { LevelRegistry } from "../../levels/storage/levelRegistry";
|
||||
import { enterXRMode } from "./xrEntryHandler";
|
||||
import log from '../logger';
|
||||
|
||||
/**
|
||||
* Interface for Main class methods needed by the level selected handler
|
||||
*/
|
||||
export interface LevelSelectedContext {
|
||||
isStarted(): boolean;
|
||||
setStarted(value: boolean): void;
|
||||
@ -25,182 +24,127 @@ export interface LevelSelectedContext {
|
||||
play(): Promise<void>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates the levelSelected event handler
|
||||
* @param context - Main instance implementing LevelSelectedContext
|
||||
* @returns Event handler function
|
||||
*/
|
||||
export function createLevelSelectedHandler(context: LevelSelectedContext): (e: CustomEvent) => Promise<void> {
|
||||
export function createLevelSelectedHandler(
|
||||
context: LevelSelectedContext
|
||||
): (e: CustomEvent) => Promise<void> {
|
||||
return async (e: CustomEvent) => {
|
||||
context.setStarted(true);
|
||||
const { levelName, config } = e.detail as { levelName: string, config: LevelConfig };
|
||||
|
||||
const { levelName, config } = e.detail as { levelName: string; config: LevelConfig };
|
||||
log.debug(`[Main] Starting level: ${levelName}`);
|
||||
|
||||
// Hide all UI elements
|
||||
const levelSelect = document.querySelector('#levelSelect') as HTMLElement;
|
||||
const appHeader = document.querySelector('#appHeader') as HTMLElement;
|
||||
|
||||
if (levelSelect) {
|
||||
levelSelect.style.display = 'none';
|
||||
}
|
||||
if (appHeader) {
|
||||
appHeader.style.display = 'none';
|
||||
}
|
||||
|
||||
// Show preloader for initialization
|
||||
hideUIElements();
|
||||
const preloader = new Preloader();
|
||||
context.setProgressCallback((percent, message) => {
|
||||
preloader.updateProgress(percent, message);
|
||||
});
|
||||
context.setProgressCallback((p, m) => preloader.updateProgress(p, m));
|
||||
|
||||
try {
|
||||
// Initialize engine if this is first time
|
||||
if (!context.isInitialized()) {
|
||||
log.debug('[Main] First level selected - initializing engine');
|
||||
preloader.updateProgress(0, 'Initializing game engine...');
|
||||
await context.initializeEngine();
|
||||
}
|
||||
|
||||
// Load assets if this is the first level being played
|
||||
if (!context.areAssetsLoaded()) {
|
||||
preloader.updateProgress(40, 'Loading 3D models and textures...');
|
||||
log.debug('[Main] Loading assets for first time');
|
||||
|
||||
// Load visual assets (meshes, particles)
|
||||
ParticleHelper.BaseAssetsUrl = window.location.href;
|
||||
await RockFactory.init();
|
||||
context.setAssetsLoaded(true);
|
||||
|
||||
log.debug('[Main] Assets loaded successfully');
|
||||
preloader.updateProgress(60, 'Assets loaded');
|
||||
}
|
||||
|
||||
preloader.updateProgress(70, 'Preparing VR session...');
|
||||
|
||||
// Initialize WebXR for this level
|
||||
await loadEngineAndAssets(context, preloader);
|
||||
await context.initializeXR();
|
||||
displayLevelInfo(preloader, levelName);
|
||||
preloader.updateProgress(90, 'Ready to enter VR...');
|
||||
|
||||
// If XR is available, enter XR immediately (while we have user activation)
|
||||
let xrSession = null;
|
||||
const engine = context.getEngine();
|
||||
if (DefaultScene.XR) {
|
||||
try {
|
||||
preloader.updateProgress(75, 'Entering VR...');
|
||||
|
||||
// Pre-position XR camera at ship cockpit before entering VR
|
||||
// This prevents camera jump on Quest when immersive mode starts
|
||||
const spawnPos = config.ship?.position || [0, 0, 0];
|
||||
const cockpitPosition = new Vector3(spawnPos[0], spawnPos[1] + 1.2, spawnPos[2]);
|
||||
const tempCamera = new FreeCamera("tempCockpit", cockpitPosition, DefaultScene.MainScene);
|
||||
DefaultScene.XR.baseExperience.camera.setTransformationFromNonVRCamera(tempCamera, true);
|
||||
tempCamera.dispose();
|
||||
log.debug('[Main] XR camera pre-positioned at cockpit:', cockpitPosition.toString());
|
||||
|
||||
xrSession = await DefaultScene.XR.baseExperience.enterXRAsync('immersive-vr', 'local-floor');
|
||||
log.debug('XR session started successfully (render loop paused until camera is ready)');
|
||||
} catch (error) {
|
||||
log.debug('Failed to enter XR, will fall back to flat mode:', error);
|
||||
DefaultScene.XR = null;
|
||||
// Show canvas for flat mode
|
||||
const canvas = document.getElementById('gameCanvas');
|
||||
if (canvas) {
|
||||
canvas.style.display = 'block';
|
||||
}
|
||||
engine.stopRenderLoop();
|
||||
engine.runRenderLoop(() => {
|
||||
DefaultScene.MainScene.render();
|
||||
});
|
||||
}
|
||||
const xrAvailable = await preloader.checkXRAvailability();
|
||||
if (!xrAvailable) {
|
||||
preloader.showVRNotAvailable();
|
||||
return;
|
||||
}
|
||||
|
||||
// Unlock audio engine on user interaction
|
||||
const audioEngine = context.getAudioEngine();
|
||||
if (audioEngine) {
|
||||
await audioEngine.unlockAsync();
|
||||
}
|
||||
|
||||
// Now load audio assets (after unlock)
|
||||
preloader.updateProgress(80, 'Loading audio...');
|
||||
await RockFactory.initAudio(audioEngine);
|
||||
|
||||
// Attach audio listener to camera for spatial audio
|
||||
const camera = DefaultScene.XR?.baseExperience?.camera || DefaultScene.MainScene.activeCamera;
|
||||
if (camera && audioEngine.listener) {
|
||||
audioEngine.listener.attach(camera);
|
||||
log.debug('[Main] Audio listener attached to camera for spatial audio');
|
||||
} else {
|
||||
log.warn('[Main] Could not attach audio listener - camera or listener not available');
|
||||
}
|
||||
|
||||
preloader.updateProgress(90, 'Creating level...');
|
||||
|
||||
// Create and initialize level from config
|
||||
const currentLevel = new Level1(config, audioEngine, false, levelName);
|
||||
context.setCurrentLevel(currentLevel);
|
||||
|
||||
// Wait for level to be ready
|
||||
currentLevel.getReadyObservable().add(async () => {
|
||||
preloader.updateProgress(95, 'Starting game...');
|
||||
|
||||
// Get ship and set up replay observable
|
||||
const level1 = currentLevel as Level1;
|
||||
const ship = (level1 as any)._ship;
|
||||
|
||||
// Listen for replay requests from the ship
|
||||
if (ship) {
|
||||
ship.onReplayRequestObservable.add(() => {
|
||||
log.debug('Replay requested - reloading page');
|
||||
window.location.reload();
|
||||
});
|
||||
}
|
||||
|
||||
// If we entered XR before level creation, manually setup camera parenting
|
||||
log.info('[Main] ========== CHECKING XR STATE ==========');
|
||||
log.info('[Main] DefaultScene.XR exists:', !!DefaultScene.XR);
|
||||
log.info('[Main] xrSession exists:', !!xrSession);
|
||||
if (DefaultScene.XR) {
|
||||
log.info('[Main] XR base experience state:', DefaultScene.XR.baseExperience.state);
|
||||
}
|
||||
|
||||
if (DefaultScene.XR && xrSession && DefaultScene.XR.baseExperience.state === 2) {
|
||||
log.debug('[Main] XR already active - using consolidated setupXRCamera()');
|
||||
level1.setupXRCamera();
|
||||
await level1.showMissionBrief();
|
||||
log.debug('[Main] XR setup and mission brief complete');
|
||||
} else {
|
||||
log.info('[Main] XR not active yet - will use onInitialXRPoseSetObservable instead');
|
||||
// Show canvas for non-XR mode
|
||||
const canvas = document.getElementById('gameCanvas');
|
||||
if (canvas) {
|
||||
canvas.style.display = 'block';
|
||||
}
|
||||
engine.stopRenderLoop();
|
||||
engine.runRenderLoop(() => {
|
||||
DefaultScene.MainScene.render();
|
||||
});
|
||||
}
|
||||
|
||||
// Hide preloader immediately - SceneFader handles visual transition
|
||||
preloader.updateProgress(100, 'Ready!');
|
||||
preloader.hide();
|
||||
|
||||
// Hide UI (no longer remove from DOM - let Svelte routing handle it)
|
||||
log.info('[Main] ========== HIDING UI FOR GAMEPLAY ==========');
|
||||
log.info('[Main] Timestamp:', Date.now());
|
||||
|
||||
// Start the game
|
||||
log.info('[Main] About to call context.play()');
|
||||
await context.play();
|
||||
log.info('[Main] context.play() completed');
|
||||
preloader.showStartButton(async () => {
|
||||
await startGameWithXR(context, config, levelName, preloader);
|
||||
});
|
||||
|
||||
// Now initialize the level (after observable is registered)
|
||||
await currentLevel.initialize();
|
||||
|
||||
} catch (error) {
|
||||
log.error('[Main] Level initialization failed:', error);
|
||||
preloader.updateProgress(0, 'Failed to load level. Please refresh and try again.');
|
||||
preloader.updateProgress(0, 'Failed to load level. Please refresh.');
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
function hideUIElements(): void {
|
||||
const levelSelect = document.querySelector('#levelSelect') as HTMLElement;
|
||||
const appHeader = document.querySelector('#appHeader') as HTMLElement;
|
||||
if (levelSelect) levelSelect.style.display = 'none';
|
||||
if (appHeader) appHeader.style.display = 'none';
|
||||
}
|
||||
|
||||
async function loadEngineAndAssets(context: LevelSelectedContext, preloader: Preloader): Promise<void> {
|
||||
if (!context.isInitialized()) {
|
||||
preloader.updateProgress(0, 'Initializing game engine...');
|
||||
await context.initializeEngine();
|
||||
}
|
||||
if (!context.areAssetsLoaded()) {
|
||||
preloader.updateProgress(40, 'Loading 3D models...');
|
||||
ParticleHelper.BaseAssetsUrl = window.location.href;
|
||||
await RockFactory.init();
|
||||
context.setAssetsLoaded(true);
|
||||
preloader.updateProgress(70, 'Assets loaded');
|
||||
}
|
||||
}
|
||||
|
||||
function displayLevelInfo(preloader: Preloader, levelName: string): void {
|
||||
const entry = LevelRegistry.getInstance().getLevelEntry(levelName);
|
||||
if (entry) {
|
||||
preloader.setLevelInfo(entry.name, entry.difficulty, entry.missionBrief || []);
|
||||
}
|
||||
}
|
||||
|
||||
async function startGameWithXR(
|
||||
context: LevelSelectedContext,
|
||||
config: LevelConfig,
|
||||
levelName: string,
|
||||
preloader: Preloader
|
||||
): Promise<void> {
|
||||
preloader.updateProgress(92, 'Entering VR...');
|
||||
const engine = context.getEngine();
|
||||
const xrSession = await enterXRMode(config, engine);
|
||||
|
||||
const audioEngine = context.getAudioEngine();
|
||||
await audioEngine?.unlockAsync();
|
||||
preloader.updateProgress(95, 'Loading audio...');
|
||||
await RockFactory.initAudio(audioEngine);
|
||||
attachAudioListener(audioEngine);
|
||||
|
||||
preloader.updateProgress(98, 'Creating level...');
|
||||
const level = new Level1(config, audioEngine, false, levelName);
|
||||
context.setCurrentLevel(level);
|
||||
|
||||
level.getReadyObservable().add(async () => {
|
||||
await finalizeLevelStart(level, xrSession, engine, preloader, context);
|
||||
});
|
||||
|
||||
await level.initialize();
|
||||
}
|
||||
|
||||
function attachAudioListener(audioEngine: AudioEngineV2): void {
|
||||
const camera = DefaultScene.XR?.baseExperience?.camera || DefaultScene.MainScene.activeCamera;
|
||||
if (camera && audioEngine?.listener) {
|
||||
audioEngine.listener.attach(camera);
|
||||
}
|
||||
}
|
||||
|
||||
async function finalizeLevelStart(
|
||||
level: Level1,
|
||||
xrSession: any,
|
||||
engine: Engine,
|
||||
preloader: Preloader,
|
||||
context: LevelSelectedContext
|
||||
): Promise<void> {
|
||||
const ship = (level as any)._ship;
|
||||
ship?.onReplayRequestObservable.add(() => window.location.reload());
|
||||
|
||||
if (DefaultScene.XR && xrSession && DefaultScene.XR.baseExperience.state === 2) {
|
||||
level.setupXRCamera();
|
||||
await level.showMissionBrief();
|
||||
} else {
|
||||
showCanvasForFlatMode(engine);
|
||||
}
|
||||
|
||||
preloader.updateProgress(100, 'Ready!');
|
||||
preloader.hide();
|
||||
await context.play();
|
||||
}
|
||||
|
||||
function showCanvasForFlatMode(engine: Engine): void {
|
||||
const canvas = document.getElementById('gameCanvas');
|
||||
if (canvas) canvas.style.display = 'block';
|
||||
engine.stopRenderLoop();
|
||||
engine.runRenderLoop(() => DefaultScene.MainScene.render());
|
||||
}
|
||||
|
||||
48
src/core/handlers/xrEntryHandler.ts
Normal file
48
src/core/handlers/xrEntryHandler.ts
Normal file
@ -0,0 +1,48 @@
|
||||
import { Engine, FreeCamera, Vector3 } from "@babylonjs/core";
|
||||
import { DefaultScene } from "../defaultScene";
|
||||
import { LevelConfig } from "../../levels/config/levelConfig";
|
||||
import log from '../logger';
|
||||
|
||||
/**
|
||||
* Pre-positions XR camera and enters immersive VR mode
|
||||
* @returns XR session if successful, null otherwise
|
||||
*/
|
||||
export async function enterXRMode(
|
||||
config: LevelConfig,
|
||||
engine: Engine
|
||||
): Promise<any> {
|
||||
if (!DefaultScene.XR) {
|
||||
return startFlatMode(engine);
|
||||
}
|
||||
|
||||
try {
|
||||
prePositionCamera(config);
|
||||
const session = await DefaultScene.XR.baseExperience.enterXRAsync(
|
||||
'immersive-vr',
|
||||
'local-floor'
|
||||
);
|
||||
log.debug('XR session started successfully');
|
||||
return session;
|
||||
} catch (error) {
|
||||
log.debug('Failed to enter XR, falling back to flat mode:', error);
|
||||
DefaultScene.XR = null;
|
||||
return startFlatMode(engine);
|
||||
}
|
||||
}
|
||||
|
||||
function prePositionCamera(config: LevelConfig): void {
|
||||
const spawnPos = config.ship?.position || [0, 0, 0];
|
||||
const cockpitPosition = new Vector3(spawnPos[0], spawnPos[1] + 1.2, spawnPos[2]);
|
||||
const tempCamera = new FreeCamera("tempCockpit", cockpitPosition, DefaultScene.MainScene);
|
||||
DefaultScene.XR!.baseExperience.camera.setTransformationFromNonVRCamera(tempCamera, true);
|
||||
tempCamera.dispose();
|
||||
log.debug('[XR] Camera pre-positioned at cockpit:', cockpitPosition.toString());
|
||||
}
|
||||
|
||||
function startFlatMode(engine: Engine): null {
|
||||
const canvas = document.getElementById('gameCanvas');
|
||||
if (canvas) canvas.style.display = 'block';
|
||||
engine.stopRenderLoop();
|
||||
engine.runRenderLoop(() => DefaultScene.MainScene.render());
|
||||
return null;
|
||||
}
|
||||
@ -1,12 +1,13 @@
|
||||
/**
|
||||
* Preloader UI - Shows loading progress and start button
|
||||
* Preloader UI - Shows loading progress, level info, and ENTER XR button
|
||||
*/
|
||||
|
||||
export class Preloader {
|
||||
private container: HTMLElement | null = null;
|
||||
private progressBar: HTMLElement | null = null;
|
||||
private statusText: HTMLElement | null = null;
|
||||
private startButton: HTMLElement | null = null;
|
||||
private levelInfoEl: HTMLElement | null = null;
|
||||
private errorEl: HTMLElement | null = null;
|
||||
private onStartCallback: (() => void) | null = null;
|
||||
|
||||
constructor() {
|
||||
@ -14,118 +15,114 @@ export class Preloader {
|
||||
}
|
||||
|
||||
private createUI(): void {
|
||||
// Create preloader container
|
||||
this.container = document.createElement('div');
|
||||
this.container.className = 'preloader';
|
||||
this.container.innerHTML = this.getTemplate();
|
||||
document.body.appendChild(this.container);
|
||||
this.cacheElements();
|
||||
this.setupButtonHandler();
|
||||
}
|
||||
|
||||
this.container.innerHTML = `
|
||||
private getTemplate(): string {
|
||||
return `
|
||||
<div class="preloader-content">
|
||||
<h1 class="preloader-title">
|
||||
🚀 Space Combat VR
|
||||
</h1>
|
||||
|
||||
<div id="preloaderStatus" class="preloader-status">
|
||||
Initializing...
|
||||
<h1 class="preloader-title">🚀 Space Combat VR</h1>
|
||||
<div id="preloaderLevelInfo" class="preloader-level-info" style="display: none;">
|
||||
<h2 id="preloaderLevelName" class="preloader-level-name"></h2>
|
||||
<span id="preloaderDifficulty" class="preloader-difficulty"></span>
|
||||
<ul id="preloaderMissionBrief" class="preloader-mission-brief"></ul>
|
||||
</div>
|
||||
|
||||
<div id="preloaderStatus" class="preloader-status">Initializing...</div>
|
||||
<div class="preloader-progress-container">
|
||||
<div id="preloaderProgress" class="preloader-progress"></div>
|
||||
</div>
|
||||
|
||||
<button id="preloaderStartBtn" class="preloader-button">
|
||||
Start Game
|
||||
</button>
|
||||
|
||||
<div class="preloader-info">
|
||||
<p>Initializing game engine... Assets will load when you select a level.</p>
|
||||
<div id="preloaderError" class="preloader-error" style="display: none;">
|
||||
VR headset not detected. This game requires a VR device.
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
<button id="preloaderStartBtn" class="preloader-button">ENTER XR</button>
|
||||
</div>`;
|
||||
}
|
||||
|
||||
// Append to body so it's visible even when other UI elements are hidden
|
||||
document.body.appendChild(this.container);
|
||||
|
||||
// Get references
|
||||
private cacheElements(): void {
|
||||
this.progressBar = document.getElementById('preloaderProgress');
|
||||
this.statusText = document.getElementById('preloaderStatus');
|
||||
this.startButton = document.getElementById('preloaderStartBtn');
|
||||
|
||||
// Add start button click handler
|
||||
if (this.startButton) {
|
||||
this.startButton.addEventListener('click', () => {
|
||||
if (this.onStartCallback) {
|
||||
this.onStartCallback();
|
||||
}
|
||||
});
|
||||
}
|
||||
this.levelInfoEl = document.getElementById('preloaderLevelInfo');
|
||||
this.errorEl = document.getElementById('preloaderError');
|
||||
}
|
||||
|
||||
private setupButtonHandler(): void {
|
||||
this.startButton?.addEventListener('click', () => this.onStartCallback?.());
|
||||
}
|
||||
|
||||
public setLevelInfo(name: string, difficulty: string, missionBrief: string[]): void {
|
||||
if (!this.levelInfoEl) return;
|
||||
const nameEl = document.getElementById('preloaderLevelName');
|
||||
const diffEl = document.getElementById('preloaderDifficulty');
|
||||
const briefEl = document.getElementById('preloaderMissionBrief');
|
||||
|
||||
if (nameEl) nameEl.textContent = name;
|
||||
if (diffEl) {
|
||||
diffEl.textContent = difficulty;
|
||||
diffEl.className = `preloader-difficulty difficulty-${difficulty.toLowerCase()}`;
|
||||
}
|
||||
if (briefEl) {
|
||||
briefEl.innerHTML = missionBrief.map(item => `<li>${item}</li>`).join('');
|
||||
}
|
||||
this.levelInfoEl.style.display = 'block';
|
||||
}
|
||||
|
||||
/**
|
||||
* Update loading progress
|
||||
* @param percent - Progress from 0 to 100
|
||||
* @param message - Status message to display
|
||||
*/
|
||||
public updateProgress(percent: number, message: string): void {
|
||||
if (this.progressBar) {
|
||||
this.progressBar.style.width = `${Math.min(100, Math.max(0, percent))}%`;
|
||||
}
|
||||
if (this.statusText) {
|
||||
this.statusText.textContent = message;
|
||||
if (this.statusText) this.statusText.textContent = message;
|
||||
}
|
||||
|
||||
public async checkXRAvailability(): Promise<boolean> {
|
||||
if (!navigator.xr) return false;
|
||||
try {
|
||||
return await navigator.xr.isSessionSupported('immersive-vr');
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Show the start button when loading is complete
|
||||
* @param onStart - Callback to invoke when user clicks start
|
||||
*/
|
||||
public showStartButton(onStart: () => void): void {
|
||||
this.onStartCallback = onStart;
|
||||
|
||||
if (this.statusText) {
|
||||
this.statusText.textContent = 'All systems ready!';
|
||||
}
|
||||
|
||||
if (this.progressBar) {
|
||||
this.progressBar.style.width = '100%';
|
||||
}
|
||||
|
||||
if (this.startButton) {
|
||||
this.startButton.style.display = 'block';
|
||||
|
||||
// Animate button appearance
|
||||
this.startButton.style.opacity = '0';
|
||||
this.startButton.style.transform = 'translateY(20px)';
|
||||
|
||||
setTimeout(() => {
|
||||
if (this.startButton) {
|
||||
this.startButton.style.transition = 'opacity 0.5s ease, transform 0.5s ease';
|
||||
this.startButton.style.opacity = '1';
|
||||
this.startButton.style.transform = 'translateY(0)';
|
||||
}
|
||||
}, 100);
|
||||
}
|
||||
if (this.statusText) this.statusText.textContent = 'Ready to enter VR!';
|
||||
if (this.progressBar) this.progressBar.style.width = '100%';
|
||||
this.animateButtonIn();
|
||||
}
|
||||
|
||||
public showVRNotAvailable(): void {
|
||||
if (this.statusText) this.statusText.textContent = 'VR Required';
|
||||
if (this.progressBar) this.progressBar.style.width = '100%';
|
||||
if (this.errorEl) this.errorEl.style.display = 'block';
|
||||
if (this.startButton) this.startButton.style.display = 'none';
|
||||
}
|
||||
|
||||
private animateButtonIn(): void {
|
||||
if (!this.startButton) return;
|
||||
this.startButton.style.display = 'block';
|
||||
this.startButton.style.opacity = '0';
|
||||
this.startButton.style.transform = 'translateY(20px)';
|
||||
setTimeout(() => {
|
||||
if (!this.startButton) return;
|
||||
this.startButton.style.transition = 'opacity 0.5s ease, transform 0.5s ease';
|
||||
this.startButton.style.opacity = '1';
|
||||
this.startButton.style.transform = 'translateY(0)';
|
||||
}, 100);
|
||||
}
|
||||
|
||||
/**
|
||||
* Hide and remove the preloader
|
||||
*/
|
||||
public hide(): void {
|
||||
if (this.container) {
|
||||
this.container.style.transition = 'opacity 0.5s ease';
|
||||
this.container.style.opacity = '0';
|
||||
|
||||
setTimeout(() => {
|
||||
if (this.container && this.container.parentElement) {
|
||||
this.container.remove();
|
||||
}
|
||||
}, 500);
|
||||
}
|
||||
if (!this.container) return;
|
||||
this.container.style.transition = 'opacity 0.5s ease';
|
||||
this.container.style.opacity = '0';
|
||||
setTimeout(() => this.container?.remove(), 500);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if preloader exists
|
||||
*/
|
||||
public isVisible(): boolean {
|
||||
return this.container !== null && this.container.parentElement !== null;
|
||||
return this.container?.parentElement !== null;
|
||||
}
|
||||
}
|
||||
|
||||
Loading…
Reference in New Issue
Block a user