Fix routing, cleanup, and game restart issues
- Switch from svelte-spa-router to svelte-routing for clean URLs without hashes - Fix relative asset paths to absolute paths (prevents 404s on nested routes) - Fix physics engine disposal using scene.disablePhysicsEngine() - Fix Ship observer cleanup to prevent stale callbacks after level disposal - Add _gameplayStarted flag to prevent false game-end triggers during init - Add hasAsteroidsToDestroy check to prevent false victory on restart - Add RockFactory.reset() to properly reinitialize asteroid mesh between games - Add null safety checks throughout RockFactory for static properties 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
parent
622e0a5259
commit
28c1b2b2aa
27
package-lock.json
generated
27
package-lock.json
generated
@ -20,7 +20,7 @@
|
||||
"@newrelic/browser-agent": "^1.302.0",
|
||||
"loglevel": "^1.9.2",
|
||||
"openai": "4.52.3",
|
||||
"svelte-spa-router": "^4.0.1"
|
||||
"svelte-routing": "^2.13.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@sveltejs/vite-plugin-svelte": "^6.2.1",
|
||||
@ -1612,15 +1612,6 @@
|
||||
"node": "^10 || ^12 || >=14"
|
||||
}
|
||||
},
|
||||
"node_modules/regexparam": {
|
||||
"version": "2.0.2",
|
||||
"resolved": "https://registry.npmjs.org/regexparam/-/regexparam-2.0.2.tgz",
|
||||
"integrity": "sha512-A1PeDEYMrkLrfyOwv2jwihXbo9qxdGD3atBYQA9JJgreAx8/7rC6IUkWOw2NQlOxLp2wL0ifQbh1HuidDfYA6w==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/resolve-pkg-maps": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/resolve-pkg-maps/-/resolve-pkg-maps-1.0.0.tgz",
|
||||
@ -1761,17 +1752,11 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/svelte-spa-router": {
|
||||
"version": "4.0.1",
|
||||
"resolved": "https://registry.npmjs.org/svelte-spa-router/-/svelte-spa-router-4.0.1.tgz",
|
||||
"integrity": "sha512-2JkmUQ2f9jRluijL58LtdQBIpynSbem2eBGp4zXdi7aDY1znbR6yjw0KsonD0aq2QLwf4Yx4tBJQjxIjgjXHKg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"regexparam": "2.0.2"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/ItalyPaleAle"
|
||||
}
|
||||
"node_modules/svelte-routing": {
|
||||
"version": "2.13.0",
|
||||
"resolved": "https://registry.npmjs.org/svelte-routing/-/svelte-routing-2.13.0.tgz",
|
||||
"integrity": "sha512-/NTxqTwLc7Dq306hARJrH2HLXOBtKd7hu8nxgoFDlK0AC4SOKnzisiX/9m8Uksei1QAWtlAEdF91YphNM8iDMg==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/tinyglobby": {
|
||||
"version": "0.2.15",
|
||||
|
||||
@ -25,9 +25,9 @@
|
||||
"@babylonjs/procedural-textures": "8.36.1",
|
||||
"@babylonjs/serializers": "8.36.1",
|
||||
"@newrelic/browser-agent": "^1.302.0",
|
||||
"openai": "4.52.3",
|
||||
"loglevel": "^1.9.2",
|
||||
"svelte-spa-router": "^4.0.1"
|
||||
"openai": "4.52.3",
|
||||
"svelte-routing": "^2.13.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@sveltejs/vite-plugin-svelte": "^6.2.1",
|
||||
|
||||
BIN
public/8192.webp
BIN
public/8192.webp
Binary file not shown.
|
Before Width: | Height: | Size: 2.0 MiB |
BIN
public/flare.png
BIN
public/flare.png
Binary file not shown.
|
Before Width: | Height: | Size: 7.4 KiB |
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@ -1,6 +1,6 @@
|
||||
<script lang="ts">
|
||||
import { onMount } from 'svelte';
|
||||
import { link } from 'svelte-spa-router';
|
||||
import { Link } from 'svelte-routing';
|
||||
import { controllerMappingStore } from '../../stores/controllerMapping';
|
||||
import { ControllerMappingConfig } from '../../ship/input/controllerMapping';
|
||||
import Button from '../shared/Button.svelte';
|
||||
@ -81,7 +81,7 @@
|
||||
</script>
|
||||
|
||||
<div class="editor-container">
|
||||
<a href="/" use:link class="back-link">← Back to Game</a>
|
||||
<Link to="/" class="back-link">← Back to Game</Link>
|
||||
|
||||
<h1>🎮 Controller Mapping</h1>
|
||||
<p class="subtitle">Customize VR controller button and stick mappings</p>
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
<script lang="ts">
|
||||
import { link } from 'svelte-spa-router';
|
||||
import { Link } from 'svelte-routing';
|
||||
import Button from '../shared/Button.svelte';
|
||||
import Section from '../shared/Section.svelte';
|
||||
|
||||
@ -8,7 +8,7 @@
|
||||
</script>
|
||||
|
||||
<div class="editor-container">
|
||||
<a href="/" use:link class="back-link">← Back to Game</a>
|
||||
<Link to="/" class="back-link">← Back to Game</Link>
|
||||
|
||||
<h1>📝 Level Editor</h1>
|
||||
<p class="subtitle">Create and customize your own asteroid field levels</p>
|
||||
|
||||
@ -1,4 +1,5 @@
|
||||
<script lang="ts">
|
||||
import { navigate } from 'svelte-routing';
|
||||
import type { LevelDirectoryEntry } from '../../levels/storage/levelRegistry';
|
||||
import { levelRegistryStore } from '../../stores/levelRegistry';
|
||||
import { authStore } from '../../stores/auth';
|
||||
@ -37,17 +38,9 @@
|
||||
return;
|
||||
}
|
||||
|
||||
// Dispatch custom event for main.ts to handle
|
||||
console.log('[LevelCard] Level unlocked, loading config...');
|
||||
const config = await levelRegistryStore.getLevel(levelId);
|
||||
if (config) {
|
||||
console.log('[LevelCard] Config loaded, dispatching levelSelected event');
|
||||
window.dispatchEvent(new CustomEvent('levelSelected', {
|
||||
detail: { levelName: levelId, config }
|
||||
}));
|
||||
} else {
|
||||
console.error('[LevelCard] Failed to load level config');
|
||||
}
|
||||
// Navigate to level play route
|
||||
console.log('[LevelCard] Level unlocked, navigating to /play/' + levelId);
|
||||
navigate(`/play/${levelId}`);
|
||||
}
|
||||
|
||||
async function handleDelete() {
|
||||
|
||||
@ -1,7 +1,6 @@
|
||||
<script lang="ts">
|
||||
import { levelRegistryStore } from '../../stores/levelRegistry';
|
||||
import { authStore } from '../../stores/auth';
|
||||
import { link } from 'svelte-spa-router';
|
||||
import LevelCard from './LevelCard.svelte';
|
||||
import ProgressBar from './ProgressBar.svelte';
|
||||
|
||||
|
||||
205
src/components/game/PlayLevel.svelte
Normal file
205
src/components/game/PlayLevel.svelte
Normal file
@ -0,0 +1,205 @@
|
||||
<script lang="ts">
|
||||
import { onMount, onDestroy } from 'svelte';
|
||||
import { navigate } from 'svelte-routing';
|
||||
import { Main } from '../../main';
|
||||
import type { LevelConfig } from '../../levels/config/levelConfig';
|
||||
import { LevelRegistry } from '../../levels/storage/levelRegistry';
|
||||
import debugLog from '../../core/debug';
|
||||
import { DefaultScene } from '../../core/defaultScene';
|
||||
|
||||
// svelte-routing passes params as an object with route params
|
||||
export let params: { levelId?: string } = {};
|
||||
// Also accept levelId directly in case it's passed that way
|
||||
export let levelId: string = '';
|
||||
|
||||
let mainInstance: Main | null = null;
|
||||
let levelName: string = '';
|
||||
let isInitialized = false;
|
||||
let error: string | null = null;
|
||||
let isExiting = false;
|
||||
|
||||
// Get the actual levelId from either source
|
||||
$: actualLevelId = params?.levelId || levelId || '';
|
||||
|
||||
// Handle browser back button
|
||||
function handleBeforeUnload(event: BeforeUnloadEvent) {
|
||||
// Only prompt if in XR session
|
||||
if (DefaultScene.XR && DefaultScene.XR.baseExperience.state === 2) {
|
||||
event.preventDefault();
|
||||
event.returnValue = 'You are currently in VR. Are you sure you want to exit?';
|
||||
return event.returnValue;
|
||||
}
|
||||
}
|
||||
|
||||
// Handle popstate (browser back/forward buttons)
|
||||
function handlePopState() {
|
||||
if (isInitialized && !isExiting && !window.location.pathname.startsWith('/play/')) {
|
||||
debugLog('[PlayLevel] Navigation detected via popstate, starting cleanup');
|
||||
isExiting = true;
|
||||
}
|
||||
}
|
||||
|
||||
onMount(async () => {
|
||||
console.log('[PlayLevel] Component mounted');
|
||||
console.log('[PlayLevel] params:', params);
|
||||
console.log('[PlayLevel] levelId prop:', levelId);
|
||||
console.log('[PlayLevel] actualLevelId:', actualLevelId);
|
||||
console.log('[PlayLevel] window.location.pathname:', window.location.pathname);
|
||||
|
||||
// Try to extract levelId from URL if props don't have it
|
||||
let extractedLevelId = actualLevelId;
|
||||
if (!extractedLevelId) {
|
||||
const match = window.location.pathname.match(/\/play\/(.+)/);
|
||||
if (match) {
|
||||
extractedLevelId = match[1];
|
||||
console.log('[PlayLevel] Extracted levelId from URL:', extractedLevelId);
|
||||
}
|
||||
}
|
||||
|
||||
levelName = extractedLevelId;
|
||||
|
||||
if (!levelName) {
|
||||
console.error('[PlayLevel] No levelId found!');
|
||||
error = 'No level specified';
|
||||
return;
|
||||
}
|
||||
|
||||
console.log('[PlayLevel] Using levelName:', levelName);
|
||||
|
||||
// Add event listeners
|
||||
window.addEventListener('beforeunload', handleBeforeUnload);
|
||||
window.addEventListener('popstate', handlePopState);
|
||||
|
||||
try {
|
||||
// Hide the Svelte UI overlay (keep in DOM for reactivity)
|
||||
const appElement = document.getElementById('app');
|
||||
if (appElement) {
|
||||
appElement.style.display = 'none';
|
||||
debugLog('[PlayLevel] App UI hidden');
|
||||
}
|
||||
|
||||
// Get the main instance (should already exist from app initialization)
|
||||
mainInstance = (window as any).__mainInstance as Main;
|
||||
|
||||
if (!mainInstance) {
|
||||
throw new Error('Main instance not found');
|
||||
}
|
||||
|
||||
// Get level config from registry
|
||||
const registry = LevelRegistry.getInstance();
|
||||
const levelEntry = await registry.getLevel(levelName);
|
||||
|
||||
if (!levelEntry) {
|
||||
throw new Error(`Level "${levelName}" not found`);
|
||||
}
|
||||
|
||||
debugLog('[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
|
||||
const event = new CustomEvent('levelSelected', {
|
||||
detail: {
|
||||
levelName: levelName,
|
||||
config: levelEntry
|
||||
}
|
||||
});
|
||||
window.dispatchEvent(event);
|
||||
|
||||
isInitialized = true;
|
||||
debugLog('[PlayLevel] Level initialization started');
|
||||
|
||||
} catch (err) {
|
||||
console.error('[PlayLevel] Error initializing level:', err);
|
||||
error = err instanceof Error ? err.message : 'Unknown error';
|
||||
|
||||
// Show UI again on error
|
||||
const appElement = document.getElementById('app');
|
||||
if (appElement) {
|
||||
appElement.style.display = 'block';
|
||||
}
|
||||
|
||||
// Navigate back to home after showing error
|
||||
setTimeout(() => {
|
||||
navigate('/', { replace: true });
|
||||
}, 3000);
|
||||
}
|
||||
});
|
||||
|
||||
onDestroy(async () => {
|
||||
debugLog('[PlayLevel] Component unmounting - cleaning up');
|
||||
|
||||
// Remove event listeners
|
||||
window.removeEventListener('beforeunload', handleBeforeUnload);
|
||||
window.removeEventListener('popstate', handlePopState);
|
||||
|
||||
try {
|
||||
// Call the cleanup method on Main instance
|
||||
if (mainInstance && typeof mainInstance.cleanupAndExit === 'function') {
|
||||
await mainInstance.cleanupAndExit();
|
||||
}
|
||||
|
||||
// Ensure UI is visible again
|
||||
const appElement = document.getElementById('app');
|
||||
if (appElement) {
|
||||
appElement.style.display = 'block';
|
||||
debugLog('[PlayLevel] App UI restored');
|
||||
}
|
||||
|
||||
} catch (err) {
|
||||
console.error('[PlayLevel] Error during cleanup:', err);
|
||||
|
||||
// Force UI to show even if cleanup failed
|
||||
const appElement = document.getElementById('app');
|
||||
if (appElement) {
|
||||
appElement.style.display = 'block';
|
||||
}
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<!-- Minimal template - BabylonJS canvas is fixed background -->
|
||||
<div class="play-level-container">
|
||||
{#if error}
|
||||
<div class="error-overlay">
|
||||
<h2>Error Loading Level</h2>
|
||||
<p>{error}</p>
|
||||
<p>Returning to level select...</p>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.play-level-container {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
pointer-events: none;
|
||||
z-index: -1;
|
||||
}
|
||||
|
||||
.error-overlay {
|
||||
position: fixed;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
background: rgba(0, 0, 0, 0.9);
|
||||
padding: 2rem;
|
||||
border-radius: 10px;
|
||||
color: white;
|
||||
text-align: center;
|
||||
z-index: 1000;
|
||||
pointer-events: auto;
|
||||
}
|
||||
|
||||
.error-overlay h2 {
|
||||
color: #ff4444;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.error-overlay p {
|
||||
margin: 0.5rem 0;
|
||||
}
|
||||
</style>
|
||||
@ -1,34 +1,17 @@
|
||||
<script lang="ts">
|
||||
import { onMount } from 'svelte';
|
||||
import Router from 'svelte-spa-router';
|
||||
import { wrap } from 'svelte-spa-router/wrap';
|
||||
import { Router, Route } from 'svelte-routing';
|
||||
import AppHeader from './AppHeader.svelte';
|
||||
import { navigationStore } from '../../stores/navigation';
|
||||
import { AuthService } from '../../services/authService';
|
||||
import { authStore } from '../../stores/auth';
|
||||
|
||||
// Import game view directly (most common route)
|
||||
// Import game views
|
||||
import LevelSelect from '../game/LevelSelect.svelte';
|
||||
|
||||
// Lazy load other views for better performance
|
||||
const routes = {
|
||||
'/': LevelSelect,
|
||||
'/editor': wrap({
|
||||
asyncComponent: () => import('../editor/LevelEditor.svelte')
|
||||
}),
|
||||
'/settings': wrap({
|
||||
asyncComponent: () => import('../settings/SettingsScreen.svelte')
|
||||
}),
|
||||
'/controls': wrap({
|
||||
asyncComponent: () => import('../controls/ControlsScreen.svelte')
|
||||
}),
|
||||
};
|
||||
|
||||
// Track route changes
|
||||
function routeLoaded(event: CustomEvent) {
|
||||
const { route } = event.detail;
|
||||
navigationStore.setRoute(route);
|
||||
}
|
||||
import PlayLevel from '../game/PlayLevel.svelte';
|
||||
import LevelEditor from '../editor/LevelEditor.svelte';
|
||||
import SettingsScreen from '../settings/SettingsScreen.svelte';
|
||||
import ControlsScreen from '../controls/ControlsScreen.svelte';
|
||||
|
||||
// Initialize Auth0 when component mounts
|
||||
onMount(async () => {
|
||||
@ -50,13 +33,21 @@
|
||||
});
|
||||
</script>
|
||||
|
||||
<Router>
|
||||
<div class="app">
|
||||
<AppHeader />
|
||||
|
||||
<div class="app-content">
|
||||
<Router {routes} on:routeLoaded={routeLoaded} />
|
||||
<Route path="/"><LevelSelect /></Route>
|
||||
<Route path="/play/:levelId" let:params>
|
||||
<PlayLevel {params} />
|
||||
</Route>
|
||||
<Route path="/editor"><LevelEditor /></Route>
|
||||
<Route path="/settings"><SettingsScreen /></Route>
|
||||
<Route path="/controls"><ControlsScreen /></Route>
|
||||
</div>
|
||||
</div>
|
||||
</Router>
|
||||
|
||||
<style>
|
||||
.app {
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
<script lang="ts">
|
||||
import { link } from 'svelte-spa-router';
|
||||
import { Link } from 'svelte-routing';
|
||||
import { authStore } from '../../stores/auth';
|
||||
import UserProfile from '../auth/UserProfile.svelte';
|
||||
|
||||
@ -16,11 +16,11 @@
|
||||
<h1 class="app-title">Space Combat VR</h1>
|
||||
</div>
|
||||
<nav class="header-nav">
|
||||
<a href="/controls" use:link class="nav-link controls-link">🎮 Customize Controls</a>
|
||||
<Link to="/controls" class="nav-link controls-link">🎮 Customize Controls</Link>
|
||||
<UserProfile />
|
||||
<a href="/editor" use:link class="nav-link editor-link">📝 Level Editor</a>
|
||||
<Link to="/editor" class="nav-link editor-link">📝 Level Editor</Link>
|
||||
|
||||
<a href="/settings" use:link class="nav-link settings-link">⚙️ Settings</a>
|
||||
<Link to="/settings" class="nav-link settings-link">⚙️ Settings</Link>
|
||||
</nav>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
<script lang="ts">
|
||||
import { onMount } from 'svelte';
|
||||
import { link } from 'svelte-spa-router';
|
||||
import { Link } from 'svelte-routing';
|
||||
import { gameConfigStore } from '../../stores/gameConfig';
|
||||
import Button from '../shared/Button.svelte';
|
||||
import Section from '../shared/Section.svelte';
|
||||
@ -46,7 +46,7 @@
|
||||
</script>
|
||||
|
||||
<div class="editor-container">
|
||||
<a href="/" use:link class="back-link">← Back to Game</a>
|
||||
<Link to="/" class="back-link">← Back to Game</Link>
|
||||
|
||||
<h1>⚙️ Game Settings</h1>
|
||||
<p class="subtitle">Configure graphics quality and physics settings</p>
|
||||
|
||||
@ -35,9 +35,9 @@ export class Rock {
|
||||
}
|
||||
|
||||
export class RockFactory {
|
||||
private static _asteroidMesh: AbstractMesh;
|
||||
private static _explosionManager: ExplosionManager;
|
||||
private static _orbitCenter: PhysicsAggregate;
|
||||
private static _asteroidMesh: AbstractMesh | null = null;
|
||||
private static _explosionManager: ExplosionManager | null = null;
|
||||
private static _orbitCenter: PhysicsAggregate | null = null;
|
||||
|
||||
/**
|
||||
* Initialize non-audio assets (meshes, explosion manager)
|
||||
@ -56,25 +56,46 @@ export class RockFactory {
|
||||
});
|
||||
await this._explosionManager.initialize();
|
||||
|
||||
if (!this._asteroidMesh) {
|
||||
// Reload mesh if not loaded or if it was disposed during cleanup
|
||||
if (!this._asteroidMesh || this._asteroidMesh.isDisposed()) {
|
||||
await this.loadMesh();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Reset static state - call during game cleanup
|
||||
*/
|
||||
public static reset(): void {
|
||||
debugLog('[RockFactory] Resetting static state');
|
||||
this._asteroidMesh = null;
|
||||
if (this._explosionManager) {
|
||||
this._explosionManager.dispose();
|
||||
this._explosionManager = null;
|
||||
}
|
||||
if (this._orbitCenter) {
|
||||
this._orbitCenter.dispose();
|
||||
this._orbitCenter = null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize audio (explosion sound)
|
||||
* Call this AFTER audio engine is unlocked
|
||||
*/
|
||||
public static async initAudio(audioEngine: AudioEngineV2) {
|
||||
debugLog('[RockFactory] Initializing audio via ExplosionManager');
|
||||
if (this._explosionManager) {
|
||||
await this._explosionManager.initAudio(audioEngine);
|
||||
}
|
||||
debugLog('[RockFactory] Audio initialization complete');
|
||||
}
|
||||
private static async loadMesh() {
|
||||
debugLog('loading mesh');
|
||||
this._asteroidMesh = (await loadAsset("asteroid.glb")).meshes.get('Asteroid');
|
||||
//this._asteroidMesh.setParent(null);
|
||||
const asset = await loadAsset("asteroid.glb");
|
||||
this._asteroidMesh = asset.meshes.get('Asteroid') || null;
|
||||
if (this._asteroidMesh) {
|
||||
this._asteroidMesh.setEnabled(false);
|
||||
}
|
||||
debugLog(this._asteroidMesh);
|
||||
}
|
||||
|
||||
@ -82,6 +103,10 @@ export class RockFactory {
|
||||
linearVelocitry: Vector3, angularVelocity: Vector3, score: Observable<ScoreEvent>,
|
||||
useOrbitConstraint: boolean = true): Promise<Rock> {
|
||||
|
||||
if (!this._asteroidMesh) {
|
||||
throw new Error('[RockFactory] Asteroid mesh not loaded. Call init() first.');
|
||||
}
|
||||
|
||||
const rock = new InstancedMesh("asteroid-" +i, this._asteroidMesh as Mesh);
|
||||
debugLog(rock.id);
|
||||
rock.scaling = new Vector3(scale, scale, scale);
|
||||
@ -104,8 +129,8 @@ export class RockFactory {
|
||||
}, DefaultScene.MainScene);
|
||||
const body = agg.body;
|
||||
|
||||
// Only apply orbit constraint if enabled for this level
|
||||
if (useOrbitConstraint) {
|
||||
// Only apply orbit constraint if enabled for this level and orbit center exists
|
||||
if (useOrbitConstraint && this._orbitCenter) {
|
||||
debugLog(`[RockFactory] Applying orbit constraint for ${rock.name}`);
|
||||
const constraint = new DistanceConstraint(Vector3.Distance(position, this._orbitCenter.body.transformNode.position), DefaultScene.MainScene);
|
||||
body.addConstraint(this._orbitCenter.body, constraint);
|
||||
@ -160,7 +185,9 @@ export class RockFactory {
|
||||
|
||||
// Play explosion (visual + audio handled by ExplosionManager)
|
||||
// Note: ExplosionManager will dispose the asteroid mesh after explosion
|
||||
if (RockFactory._explosionManager) {
|
||||
RockFactory._explosionManager.playExplosion(asteroidMesh);
|
||||
}
|
||||
|
||||
// Dispose projectile physics objects
|
||||
debugLog('[RockFactory] Disposing projectile physics objects...');
|
||||
|
||||
@ -193,6 +193,9 @@ export class Level1 implements Level {
|
||||
this._gameStarted = true;
|
||||
debugLog('[Level1] Starting gameplay');
|
||||
|
||||
// Enable game end condition checking on ship
|
||||
this._ship.startGameplay();
|
||||
|
||||
// Start game timer
|
||||
this._ship.gameStats.startTimer();
|
||||
debugLog('Game timer started');
|
||||
@ -275,7 +278,9 @@ export class Level1 implements Level {
|
||||
if (this._startBase) {
|
||||
this._startBase.dispose();
|
||||
}
|
||||
if (this._endBase) {
|
||||
this._endBase.dispose();
|
||||
}
|
||||
if (this._backgroundStars) {
|
||||
this._backgroundStars.dispose();
|
||||
}
|
||||
@ -285,6 +290,12 @@ export class Level1 implements Level {
|
||||
if (this._missionBrief) {
|
||||
this._missionBrief.dispose();
|
||||
}
|
||||
if (this._ship) {
|
||||
this._ship.dispose();
|
||||
}
|
||||
if (this._backgroundMusic) {
|
||||
this._backgroundMusic.dispose();
|
||||
}
|
||||
}
|
||||
|
||||
public async initialize() {
|
||||
|
||||
124
src/main.ts
124
src/main.ts
@ -262,14 +262,12 @@ export class Main {
|
||||
preloader.hide();
|
||||
}, 500);
|
||||
|
||||
// Remove UI
|
||||
console.log('[Main] ========== ABOUT TO REMOVE MAIN DIV ==========');
|
||||
// Hide UI (no longer remove from DOM - let Svelte routing handle it)
|
||||
console.log('[Main] ========== HIDING UI FOR GAMEPLAY ==========');
|
||||
console.log('[Main] mainDiv exists:', !!mainDiv);
|
||||
console.log('[Main] Timestamp:', Date.now());
|
||||
if (mainDiv) {
|
||||
mainDiv.remove();
|
||||
console.log('[Main] mainDiv removed from DOM');
|
||||
}
|
||||
// Note: With route-based loading, the app will be hidden by PlayLevel component
|
||||
// This code path is only used when dispatching levelSelected event (legacy support)
|
||||
|
||||
// Start the game (XR session already active, or flat mode)
|
||||
console.log('[Main] About to call this.play()');
|
||||
@ -346,10 +344,12 @@ export class Main {
|
||||
debugLog('[Main] ========== TEST LEVEL READY OBSERVABLE FIRED ==========');
|
||||
setLoadingMessage("Test Scene Ready! Entering VR...");
|
||||
|
||||
// Remove UI and play immediately (must maintain user activation for XR)
|
||||
if (mainDiv) {
|
||||
mainDiv.remove();
|
||||
debugLog('[Main] mainDiv removed');
|
||||
// Hide UI for gameplay (no longer remove from DOM)
|
||||
// Test level doesn't use routing, so we need to hide the app element
|
||||
const appElement = document.getElementById('app');
|
||||
if (appElement) {
|
||||
appElement.style.display = 'none';
|
||||
debugLog('[Main] App UI hidden for test level');
|
||||
}
|
||||
debugLog('[Main] About to call this.play()...');
|
||||
await this.play();
|
||||
@ -493,6 +493,102 @@ export class Main {
|
||||
return this._audioEngine;
|
||||
}
|
||||
|
||||
/**
|
||||
* Cleanup and exit XR gracefully, returning to main menu
|
||||
*/
|
||||
public async cleanupAndExit(): Promise<void> {
|
||||
debugLog('[Main] cleanupAndExit() called - starting graceful shutdown');
|
||||
|
||||
try {
|
||||
// 1. Stop render loop first (before disposing anything)
|
||||
debugLog('[Main] Stopping render loop...');
|
||||
this._engine.stopRenderLoop();
|
||||
|
||||
// 2. Dispose current level and all its resources (includes ship, weapons, etc.)
|
||||
if (this._currentLevel) {
|
||||
debugLog('[Main] Disposing level...');
|
||||
this._currentLevel.dispose();
|
||||
this._currentLevel = null;
|
||||
}
|
||||
|
||||
// 2.5. Reset RockFactory static state (asteroid mesh, explosion manager, etc.)
|
||||
RockFactory.reset();
|
||||
|
||||
// 3. Exit XR session if active (after disposing level to avoid state issues)
|
||||
if (DefaultScene.XR && DefaultScene.XR.baseExperience.state === 2) { // WebXRState.IN_XR = 2
|
||||
debugLog('[Main] Exiting XR session...');
|
||||
try {
|
||||
await DefaultScene.XR.baseExperience.exitXRAsync();
|
||||
debugLog('[Main] XR session exited successfully');
|
||||
} catch (error) {
|
||||
debugLog('[Main] Error exiting XR session:', error);
|
||||
}
|
||||
}
|
||||
|
||||
// 4. Clear remaining scene objects (anything not disposed by level)
|
||||
if (DefaultScene.MainScene) {
|
||||
debugLog('[Main] Disposing remaining scene meshes and materials...');
|
||||
// Clone arrays to avoid modification during iteration
|
||||
const meshes = DefaultScene.MainScene.meshes.slice();
|
||||
const materials = DefaultScene.MainScene.materials.slice();
|
||||
|
||||
meshes.forEach(mesh => {
|
||||
if (!mesh.isDisposed()) {
|
||||
try {
|
||||
mesh.dispose();
|
||||
} catch (error) {
|
||||
debugLog('[Main] Error disposing mesh:', error);
|
||||
}
|
||||
}
|
||||
});
|
||||
materials.forEach(material => {
|
||||
try {
|
||||
material.dispose();
|
||||
} catch (error) {
|
||||
debugLog('[Main] Error disposing material:', error);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// 5. Disable physics engine (properly disposes AND clears scene reference)
|
||||
if (DefaultScene.MainScene && DefaultScene.MainScene.isPhysicsEnabled()) {
|
||||
debugLog('[Main] Disabling physics engine...');
|
||||
DefaultScene.MainScene.disablePhysicsEngine();
|
||||
}
|
||||
|
||||
// 6. Clear XR reference (will be recreated on next game start)
|
||||
DefaultScene.XR = null;
|
||||
|
||||
// 7. Reset initialization flags so game can be restarted
|
||||
this._initialized = false;
|
||||
this._assetsLoaded = false;
|
||||
this._started = false;
|
||||
|
||||
// 8. Restart render loop with empty scene
|
||||
debugLog('[Main] Restarting render loop with empty scene...');
|
||||
this._engine.runRenderLoop(() => {
|
||||
if (DefaultScene.MainScene) {
|
||||
DefaultScene.MainScene.render();
|
||||
}
|
||||
});
|
||||
|
||||
// 9. Show Discord widget (UI will be shown by Svelte router)
|
||||
const discord = (window as any).__discordWidget as DiscordWidget;
|
||||
if (discord) {
|
||||
debugLog('[Main] Showing Discord widget');
|
||||
discord.show();
|
||||
}
|
||||
|
||||
debugLog('[Main] Cleanup complete - ready for new game');
|
||||
|
||||
} catch (error) {
|
||||
console.error('[Main] Error during cleanup:', error);
|
||||
// If cleanup fails, fall back to page reload
|
||||
debugLog('[Main] Cleanup failed, falling back to page reload');
|
||||
window.location.reload();
|
||||
}
|
||||
}
|
||||
|
||||
public async play() {
|
||||
debugLog('[Main] play() called');
|
||||
debugLog('[Main] Current level exists:', !!this._currentLevel);
|
||||
@ -789,7 +885,7 @@ async function initializeApp() {
|
||||
await LevelRegistry.getInstance().initialize();
|
||||
console.log('[Main] LevelRegistry.initialize() completed successfully [AFTER MIGRATION]');
|
||||
debugLog('[Main] LevelRegistry initialized after migration');
|
||||
// NOTE: Old router disabled - now using svelte-spa-router
|
||||
// NOTE: Old router disabled - now using svelte-routing
|
||||
// router.start();
|
||||
|
||||
// Mount Svelte app
|
||||
@ -817,7 +913,7 @@ async function initializeApp() {
|
||||
resolve();
|
||||
} catch (error) {
|
||||
console.error('[Main] Failed to initialize LevelRegistry after migration:', error);
|
||||
// NOTE: Old router disabled - now using svelte-spa-router
|
||||
// NOTE: Old router disabled - now using svelte-routing
|
||||
// router.start(); // Start anyway to show error state
|
||||
resolve();
|
||||
}
|
||||
@ -844,7 +940,7 @@ async function initializeApp() {
|
||||
console.log('[Main] To clear caches: window.__levelRegistry.clearAllCaches().then(() => location.reload())');
|
||||
}
|
||||
|
||||
// NOTE: Old router disabled - now using svelte-spa-router
|
||||
// NOTE: Old router disabled - now using svelte-routing
|
||||
// console.log('[Main] About to call router.start()');
|
||||
// router.start();
|
||||
// console.log('[Main] router.start() completed');
|
||||
@ -852,7 +948,7 @@ async function initializeApp() {
|
||||
console.error('[Main] !!!!! EXCEPTION in LevelRegistry initialization !!!!!');
|
||||
console.error('[Main] Failed to initialize LevelRegistry:', error);
|
||||
console.error('[Main] Error stack:', error?.stack);
|
||||
// NOTE: Old router disabled - now using svelte-spa-router
|
||||
// NOTE: Old router disabled - now using svelte-routing
|
||||
// router.start(); // Start anyway to show error state
|
||||
}
|
||||
}
|
||||
|
||||
@ -68,9 +68,16 @@ export class Ship {
|
||||
// Auto-show status screen flag
|
||||
private _statusScreenAutoShown: boolean = false;
|
||||
|
||||
// Flag to prevent game end checks until gameplay has started
|
||||
private _gameplayStarted: boolean = false;
|
||||
|
||||
// Controls enabled state
|
||||
private _controlsEnabled: boolean = true;
|
||||
|
||||
// Scene observer references (for cleanup)
|
||||
private _physicsObserver: any = null;
|
||||
private _renderObserver: any = null;
|
||||
|
||||
constructor(audioEngine?: AudioEngineV2, isReplayMode: boolean = false) {
|
||||
this._audioEngine = audioEngine;
|
||||
this._isReplayMode = isReplayMode;
|
||||
@ -92,6 +99,15 @@ export class Ship {
|
||||
return this._isInLandingZone;
|
||||
}
|
||||
|
||||
/**
|
||||
* Start gameplay - enables game end condition checking
|
||||
* Call this after level initialization is complete
|
||||
*/
|
||||
public startGameplay(): void {
|
||||
this._gameplayStarted = true;
|
||||
debugLog('[Ship] Gameplay started - game end conditions now active');
|
||||
}
|
||||
|
||||
public get onMissionBriefTriggerObservable(): Observable<void> {
|
||||
return this._onMissionBriefTriggerObservable;
|
||||
}
|
||||
@ -316,10 +332,10 @@ export class Ship {
|
||||
this._physics.setGameStats(this._gameStats);
|
||||
|
||||
// Setup physics update loop (every 10 frames)
|
||||
DefaultScene.MainScene.onAfterPhysicsObservable.add(() => {
|
||||
this._physicsObserver = DefaultScene.MainScene.onAfterPhysicsObservable.add(() => {
|
||||
this.updatePhysics();
|
||||
})
|
||||
DefaultScene.MainScene.onAfterRenderObservable.add(() => {
|
||||
});
|
||||
this._renderObserver = DefaultScene.MainScene.onAfterRenderObservable.add(() => {
|
||||
// Update voice audio system (checks for completed sounds and plays next in queue)
|
||||
if (this._voiceAudio) {
|
||||
this._voiceAudio.update();
|
||||
@ -422,10 +438,19 @@ export class Ship {
|
||||
/**
|
||||
* Handle exit VR button click from status screen
|
||||
*/
|
||||
private handleExitVR(): void {
|
||||
debugLog('Exit VR button clicked - refreshing browser');
|
||||
private async handleExitVR(): Promise<void> {
|
||||
debugLog('Exit VR button clicked - navigating to home');
|
||||
|
||||
try {
|
||||
// Navigate back to home route
|
||||
// The PlayLevel component's onDestroy will handle cleanup
|
||||
const { navigate } = await import('svelte-routing');
|
||||
navigate('/', { replace: true });
|
||||
} catch (error) {
|
||||
console.error('Failed to navigate, falling back to reload:', error);
|
||||
window.location.reload();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle resume button click from status screen
|
||||
@ -454,6 +479,11 @@ export class Ship {
|
||||
* 3. All asteroids destroyed AND ship inside landing zone (victory)
|
||||
*/
|
||||
private checkGameEndConditions(): void {
|
||||
// Skip if gameplay hasn't started yet (prevents false triggers during initialization)
|
||||
if (!this._gameplayStarted) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Skip if already auto-shown or status screen doesn't exist
|
||||
if (this._statusScreenAutoShown || !this._statusScreen || !this._scoreboard) {
|
||||
return;
|
||||
@ -492,7 +522,8 @@ export class Ship {
|
||||
}
|
||||
|
||||
// Check condition 3: Victory (all asteroids destroyed, inside landing zone)
|
||||
if (asteroidsRemaining <= 0 && this._isInLandingZone) {
|
||||
// Must have had asteroids to destroy in the first place (prevents false victory on init)
|
||||
if (asteroidsRemaining <= 0 && this._isInLandingZone && this._scoreboard.hasAsteroidsToDestroy) {
|
||||
debugLog('Game end condition met: Victory (all asteroids destroyed)');
|
||||
this._statusScreen.show(true, true); // Game ended, VICTORY!
|
||||
// InputControlManager will handle disabling controls when status screen shows
|
||||
@ -679,6 +710,17 @@ export class Ship {
|
||||
* Dispose of ship resources
|
||||
*/
|
||||
public dispose(): void {
|
||||
// Remove scene observers first to stop update loops
|
||||
if (this._physicsObserver) {
|
||||
DefaultScene.MainScene?.onAfterPhysicsObservable.remove(this._physicsObserver);
|
||||
this._physicsObserver = null;
|
||||
}
|
||||
|
||||
if (this._renderObserver) {
|
||||
DefaultScene.MainScene?.onAfterRenderObservable.remove(this._renderObserver);
|
||||
this._renderObserver = null;
|
||||
}
|
||||
|
||||
if (this._sight) {
|
||||
this._sight.dispose();
|
||||
}
|
||||
|
||||
@ -66,7 +66,7 @@ export class ShipEngine {
|
||||
//myParticleSystem.minEmitPower = 2;
|
||||
//myParticleSystem.maxEmitPower = 10;
|
||||
|
||||
myParticleSystem.particleTexture = new Texture("./flare.png");
|
||||
myParticleSystem.particleTexture = new Texture("/flare.png");
|
||||
myParticleSystem.emitter = mesh;
|
||||
const coneEmitter = myParticleSystem.createConeEmitter(0.1, Math.PI / 9);
|
||||
myParticleSystem.addSizeGradient(0, .01);
|
||||
|
||||
@ -18,6 +18,7 @@ export type ScoreEvent = {
|
||||
export class Scoreboard {
|
||||
private _score: number = 0;
|
||||
private _remaining: number = 0;
|
||||
private _initialAsteroidCount: number = 0;
|
||||
private _startTime: number = Date.now();
|
||||
|
||||
private _active = false;
|
||||
@ -76,6 +77,17 @@ export class Scoreboard {
|
||||
|
||||
public setRemainingCount(count: number) {
|
||||
this._remaining = count;
|
||||
// Track initial count for victory validation
|
||||
if (this._initialAsteroidCount === 0 && count > 0) {
|
||||
this._initialAsteroidCount = count;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if asteroids were properly initialized (count > 0)
|
||||
*/
|
||||
public get hasAsteroidsToDestroy(): boolean {
|
||||
return this._initialAsteroidCount > 0;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@ -7,7 +7,7 @@ export type LoadedAsset = {
|
||||
meshes: Map<string, AbstractMesh>,
|
||||
}
|
||||
export default async function loadAsset(file: string, theme: string = "default"): Promise<LoadedAsset> {
|
||||
const assetPath = `assets/themes/${theme}/models/${file}`;
|
||||
const assetPath = `/assets/themes/${theme}/models/${file}`;
|
||||
debugLog(`[loadAsset] Loading: ${assetPath}`);
|
||||
|
||||
try {
|
||||
|
||||
@ -39,9 +39,9 @@ export default defineConfig({
|
||||
},
|
||||
server: {
|
||||
port: 3000,
|
||||
|
||||
allowedHosts: true
|
||||
},
|
||||
// appType: 'spa' is default - Vite automatically serves index.html for SPA routes
|
||||
preview: {
|
||||
port: 3000,
|
||||
},
|
||||
|
||||
Loading…
Reference in New Issue
Block a user