Compare commits
No commits in common. "3f164df9e806fe99703739f8fc511ca6328904a5" and "622e0a52596bb368f3e2f89489fb018b8e74537c" have entirely different histories.
3f164df9e8
...
622e0a5259
27
package-lock.json
generated
27
package-lock.json
generated
@ -20,7 +20,7 @@
|
|||||||
"@newrelic/browser-agent": "^1.302.0",
|
"@newrelic/browser-agent": "^1.302.0",
|
||||||
"loglevel": "^1.9.2",
|
"loglevel": "^1.9.2",
|
||||||
"openai": "4.52.3",
|
"openai": "4.52.3",
|
||||||
"svelte-routing": "^2.13.0"
|
"svelte-spa-router": "^4.0.1"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@sveltejs/vite-plugin-svelte": "^6.2.1",
|
"@sveltejs/vite-plugin-svelte": "^6.2.1",
|
||||||
@ -1612,6 +1612,15 @@
|
|||||||
"node": "^10 || ^12 || >=14"
|
"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": {
|
"node_modules/resolve-pkg-maps": {
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/resolve-pkg-maps/-/resolve-pkg-maps-1.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/resolve-pkg-maps/-/resolve-pkg-maps-1.0.0.tgz",
|
||||||
@ -1752,11 +1761,17 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/svelte-routing": {
|
"node_modules/svelte-spa-router": {
|
||||||
"version": "2.13.0",
|
"version": "4.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/svelte-routing/-/svelte-routing-2.13.0.tgz",
|
"resolved": "https://registry.npmjs.org/svelte-spa-router/-/svelte-spa-router-4.0.1.tgz",
|
||||||
"integrity": "sha512-/NTxqTwLc7Dq306hARJrH2HLXOBtKd7hu8nxgoFDlK0AC4SOKnzisiX/9m8Uksei1QAWtlAEdF91YphNM8iDMg==",
|
"integrity": "sha512-2JkmUQ2f9jRluijL58LtdQBIpynSbem2eBGp4zXdi7aDY1znbR6yjw0KsonD0aq2QLwf4Yx4tBJQjxIjgjXHKg==",
|
||||||
"license": "MIT"
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"regexparam": "2.0.2"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/ItalyPaleAle"
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"node_modules/tinyglobby": {
|
"node_modules/tinyglobby": {
|
||||||
"version": "0.2.15",
|
"version": "0.2.15",
|
||||||
|
|||||||
@ -25,9 +25,9 @@
|
|||||||
"@babylonjs/procedural-textures": "8.36.1",
|
"@babylonjs/procedural-textures": "8.36.1",
|
||||||
"@babylonjs/serializers": "8.36.1",
|
"@babylonjs/serializers": "8.36.1",
|
||||||
"@newrelic/browser-agent": "^1.302.0",
|
"@newrelic/browser-agent": "^1.302.0",
|
||||||
"loglevel": "^1.9.2",
|
|
||||||
"openai": "4.52.3",
|
"openai": "4.52.3",
|
||||||
"svelte-routing": "^2.13.0"
|
"loglevel": "^1.9.2",
|
||||||
|
"svelte-spa-router": "^4.0.1"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@sveltejs/vite-plugin-svelte": "^6.2.1",
|
"@sveltejs/vite-plugin-svelte": "^6.2.1",
|
||||||
|
|||||||
BIN
public/8192.webp
Normal file
BIN
public/8192.webp
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 2.0 MiB |
BIN
public/flare.png
Normal file
BIN
public/flare.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 7.4 KiB |
@ -179,16 +179,6 @@ body {
|
|||||||
transform: scale(1.05);
|
transform: scale(1.05);
|
||||||
}
|
}
|
||||||
|
|
||||||
.leaderboard-link {
|
|
||||||
background: rgba(255, 215, 0, 0.8);
|
|
||||||
color: #000 !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.leaderboard-link:hover {
|
|
||||||
background: rgba(255, 215, 0, 1);
|
|
||||||
transform: scale(1.05);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* User Profile in Header */
|
/* User Profile in Header */
|
||||||
.user-profile {
|
.user-profile {
|
||||||
display: flex;
|
display: flex;
|
||||||
|
|||||||
BIN
public/thrust.mp3
Normal file
BIN
public/thrust.mp3
Normal file
Binary file not shown.
BIN
public/thrust2.mp3
Normal file
BIN
public/thrust2.mp3
Normal file
Binary file not shown.
BIN
public/thrust3.mp3
Normal file
BIN
public/thrust3.mp3
Normal file
Binary file not shown.
BIN
public/thrust4.mp3
Normal file
BIN
public/thrust4.mp3
Normal file
Binary file not shown.
BIN
public/thrust5.mp3
Normal file
BIN
public/thrust5.mp3
Normal file
Binary file not shown.
BIN
public/thust2.mp3
Normal file
BIN
public/thust2.mp3
Normal file
Binary file not shown.
BIN
public/yehrat.mp3
Normal file
BIN
public/yehrat.mp3
Normal file
Binary file not shown.
@ -1,6 +1,6 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { onMount } from 'svelte';
|
import { onMount } from 'svelte';
|
||||||
import { Link } from 'svelte-routing';
|
import { link } from 'svelte-spa-router';
|
||||||
import { controllerMappingStore } from '../../stores/controllerMapping';
|
import { controllerMappingStore } from '../../stores/controllerMapping';
|
||||||
import { ControllerMappingConfig } from '../../ship/input/controllerMapping';
|
import { ControllerMappingConfig } from '../../ship/input/controllerMapping';
|
||||||
import Button from '../shared/Button.svelte';
|
import Button from '../shared/Button.svelte';
|
||||||
@ -81,7 +81,7 @@
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="editor-container">
|
<div class="editor-container">
|
||||||
<Link to="/" class="back-link">← Back to Game</Link>
|
<a href="/" use:link class="back-link">← Back to Game</a>
|
||||||
|
|
||||||
<h1>🎮 Controller Mapping</h1>
|
<h1>🎮 Controller Mapping</h1>
|
||||||
<p class="subtitle">Customize VR controller button and stick mappings</p>
|
<p class="subtitle">Customize VR controller button and stick mappings</p>
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { Link } from 'svelte-routing';
|
import { link } from 'svelte-spa-router';
|
||||||
import Button from '../shared/Button.svelte';
|
import Button from '../shared/Button.svelte';
|
||||||
import Section from '../shared/Section.svelte';
|
import Section from '../shared/Section.svelte';
|
||||||
|
|
||||||
@ -8,7 +8,7 @@
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="editor-container">
|
<div class="editor-container">
|
||||||
<Link to="/" class="back-link">← Back to Game</Link>
|
<a href="/" use:link class="back-link">← Back to Game</a>
|
||||||
|
|
||||||
<h1>📝 Level Editor</h1>
|
<h1>📝 Level Editor</h1>
|
||||||
<p class="subtitle">Create and customize your own asteroid field levels</p>
|
<p class="subtitle">Create and customize your own asteroid field levels</p>
|
||||||
|
|||||||
@ -1,5 +1,4 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { navigate } from 'svelte-routing';
|
|
||||||
import type { LevelDirectoryEntry } from '../../levels/storage/levelRegistry';
|
import type { LevelDirectoryEntry } from '../../levels/storage/levelRegistry';
|
||||||
import { levelRegistryStore } from '../../stores/levelRegistry';
|
import { levelRegistryStore } from '../../stores/levelRegistry';
|
||||||
import { authStore } from '../../stores/auth';
|
import { authStore } from '../../stores/auth';
|
||||||
@ -38,9 +37,17 @@
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Navigate to level play route
|
// Dispatch custom event for main.ts to handle
|
||||||
console.log('[LevelCard] Level unlocked, navigating to /play/' + levelId);
|
console.log('[LevelCard] Level unlocked, loading config...');
|
||||||
navigate(`/play/${levelId}`);
|
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');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function handleDelete() {
|
async function handleDelete() {
|
||||||
|
|||||||
@ -1,6 +1,7 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { levelRegistryStore } from '../../stores/levelRegistry';
|
import { levelRegistryStore } from '../../stores/levelRegistry';
|
||||||
import { authStore } from '../../stores/auth';
|
import { authStore } from '../../stores/auth';
|
||||||
|
import { link } from 'svelte-spa-router';
|
||||||
import LevelCard from './LevelCard.svelte';
|
import LevelCard from './LevelCard.svelte';
|
||||||
import ProgressBar from './ProgressBar.svelte';
|
import ProgressBar from './ProgressBar.svelte';
|
||||||
|
|
||||||
|
|||||||
@ -1,200 +0,0 @@
|
|||||||
<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 () => {
|
|
||||||
console.log('[PlayLevel] Component unmounting - cleaning up');
|
|
||||||
debugLog('[PlayLevel] Component unmounting - cleaning up');
|
|
||||||
|
|
||||||
// Remove event listeners
|
|
||||||
window.removeEventListener('beforeunload', handleBeforeUnload);
|
|
||||||
window.removeEventListener('popstate', handlePopState);
|
|
||||||
|
|
||||||
// Ensure UI is visible again FIRST (before any async operations)
|
|
||||||
const appElement = document.getElementById('app');
|
|
||||||
if (appElement) {
|
|
||||||
appElement.style.display = 'block';
|
|
||||||
console.log('[PlayLevel] App UI restored');
|
|
||||||
debugLog('[PlayLevel] App UI restored');
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
// Call the cleanup method on Main instance
|
|
||||||
if (mainInstance && typeof mainInstance.cleanupAndExit === 'function') {
|
|
||||||
await mainInstance.cleanupAndExit();
|
|
||||||
}
|
|
||||||
} catch (err) {
|
|
||||||
console.error('[PlayLevel] Error during cleanup:', err);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
</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,18 +1,34 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { onMount } from 'svelte';
|
import { onMount } from 'svelte';
|
||||||
import { Router, Route } from 'svelte-routing';
|
import Router from 'svelte-spa-router';
|
||||||
|
import { wrap } from 'svelte-spa-router/wrap';
|
||||||
import AppHeader from './AppHeader.svelte';
|
import AppHeader from './AppHeader.svelte';
|
||||||
import { navigationStore } from '../../stores/navigation';
|
import { navigationStore } from '../../stores/navigation';
|
||||||
import { AuthService } from '../../services/authService';
|
import { AuthService } from '../../services/authService';
|
||||||
import { authStore } from '../../stores/auth';
|
import { authStore } from '../../stores/auth';
|
||||||
|
|
||||||
// Import game views
|
// Import game view directly (most common route)
|
||||||
import LevelSelect from '../game/LevelSelect.svelte';
|
import LevelSelect from '../game/LevelSelect.svelte';
|
||||||
import PlayLevel from '../game/PlayLevel.svelte';
|
|
||||||
import LevelEditor from '../editor/LevelEditor.svelte';
|
// Lazy load other views for better performance
|
||||||
import SettingsScreen from '../settings/SettingsScreen.svelte';
|
const routes = {
|
||||||
import ControlsScreen from '../controls/ControlsScreen.svelte';
|
'/': LevelSelect,
|
||||||
import Leaderboard from '../leaderboard/Leaderboard.svelte';
|
'/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);
|
||||||
|
}
|
||||||
|
|
||||||
// Initialize Auth0 when component mounts
|
// Initialize Auth0 when component mounts
|
||||||
onMount(async () => {
|
onMount(async () => {
|
||||||
@ -34,22 +50,13 @@
|
|||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<Router>
|
<div class="app">
|
||||||
<div class="app">
|
<AppHeader />
|
||||||
<AppHeader />
|
|
||||||
|
|
||||||
<div class="app-content">
|
<div class="app-content">
|
||||||
<Route path="/"><LevelSelect /></Route>
|
<Router {routes} on:routeLoaded={routeLoaded} />
|
||||||
<Route path="/play/:levelId" let:params>
|
|
||||||
<PlayLevel {params} />
|
|
||||||
</Route>
|
|
||||||
<Route path="/editor"><LevelEditor /></Route>
|
|
||||||
<Route path="/settings"><SettingsScreen /></Route>
|
|
||||||
<Route path="/controls"><ControlsScreen /></Route>
|
|
||||||
<Route path="/leaderboard"><Leaderboard /></Route>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</Router>
|
</div>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
.app {
|
.app {
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { Link } from 'svelte-routing';
|
import { link } from 'svelte-spa-router';
|
||||||
import { authStore } from '../../stores/auth';
|
import { authStore } from '../../stores/auth';
|
||||||
import UserProfile from '../auth/UserProfile.svelte';
|
import UserProfile from '../auth/UserProfile.svelte';
|
||||||
|
|
||||||
@ -16,11 +16,11 @@
|
|||||||
<h1 class="app-title">Space Combat VR</h1>
|
<h1 class="app-title">Space Combat VR</h1>
|
||||||
</div>
|
</div>
|
||||||
<nav class="header-nav">
|
<nav class="header-nav">
|
||||||
<Link to="/controls" class="nav-link controls-link">🎮 Customize Controls</Link>
|
<a href="/controls" use:link class="nav-link controls-link">🎮 Customize Controls</a>
|
||||||
<Link to="/leaderboard" class="nav-link leaderboard-link">🏆 Leaderboard</Link>
|
|
||||||
<UserProfile />
|
<UserProfile />
|
||||||
<Link to="/editor" class="nav-link editor-link">📝 Level Editor</Link>
|
<a href="/editor" use:link class="nav-link editor-link">📝 Level Editor</a>
|
||||||
<Link to="/settings" class="nav-link settings-link">⚙️ Settings</Link>
|
|
||||||
|
<a href="/settings" use:link class="nav-link settings-link">⚙️ Settings</a>
|
||||||
</nav>
|
</nav>
|
||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
|
|||||||
@ -1,264 +0,0 @@
|
|||||||
<script lang="ts">
|
|
||||||
import { onMount } from 'svelte';
|
|
||||||
import { Link } from 'svelte-routing';
|
|
||||||
import { gameResultsStore } from '../../stores/gameResults';
|
|
||||||
import type { GameResult } from '../../services/gameResultsService';
|
|
||||||
import { formatStars } from '../../game/scoreCalculator';
|
|
||||||
|
|
||||||
// Refresh data on mount
|
|
||||||
onMount(() => {
|
|
||||||
gameResultsStore.refresh();
|
|
||||||
});
|
|
||||||
|
|
||||||
// Format time as MM:SS
|
|
||||||
function formatTime(seconds: number): string {
|
|
||||||
const mins = Math.floor(seconds / 60);
|
|
||||||
const secs = Math.floor(seconds % 60);
|
|
||||||
return `${mins.toString().padStart(2, '0')}:${secs.toString().padStart(2, '0')}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Format date as readable string
|
|
||||||
function formatDate(timestamp: number): string {
|
|
||||||
const date = new Date(timestamp);
|
|
||||||
return date.toLocaleDateString(undefined, {
|
|
||||||
month: 'short',
|
|
||||||
day: 'numeric',
|
|
||||||
year: 'numeric'
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get color for end reason badge
|
|
||||||
function getEndReasonColor(result: GameResult): string {
|
|
||||||
if (result.endReason === 'victory') return '#4ade80';
|
|
||||||
if (result.endReason === 'death') return '#ef4444';
|
|
||||||
return '#f59e0b'; // stranded
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get emoji for end reason
|
|
||||||
function getEndReasonEmoji(result: GameResult): string {
|
|
||||||
if (result.endReason === 'victory') return '';
|
|
||||||
if (result.endReason === 'death') return '';
|
|
||||||
return '';
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<div class="editor-container">
|
|
||||||
<Link to="/" class="back-link">← Back to Game</Link>
|
|
||||||
|
|
||||||
<h1>Leaderboard</h1>
|
|
||||||
<p class="subtitle">Top 20 High Scores</p>
|
|
||||||
|
|
||||||
<div class="leaderboard-wrapper">
|
|
||||||
{#if $gameResultsStore.length === 0}
|
|
||||||
<div class="no-results">
|
|
||||||
<p>No game results yet!</p>
|
|
||||||
<p class="muted">Play a level to see your scores here.</p>
|
|
||||||
</div>
|
|
||||||
{:else}
|
|
||||||
<table class="leaderboard-table">
|
|
||||||
<thead>
|
|
||||||
<tr>
|
|
||||||
<th class="rank-col">Rank</th>
|
|
||||||
<th class="player-col">Player</th>
|
|
||||||
<th class="level-col">Level</th>
|
|
||||||
<th class="score-col">Score</th>
|
|
||||||
<th class="stars-col">Stars</th>
|
|
||||||
<th class="result-col">Result</th>
|
|
||||||
<th class="time-col">Time</th>
|
|
||||||
<th class="date-col">Date</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody>
|
|
||||||
{#each $gameResultsStore as result, i}
|
|
||||||
<tr class:victory={result.completed}>
|
|
||||||
<td class="rank-col">
|
|
||||||
<span class="rank-badge" class:gold={i === 0} class:silver={i === 1} class:bronze={i === 2}>
|
|
||||||
{i + 1}
|
|
||||||
</span>
|
|
||||||
</td>
|
|
||||||
<td class="player-col">{result.playerName}</td>
|
|
||||||
<td class="level-col">{result.levelName}</td>
|
|
||||||
<td class="score-col">
|
|
||||||
<span class="score-value">{result.finalScore.toLocaleString()}</span>
|
|
||||||
</td>
|
|
||||||
<td class="stars-col">
|
|
||||||
<span class="star-display">{formatStars(result.starRating)}</span>
|
|
||||||
<span class="star-count">{result.starRating}/12</span>
|
|
||||||
</td>
|
|
||||||
<td class="result-col">
|
|
||||||
<span class="result-badge" style="background-color: {getEndReasonColor(result)}">
|
|
||||||
{getEndReasonEmoji(result)} {result.endReason}
|
|
||||||
</span>
|
|
||||||
</td>
|
|
||||||
<td class="time-col">{formatTime(result.gameTimeSeconds)}</td>
|
|
||||||
<td class="date-col">{formatDate(result.timestamp)}</td>
|
|
||||||
</tr>
|
|
||||||
{/each}
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
{/if}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="leaderboard-footer">
|
|
||||||
<p class="muted">Showing top 20 scores sorted by highest score</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<style>
|
|
||||||
.leaderboard-wrapper {
|
|
||||||
background: var(--color-bg-card, rgba(20, 20, 40, 0.9));
|
|
||||||
border: 1px solid var(--color-border-default, rgba(255, 255, 255, 0.2));
|
|
||||||
border-radius: var(--radius-lg, 10px);
|
|
||||||
overflow: hidden;
|
|
||||||
margin-top: var(--space-xl, 32px);
|
|
||||||
}
|
|
||||||
|
|
||||||
.no-results {
|
|
||||||
text-align: center;
|
|
||||||
padding: var(--space-3xl, 64px) var(--space-xl, 32px);
|
|
||||||
color: var(--color-text-secondary, #e8e8e8);
|
|
||||||
}
|
|
||||||
|
|
||||||
.no-results .muted {
|
|
||||||
color: var(--color-text-muted, #aaaaaa);
|
|
||||||
margin-top: var(--space-md, 16px);
|
|
||||||
}
|
|
||||||
|
|
||||||
.leaderboard-table {
|
|
||||||
width: 100%;
|
|
||||||
border-collapse: collapse;
|
|
||||||
font-size: var(--font-size-sm, 0.9rem);
|
|
||||||
}
|
|
||||||
|
|
||||||
.leaderboard-table thead {
|
|
||||||
background: rgba(0, 0, 0, 0.4);
|
|
||||||
}
|
|
||||||
|
|
||||||
.leaderboard-table th {
|
|
||||||
padding: var(--space-md, 16px) var(--space-sm, 8px);
|
|
||||||
text-align: left;
|
|
||||||
font-weight: bold;
|
|
||||||
color: var(--color-text-secondary, #e8e8e8);
|
|
||||||
text-transform: uppercase;
|
|
||||||
font-size: var(--font-size-xs, 0.8rem);
|
|
||||||
letter-spacing: 0.5px;
|
|
||||||
border-bottom: 2px solid rgba(255, 255, 255, 0.1);
|
|
||||||
}
|
|
||||||
|
|
||||||
.leaderboard-table td {
|
|
||||||
padding: var(--space-md, 16px) var(--space-sm, 8px);
|
|
||||||
border-bottom: 1px solid rgba(255, 255, 255, 0.05);
|
|
||||||
vertical-align: middle;
|
|
||||||
}
|
|
||||||
|
|
||||||
.leaderboard-table tbody tr {
|
|
||||||
transition: background var(--transition-fast, 0.2s ease);
|
|
||||||
}
|
|
||||||
|
|
||||||
.leaderboard-table tbody tr:hover {
|
|
||||||
background: rgba(255, 255, 255, 0.05);
|
|
||||||
}
|
|
||||||
|
|
||||||
.leaderboard-table tbody tr.victory {
|
|
||||||
background: rgba(74, 222, 128, 0.05);
|
|
||||||
}
|
|
||||||
|
|
||||||
.leaderboard-table tbody tr.victory:hover {
|
|
||||||
background: rgba(74, 222, 128, 0.1);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Column widths */
|
|
||||||
.rank-col { width: 60px; text-align: center; }
|
|
||||||
.player-col { min-width: 120px; }
|
|
||||||
.level-col { min-width: 140px; }
|
|
||||||
.score-col { width: 100px; text-align: right; }
|
|
||||||
.stars-col { width: 100px; text-align: center; }
|
|
||||||
.result-col { width: 100px; text-align: center; }
|
|
||||||
.time-col { width: 70px; text-align: center; }
|
|
||||||
.date-col { width: 100px; text-align: right; }
|
|
||||||
|
|
||||||
.rank-badge {
|
|
||||||
display: inline-flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
width: 28px;
|
|
||||||
height: 28px;
|
|
||||||
border-radius: 50%;
|
|
||||||
background: rgba(255, 255, 255, 0.1);
|
|
||||||
font-weight: bold;
|
|
||||||
font-size: var(--font-size-sm, 0.9rem);
|
|
||||||
}
|
|
||||||
|
|
||||||
.rank-badge.gold {
|
|
||||||
background: linear-gradient(135deg, #FFD700, #FFA500);
|
|
||||||
color: #000;
|
|
||||||
box-shadow: 0 2px 8px rgba(255, 215, 0, 0.5);
|
|
||||||
}
|
|
||||||
|
|
||||||
.rank-badge.silver {
|
|
||||||
background: linear-gradient(135deg, #C0C0C0, #A0A0A0);
|
|
||||||
color: #000;
|
|
||||||
box-shadow: 0 2px 8px rgba(192, 192, 192, 0.5);
|
|
||||||
}
|
|
||||||
|
|
||||||
.rank-badge.bronze {
|
|
||||||
background: linear-gradient(135deg, #CD7F32, #B87333);
|
|
||||||
color: #000;
|
|
||||||
box-shadow: 0 2px 8px rgba(205, 127, 50, 0.5);
|
|
||||||
}
|
|
||||||
|
|
||||||
.score-value {
|
|
||||||
color: #FFD700;
|
|
||||||
font-weight: bold;
|
|
||||||
font-size: var(--font-size-base, 1rem);
|
|
||||||
}
|
|
||||||
|
|
||||||
.star-display {
|
|
||||||
display: block;
|
|
||||||
font-size: var(--font-size-sm, 0.9rem);
|
|
||||||
color: #FFD700;
|
|
||||||
}
|
|
||||||
|
|
||||||
.star-count {
|
|
||||||
display: block;
|
|
||||||
font-size: var(--font-size-xs, 0.8rem);
|
|
||||||
color: var(--color-text-muted, #aaaaaa);
|
|
||||||
}
|
|
||||||
|
|
||||||
.result-badge {
|
|
||||||
display: inline-block;
|
|
||||||
padding: 4px 8px;
|
|
||||||
border-radius: 4px;
|
|
||||||
font-size: var(--font-size-xs, 0.8rem);
|
|
||||||
font-weight: bold;
|
|
||||||
text-transform: uppercase;
|
|
||||||
color: #000;
|
|
||||||
}
|
|
||||||
|
|
||||||
.leaderboard-footer {
|
|
||||||
text-align: center;
|
|
||||||
padding: var(--space-lg, 24px);
|
|
||||||
}
|
|
||||||
|
|
||||||
.leaderboard-footer .muted {
|
|
||||||
color: var(--color-text-muted, #aaaaaa);
|
|
||||||
font-size: var(--font-size-sm, 0.9rem);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Responsive adjustments */
|
|
||||||
@media (max-width: 768px) {
|
|
||||||
.leaderboard-table {
|
|
||||||
display: block;
|
|
||||||
overflow-x: auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
.leaderboard-table th,
|
|
||||||
.leaderboard-table td {
|
|
||||||
padding: var(--space-sm, 8px) var(--space-xs, 4px);
|
|
||||||
}
|
|
||||||
|
|
||||||
.level-col, .date-col {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
@ -1,6 +1,6 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { onMount } from 'svelte';
|
import { onMount } from 'svelte';
|
||||||
import { Link } from 'svelte-routing';
|
import { link } from 'svelte-spa-router';
|
||||||
import { gameConfigStore } from '../../stores/gameConfig';
|
import { gameConfigStore } from '../../stores/gameConfig';
|
||||||
import Button from '../shared/Button.svelte';
|
import Button from '../shared/Button.svelte';
|
||||||
import Section from '../shared/Section.svelte';
|
import Section from '../shared/Section.svelte';
|
||||||
@ -46,7 +46,7 @@
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="editor-container">
|
<div class="editor-container">
|
||||||
<Link to="/" class="back-link">← Back to Game</Link>
|
<a href="/" use:link class="back-link">← Back to Game</a>
|
||||||
|
|
||||||
<h1>⚙️ Game Settings</h1>
|
<h1>⚙️ Game Settings</h1>
|
||||||
<p class="subtitle">Configure graphics quality and physics settings</p>
|
<p class="subtitle">Configure graphics quality and physics settings</p>
|
||||||
|
|||||||
@ -35,9 +35,9 @@ export class Rock {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export class RockFactory {
|
export class RockFactory {
|
||||||
private static _asteroidMesh: AbstractMesh | null = null;
|
private static _asteroidMesh: AbstractMesh;
|
||||||
private static _explosionManager: ExplosionManager | null = null;
|
private static _explosionManager: ExplosionManager;
|
||||||
private static _orbitCenter: PhysicsAggregate | null = null;
|
private static _orbitCenter: PhysicsAggregate;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Initialize non-audio assets (meshes, explosion manager)
|
* Initialize non-audio assets (meshes, explosion manager)
|
||||||
@ -56,46 +56,25 @@ export class RockFactory {
|
|||||||
});
|
});
|
||||||
await this._explosionManager.initialize();
|
await this._explosionManager.initialize();
|
||||||
|
|
||||||
// Reload mesh if not loaded or if it was disposed during cleanup
|
if (!this._asteroidMesh) {
|
||||||
if (!this._asteroidMesh || this._asteroidMesh.isDisposed()) {
|
|
||||||
await this.loadMesh();
|
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)
|
* Initialize audio (explosion sound)
|
||||||
* Call this AFTER audio engine is unlocked
|
* Call this AFTER audio engine is unlocked
|
||||||
*/
|
*/
|
||||||
public static async initAudio(audioEngine: AudioEngineV2) {
|
public static async initAudio(audioEngine: AudioEngineV2) {
|
||||||
debugLog('[RockFactory] Initializing audio via ExplosionManager');
|
debugLog('[RockFactory] Initializing audio via ExplosionManager');
|
||||||
if (this._explosionManager) {
|
await this._explosionManager.initAudio(audioEngine);
|
||||||
await this._explosionManager.initAudio(audioEngine);
|
|
||||||
}
|
|
||||||
debugLog('[RockFactory] Audio initialization complete');
|
debugLog('[RockFactory] Audio initialization complete');
|
||||||
}
|
}
|
||||||
private static async loadMesh() {
|
private static async loadMesh() {
|
||||||
debugLog('loading mesh');
|
debugLog('loading mesh');
|
||||||
const asset = await loadAsset("asteroid.glb");
|
this._asteroidMesh = (await loadAsset("asteroid.glb")).meshes.get('Asteroid');
|
||||||
this._asteroidMesh = asset.meshes.get('Asteroid') || null;
|
//this._asteroidMesh.setParent(null);
|
||||||
if (this._asteroidMesh) {
|
this._asteroidMesh.setEnabled(false);
|
||||||
this._asteroidMesh.setEnabled(false);
|
|
||||||
}
|
|
||||||
debugLog(this._asteroidMesh);
|
debugLog(this._asteroidMesh);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -103,10 +82,6 @@ export class RockFactory {
|
|||||||
linearVelocitry: Vector3, angularVelocity: Vector3, score: Observable<ScoreEvent>,
|
linearVelocitry: Vector3, angularVelocity: Vector3, score: Observable<ScoreEvent>,
|
||||||
useOrbitConstraint: boolean = true): Promise<Rock> {
|
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);
|
const rock = new InstancedMesh("asteroid-" +i, this._asteroidMesh as Mesh);
|
||||||
debugLog(rock.id);
|
debugLog(rock.id);
|
||||||
rock.scaling = new Vector3(scale, scale, scale);
|
rock.scaling = new Vector3(scale, scale, scale);
|
||||||
@ -129,8 +104,8 @@ export class RockFactory {
|
|||||||
}, DefaultScene.MainScene);
|
}, DefaultScene.MainScene);
|
||||||
const body = agg.body;
|
const body = agg.body;
|
||||||
|
|
||||||
// Only apply orbit constraint if enabled for this level and orbit center exists
|
// Only apply orbit constraint if enabled for this level
|
||||||
if (useOrbitConstraint && this._orbitCenter) {
|
if (useOrbitConstraint) {
|
||||||
debugLog(`[RockFactory] Applying orbit constraint for ${rock.name}`);
|
debugLog(`[RockFactory] Applying orbit constraint for ${rock.name}`);
|
||||||
const constraint = new DistanceConstraint(Vector3.Distance(position, this._orbitCenter.body.transformNode.position), DefaultScene.MainScene);
|
const constraint = new DistanceConstraint(Vector3.Distance(position, this._orbitCenter.body.transformNode.position), DefaultScene.MainScene);
|
||||||
body.addConstraint(this._orbitCenter.body, constraint);
|
body.addConstraint(this._orbitCenter.body, constraint);
|
||||||
@ -185,9 +160,7 @@ export class RockFactory {
|
|||||||
|
|
||||||
// Play explosion (visual + audio handled by ExplosionManager)
|
// Play explosion (visual + audio handled by ExplosionManager)
|
||||||
// Note: ExplosionManager will dispose the asteroid mesh after explosion
|
// Note: ExplosionManager will dispose the asteroid mesh after explosion
|
||||||
if (RockFactory._explosionManager) {
|
RockFactory._explosionManager.playExplosion(asteroidMesh);
|
||||||
RockFactory._explosionManager.playExplosion(asteroidMesh);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Dispose projectile physics objects
|
// Dispose projectile physics objects
|
||||||
debugLog('[RockFactory] Disposing projectile physics objects...');
|
debugLog('[RockFactory] Disposing projectile physics objects...');
|
||||||
|
|||||||
@ -193,9 +193,6 @@ export class Level1 implements Level {
|
|||||||
this._gameStarted = true;
|
this._gameStarted = true;
|
||||||
debugLog('[Level1] Starting gameplay');
|
debugLog('[Level1] Starting gameplay');
|
||||||
|
|
||||||
// Enable game end condition checking on ship
|
|
||||||
this._ship.startGameplay();
|
|
||||||
|
|
||||||
// Start game timer
|
// Start game timer
|
||||||
this._ship.gameStats.startTimer();
|
this._ship.gameStats.startTimer();
|
||||||
debugLog('Game timer started');
|
debugLog('Game timer started');
|
||||||
@ -278,9 +275,7 @@ export class Level1 implements Level {
|
|||||||
if (this._startBase) {
|
if (this._startBase) {
|
||||||
this._startBase.dispose();
|
this._startBase.dispose();
|
||||||
}
|
}
|
||||||
if (this._endBase) {
|
this._endBase.dispose();
|
||||||
this._endBase.dispose();
|
|
||||||
}
|
|
||||||
if (this._backgroundStars) {
|
if (this._backgroundStars) {
|
||||||
this._backgroundStars.dispose();
|
this._backgroundStars.dispose();
|
||||||
}
|
}
|
||||||
@ -290,12 +285,6 @@ export class Level1 implements Level {
|
|||||||
if (this._missionBrief) {
|
if (this._missionBrief) {
|
||||||
this._missionBrief.dispose();
|
this._missionBrief.dispose();
|
||||||
}
|
}
|
||||||
if (this._ship) {
|
|
||||||
this._ship.dispose();
|
|
||||||
}
|
|
||||||
if (this._backgroundMusic) {
|
|
||||||
this._backgroundMusic.dispose();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public async initialize() {
|
public async initialize() {
|
||||||
@ -390,24 +379,12 @@ export class Level1 implements Level {
|
|||||||
|
|
||||||
this._initialized = true;
|
this._initialized = true;
|
||||||
|
|
||||||
// Set par time and level info for score calculation and results recording
|
// Set par time for score calculation based on difficulty
|
||||||
const parTime = this.getParTimeForDifficulty(this._levelConfig.difficulty);
|
const parTime = this.getParTimeForDifficulty(this._levelConfig.difficulty);
|
||||||
const statusScreen = this._ship.statusScreen;
|
const statusScreen = (this._ship as any)._statusScreen; // Access private status screen
|
||||||
console.log('[Level1] StatusScreen reference:', statusScreen);
|
|
||||||
console.log('[Level1] Level config metadata:', this._levelConfig.metadata);
|
|
||||||
console.log('[Level1] Asteroids count:', entities.asteroids.length);
|
|
||||||
if (statusScreen) {
|
if (statusScreen) {
|
||||||
statusScreen.setParTime(parTime);
|
statusScreen.setParTime(parTime);
|
||||||
console.log(`[Level1] Set par time to ${parTime}s for difficulty: ${this._levelConfig.difficulty}`);
|
debugLog(`Set par time to ${parTime}s for difficulty: ${this._levelConfig.difficulty}`);
|
||||||
|
|
||||||
// Set level info for game results recording
|
|
||||||
const levelId = this._levelId || 'unknown';
|
|
||||||
const levelName = this._levelConfig.metadata?.description || 'Unknown Level';
|
|
||||||
console.log('[Level1] About to call setCurrentLevel with:', { levelId, levelName, asteroidCount: entities.asteroids.length });
|
|
||||||
statusScreen.setCurrentLevel(levelId, levelName, entities.asteroids.length);
|
|
||||||
console.log('[Level1] setCurrentLevel called successfully');
|
|
||||||
} else {
|
|
||||||
console.error('[Level1] StatusScreen is null/undefined!');
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Notify that initialization is complete
|
// Notify that initialization is complete
|
||||||
|
|||||||
138
src/main.ts
138
src/main.ts
@ -3,7 +3,6 @@ import {
|
|||||||
Color3,
|
Color3,
|
||||||
CreateAudioEngineAsync,
|
CreateAudioEngineAsync,
|
||||||
Engine,
|
Engine,
|
||||||
FreeCamera,
|
|
||||||
HavokPlugin,
|
HavokPlugin,
|
||||||
ParticleHelper,
|
ParticleHelper,
|
||||||
Scene,
|
Scene,
|
||||||
@ -204,7 +203,11 @@ export class Main {
|
|||||||
|
|
||||||
// Listen for replay requests from the ship
|
// Listen for replay requests from the ship
|
||||||
if (ship) {
|
if (ship) {
|
||||||
// Note: Level info for progression/results is now set in Level1.initialize()
|
// Set current level name for progression tracking
|
||||||
|
if (ship._statusScreen) {
|
||||||
|
ship._statusScreen.setCurrentLevel(levelName);
|
||||||
|
debugLog(`Set current level for progression: ${levelName}`);
|
||||||
|
}
|
||||||
|
|
||||||
ship.onReplayRequestObservable.add(() => {
|
ship.onReplayRequestObservable.add(() => {
|
||||||
debugLog('Replay requested - reloading page');
|
debugLog('Replay requested - reloading page');
|
||||||
@ -259,12 +262,14 @@ export class Main {
|
|||||||
preloader.hide();
|
preloader.hide();
|
||||||
}, 500);
|
}, 500);
|
||||||
|
|
||||||
// Hide UI (no longer remove from DOM - let Svelte routing handle it)
|
// Remove UI
|
||||||
console.log('[Main] ========== HIDING UI FOR GAMEPLAY ==========');
|
console.log('[Main] ========== ABOUT TO REMOVE MAIN DIV ==========');
|
||||||
console.log('[Main] mainDiv exists:', !!mainDiv);
|
console.log('[Main] mainDiv exists:', !!mainDiv);
|
||||||
console.log('[Main] Timestamp:', Date.now());
|
console.log('[Main] Timestamp:', Date.now());
|
||||||
// Note: With route-based loading, the app will be hidden by PlayLevel component
|
if (mainDiv) {
|
||||||
// This code path is only used when dispatching levelSelected event (legacy support)
|
mainDiv.remove();
|
||||||
|
console.log('[Main] mainDiv removed from DOM');
|
||||||
|
}
|
||||||
|
|
||||||
// Start the game (XR session already active, or flat mode)
|
// Start the game (XR session already active, or flat mode)
|
||||||
console.log('[Main] About to call this.play()');
|
console.log('[Main] About to call this.play()');
|
||||||
@ -341,12 +346,10 @@ export class Main {
|
|||||||
debugLog('[Main] ========== TEST LEVEL READY OBSERVABLE FIRED ==========');
|
debugLog('[Main] ========== TEST LEVEL READY OBSERVABLE FIRED ==========');
|
||||||
setLoadingMessage("Test Scene Ready! Entering VR...");
|
setLoadingMessage("Test Scene Ready! Entering VR...");
|
||||||
|
|
||||||
// Hide UI for gameplay (no longer remove from DOM)
|
// Remove UI and play immediately (must maintain user activation for XR)
|
||||||
// Test level doesn't use routing, so we need to hide the app element
|
if (mainDiv) {
|
||||||
const appElement = document.getElementById('app');
|
mainDiv.remove();
|
||||||
if (appElement) {
|
debugLog('[Main] mainDiv removed');
|
||||||
appElement.style.display = 'none';
|
|
||||||
debugLog('[Main] App UI hidden for test level');
|
|
||||||
}
|
}
|
||||||
debugLog('[Main] About to call this.play()...');
|
debugLog('[Main] About to call this.play()...');
|
||||||
await this.play();
|
await this.play();
|
||||||
@ -490,109 +493,6 @@ export class Main {
|
|||||||
return this._audioEngine;
|
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. Clear the canvas so it doesn't show the last frame
|
|
||||||
debugLog('[Main] Clearing canvas...');
|
|
||||||
const canvas = document.getElementById('gameCanvas') as HTMLCanvasElement;
|
|
||||||
if (canvas) {
|
|
||||||
const gl = canvas.getContext('webgl2') || canvas.getContext('webgl');
|
|
||||||
if (gl) {
|
|
||||||
gl.clearColor(0, 0, 0, 1);
|
|
||||||
gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 9. Keep render loop stopped until next game starts
|
|
||||||
// No need to render an empty scene - saves resources
|
|
||||||
debugLog('[Main] Render loop stopped - will restart when game starts');
|
|
||||||
|
|
||||||
// 10. 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() {
|
public async play() {
|
||||||
debugLog('[Main] play() called');
|
debugLog('[Main] play() called');
|
||||||
debugLog('[Main] Current level exists:', !!this._currentLevel);
|
debugLog('[Main] Current level exists:', !!this._currentLevel);
|
||||||
@ -889,7 +789,7 @@ async function initializeApp() {
|
|||||||
await LevelRegistry.getInstance().initialize();
|
await LevelRegistry.getInstance().initialize();
|
||||||
console.log('[Main] LevelRegistry.initialize() completed successfully [AFTER MIGRATION]');
|
console.log('[Main] LevelRegistry.initialize() completed successfully [AFTER MIGRATION]');
|
||||||
debugLog('[Main] LevelRegistry initialized after migration');
|
debugLog('[Main] LevelRegistry initialized after migration');
|
||||||
// NOTE: Old router disabled - now using svelte-routing
|
// NOTE: Old router disabled - now using svelte-spa-router
|
||||||
// router.start();
|
// router.start();
|
||||||
|
|
||||||
// Mount Svelte app
|
// Mount Svelte app
|
||||||
@ -917,7 +817,7 @@ async function initializeApp() {
|
|||||||
resolve();
|
resolve();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('[Main] Failed to initialize LevelRegistry after migration:', error);
|
console.error('[Main] Failed to initialize LevelRegistry after migration:', error);
|
||||||
// NOTE: Old router disabled - now using svelte-routing
|
// NOTE: Old router disabled - now using svelte-spa-router
|
||||||
// router.start(); // Start anyway to show error state
|
// router.start(); // Start anyway to show error state
|
||||||
resolve();
|
resolve();
|
||||||
}
|
}
|
||||||
@ -944,7 +844,7 @@ async function initializeApp() {
|
|||||||
console.log('[Main] To clear caches: window.__levelRegistry.clearAllCaches().then(() => location.reload())');
|
console.log('[Main] To clear caches: window.__levelRegistry.clearAllCaches().then(() => location.reload())');
|
||||||
}
|
}
|
||||||
|
|
||||||
// NOTE: Old router disabled - now using svelte-routing
|
// NOTE: Old router disabled - now using svelte-spa-router
|
||||||
// console.log('[Main] About to call router.start()');
|
// console.log('[Main] About to call router.start()');
|
||||||
// router.start();
|
// router.start();
|
||||||
// console.log('[Main] router.start() completed');
|
// console.log('[Main] router.start() completed');
|
||||||
@ -952,7 +852,7 @@ async function initializeApp() {
|
|||||||
console.error('[Main] !!!!! EXCEPTION in LevelRegistry initialization !!!!!');
|
console.error('[Main] !!!!! EXCEPTION in LevelRegistry initialization !!!!!');
|
||||||
console.error('[Main] Failed to initialize LevelRegistry:', error);
|
console.error('[Main] Failed to initialize LevelRegistry:', error);
|
||||||
console.error('[Main] Error stack:', error?.stack);
|
console.error('[Main] Error stack:', error?.stack);
|
||||||
// NOTE: Old router disabled - now using svelte-routing
|
// NOTE: Old router disabled - now using svelte-spa-router
|
||||||
// router.start(); // Start anyway to show error state
|
// router.start(); // Start anyway to show error state
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,156 +0,0 @@
|
|||||||
import { AuthService } from './authService';
|
|
||||||
import { GameStats } from '../game/gameStats';
|
|
||||||
import { Scoreboard } from '../ui/hud/scoreboard';
|
|
||||||
import debugLog from '../core/debug';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Represents a completed game session result
|
|
||||||
*/
|
|
||||||
export interface GameResult {
|
|
||||||
id: string;
|
|
||||||
timestamp: number;
|
|
||||||
playerName: string;
|
|
||||||
levelId: string;
|
|
||||||
levelName: string;
|
|
||||||
completed: boolean;
|
|
||||||
endReason: 'victory' | 'death' | 'stranded';
|
|
||||||
|
|
||||||
// Game statistics
|
|
||||||
gameTimeSeconds: number;
|
|
||||||
asteroidsDestroyed: number;
|
|
||||||
totalAsteroids: number;
|
|
||||||
accuracy: number;
|
|
||||||
hullDamageTaken: number;
|
|
||||||
fuelConsumed: number;
|
|
||||||
|
|
||||||
// Scoring
|
|
||||||
finalScore: number;
|
|
||||||
starRating: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
const STORAGE_KEY = 'space-game-results';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Service for storing and retrieving game results
|
|
||||||
* Uses localStorage for persistence, designed for future cloud storage expansion
|
|
||||||
*/
|
|
||||||
export class GameResultsService {
|
|
||||||
private static _instance: GameResultsService;
|
|
||||||
|
|
||||||
private constructor() {}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get the singleton instance
|
|
||||||
*/
|
|
||||||
public static getInstance(): GameResultsService {
|
|
||||||
if (!GameResultsService._instance) {
|
|
||||||
GameResultsService._instance = new GameResultsService();
|
|
||||||
}
|
|
||||||
return GameResultsService._instance;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Save a game result to storage
|
|
||||||
*/
|
|
||||||
public saveResult(result: GameResult): void {
|
|
||||||
console.log('[GameResultsService] saveResult called with:', result);
|
|
||||||
const results = this.getAllResults();
|
|
||||||
console.log('[GameResultsService] Existing results count:', results.length);
|
|
||||||
results.push(result);
|
|
||||||
this.saveToStorage(results);
|
|
||||||
console.log('[GameResultsService] Saved result:', result.id, result.finalScore);
|
|
||||||
debugLog('[GameResultsService] Saved result:', result.id, result.finalScore);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get all stored results
|
|
||||||
*/
|
|
||||||
public getAllResults(): GameResult[] {
|
|
||||||
try {
|
|
||||||
const data = localStorage.getItem(STORAGE_KEY);
|
|
||||||
if (!data) {
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
return JSON.parse(data) as GameResult[];
|
|
||||||
} catch (error) {
|
|
||||||
debugLog('[GameResultsService] Error loading results:', error);
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get top results sorted by highest score
|
|
||||||
*/
|
|
||||||
public getTopResults(limit: number = 20): GameResult[] {
|
|
||||||
const results = this.getAllResults();
|
|
||||||
return results
|
|
||||||
.sort((a, b) => b.finalScore - a.finalScore)
|
|
||||||
.slice(0, limit);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Clear all stored results (for testing/reset)
|
|
||||||
*/
|
|
||||||
public clearAll(): void {
|
|
||||||
localStorage.removeItem(STORAGE_KEY);
|
|
||||||
debugLog('[GameResultsService] Cleared all results');
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Save results array to localStorage
|
|
||||||
*/
|
|
||||||
private saveToStorage(results: GameResult[]): void {
|
|
||||||
try {
|
|
||||||
const json = JSON.stringify(results);
|
|
||||||
console.log('[GameResultsService] Saving to localStorage, key:', STORAGE_KEY, 'size:', json.length);
|
|
||||||
localStorage.setItem(STORAGE_KEY, json);
|
|
||||||
console.log('[GameResultsService] Successfully saved to localStorage');
|
|
||||||
// Verify it was saved
|
|
||||||
const verify = localStorage.getItem(STORAGE_KEY);
|
|
||||||
console.log('[GameResultsService] Verification - stored data exists:', !!verify);
|
|
||||||
} catch (error) {
|
|
||||||
console.error('[GameResultsService] Error saving results:', error);
|
|
||||||
debugLog('[GameResultsService] Error saving results:', error);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Build a GameResult from current game state
|
|
||||||
* Call this when the game ends (victory, death, or stranded)
|
|
||||||
*/
|
|
||||||
public static buildResult(
|
|
||||||
levelId: string,
|
|
||||||
levelName: string,
|
|
||||||
gameStats: GameStats,
|
|
||||||
totalAsteroids: number,
|
|
||||||
endReason: 'victory' | 'death' | 'stranded',
|
|
||||||
parTime: number
|
|
||||||
): GameResult {
|
|
||||||
// Get player name from auth service
|
|
||||||
const authService = AuthService.getInstance();
|
|
||||||
const user = authService.getUser();
|
|
||||||
const playerName = user?.name || user?.email || '<Anonymous>';
|
|
||||||
|
|
||||||
// Get stats
|
|
||||||
const stats = gameStats.getStats();
|
|
||||||
const scoreCalc = gameStats.calculateFinalScore(parTime);
|
|
||||||
|
|
||||||
return {
|
|
||||||
id: crypto.randomUUID(),
|
|
||||||
timestamp: Date.now(),
|
|
||||||
playerName,
|
|
||||||
levelId,
|
|
||||||
levelName,
|
|
||||||
completed: endReason === 'victory',
|
|
||||||
endReason,
|
|
||||||
gameTimeSeconds: gameStats.getGameTime(),
|
|
||||||
asteroidsDestroyed: stats.asteroidsDestroyed,
|
|
||||||
totalAsteroids,
|
|
||||||
accuracy: stats.accuracy,
|
|
||||||
hullDamageTaken: stats.hullDamageTaken,
|
|
||||||
fuelConsumed: stats.fuelConsumed,
|
|
||||||
finalScore: scoreCalc.finalScore,
|
|
||||||
starRating: scoreCalc.stars.total
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -68,16 +68,9 @@ export class Ship {
|
|||||||
// Auto-show status screen flag
|
// Auto-show status screen flag
|
||||||
private _statusScreenAutoShown: boolean = false;
|
private _statusScreenAutoShown: boolean = false;
|
||||||
|
|
||||||
// Flag to prevent game end checks until gameplay has started
|
|
||||||
private _gameplayStarted: boolean = false;
|
|
||||||
|
|
||||||
// Controls enabled state
|
// Controls enabled state
|
||||||
private _controlsEnabled: boolean = true;
|
private _controlsEnabled: boolean = true;
|
||||||
|
|
||||||
// Scene observer references (for cleanup)
|
|
||||||
private _physicsObserver: any = null;
|
|
||||||
private _renderObserver: any = null;
|
|
||||||
|
|
||||||
constructor(audioEngine?: AudioEngineV2, isReplayMode: boolean = false) {
|
constructor(audioEngine?: AudioEngineV2, isReplayMode: boolean = false) {
|
||||||
this._audioEngine = audioEngine;
|
this._audioEngine = audioEngine;
|
||||||
this._isReplayMode = isReplayMode;
|
this._isReplayMode = isReplayMode;
|
||||||
@ -91,10 +84,6 @@ export class Ship {
|
|||||||
return this._gameStats;
|
return this._gameStats;
|
||||||
}
|
}
|
||||||
|
|
||||||
public get statusScreen(): StatusScreen {
|
|
||||||
return this._statusScreen;
|
|
||||||
}
|
|
||||||
|
|
||||||
public get keyboardInput(): KeyboardInput {
|
public get keyboardInput(): KeyboardInput {
|
||||||
return this._keyboardInput;
|
return this._keyboardInput;
|
||||||
}
|
}
|
||||||
@ -103,15 +92,6 @@ export class Ship {
|
|||||||
return this._isInLandingZone;
|
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> {
|
public get onMissionBriefTriggerObservable(): Observable<void> {
|
||||||
return this._onMissionBriefTriggerObservable;
|
return this._onMissionBriefTriggerObservable;
|
||||||
}
|
}
|
||||||
@ -336,10 +316,10 @@ export class Ship {
|
|||||||
this._physics.setGameStats(this._gameStats);
|
this._physics.setGameStats(this._gameStats);
|
||||||
|
|
||||||
// Setup physics update loop (every 10 frames)
|
// Setup physics update loop (every 10 frames)
|
||||||
this._physicsObserver = DefaultScene.MainScene.onAfterPhysicsObservable.add(() => {
|
DefaultScene.MainScene.onAfterPhysicsObservable.add(() => {
|
||||||
this.updatePhysics();
|
this.updatePhysics();
|
||||||
});
|
})
|
||||||
this._renderObserver = DefaultScene.MainScene.onAfterRenderObservable.add(() => {
|
DefaultScene.MainScene.onAfterRenderObservable.add(() => {
|
||||||
// Update voice audio system (checks for completed sounds and plays next in queue)
|
// Update voice audio system (checks for completed sounds and plays next in queue)
|
||||||
if (this._voiceAudio) {
|
if (this._voiceAudio) {
|
||||||
this._voiceAudio.update();
|
this._voiceAudio.update();
|
||||||
@ -442,29 +422,9 @@ export class Ship {
|
|||||||
/**
|
/**
|
||||||
* Handle exit VR button click from status screen
|
* Handle exit VR button click from status screen
|
||||||
*/
|
*/
|
||||||
private async handleExitVR(): Promise<void> {
|
private handleExitVR(): void {
|
||||||
debugLog('Exit VR button clicked - navigating to home');
|
debugLog('Exit VR button clicked - refreshing browser');
|
||||||
|
window.location.reload();
|
||||||
try {
|
|
||||||
// Ensure the app UI is visible before navigating (safety net)
|
|
||||||
const appElement = document.getElementById('app');
|
|
||||||
if (appElement) {
|
|
||||||
appElement.style.display = 'block';
|
|
||||||
}
|
|
||||||
const headerElement = document.getElementById('appHeader');
|
|
||||||
if (headerElement) {
|
|
||||||
headerElement.style.display = 'block';
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
// 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();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -494,11 +454,6 @@ export class Ship {
|
|||||||
* 3. All asteroids destroyed AND ship inside landing zone (victory)
|
* 3. All asteroids destroyed AND ship inside landing zone (victory)
|
||||||
*/
|
*/
|
||||||
private checkGameEndConditions(): void {
|
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
|
// Skip if already auto-shown or status screen doesn't exist
|
||||||
if (this._statusScreenAutoShown || !this._statusScreen || !this._scoreboard) {
|
if (this._statusScreenAutoShown || !this._statusScreen || !this._scoreboard) {
|
||||||
return;
|
return;
|
||||||
@ -521,7 +476,7 @@ export class Ship {
|
|||||||
// Check condition 1: Death by hull damage (outside landing zone)
|
// Check condition 1: Death by hull damage (outside landing zone)
|
||||||
if (!this._isInLandingZone && hull < 0.01) {
|
if (!this._isInLandingZone && hull < 0.01) {
|
||||||
debugLog('Game end condition met: Hull critical outside landing zone');
|
debugLog('Game end condition met: Hull critical outside landing zone');
|
||||||
this._statusScreen.show(true, false, 'death'); // Game ended, not victory, death reason
|
this._statusScreen.show(true, false); // Game ended, not victory
|
||||||
// InputControlManager will handle disabling controls when status screen shows
|
// InputControlManager will handle disabling controls when status screen shows
|
||||||
this._statusScreenAutoShown = true;
|
this._statusScreenAutoShown = true;
|
||||||
return;
|
return;
|
||||||
@ -530,17 +485,16 @@ export class Ship {
|
|||||||
// Check condition 2: Stranded (outside landing zone, no fuel, low velocity)
|
// Check condition 2: Stranded (outside landing zone, no fuel, low velocity)
|
||||||
if (!this._isInLandingZone && fuel < 0.01 && totalVelocity < 5) {
|
if (!this._isInLandingZone && fuel < 0.01 && totalVelocity < 5) {
|
||||||
debugLog('Game end condition met: Stranded (no fuel, low velocity)');
|
debugLog('Game end condition met: Stranded (no fuel, low velocity)');
|
||||||
this._statusScreen.show(true, false, 'stranded'); // Game ended, not victory, stranded reason
|
this._statusScreen.show(true, false); // Game ended, not victory
|
||||||
// InputControlManager will handle disabling controls when status screen shows
|
// InputControlManager will handle disabling controls when status screen shows
|
||||||
this._statusScreenAutoShown = true;
|
this._statusScreenAutoShown = true;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check condition 3: Victory (all asteroids destroyed, inside landing zone)
|
// Check condition 3: Victory (all asteroids destroyed, inside landing zone)
|
||||||
// Must have had asteroids to destroy in the first place (prevents false victory on init)
|
if (asteroidsRemaining <= 0 && this._isInLandingZone) {
|
||||||
if (asteroidsRemaining <= 0 && this._isInLandingZone && this._scoreboard.hasAsteroidsToDestroy) {
|
|
||||||
debugLog('Game end condition met: Victory (all asteroids destroyed)');
|
debugLog('Game end condition met: Victory (all asteroids destroyed)');
|
||||||
this._statusScreen.show(true, true, 'victory'); // Game ended, VICTORY!
|
this._statusScreen.show(true, true); // Game ended, VICTORY!
|
||||||
// InputControlManager will handle disabling controls when status screen shows
|
// InputControlManager will handle disabling controls when status screen shows
|
||||||
this._statusScreenAutoShown = true;
|
this._statusScreenAutoShown = true;
|
||||||
return;
|
return;
|
||||||
@ -725,17 +679,6 @@ export class Ship {
|
|||||||
* Dispose of ship resources
|
* Dispose of ship resources
|
||||||
*/
|
*/
|
||||||
public dispose(): void {
|
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) {
|
if (this._sight) {
|
||||||
this._sight.dispose();
|
this._sight.dispose();
|
||||||
}
|
}
|
||||||
|
|||||||
@ -66,7 +66,7 @@ export class ShipEngine {
|
|||||||
//myParticleSystem.minEmitPower = 2;
|
//myParticleSystem.minEmitPower = 2;
|
||||||
//myParticleSystem.maxEmitPower = 10;
|
//myParticleSystem.maxEmitPower = 10;
|
||||||
|
|
||||||
myParticleSystem.particleTexture = new Texture("/flare.png");
|
myParticleSystem.particleTexture = new Texture("./flare.png");
|
||||||
myParticleSystem.emitter = mesh;
|
myParticleSystem.emitter = mesh;
|
||||||
const coneEmitter = myParticleSystem.createConeEmitter(0.1, Math.PI / 9);
|
const coneEmitter = myParticleSystem.createConeEmitter(0.1, Math.PI / 9);
|
||||||
myParticleSystem.addSizeGradient(0, .01);
|
myParticleSystem.addSizeGradient(0, .01);
|
||||||
|
|||||||
@ -1,47 +0,0 @@
|
|||||||
import { writable } from 'svelte/store';
|
|
||||||
import { GameResultsService, GameResult } from '../services/gameResultsService';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Svelte store for game results
|
|
||||||
* Provides reactive access to leaderboard data
|
|
||||||
*/
|
|
||||||
function createGameResultsStore() {
|
|
||||||
const service = GameResultsService.getInstance();
|
|
||||||
const { subscribe, set } = writable<GameResult[]>(service.getTopResults(20));
|
|
||||||
|
|
||||||
return {
|
|
||||||
subscribe,
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Refresh the store with latest top results
|
|
||||||
*/
|
|
||||||
refresh: () => {
|
|
||||||
set(service.getTopResults(20));
|
|
||||||
},
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Add a new result and refresh the store
|
|
||||||
*/
|
|
||||||
addResult: (result: GameResult) => {
|
|
||||||
service.saveResult(result);
|
|
||||||
set(service.getTopResults(20));
|
|
||||||
},
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get all results (not just top 20)
|
|
||||||
*/
|
|
||||||
getAll: () => {
|
|
||||||
return service.getAllResults();
|
|
||||||
},
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Clear all results (for testing/reset)
|
|
||||||
*/
|
|
||||||
clear: () => {
|
|
||||||
service.clearAll();
|
|
||||||
set([]);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
export const gameResultsStore = createGameResultsStore();
|
|
||||||
@ -18,7 +18,6 @@ export type ScoreEvent = {
|
|||||||
export class Scoreboard {
|
export class Scoreboard {
|
||||||
private _score: number = 0;
|
private _score: number = 0;
|
||||||
private _remaining: number = 0;
|
private _remaining: number = 0;
|
||||||
private _initialAsteroidCount: number = 0;
|
|
||||||
private _startTime: number = Date.now();
|
private _startTime: number = Date.now();
|
||||||
|
|
||||||
private _active = false;
|
private _active = false;
|
||||||
@ -77,17 +76,6 @@ export class Scoreboard {
|
|||||||
|
|
||||||
public setRemainingCount(count: number) {
|
public setRemainingCount(count: number) {
|
||||||
this._remaining = count;
|
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;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@ -21,8 +21,6 @@ import { AuthService } from "../../services/authService";
|
|||||||
import { FacebookShare, ShareData } from "../../services/facebookShare";
|
import { FacebookShare, ShareData } from "../../services/facebookShare";
|
||||||
import { InputControlManager } from "../../ship/input/inputControlManager";
|
import { InputControlManager } from "../../ship/input/inputControlManager";
|
||||||
import { formatStars, getStarColor } from "../../game/scoreCalculator";
|
import { formatStars, getStarColor } from "../../game/scoreCalculator";
|
||||||
import { GameResultsService } from "../../services/gameResultsService";
|
|
||||||
import debugLog from "../../core/debug";
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Status screen that displays game statistics
|
* Status screen that displays game statistics
|
||||||
@ -66,13 +64,8 @@ export class StatusScreen {
|
|||||||
// Track whether game has ended
|
// Track whether game has ended
|
||||||
private _isGameEnded: boolean = false;
|
private _isGameEnded: boolean = false;
|
||||||
|
|
||||||
// Track current level info for progression and results
|
// Track current level name for progression
|
||||||
private _currentLevelName: string | null = null;
|
private _currentLevelName: string | null = null;
|
||||||
private _currentLevelId: string | null = null;
|
|
||||||
private _totalAsteroids: number = 0;
|
|
||||||
|
|
||||||
// Track if result has been recorded (prevent duplicates)
|
|
||||||
private _resultRecorded: boolean = false;
|
|
||||||
|
|
||||||
constructor(scene: Scene, gameStats: GameStats, onReplay?: () => void, onExit?: () => void, onResume?: () => void, onNextLevel?: () => void) {
|
constructor(scene: Scene, gameStats: GameStats, onReplay?: () => void, onExit?: () => void, onResume?: () => void, onNextLevel?: () => void) {
|
||||||
this._scene = scene;
|
this._scene = scene;
|
||||||
@ -371,13 +364,10 @@ export class StatusScreen {
|
|||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Set the current level info for progression tracking and results
|
* Set the current level name for progression tracking
|
||||||
*/
|
*/
|
||||||
public setCurrentLevel(levelId: string, levelName: string, totalAsteroids: number): void {
|
public setCurrentLevel(levelName: string): void {
|
||||||
console.log('[StatusScreen] setCurrentLevel called:', { levelId, levelName, totalAsteroids });
|
|
||||||
this._currentLevelId = levelId;
|
|
||||||
this._currentLevelName = levelName;
|
this._currentLevelName = levelName;
|
||||||
this._totalAsteroids = totalAsteroids;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -392,9 +382,8 @@ export class StatusScreen {
|
|||||||
* Show the status screen
|
* Show the status screen
|
||||||
* @param isGameEnded - true if game has ended (death/stranded/victory), false if manually paused
|
* @param isGameEnded - true if game has ended (death/stranded/victory), false if manually paused
|
||||||
* @param victory - true if the level was completed successfully
|
* @param victory - true if the level was completed successfully
|
||||||
* @param endReason - specific reason for game end ('victory' | 'death' | 'stranded')
|
|
||||||
*/
|
*/
|
||||||
public show(isGameEnded: boolean = false, victory: boolean = false, endReason?: 'victory' | 'death' | 'stranded'): void {
|
public show(isGameEnded: boolean = false, victory: boolean = false): void {
|
||||||
if (!this._screenMesh) {
|
if (!this._screenMesh) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -413,12 +402,6 @@ export class StatusScreen {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Record game result when game ends (not on manual pause)
|
|
||||||
if (isGameEnded && endReason && !this._resultRecorded) {
|
|
||||||
this.recordGameResult(endReason);
|
|
||||||
this._resultRecorded = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Determine if there's a next level
|
// Determine if there's a next level
|
||||||
const nextLevel = progression.getNextLevel();
|
const nextLevel = progression.getNextLevel();
|
||||||
const hasNextLevel = nextLevel !== null;
|
const hasNextLevel = nextLevel !== null;
|
||||||
@ -601,47 +584,6 @@ export class StatusScreen {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Record game result to the results service
|
|
||||||
*/
|
|
||||||
private recordGameResult(endReason: 'victory' | 'death' | 'stranded'): void {
|
|
||||||
console.log('[StatusScreen] recordGameResult called with endReason:', endReason);
|
|
||||||
console.log('[StatusScreen] Level info:', {
|
|
||||||
levelId: this._currentLevelId,
|
|
||||||
levelName: this._currentLevelName,
|
|
||||||
totalAsteroids: this._totalAsteroids,
|
|
||||||
parTime: this._parTime
|
|
||||||
});
|
|
||||||
|
|
||||||
// Only record if we have level info
|
|
||||||
if (!this._currentLevelId || !this._currentLevelName) {
|
|
||||||
console.warn('[StatusScreen] Cannot record result - missing level info');
|
|
||||||
debugLog('[StatusScreen] Cannot record result - missing level info');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
const result = GameResultsService.buildResult(
|
|
||||||
this._currentLevelId,
|
|
||||||
this._currentLevelName,
|
|
||||||
this._gameStats,
|
|
||||||
this._totalAsteroids,
|
|
||||||
endReason,
|
|
||||||
this._parTime
|
|
||||||
);
|
|
||||||
|
|
||||||
console.log('[StatusScreen] Built result:', result);
|
|
||||||
|
|
||||||
const service = GameResultsService.getInstance();
|
|
||||||
service.saveResult(result);
|
|
||||||
console.log('[StatusScreen] Game result saved successfully');
|
|
||||||
debugLog('[StatusScreen] Game result recorded:', result.id, result.finalScore, result.endReason);
|
|
||||||
} catch (error) {
|
|
||||||
console.error('[StatusScreen] Failed to record game result:', error);
|
|
||||||
debugLog('[StatusScreen] Failed to record game result:', error);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Dispose of status screen resources
|
* Dispose of status screen resources
|
||||||
*/
|
*/
|
||||||
|
|||||||
@ -7,7 +7,7 @@ export type LoadedAsset = {
|
|||||||
meshes: Map<string, AbstractMesh>,
|
meshes: Map<string, AbstractMesh>,
|
||||||
}
|
}
|
||||||
export default async function loadAsset(file: string, theme: string = "default"): Promise<LoadedAsset> {
|
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}`);
|
debugLog(`[loadAsset] Loading: ${assetPath}`);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
|||||||
@ -39,9 +39,9 @@ export default defineConfig({
|
|||||||
},
|
},
|
||||||
server: {
|
server: {
|
||||||
port: 3000,
|
port: 3000,
|
||||||
|
|
||||||
allowedHosts: true
|
allowedHosts: true
|
||||||
},
|
},
|
||||||
// appType: 'spa' is default - Vite automatically serves index.html for SPA routes
|
|
||||||
preview: {
|
preview: {
|
||||||
port: 3000,
|
port: 3000,
|
||||||
},
|
},
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user