Refactor asteroid scaling and reorganize assets
All checks were successful
Build / build (push) Successful in 1m21s
Major changes: - Change asteroid config to use single scale number instead of Vector3 - Move planetTextures to public/assets/materials/planetTextures - Add GLB path configuration for start base - Fix inspector toggle to work bidirectionally - Add progression system support Asteroid Scaling Changes: - Update AsteroidConfig interface to use 'scale: number' instead of 'scaling: Vector3Array' - Modify RockFactory.createRock() to accept single scale parameter - Update level serializer/deserializer to use uniform scale - Simplify level generation code in levelEditor and levelGenerator - Update validation to check for positive number instead of 3-element array Asset Organization: - Move public/planetTextures → public/assets/materials/planetTextures - Update all texture path references in planetTextures.ts (210 paths) - Update default texture paths in createSun.ts and levelSerializer.ts - Update CLAUDE.md documentation with new asset structure Start Base Improvements: - Add baseGlbPath and landingGlbPath to StartBaseConfig - Update StarBase.buildStarBase() to accept GLB path parameter - Add position parameter support to StarBase - Store GLB path in mesh metadata for serialization - Add UI field in level editor for base GLB path Inspector Toggle: - Fix 'i' key to toggle inspector on/off instead of only on - Use scene.debugLayer.isVisible() for state checking - Consistent with ReplayManager implementation 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
@ -101,7 +101,7 @@ Located in `level1.ts:getDifficultyConfig()`
|
||||
### Asset Loading
|
||||
- 3D models: GLB format (cockpit, asteroids)
|
||||
- Particle systems: JSON format in `public/systems/`
|
||||
- Planet textures: Organized by biome in `public/planetTextures/`
|
||||
- Planet textures: Organized by biome in `public/assets/materials/planetTextures/`
|
||||
- Audio: MP3 format in public root
|
||||
|
||||
### Performance Considerations
|
||||
@ -128,7 +128,10 @@ src/
|
||||
|
||||
public/
|
||||
systems/ - Particle system definitions
|
||||
planetTextures/ - Biome-based planet textures
|
||||
assets/
|
||||
materials/
|
||||
planetTextures/ - Biome-based planet textures
|
||||
themes/ - Themed assets
|
||||
cockpit*.glb - Ship interior models
|
||||
asteroid*.glb - Asteroid mesh variants
|
||||
*.mp3 - Audio assets
|
||||
|
||||
45
index.html
@ -12,8 +12,6 @@
|
||||
}
|
||||
});
|
||||
</script>
|
||||
<link rel="prefetch" href="/background.mp3"/>
|
||||
<link rel="prefetch" href="/8192.webp"/>
|
||||
</head>
|
||||
<body>
|
||||
<!-- Game View -->
|
||||
@ -26,9 +24,32 @@
|
||||
<div id="levelSelect">
|
||||
|
||||
|
||||
<!-- Controls Section -->
|
||||
<div class="controls-info">
|
||||
<h2>🎮 How to Play</h2>
|
||||
<!-- Hero Section -->
|
||||
<div style="text-align: center; margin-bottom: 40px;">
|
||||
<h1 style="font-size: 3em; margin-bottom: 15px; background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); -webkit-background-clip: text; -webkit-text-fill-color: transparent; background-clip: text;">
|
||||
🚀 Space Combat VR
|
||||
</h1>
|
||||
<p style="font-size: 1.3em; color: #aaa; margin-bottom: 30px;">
|
||||
Pilot your spaceship through asteroid fields and complete missions
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<!-- Level Selection Section -->
|
||||
<div style="margin-bottom: 40px;">
|
||||
<h2 style="text-align: center; margin-bottom: 10px; font-size: 1.8em;">Your Mission</h2>
|
||||
<p style="text-align: center; color: #888; margin-bottom: 30px; font-size: 1.1em;">
|
||||
Complete levels to unlock new challenges and the level editor
|
||||
</p>
|
||||
<div id="levelCardsContainer" class="card-container">
|
||||
<!-- Level cards will be dynamically populated from localStorage -->
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Controls Section (Collapsed by default) -->
|
||||
<details class="controls-info" style="margin-top: 50px; border-top: 1px solid rgba(255,255,255,0.1); padding-top: 30px;">
|
||||
<summary style="cursor: pointer; font-size: 1.3em; font-weight: bold; margin-bottom: 20px; user-select: none; color: #667eea;">
|
||||
🎮 How to Play (Click to expand)
|
||||
</summary>
|
||||
<div class="controls-grid">
|
||||
<div class="control-section">
|
||||
<h3>VR Controllers (Required for VR)</h3>
|
||||
@ -52,11 +73,7 @@
|
||||
<p class="controls-note">
|
||||
⚠️ <strong>Note:</strong> This game is designed for VR headsets with controllers. Desktop controls are provided for preview and testing purposes only.
|
||||
</p>
|
||||
</div>
|
||||
<h1>Select Your Level</h1>
|
||||
<div id="levelCardsContainer" class="card-container">
|
||||
<!-- Level cards will be dynamically populated from localStorage -->
|
||||
</div>
|
||||
</details>
|
||||
<div style="text-align: center; margin-top: 20px; display: none;">
|
||||
<button id="testLevelBtn" class="test-level-button">
|
||||
🧪 Test Scene (Debug)
|
||||
@ -165,12 +182,8 @@
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="baseDiameter">Diameter</label>
|
||||
<input type="number" id="baseDiameter" value="10" step="1" min="1">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="baseHeight">Height</label>
|
||||
<input type="number" id="baseHeight" value="1" step="0.1" min="0.1">
|
||||
<label>Base GLB Path</label>
|
||||
<input type="text" id="baseGlbPath" value="base.glb" placeholder="base.glb">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "space-game",
|
||||
"deployHostname": "space.digital-experiment.com",
|
||||
"deployHostname": "www.flatearthdefense.com",
|
||||
"private": false,
|
||||
"version": "0.0.1",
|
||||
"type": "module",
|
||||
|
||||
|
Before Width: | Height: | Size: 573 KiB After Width: | Height: | Size: 573 KiB |
|
Before Width: | Height: | Size: 565 KiB After Width: | Height: | Size: 565 KiB |
|
Before Width: | Height: | Size: 585 KiB After Width: | Height: | Size: 585 KiB |
|
Before Width: | Height: | Size: 560 KiB After Width: | Height: | Size: 560 KiB |
|
Before Width: | Height: | Size: 570 KiB After Width: | Height: | Size: 570 KiB |
|
Before Width: | Height: | Size: 352 KiB After Width: | Height: | Size: 352 KiB |
|
Before Width: | Height: | Size: 360 KiB After Width: | Height: | Size: 360 KiB |
|
Before Width: | Height: | Size: 347 KiB After Width: | Height: | Size: 347 KiB |
|
Before Width: | Height: | Size: 359 KiB After Width: | Height: | Size: 359 KiB |
|
Before Width: | Height: | Size: 374 KiB After Width: | Height: | Size: 374 KiB |
|
Before Width: | Height: | Size: 567 KiB After Width: | Height: | Size: 567 KiB |
|
Before Width: | Height: | Size: 564 KiB After Width: | Height: | Size: 564 KiB |
|
Before Width: | Height: | Size: 572 KiB After Width: | Height: | Size: 572 KiB |
|
Before Width: | Height: | Size: 573 KiB After Width: | Height: | Size: 573 KiB |
|
Before Width: | Height: | Size: 560 KiB After Width: | Height: | Size: 560 KiB |
|
Before Width: | Height: | Size: 98 KiB After Width: | Height: | Size: 98 KiB |
|
Before Width: | Height: | Size: 142 KiB After Width: | Height: | Size: 142 KiB |
|
Before Width: | Height: | Size: 136 KiB After Width: | Height: | Size: 136 KiB |
|
Before Width: | Height: | Size: 171 KiB After Width: | Height: | Size: 171 KiB |
|
Before Width: | Height: | Size: 168 KiB After Width: | Height: | Size: 168 KiB |
|
Before Width: | Height: | Size: 188 KiB After Width: | Height: | Size: 188 KiB |
|
Before Width: | Height: | Size: 132 KiB After Width: | Height: | Size: 132 KiB |
|
Before Width: | Height: | Size: 139 KiB After Width: | Height: | Size: 139 KiB |
|
Before Width: | Height: | Size: 118 KiB After Width: | Height: | Size: 118 KiB |
|
Before Width: | Height: | Size: 118 KiB After Width: | Height: | Size: 118 KiB |
|
Before Width: | Height: | Size: 131 KiB After Width: | Height: | Size: 131 KiB |
|
Before Width: | Height: | Size: 201 KiB After Width: | Height: | Size: 201 KiB |
|
Before Width: | Height: | Size: 134 KiB After Width: | Height: | Size: 134 KiB |
|
Before Width: | Height: | Size: 165 KiB After Width: | Height: | Size: 165 KiB |
|
Before Width: | Height: | Size: 166 KiB After Width: | Height: | Size: 166 KiB |
|
Before Width: | Height: | Size: 218 KiB After Width: | Height: | Size: 218 KiB |
|
Before Width: | Height: | Size: 139 KiB After Width: | Height: | Size: 139 KiB |
|
Before Width: | Height: | Size: 144 KiB After Width: | Height: | Size: 144 KiB |
|
Before Width: | Height: | Size: 145 KiB After Width: | Height: | Size: 145 KiB |
|
Before Width: | Height: | Size: 205 KiB After Width: | Height: | Size: 205 KiB |
|
Before Width: | Height: | Size: 678 KiB After Width: | Height: | Size: 678 KiB |
|
Before Width: | Height: | Size: 626 KiB After Width: | Height: | Size: 626 KiB |
|
Before Width: | Height: | Size: 670 KiB After Width: | Height: | Size: 670 KiB |
|
Before Width: | Height: | Size: 675 KiB After Width: | Height: | Size: 675 KiB |
|
Before Width: | Height: | Size: 666 KiB After Width: | Height: | Size: 666 KiB |
|
Before Width: | Height: | Size: 634 KiB After Width: | Height: | Size: 634 KiB |
|
Before Width: | Height: | Size: 602 KiB After Width: | Height: | Size: 602 KiB |
|
Before Width: | Height: | Size: 614 KiB After Width: | Height: | Size: 614 KiB |
|
Before Width: | Height: | Size: 600 KiB After Width: | Height: | Size: 600 KiB |
|
Before Width: | Height: | Size: 656 KiB After Width: | Height: | Size: 656 KiB |
|
Before Width: | Height: | Size: 710 KiB After Width: | Height: | Size: 710 KiB |
|
Before Width: | Height: | Size: 714 KiB After Width: | Height: | Size: 714 KiB |
|
Before Width: | Height: | Size: 713 KiB After Width: | Height: | Size: 713 KiB |
|
Before Width: | Height: | Size: 727 KiB After Width: | Height: | Size: 727 KiB |
|
Before Width: | Height: | Size: 713 KiB After Width: | Height: | Size: 713 KiB |
|
Before Width: | Height: | Size: 517 KiB After Width: | Height: | Size: 517 KiB |
|
Before Width: | Height: | Size: 515 KiB After Width: | Height: | Size: 515 KiB |
|
Before Width: | Height: | Size: 526 KiB After Width: | Height: | Size: 526 KiB |
|
Before Width: | Height: | Size: 517 KiB After Width: | Height: | Size: 517 KiB |
|
Before Width: | Height: | Size: 526 KiB After Width: | Height: | Size: 526 KiB |
|
Before Width: | Height: | Size: 656 KiB After Width: | Height: | Size: 656 KiB |
|
Before Width: | Height: | Size: 663 KiB After Width: | Height: | Size: 663 KiB |
|
Before Width: | Height: | Size: 648 KiB After Width: | Height: | Size: 648 KiB |
|
Before Width: | Height: | Size: 665 KiB After Width: | Height: | Size: 665 KiB |
|
Before Width: | Height: | Size: 670 KiB After Width: | Height: | Size: 670 KiB |
|
Before Width: | Height: | Size: 384 KiB After Width: | Height: | Size: 384 KiB |
|
Before Width: | Height: | Size: 327 KiB After Width: | Height: | Size: 327 KiB |
|
Before Width: | Height: | Size: 336 KiB After Width: | Height: | Size: 336 KiB |
|
Before Width: | Height: | Size: 325 KiB After Width: | Height: | Size: 325 KiB |
|
Before Width: | Height: | Size: 421 KiB After Width: | Height: | Size: 421 KiB |
|
Before Width: | Height: | Size: 576 KiB After Width: | Height: | Size: 576 KiB |
|
Before Width: | Height: | Size: 589 KiB After Width: | Height: | Size: 589 KiB |
|
Before Width: | Height: | Size: 591 KiB After Width: | Height: | Size: 591 KiB |
|
Before Width: | Height: | Size: 605 KiB After Width: | Height: | Size: 605 KiB |
|
Before Width: | Height: | Size: 585 KiB After Width: | Height: | Size: 585 KiB |
|
Before Width: | Height: | Size: 373 KiB After Width: | Height: | Size: 373 KiB |
|
Before Width: | Height: | Size: 374 KiB After Width: | Height: | Size: 374 KiB |
|
Before Width: | Height: | Size: 389 KiB After Width: | Height: | Size: 389 KiB |
|
Before Width: | Height: | Size: 399 KiB After Width: | Height: | Size: 399 KiB |
|
Before Width: | Height: | Size: 396 KiB After Width: | Height: | Size: 396 KiB |
@ -36,7 +36,7 @@ export function createSun() : AbstractMesh {
|
||||
export function createPlanet(position: Vector3, diameter: number, name: string) : AbstractMesh {
|
||||
const planet = MeshBuilder.CreateSphere(name, {diameter: diameter, segments: 32}, DefaultScene.MainScene);
|
||||
const material = new StandardMaterial(name + "-material", DefaultScene.MainScene);
|
||||
const texture = new Texture("/planetTextures/Arid/Arid_01-512x512.png", DefaultScene.MainScene);
|
||||
const texture = new Texture("/assets/materials/planetTextures/Arid/Arid_01-512x512.png", DefaultScene.MainScene);
|
||||
material.diffuseTexture = texture;
|
||||
material.ambientTexture = texture;
|
||||
material.roughness = 1;
|
||||
|
||||
@ -9,6 +9,9 @@ export class GameConfig {
|
||||
// Physics settings
|
||||
public physicsEnabled: boolean = true;
|
||||
|
||||
// Feature flags
|
||||
public progressionEnabled: boolean = false; // Set to false for simple rookie level
|
||||
|
||||
// Ship physics tuning parameters
|
||||
public shipPhysics = {
|
||||
maxLinearVelocity: 200,
|
||||
@ -42,6 +45,7 @@ export class GameConfig {
|
||||
const config = {
|
||||
physicsEnabled: this.physicsEnabled,
|
||||
debug: this.debug,
|
||||
progressionEnabled: this.progressionEnabled,
|
||||
shipPhysics: this.shipPhysics
|
||||
};
|
||||
localStorage.setItem('game-config', JSON.stringify(config));
|
||||
@ -57,6 +61,7 @@ export class GameConfig {
|
||||
const config = JSON.parse(stored);
|
||||
this.physicsEnabled = config.physicsEnabled ?? true;
|
||||
this.debug = config.debug ?? false;
|
||||
this.progressionEnabled = config.progressionEnabled ?? false;
|
||||
|
||||
// Load ship physics with fallback to defaults
|
||||
if (config.shipPhysics) {
|
||||
@ -81,6 +86,7 @@ export class GameConfig {
|
||||
public reset(): void {
|
||||
this.physicsEnabled = true;
|
||||
this.debug = false;
|
||||
this.progressionEnabled = false;
|
||||
this.shipPhysics = {
|
||||
maxLinearVelocity: 200,
|
||||
maxAngularVelocity: 1.4,
|
||||
|
||||
@ -100,13 +100,15 @@ export class KeyboardInput {
|
||||
document.onkeydown = (ev) => {
|
||||
// Always allow inspector and camera toggle, even when disabled
|
||||
if (ev.key === 'i') {
|
||||
// Open Babylon Inspector
|
||||
import("@babylonjs/inspector").then((inspector) => {
|
||||
inspector.Inspector.Show(this._scene, {
|
||||
// Toggle Babylon Inspector
|
||||
if (this._scene.debugLayer.isVisible()) {
|
||||
this._scene.debugLayer.hide();
|
||||
} else {
|
||||
this._scene.debugLayer.show({
|
||||
overlay: true,
|
||||
showExplorer: true,
|
||||
});
|
||||
});
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
@ -74,10 +74,9 @@ export interface ShipConfig {
|
||||
* All fields optional to allow levels without start bases
|
||||
*/
|
||||
export interface StartBaseConfig {
|
||||
position?: Vector3Array;
|
||||
diameter?: number;
|
||||
height?: number;
|
||||
color?: Vector3Array; // RGB values 0-1
|
||||
position?: Vector3Array; // Defaults to [0, 0, 0] if not specified
|
||||
baseGlbPath?: string; // Path to base GLB model (defaults to 'base.glb')
|
||||
landingGlbPath?: string; // Path to landing zone GLB model (uses same file as base, different mesh name)
|
||||
}
|
||||
|
||||
/**
|
||||
@ -106,7 +105,7 @@ export interface PlanetConfig {
|
||||
export interface AsteroidConfig {
|
||||
id: string;
|
||||
position: Vector3Array;
|
||||
scaling: Vector3Array;
|
||||
scale: number; // Uniform scale applied to all axes
|
||||
linearVelocity: Vector3Array;
|
||||
angularVelocity?: Vector3Array;
|
||||
mass?: number;
|
||||
@ -192,12 +191,6 @@ export function validateLevelConfig(config: any): ValidationResult {
|
||||
if (config.startBase.position && (!Array.isArray(config.startBase.position) || config.startBase.position.length !== 3)) {
|
||||
errors.push('Invalid startBase.position - must be [x, y, z] array');
|
||||
}
|
||||
if (config.startBase.diameter !== undefined && typeof config.startBase.diameter !== 'number') {
|
||||
errors.push('Invalid startBase.diameter - must be a number');
|
||||
}
|
||||
if (config.startBase.height !== undefined && typeof config.startBase.height !== 'number') {
|
||||
errors.push('Invalid startBase.height - must be a number');
|
||||
}
|
||||
}
|
||||
|
||||
// Check sun
|
||||
@ -236,6 +229,17 @@ export function validateLevelConfig(config: any): ValidationResult {
|
||||
if (!Array.isArray(config.asteroids)) {
|
||||
errors.push('Missing or invalid asteroids array');
|
||||
} else {
|
||||
// HYBRID MIGRATION NOTE: If we need to support legacy localStorage data,
|
||||
// add migration here before validation:
|
||||
//
|
||||
// config.asteroids = config.asteroids.map((a, i) => ({
|
||||
// ...a,
|
||||
// id: a.id || `asteroid-${i}`, // Auto-generate missing ids
|
||||
// scale: a.scale || a.scaling?.[0] || a.size || 1 // Migrate from old formats
|
||||
// }));
|
||||
//
|
||||
// This would auto-heal old data with "scaling" array or "size" property
|
||||
|
||||
config.asteroids.forEach((asteroid: any, idx: number) => {
|
||||
if (!asteroid.id || typeof asteroid.id !== 'string') {
|
||||
errors.push(`Asteroid ${idx}: missing or invalid id`);
|
||||
@ -243,8 +247,8 @@ export function validateLevelConfig(config: any): ValidationResult {
|
||||
if (!Array.isArray(asteroid.position) || asteroid.position.length !== 3) {
|
||||
errors.push(`Asteroid ${idx}: invalid position - must be [x, y, z] array`);
|
||||
}
|
||||
if (!Array.isArray(asteroid.scaling) || asteroid.scaling.length !== 3) {
|
||||
errors.push(`Asteroid ${idx}: invalid scaling - must be [x, y, z] array`);
|
||||
if (typeof asteroid.scale !== 'number' || asteroid.scale <= 0) {
|
||||
errors.push(`Asteroid ${idx}: invalid scale - must be a positive number`);
|
||||
}
|
||||
if (!Array.isArray(asteroid.linearVelocity) || asteroid.linearVelocity.length !== 3) {
|
||||
errors.push(`Asteroid ${idx}: invalid linearVelocity - must be [x, y, z] array`);
|
||||
|
||||
@ -30,6 +30,14 @@ export class LevelDeserializer {
|
||||
private config: LevelConfig;
|
||||
|
||||
constructor(config: LevelConfig) {
|
||||
// HYBRID MIGRATION NOTE: If validation fails due to legacy data,
|
||||
// consider adding migration logic here before validation:
|
||||
//
|
||||
// config = migrateLegacyFormat(config);
|
||||
//
|
||||
// This would allow smooth transition for users with old localStorage data
|
||||
// See levelConfig.ts validateLevelConfig() for example migration code
|
||||
|
||||
// Validate config first
|
||||
const validation = validateLevelConfig(config);
|
||||
if (!validation.valid) {
|
||||
@ -72,7 +80,9 @@ export class LevelDeserializer {
|
||||
* Create the start base from config
|
||||
*/
|
||||
private async createStartBase() {
|
||||
return await StarBase.buildStarBase();
|
||||
const position = this.config.startBase?.position;
|
||||
const baseGlbPath = this.config.startBase?.baseGlbPath || 'base.glb';
|
||||
return await StarBase.buildStarBase(position, baseGlbPath);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -166,7 +176,7 @@ export class LevelDeserializer {
|
||||
const rock = await RockFactory.createRock(
|
||||
i,
|
||||
this.arrayToVector3(asteroidConfig.position),
|
||||
this.arrayToVector3(asteroidConfig.scaling),
|
||||
asteroidConfig.scale,
|
||||
this.arrayToVector3(asteroidConfig.linearVelocity),
|
||||
this.arrayToVector3(asteroidConfig.angularVelocity),
|
||||
scoreObservable
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
import { LevelGenerator } from "./levelGenerator";
|
||||
import { LevelConfig, DifficultyConfig, validateLevelConfig } from "./levelConfig";
|
||||
import { LevelConfig, DifficultyConfig, validateLevelConfig, Vector3Array } from "./levelConfig";
|
||||
import debugLog from './debug';
|
||||
|
||||
const STORAGE_KEY = 'space-game-levels';
|
||||
@ -171,8 +171,7 @@ class LevelEditor {
|
||||
(document.getElementById('baseX') as HTMLInputElement).value = config.startBase.position[0].toString();
|
||||
(document.getElementById('baseY') as HTMLInputElement).value = config.startBase.position[1].toString();
|
||||
(document.getElementById('baseZ') as HTMLInputElement).value = config.startBase.position[2].toString();
|
||||
(document.getElementById('baseDiameter') as HTMLInputElement).value = config.startBase.diameter.toString();
|
||||
(document.getElementById('baseHeight') as HTMLInputElement).value = config.startBase.height.toString();
|
||||
(document.getElementById('baseGlbPath') as HTMLInputElement).value = config.startBase.baseGlbPath || 'base.glb';
|
||||
|
||||
// Sun
|
||||
(document.getElementById('sunX') as HTMLInputElement).value = config.sun.position[0].toString();
|
||||
@ -596,9 +595,111 @@ export function getSavedLevel(name: string): LevelConfig | null {
|
||||
return levels.get(name) || null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate a simple rookie level with 4 asteroids
|
||||
* Asteroids at 100-200 distance with 20-100 tangential velocities
|
||||
*/
|
||||
function generateSimpleRookieLevel(): void {
|
||||
debugLog('Creating simple rookie level with 4 asteroids...');
|
||||
|
||||
const levelsMap = new Map<string, LevelConfig>();
|
||||
|
||||
// Create base level structure
|
||||
const config: LevelConfig = {
|
||||
version: "1.0",
|
||||
difficulty: "rookie",
|
||||
timestamp: new Date().toISOString(),
|
||||
metadata: {
|
||||
author: 'System',
|
||||
description: 'Simple rookie training mission with 4 asteroids',
|
||||
type: 'default'
|
||||
},
|
||||
ship: {
|
||||
position: [0, 1, 0],
|
||||
rotation: [0, 0, 0],
|
||||
linearVelocity: [0, 0, 0],
|
||||
angularVelocity: [0, 0, 0]
|
||||
},
|
||||
startBase: {
|
||||
position: [0, 0, 0],
|
||||
baseGlbPath: 'base.glb'
|
||||
},
|
||||
sun: {
|
||||
position: [0, 0, 400],
|
||||
diameter: 50,
|
||||
intensity: 1000000
|
||||
},
|
||||
planets: [],
|
||||
asteroids: [],
|
||||
difficultyConfig: {
|
||||
rockCount: 4,
|
||||
forceMultiplier: 1.0,
|
||||
rockSizeMin: 3,
|
||||
rockSizeMax: 5,
|
||||
distanceMin: 100,
|
||||
distanceMax: 200
|
||||
}
|
||||
};
|
||||
|
||||
// Generate 4 asteroids with tangential velocities
|
||||
const basePosition = [0, 0, 0]; // Start base position
|
||||
|
||||
for (let i = 0; i < 4; i++) {
|
||||
// Random distance between 100-200
|
||||
const distance = 100 + Math.random() * 100;
|
||||
|
||||
// Random angle around the base
|
||||
const angle = (Math.PI * 2 / 4) * i + (Math.random() - 0.5) * 0.5;
|
||||
|
||||
// Position at distance and angle
|
||||
const x = basePosition[0] + distance * Math.cos(angle);
|
||||
const z = basePosition[2] + distance * Math.sin(angle);
|
||||
const y = basePosition[1] + (Math.random() - 0.5) * 20; // Some vertical variation
|
||||
|
||||
// Calculate tangent direction (perpendicular to radial)
|
||||
const tangentX = -Math.sin(angle);
|
||||
const tangentZ = Math.cos(angle);
|
||||
|
||||
// Random tangential speed between 20-100
|
||||
const speed = 20 + Math.random() * 80;
|
||||
|
||||
const linearVelocity: Vector3Array = [
|
||||
tangentX * speed,
|
||||
(Math.random() - 0.5) * 10, // Small vertical velocity
|
||||
tangentZ * speed
|
||||
];
|
||||
|
||||
// Random size between min and max
|
||||
const scale = 3 + Math.random() * 2;
|
||||
|
||||
// Random rotation
|
||||
const angularVelocity: Vector3Array = [
|
||||
(Math.random() - 0.5) * 2,
|
||||
(Math.random() - 0.5) * 2,
|
||||
(Math.random() - 0.5) * 2
|
||||
];
|
||||
|
||||
config.asteroids.push({
|
||||
id: `asteroid-${i}`,
|
||||
position: [x, y, z],
|
||||
scale,
|
||||
linearVelocity,
|
||||
angularVelocity
|
||||
});
|
||||
}
|
||||
|
||||
levelsMap.set('Rookie Training', config);
|
||||
debugLog('Generated simple rookie level with 4 asteroids');
|
||||
|
||||
// Save to localStorage
|
||||
const levelsArray = Array.from(levelsMap.entries());
|
||||
localStorage.setItem(STORAGE_KEY, JSON.stringify(levelsArray));
|
||||
debugLog('Simple rookie level saved to localStorage');
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate default levels if localStorage is empty
|
||||
* Creates 4 levels: recruit, pilot, captain, commander
|
||||
* Creates either a simple rookie level or 6 themed levels based on progression flag
|
||||
*/
|
||||
export function generateDefaultLevels(): void {
|
||||
const existing = getSavedLevels();
|
||||
@ -607,30 +708,82 @@ export function generateDefaultLevels(): void {
|
||||
return;
|
||||
}
|
||||
|
||||
debugLog('No saved levels found, generating 4 default levels...');
|
||||
// Check progression flag from GameConfig
|
||||
const GameConfig = (window as any).GameConfig;
|
||||
const progressionEnabled = GameConfig?.getInstance().progressionEnabled ?? false;
|
||||
|
||||
if (!progressionEnabled) {
|
||||
debugLog('Progression disabled - generating simple rookie level...');
|
||||
generateSimpleRookieLevel();
|
||||
return;
|
||||
}
|
||||
|
||||
debugLog('No saved levels found, generating 6 default levels...');
|
||||
|
||||
// Define themed default levels with descriptions
|
||||
const defaultLevels = [
|
||||
{
|
||||
name: 'Tutorial: Asteroid Field',
|
||||
difficulty: 'recruit',
|
||||
description: 'Learn the basics of ship control and asteroid destruction in a calm sector of space.',
|
||||
estimatedTime: '3-5 minutes'
|
||||
},
|
||||
{
|
||||
name: 'Rescue Mission',
|
||||
difficulty: 'pilot',
|
||||
description: 'Clear a path through moderate asteroid density to reach the stranded station.',
|
||||
estimatedTime: '5-8 minutes'
|
||||
},
|
||||
{
|
||||
name: 'Deep Space Patrol',
|
||||
difficulty: 'captain',
|
||||
description: 'Patrol a dangerous sector with heavy asteroid activity. Watch your fuel!',
|
||||
estimatedTime: '8-12 minutes'
|
||||
},
|
||||
{
|
||||
name: 'Enemy Territory',
|
||||
difficulty: 'commander',
|
||||
description: 'Navigate through hostile space with high-speed asteroids and limited resources.',
|
||||
estimatedTime: '12-15 minutes'
|
||||
},
|
||||
{
|
||||
name: 'The Gauntlet',
|
||||
difficulty: 'commander',
|
||||
description: 'Face maximum asteroid density in this ultimate test of piloting skill.',
|
||||
estimatedTime: '15-20 minutes'
|
||||
},
|
||||
{
|
||||
name: 'Final Challenge',
|
||||
difficulty: 'commander',
|
||||
description: 'The ultimate challenge - survive the most chaotic asteroid field in known space.',
|
||||
estimatedTime: '20+ minutes'
|
||||
}
|
||||
];
|
||||
|
||||
const difficulties = ['recruit', 'pilot', 'captain', 'commander'];
|
||||
const levelsMap = new Map<string, LevelConfig>();
|
||||
|
||||
for (const difficulty of difficulties) {
|
||||
const generator = new LevelGenerator(difficulty);
|
||||
for (const level of defaultLevels) {
|
||||
const generator = new LevelGenerator(level.difficulty);
|
||||
const config = generator.generate();
|
||||
|
||||
// Add metadata
|
||||
// Add rich metadata
|
||||
config.metadata = {
|
||||
author: 'System',
|
||||
description: `Default ${difficulty} level`
|
||||
description: level.description,
|
||||
estimatedTime: level.estimatedTime,
|
||||
type: 'default',
|
||||
difficulty: level.difficulty
|
||||
};
|
||||
|
||||
levelsMap.set(difficulty, config);
|
||||
debugLog(`Generated default level: ${difficulty}`);
|
||||
levelsMap.set(level.name, config);
|
||||
debugLog(`Generated default level: ${level.name} (${level.difficulty})`);
|
||||
}
|
||||
|
||||
// Save all levels to localStorage
|
||||
const levelsArray = Array.from(levelsMap.entries());
|
||||
localStorage.setItem(STORAGE_KEY, JSON.stringify(levelsArray));
|
||||
|
||||
debugLog('Default levels saved to localStorage');
|
||||
debugLog(`${defaultLevels.length} default levels saved to localStorage`);
|
||||
}
|
||||
|
||||
// Export for manual initialization if needed
|
||||
|
||||
@ -159,10 +159,9 @@ export class LevelGenerator {
|
||||
|
||||
const position: Vector3Array = [x, y, z];
|
||||
|
||||
// Random size
|
||||
// Random size (uniform scale)
|
||||
const sizeRange = config.rockSizeMax - config.rockSizeMin;
|
||||
const size = Math.random() * sizeRange + config.rockSizeMin;
|
||||
const scaling: Vector3Array = [size, size, size];
|
||||
const scale = Math.random() * sizeRange + config.rockSizeMin;
|
||||
|
||||
// Calculate initial velocity based on force applied in Level1
|
||||
// Velocity should be tangential to the sphere (perpendicular to radius)
|
||||
@ -182,7 +181,7 @@ export class LevelGenerator {
|
||||
asteroids.push({
|
||||
id: `asteroid-${i}`,
|
||||
position,
|
||||
scaling,
|
||||
scale,
|
||||
linearVelocity,
|
||||
angularVelocity: [0, 0, 0],
|
||||
mass
|
||||
|
||||
@ -1,11 +1,14 @@
|
||||
import { getSavedLevels } from "./levelEditor";
|
||||
import { LevelConfig } from "./levelConfig";
|
||||
import { ProgressionManager } from "./progression";
|
||||
import { GameConfig } from "./gameConfig";
|
||||
import debugLog from './debug';
|
||||
|
||||
const SELECTED_LEVEL_KEY = 'space-game-selected-level';
|
||||
|
||||
/**
|
||||
* Populate the level selection screen with saved levels
|
||||
* Shows default levels and custom levels with progression tracking
|
||||
*/
|
||||
export function populateLevelSelector(): boolean {
|
||||
const container = document.getElementById('levelCardsContainer');
|
||||
@ -15,16 +18,11 @@ export function populateLevelSelector(): boolean {
|
||||
}
|
||||
|
||||
const savedLevels = getSavedLevels();
|
||||
const gameConfig = GameConfig.getInstance();
|
||||
const progressionEnabled = gameConfig.progressionEnabled;
|
||||
const progression = ProgressionManager.getInstance();
|
||||
|
||||
// Filter to only show recruit and pilot difficulty levels
|
||||
const filteredLevels = new Map<string, LevelConfig>();
|
||||
for (const [name, config] of savedLevels.entries()) {
|
||||
if (config.difficulty === 'recruit' || config.difficulty === 'pilot') {
|
||||
filteredLevels.set(name, config);
|
||||
}
|
||||
}
|
||||
|
||||
if (filteredLevels.size === 0) {
|
||||
if (savedLevels.size === 0) {
|
||||
container.innerHTML = `
|
||||
<div style="
|
||||
grid-column: 1 / -1;
|
||||
@ -33,7 +31,7 @@ export function populateLevelSelector(): boolean {
|
||||
color: #ccc;
|
||||
">
|
||||
<h2 style="margin-bottom: 20px;">No Levels Found</h2>
|
||||
<p style="margin-bottom: 30px;">Create your first level to get started!</p>
|
||||
<p style="margin-bottom: 30px;">Something went wrong - default levels should be auto-generated!</p>
|
||||
<a href="#/editor" style="
|
||||
display: inline-block;
|
||||
padding: 15px 30px;
|
||||
@ -49,25 +47,285 @@ export function populateLevelSelector(): boolean {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Create level cards
|
||||
// Separate default and custom levels
|
||||
const defaultLevels = new Map<string, LevelConfig>();
|
||||
const customLevels = new Map<string, LevelConfig>();
|
||||
|
||||
for (const [name, config] of savedLevels.entries()) {
|
||||
if (config.metadata?.type === 'default') {
|
||||
defaultLevels.set(name, config);
|
||||
} else {
|
||||
customLevels.set(name, config);
|
||||
}
|
||||
}
|
||||
|
||||
let html = '';
|
||||
for (const [name, config] of filteredLevels.entries()) {
|
||||
const timestamp = config.timestamp ? new Date(config.timestamp).toLocaleDateString() : '';
|
||||
const description = config.metadata?.description || `${config.asteroids.length} asteroids • ${config.planets.length} planets`;
|
||||
|
||||
// Show progression stats only if progression is enabled
|
||||
if (progressionEnabled) {
|
||||
const completedCount = progression.getCompletedCount();
|
||||
const totalCount = progression.getTotalDefaultLevels();
|
||||
const completionPercent = progression.getCompletionPercentage();
|
||||
const nextLevel = progression.getNextLevel();
|
||||
|
||||
html += `
|
||||
<div class="level-card">
|
||||
<h2>${name}</h2>
|
||||
<div style="font-size: 0.9em; color: #aaa; margin: 10px 0;">
|
||||
Difficulty: ${config.difficulty}
|
||||
<div style="
|
||||
grid-column: 1 / -1;
|
||||
background: linear-gradient(135deg, rgba(102, 126, 234, 0.1) 0%, rgba(118, 75, 162, 0.1) 100%);
|
||||
border-radius: 10px;
|
||||
padding: 20px;
|
||||
margin-bottom: 20px;
|
||||
border: 1px solid rgba(102, 126, 234, 0.3);
|
||||
">
|
||||
<h3 style="margin: 0 0 10px 0; color: #fff;">Progress</h3>
|
||||
<div style="color: #ccc; margin-bottom: 10px;">
|
||||
${completedCount} of ${totalCount} default levels completed (${completionPercent.toFixed(0)}%)
|
||||
</div>
|
||||
<p>${description}</p>
|
||||
${timestamp ? `<div style="font-size: 0.8em; color: #888; margin-bottom: 10px;">${timestamp}</div>` : ''}
|
||||
<button class="level-button" data-level="${name}">Play Level</button>
|
||||
<div style="
|
||||
width: 100%;
|
||||
height: 10px;
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
border-radius: 5px;
|
||||
overflow: hidden;
|
||||
">
|
||||
<div style="
|
||||
width: ${completionPercent}%;
|
||||
height: 100%;
|
||||
background: linear-gradient(90deg, #667eea 0%, #764ba2 100%);
|
||||
transition: width 0.3s ease;
|
||||
"></div>
|
||||
</div>
|
||||
${nextLevel ? `<div style="color: #888; margin-top: 10px; font-size: 0.9em;">Next: ${nextLevel}</div>` : ''}
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
// Default levels section - show all levels if progression disabled, or current/next if enabled
|
||||
if (defaultLevels.size > 0) {
|
||||
html += `
|
||||
<div style="grid-column: 1 / -1; margin: 20px 0 10px 0;">
|
||||
<h3 style="color: #fff; margin: 0;">Available Levels</h3>
|
||||
</div>
|
||||
`;
|
||||
|
||||
// If progression is disabled, just show all default levels
|
||||
if (!progressionEnabled) {
|
||||
for (const [name, config] of defaultLevels.entries()) {
|
||||
const description = config.metadata?.description || `${config.asteroids.length} asteroids • ${config.planets.length} planets`;
|
||||
const estimatedTime = config.metadata?.estimatedTime || '';
|
||||
|
||||
html += `
|
||||
<div class="level-card">
|
||||
<h2 style="margin: 0;">${name}</h2>
|
||||
<div style="font-size: 0.9em; color: #aaa; margin: 10px 0;">
|
||||
Difficulty: ${config.difficulty}${estimatedTime ? ` • ${estimatedTime}` : ''}
|
||||
</div>
|
||||
<p style="margin: 10px 0;">${description}</p>
|
||||
<button class="level-button" data-level="${name}">Play Level</button>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
} else {
|
||||
// Progression enabled - show current and next level only
|
||||
// Get the default level names in order
|
||||
const defaultLevelNames = [
|
||||
'Tutorial: Asteroid Field',
|
||||
'Rescue Mission',
|
||||
'Deep Space Patrol',
|
||||
'Enemy Territory',
|
||||
'The Gauntlet',
|
||||
'Final Challenge'
|
||||
];
|
||||
|
||||
// Find current level (last completed or first if none completed)
|
||||
let currentLevelName: string | null = null;
|
||||
let nextLevelName: string | null = null;
|
||||
|
||||
// Find the first incomplete level (this is the "next" level)
|
||||
for (let i = 0; i < defaultLevelNames.length; i++) {
|
||||
const levelName = defaultLevelNames[i];
|
||||
if (!progression.isLevelComplete(levelName)) {
|
||||
nextLevelName = levelName;
|
||||
// Current level is the one before (if it exists)
|
||||
if (i > 0) {
|
||||
currentLevelName = defaultLevelNames[i - 1];
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// If all levels complete, show the last level as current
|
||||
if (!nextLevelName) {
|
||||
currentLevelName = defaultLevelNames[defaultLevelNames.length - 1];
|
||||
}
|
||||
|
||||
// If no levels completed yet, show first as next (no current)
|
||||
if (!currentLevelName && nextLevelName) {
|
||||
// First time player - just show the first level
|
||||
const config = defaultLevels.get(nextLevelName);
|
||||
if (config) {
|
||||
const description = config.metadata?.description || `${config.asteroids.length} asteroids • ${config.planets.length} planets`;
|
||||
const estimatedTime = config.metadata?.estimatedTime || '';
|
||||
|
||||
html += `
|
||||
<div class="level-card" style="border: 2px solid #667eea; box-shadow: 0 0 20px rgba(102, 126, 234, 0.3);">
|
||||
<div style="display: flex; justify-content: space-between; align-items: start;">
|
||||
<h2 style="margin: 0;">${nextLevelName}</h2>
|
||||
<div style="font-size: 0.8em; background: #667eea; padding: 4px 8px; border-radius: 4px; color: white; font-weight: bold;">START HERE</div>
|
||||
</div>
|
||||
<div style="font-size: 0.9em; color: #aaa; margin: 10px 0;">
|
||||
Difficulty: ${config.difficulty}${estimatedTime ? ` • ${estimatedTime}` : ''}
|
||||
</div>
|
||||
<p style="margin: 10px 0;">${description}</p>
|
||||
<button class="level-button" data-level="${nextLevelName}">Play Level</button>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
} else {
|
||||
// Show current (completed) level
|
||||
if (currentLevelName) {
|
||||
const config = defaultLevels.get(currentLevelName);
|
||||
if (config) {
|
||||
const levelProgress = progression.getLevelProgress(currentLevelName);
|
||||
const description = config.metadata?.description || `${config.asteroids.length} asteroids • ${config.planets.length} planets`;
|
||||
const estimatedTime = config.metadata?.estimatedTime || '';
|
||||
|
||||
html += `
|
||||
<div class="level-card">
|
||||
<div style="display: flex; justify-content: space-between; align-items: start;">
|
||||
<h2 style="margin: 0;">${currentLevelName}</h2>
|
||||
<div style="font-size: 1.5em; color: #4ade80;">✓</div>
|
||||
</div>
|
||||
<div style="font-size: 0.9em; color: #aaa; margin: 10px 0;">
|
||||
Difficulty: ${config.difficulty}${estimatedTime ? ` • ${estimatedTime}` : ''}
|
||||
</div>
|
||||
<p style="margin: 10px 0;">${description}</p>
|
||||
${levelProgress?.playCount ? `<div style="font-size: 0.8em; color: #888; margin-bottom: 10px;">Played ${levelProgress.playCount} time${levelProgress.playCount > 1 ? 's' : ''}</div>` : ''}
|
||||
<button class="level-button" data-level="${currentLevelName}">Play Again</button>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
// Show next level if it exists
|
||||
if (nextLevelName) {
|
||||
const config = defaultLevels.get(nextLevelName);
|
||||
if (config) {
|
||||
const description = config.metadata?.description || `${config.asteroids.length} asteroids • ${config.planets.length} planets`;
|
||||
const estimatedTime = config.metadata?.estimatedTime || '';
|
||||
|
||||
html += `
|
||||
<div class="level-card" style="border: 2px solid #667eea; box-shadow: 0 0 20px rgba(102, 126, 234, 0.3);">
|
||||
<div style="display: flex; justify-content: space-between; align-items: start;">
|
||||
<h2 style="margin: 0;">${nextLevelName}</h2>
|
||||
<div style="font-size: 0.8em; background: #667eea; padding: 4px 8px; border-radius: 4px; color: white; font-weight: bold;">NEXT</div>
|
||||
</div>
|
||||
<div style="font-size: 0.9em; color: #aaa; margin: 10px 0;">
|
||||
Difficulty: ${config.difficulty}${estimatedTime ? ` • ${estimatedTime}` : ''}
|
||||
</div>
|
||||
<p style="margin: 10px 0;">${description}</p>
|
||||
<button class="level-button" data-level="${nextLevelName}">Play Level</button>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Show "more levels beyond" indicator if there are additional levels after next
|
||||
const nextLevelIndex = defaultLevelNames.indexOf(nextLevelName || '');
|
||||
const hasMoreLevels = nextLevelIndex >= 0 && nextLevelIndex < defaultLevelNames.length - 1;
|
||||
|
||||
if (hasMoreLevels) {
|
||||
const remainingCount = defaultLevelNames.length - nextLevelIndex - 1;
|
||||
html += `
|
||||
<div style="
|
||||
grid-column: 1 / -1;
|
||||
text-align: center;
|
||||
padding: 30px;
|
||||
color: #888;
|
||||
font-size: 1.1em;
|
||||
background: linear-gradient(135deg, rgba(102, 126, 234, 0.05) 0%, rgba(118, 75, 162, 0.05) 100%);
|
||||
border-radius: 10px;
|
||||
border: 1px dashed rgba(102, 126, 234, 0.3);
|
||||
margin-top: 10px;
|
||||
">
|
||||
<div style="font-size: 2em; margin-bottom: 10px; opacity: 0.5;">✦ ✦ ✦</div>
|
||||
<div style="font-weight: bold; color: #aaa;">
|
||||
${remainingCount} more level${remainingCount > 1 ? 's' : ''} beyond...
|
||||
</div>
|
||||
<div style="font-size: 0.9em; margin-top: 5px; color: #777;">
|
||||
Complete challenges to unlock new missions
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
} // End of progressionEnabled else block
|
||||
}
|
||||
|
||||
// Custom levels section
|
||||
if (customLevels.size > 0) {
|
||||
html += `
|
||||
<div style="grid-column: 1 / -1; margin: 30px 0 10px 0;">
|
||||
<h3 style="color: #fff; margin: 0;">Custom Levels</h3>
|
||||
</div>
|
||||
`;
|
||||
|
||||
for (const [name, config] of customLevels.entries()) {
|
||||
const timestamp = config.timestamp ? new Date(config.timestamp).toLocaleDateString() : '';
|
||||
const description = config.metadata?.description || `${config.asteroids.length} asteroids • ${config.planets.length} planets`;
|
||||
const author = config.metadata?.author || 'Unknown';
|
||||
|
||||
html += `
|
||||
<div class="level-card">
|
||||
<h2>${name}</h2>
|
||||
<div style="font-size: 0.9em; color: #aaa; margin: 10px 0;">
|
||||
Difficulty: ${config.difficulty} • By ${author}
|
||||
</div>
|
||||
<p>${description}</p>
|
||||
${timestamp ? `<div style="font-size: 0.8em; color: #888; margin-bottom: 10px;">Created ${timestamp}</div>` : ''}
|
||||
<button class="level-button" data-level="${name}">Play Level</button>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
// Editor unlock button (always unlocked if progression disabled)
|
||||
const isEditorUnlocked = !progressionEnabled || progression.isEditorUnlocked();
|
||||
const completedCount = progression.getCompletedCount();
|
||||
|
||||
html += `
|
||||
<div style="grid-column: 1 / -1; margin-top: 20px; text-align: center;">
|
||||
${isEditorUnlocked ? `
|
||||
<a href="#/editor" style="
|
||||
display: inline-block;
|
||||
padding: 15px 40px;
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
color: white;
|
||||
text-decoration: none;
|
||||
border-radius: 8px;
|
||||
font-weight: bold;
|
||||
font-size: 1.1em;
|
||||
transition: transform 0.2s;
|
||||
" onmouseover="this.style.transform='scale(1.05)'" onmouseout="this.style.transform='scale(1)'">
|
||||
🎨 Create Custom Level
|
||||
</a>
|
||||
` : `
|
||||
<div style="
|
||||
padding: 15px 40px;
|
||||
background: rgba(100, 100, 100, 0.3);
|
||||
color: #888;
|
||||
border-radius: 8px;
|
||||
font-weight: bold;
|
||||
font-size: 1.1em;
|
||||
display: inline-block;
|
||||
cursor: not-allowed;
|
||||
" title="Complete ${progression.getTotalDefaultLevels() - progression.getCompletedCount()} more default level(s) to unlock">
|
||||
🔒 Level Editor (Complete ${3 - completedCount} more level${(3 - completedCount) !== 1 ? 's' : ''})
|
||||
</div>
|
||||
`}
|
||||
</div>
|
||||
`;
|
||||
|
||||
container.innerHTML = html;
|
||||
|
||||
// Add event listeners to level buttons
|
||||
|
||||
@ -100,7 +100,7 @@ export class LevelSerializer {
|
||||
}
|
||||
|
||||
/**
|
||||
* Serialize start base state
|
||||
* Serialize start base state (position and GLB paths)
|
||||
*/
|
||||
private serializeStartBase(): StartBaseConfig {
|
||||
const startBase = this.scene.getMeshByName("startBase");
|
||||
@ -109,31 +109,18 @@ export class LevelSerializer {
|
||||
console.warn("Start base not found, using defaults");
|
||||
return {
|
||||
position: [0, 0, 0],
|
||||
diameter: 10,
|
||||
height: 1,
|
||||
color: [1, 1, 0]
|
||||
baseGlbPath: 'base.glb'
|
||||
};
|
||||
}
|
||||
|
||||
const position = this.vector3ToArray(startBase.position);
|
||||
|
||||
// Try to extract diameter and height from scaling or metadata
|
||||
// Assuming cylinder was created with specific dimensions
|
||||
const diameter = 10; // Default from Level1
|
||||
const height = 1; // Default from Level1
|
||||
|
||||
// Get color from material if available
|
||||
let color: Vector3Array = [1, 1, 0]; // Default yellow
|
||||
if (startBase.material && (startBase.material as any).diffuseColor) {
|
||||
const diffuseColor = (startBase.material as any).diffuseColor;
|
||||
color = [diffuseColor.r, diffuseColor.g, diffuseColor.b];
|
||||
}
|
||||
// Capture GLB path from metadata if available, otherwise use default
|
||||
const baseGlbPath = startBase.metadata?.baseGlbPath || 'base.glb';
|
||||
|
||||
return {
|
||||
position,
|
||||
diameter,
|
||||
height,
|
||||
color
|
||||
baseGlbPath
|
||||
};
|
||||
}
|
||||
|
||||
@ -191,7 +178,7 @@ export class LevelSerializer {
|
||||
const diameter = boundingInfo.boundingSphere.radiusWorld * 2;
|
||||
|
||||
// Get texture path from material
|
||||
let texturePath = "/planetTextures/Arid/Arid_01-512x512.png"; // Default
|
||||
let texturePath = "/assets/materials/planetTextures/Arid/Arid_01-512x512.png"; // Default
|
||||
if (mesh.material && (mesh.material as any).diffuseTexture) {
|
||||
const texture = (mesh.material as any).diffuseTexture;
|
||||
texturePath = texture.url || texturePath;
|
||||
@ -222,7 +209,8 @@ export class LevelSerializer {
|
||||
|
||||
for (const mesh of asteroidMeshes) {
|
||||
const position = this.vector3ToArray(mesh.position);
|
||||
const scaling = this.vector3ToArray(mesh.scaling);
|
||||
// Use uniform scale (assume uniform scaling, take x component)
|
||||
const scale = parseFloat(mesh.scaling.x.toFixed(3));
|
||||
|
||||
// Get velocities from physics body
|
||||
let linearVelocity: Vector3Array = [0, 0, 0];
|
||||
@ -238,7 +226,7 @@ export class LevelSerializer {
|
||||
asteroids.push({
|
||||
id: mesh.name,
|
||||
position,
|
||||
scaling,
|
||||
scale,
|
||||
linearVelocity,
|
||||
angularVelocity,
|
||||
mass
|
||||
|
||||
16
src/main.ts
@ -118,6 +118,12 @@ export class Main {
|
||||
|
||||
// Listen for replay requests from the ship
|
||||
if (ship) {
|
||||
// Set current level name for progression tracking
|
||||
if (ship._statusScreen) {
|
||||
ship._statusScreen.setCurrentLevel(levelName);
|
||||
debugLog(`Set current level for progression: ${levelName}`);
|
||||
}
|
||||
|
||||
ship.onReplayRequestObservable.add(() => {
|
||||
debugLog('Replay requested - reloading page');
|
||||
window.location.reload();
|
||||
@ -472,16 +478,10 @@ export class Main {
|
||||
|
||||
// Setup router
|
||||
router.on('/', () => {
|
||||
// Check if there are saved levels
|
||||
if (!hasSavedLevels()) {
|
||||
debugLog('No saved levels found, redirecting to editor');
|
||||
router.navigate('/editor');
|
||||
return;
|
||||
}
|
||||
|
||||
// Always show game view with level selector (no editor redirect)
|
||||
showView('game');
|
||||
|
||||
// Populate level selector
|
||||
// Populate level selector (will show default levels if no custom levels)
|
||||
populateLevelSelector();
|
||||
|
||||
// Initialize game if not in debug mode
|
||||
|
||||
@ -5,103 +5,103 @@
|
||||
|
||||
export const PLANET_TEXTURES = [
|
||||
// Arid planets (5 textures)
|
||||
"/planetTextures/Arid/Arid_01-512x512.png",
|
||||
"/planetTextures/Arid/Arid_02-512x512.png",
|
||||
"/planetTextures/Arid/Arid_03-512x512.png",
|
||||
"/planetTextures/Arid/Arid_04-512x512.png",
|
||||
"/planetTextures/Arid/Arid_05-512x512.png",
|
||||
"/assets/materials/planetTextures/Arid/Arid_01-512x512.png",
|
||||
"/assets/materials/planetTextures/Arid/Arid_02-512x512.png",
|
||||
"/assets/materials/planetTextures/Arid/Arid_03-512x512.png",
|
||||
"/assets/materials/planetTextures/Arid/Arid_04-512x512.png",
|
||||
"/assets/materials/planetTextures/Arid/Arid_05-512x512.png",
|
||||
|
||||
// Barren planets (5 textures)
|
||||
"/planetTextures/Barren/Barren_01-512x512.png",
|
||||
"/planetTextures/Barren/Barren_02-512x512.png",
|
||||
"/planetTextures/Barren/Barren_03-512x512.png",
|
||||
"/planetTextures/Barren/Barren_04-512x512.png",
|
||||
"/planetTextures/Barren/Barren_05-512x512.png",
|
||||
"/assets/materials/planetTextures/Barren/Barren_01-512x512.png",
|
||||
"/assets/materials/planetTextures/Barren/Barren_02-512x512.png",
|
||||
"/assets/materials/planetTextures/Barren/Barren_03-512x512.png",
|
||||
"/assets/materials/planetTextures/Barren/Barren_04-512x512.png",
|
||||
"/assets/materials/planetTextures/Barren/Barren_05-512x512.png",
|
||||
|
||||
// Dusty planets (5 textures)
|
||||
"/planetTextures/Dusty/Dusty_01-512x512.png",
|
||||
"/planetTextures/Dusty/Dusty_02-512x512.png",
|
||||
"/planetTextures/Dusty/Dusty_03-512x512.png",
|
||||
"/planetTextures/Dusty/Dusty_04-512x512.png",
|
||||
"/planetTextures/Dusty/Dusty_05-512x512.png",
|
||||
"/assets/materials/planetTextures/Dusty/Dusty_01-512x512.png",
|
||||
"/assets/materials/planetTextures/Dusty/Dusty_02-512x512.png",
|
||||
"/assets/materials/planetTextures/Dusty/Dusty_03-512x512.png",
|
||||
"/assets/materials/planetTextures/Dusty/Dusty_04-512x512.png",
|
||||
"/assets/materials/planetTextures/Dusty/Dusty_05-512x512.png",
|
||||
|
||||
// Gaseous planets (20 textures)
|
||||
"/planetTextures/Gaseous/Gaseous_01-512x512.png",
|
||||
"/planetTextures/Gaseous/Gaseous_02-512x512.png",
|
||||
"/planetTextures/Gaseous/Gaseous_03-512x512.png",
|
||||
"/planetTextures/Gaseous/Gaseous_04-512x512.png",
|
||||
"/planetTextures/Gaseous/Gaseous_05-512x512.png",
|
||||
"/planetTextures/Gaseous/Gaseous_06-512x512.png",
|
||||
"/planetTextures/Gaseous/Gaseous_07-512x512.png",
|
||||
"/planetTextures/Gaseous/Gaseous_08-512x512.png",
|
||||
"/planetTextures/Gaseous/Gaseous_09-512x512.png",
|
||||
"/planetTextures/Gaseous/Gaseous_10-512x512.png",
|
||||
"/planetTextures/Gaseous/Gaseous_11-512x512.png",
|
||||
"/planetTextures/Gaseous/Gaseous_12-512x512.png",
|
||||
"/planetTextures/Gaseous/Gaseous_13-512x512.png",
|
||||
"/planetTextures/Gaseous/Gaseous_14-512x512.png",
|
||||
"/planetTextures/Gaseous/Gaseous_15-512x512.png",
|
||||
"/planetTextures/Gaseous/Gaseous_16-512x512.png",
|
||||
"/planetTextures/Gaseous/Gaseous_17-512x512.png",
|
||||
"/planetTextures/Gaseous/Gaseous_18-512x512.png",
|
||||
"/planetTextures/Gaseous/Gaseous_19-512x512.png",
|
||||
"/planetTextures/Gaseous/Gaseous_20-512x512.png",
|
||||
"/assets/materials/planetTextures/Gaseous/Gaseous_01-512x512.png",
|
||||
"/assets/materials/planetTextures/Gaseous/Gaseous_02-512x512.png",
|
||||
"/assets/materials/planetTextures/Gaseous/Gaseous_03-512x512.png",
|
||||
"/assets/materials/planetTextures/Gaseous/Gaseous_04-512x512.png",
|
||||
"/assets/materials/planetTextures/Gaseous/Gaseous_05-512x512.png",
|
||||
"/assets/materials/planetTextures/Gaseous/Gaseous_06-512x512.png",
|
||||
"/assets/materials/planetTextures/Gaseous/Gaseous_07-512x512.png",
|
||||
"/assets/materials/planetTextures/Gaseous/Gaseous_08-512x512.png",
|
||||
"/assets/materials/planetTextures/Gaseous/Gaseous_09-512x512.png",
|
||||
"/assets/materials/planetTextures/Gaseous/Gaseous_10-512x512.png",
|
||||
"/assets/materials/planetTextures/Gaseous/Gaseous_11-512x512.png",
|
||||
"/assets/materials/planetTextures/Gaseous/Gaseous_12-512x512.png",
|
||||
"/assets/materials/planetTextures/Gaseous/Gaseous_13-512x512.png",
|
||||
"/assets/materials/planetTextures/Gaseous/Gaseous_14-512x512.png",
|
||||
"/assets/materials/planetTextures/Gaseous/Gaseous_15-512x512.png",
|
||||
"/assets/materials/planetTextures/Gaseous/Gaseous_16-512x512.png",
|
||||
"/assets/materials/planetTextures/Gaseous/Gaseous_17-512x512.png",
|
||||
"/assets/materials/planetTextures/Gaseous/Gaseous_18-512x512.png",
|
||||
"/assets/materials/planetTextures/Gaseous/Gaseous_19-512x512.png",
|
||||
"/assets/materials/planetTextures/Gaseous/Gaseous_20-512x512.png",
|
||||
|
||||
// Grassland planets (5 textures)
|
||||
"/planetTextures/Grassland/Grassland_01-512x512.png",
|
||||
"/planetTextures/Grassland/Grassland_02-512x512.png",
|
||||
"/planetTextures/Grassland/Grassland_03-512x512.png",
|
||||
"/planetTextures/Grassland/Grassland_04-512x512.png",
|
||||
"/planetTextures/Grassland/Grassland_05-512x512.png",
|
||||
"/assets/materials/planetTextures/Grassland/Grassland_01-512x512.png",
|
||||
"/assets/materials/planetTextures/Grassland/Grassland_02-512x512.png",
|
||||
"/assets/materials/planetTextures/Grassland/Grassland_03-512x512.png",
|
||||
"/assets/materials/planetTextures/Grassland/Grassland_04-512x512.png",
|
||||
"/assets/materials/planetTextures/Grassland/Grassland_05-512x512.png",
|
||||
|
||||
// Jungle planets (5 textures)
|
||||
"/planetTextures/Jungle/Jungle_01-512x512.png",
|
||||
"/planetTextures/Jungle/Jungle_02-512x512.png",
|
||||
"/planetTextures/Jungle/Jungle_03-512x512.png",
|
||||
"/planetTextures/Jungle/Jungle_04-512x512.png",
|
||||
"/planetTextures/Jungle/Jungle_05-512x512.png",
|
||||
"/assets/materials/planetTextures/Jungle/Jungle_01-512x512.png",
|
||||
"/assets/materials/planetTextures/Jungle/Jungle_02-512x512.png",
|
||||
"/assets/materials/planetTextures/Jungle/Jungle_03-512x512.png",
|
||||
"/assets/materials/planetTextures/Jungle/Jungle_04-512x512.png",
|
||||
"/assets/materials/planetTextures/Jungle/Jungle_05-512x512.png",
|
||||
|
||||
// Marshy planets (5 textures)
|
||||
"/planetTextures/Marshy/Marshy_01-512x512.png",
|
||||
"/planetTextures/Marshy/Marshy_02-512x512.png",
|
||||
"/planetTextures/Marshy/Marshy_03-512x512.png",
|
||||
"/planetTextures/Marshy/Marshy_04-512x512.png",
|
||||
"/planetTextures/Marshy/Marshy_05-512x512.png",
|
||||
"/assets/materials/planetTextures/Marshy/Marshy_01-512x512.png",
|
||||
"/assets/materials/planetTextures/Marshy/Marshy_02-512x512.png",
|
||||
"/assets/materials/planetTextures/Marshy/Marshy_03-512x512.png",
|
||||
"/assets/materials/planetTextures/Marshy/Marshy_04-512x512.png",
|
||||
"/assets/materials/planetTextures/Marshy/Marshy_05-512x512.png",
|
||||
|
||||
// Martian planets (5 textures)
|
||||
"/planetTextures/Martian/Martian_01-512x512.png",
|
||||
"/planetTextures/Martian/Martian_02-512x512.png",
|
||||
"/planetTextures/Martian/Martian_03-512x512.png",
|
||||
"/planetTextures/Martian/Martian_04-512x512.png",
|
||||
"/planetTextures/Martian/Martian_05-512x512.png",
|
||||
"/assets/materials/planetTextures/Martian/Martian_01-512x512.png",
|
||||
"/assets/materials/planetTextures/Martian/Martian_02-512x512.png",
|
||||
"/assets/materials/planetTextures/Martian/Martian_03-512x512.png",
|
||||
"/assets/materials/planetTextures/Martian/Martian_04-512x512.png",
|
||||
"/assets/materials/planetTextures/Martian/Martian_05-512x512.png",
|
||||
|
||||
// Methane planets (5 textures)
|
||||
"/planetTextures/Methane/Methane_01-512x512.png",
|
||||
"/planetTextures/Methane/Methane_02-512x512.png",
|
||||
"/planetTextures/Methane/Methane_03-512x512.png",
|
||||
"/planetTextures/Methane/Methane_04-512x512.png",
|
||||
"/planetTextures/Methane/Methane_05-512x512.png",
|
||||
"/assets/materials/planetTextures/Methane/Methane_01-512x512.png",
|
||||
"/assets/materials/planetTextures/Methane/Methane_02-512x512.png",
|
||||
"/assets/materials/planetTextures/Methane/Methane_03-512x512.png",
|
||||
"/assets/materials/planetTextures/Methane/Methane_04-512x512.png",
|
||||
"/assets/materials/planetTextures/Methane/Methane_05-512x512.png",
|
||||
|
||||
// Sandy planets (5 textures)
|
||||
"/planetTextures/Sandy/Sandy_01-512x512.png",
|
||||
"/planetTextures/Sandy/Sandy_02-512x512.png",
|
||||
"/planetTextures/Sandy/Sandy_03-512x512.png",
|
||||
"/planetTextures/Sandy/Sandy_04-512x512.png",
|
||||
"/planetTextures/Sandy/Sandy_05-512x512.png",
|
||||
"/assets/materials/planetTextures/Sandy/Sandy_01-512x512.png",
|
||||
"/assets/materials/planetTextures/Sandy/Sandy_02-512x512.png",
|
||||
"/assets/materials/planetTextures/Sandy/Sandy_03-512x512.png",
|
||||
"/assets/materials/planetTextures/Sandy/Sandy_04-512x512.png",
|
||||
"/assets/materials/planetTextures/Sandy/Sandy_05-512x512.png",
|
||||
|
||||
// Snowy planets (5 textures)
|
||||
"/planetTextures/Snowy/Snowy_01-512x512.png",
|
||||
"/planetTextures/Snowy/Snowy_02-512x512.png",
|
||||
"/planetTextures/Snowy/Snowy_03-512x512.png",
|
||||
"/planetTextures/Snowy/Snowy_04-512x512.png",
|
||||
"/planetTextures/Snowy/Snowy_05-512x512.png",
|
||||
"/assets/materials/planetTextures/Snowy/Snowy_01-512x512.png",
|
||||
"/assets/materials/planetTextures/Snowy/Snowy_02-512x512.png",
|
||||
"/assets/materials/planetTextures/Snowy/Snowy_03-512x512.png",
|
||||
"/assets/materials/planetTextures/Snowy/Snowy_04-512x512.png",
|
||||
"/assets/materials/planetTextures/Snowy/Snowy_05-512x512.png",
|
||||
|
||||
// Tundra planets (5 textures)
|
||||
"/planetTextures/Tundra/Tundra_01-512x512.png",
|
||||
"/planetTextures/Tundra/Tundra_02-512x512.png",
|
||||
"/planetTextures/Tundra/Tundra_03-512x512.png",
|
||||
"/planetTextures/Tundra/Tundra_04-512x512.png",
|
||||
"/planetTextures/Tundra/Tundral-EQUIRECTANGULAR-5-512x512.png",
|
||||
"/assets/materials/planetTextures/Tundra/Tundra_01-512x512.png",
|
||||
"/assets/materials/planetTextures/Tundra/Tundra_02-512x512.png",
|
||||
"/assets/materials/planetTextures/Tundra/Tundra_03-512x512.png",
|
||||
"/assets/materials/planetTextures/Tundra/Tundra_04-512x512.png",
|
||||
"/assets/materials/planetTextures/Tundra/Tundral-EQUIRECTANGULAR-5-512x512.png",
|
||||
];
|
||||
|
||||
/**
|
||||
@ -116,103 +116,103 @@ export function getRandomPlanetTexture(): string {
|
||||
*/
|
||||
export const PLANET_TEXTURES_BY_TYPE = {
|
||||
arid: [
|
||||
"/planetTextures/Arid/Arid_01-512x512.png",
|
||||
"/planetTextures/Arid/Arid_02-512x512.png",
|
||||
"/planetTextures/Arid/Arid_03-512x512.png",
|
||||
"/planetTextures/Arid/Arid_04-512x512.png",
|
||||
"/planetTextures/Arid/Arid_05-512x512.png",
|
||||
"/assets/materials/planetTextures/Arid/Arid_01-512x512.png",
|
||||
"/assets/materials/planetTextures/Arid/Arid_02-512x512.png",
|
||||
"/assets/materials/planetTextures/Arid/Arid_03-512x512.png",
|
||||
"/assets/materials/planetTextures/Arid/Arid_04-512x512.png",
|
||||
"/assets/materials/planetTextures/Arid/Arid_05-512x512.png",
|
||||
],
|
||||
barren: [
|
||||
"/planetTextures/Barren/Barren_01-512x512.png",
|
||||
"/planetTextures/Barren/Barren_02-512x512.png",
|
||||
"/planetTextures/Barren/Barren_03-512x512.png",
|
||||
"/planetTextures/Barren/Barren_04-512x512.png",
|
||||
"/planetTextures/Barren/Barren_05-512x512.png",
|
||||
"/assets/materials/planetTextures/Barren/Barren_01-512x512.png",
|
||||
"/assets/materials/planetTextures/Barren/Barren_02-512x512.png",
|
||||
"/assets/materials/planetTextures/Barren/Barren_03-512x512.png",
|
||||
"/assets/materials/planetTextures/Barren/Barren_04-512x512.png",
|
||||
"/assets/materials/planetTextures/Barren/Barren_05-512x512.png",
|
||||
],
|
||||
dusty: [
|
||||
"/planetTextures/Dusty/Dusty_01-512x512.png",
|
||||
"/planetTextures/Dusty/Dusty_02-512x512.png",
|
||||
"/planetTextures/Dusty/Dusty_03-512x512.png",
|
||||
"/planetTextures/Dusty/Dusty_04-512x512.png",
|
||||
"/planetTextures/Dusty/Dusty_05-512x512.png",
|
||||
"/assets/materials/planetTextures/Dusty/Dusty_01-512x512.png",
|
||||
"/assets/materials/planetTextures/Dusty/Dusty_02-512x512.png",
|
||||
"/assets/materials/planetTextures/Dusty/Dusty_03-512x512.png",
|
||||
"/assets/materials/planetTextures/Dusty/Dusty_04-512x512.png",
|
||||
"/assets/materials/planetTextures/Dusty/Dusty_05-512x512.png",
|
||||
],
|
||||
gaseous: [
|
||||
"/planetTextures/Gaseous/Gaseous_01-512x512.png",
|
||||
"/planetTextures/Gaseous/Gaseous_02-512x512.png",
|
||||
"/planetTextures/Gaseous/Gaseous_03-512x512.png",
|
||||
"/planetTextures/Gaseous/Gaseous_04-512x512.png",
|
||||
"/planetTextures/Gaseous/Gaseous_05-512x512.png",
|
||||
"/planetTextures/Gaseous/Gaseous_06-512x512.png",
|
||||
"/planetTextures/Gaseous/Gaseous_07-512x512.png",
|
||||
"/planetTextures/Gaseous/Gaseous_08-512x512.png",
|
||||
"/planetTextures/Gaseous/Gaseous_09-512x512.png",
|
||||
"/planetTextures/Gaseous/Gaseous_10-512x512.png",
|
||||
"/planetTextures/Gaseous/Gaseous_11-512x512.png",
|
||||
"/planetTextures/Gaseous/Gaseous_12-512x512.png",
|
||||
"/planetTextures/Gaseous/Gaseous_13-512x512.png",
|
||||
"/planetTextures/Gaseous/Gaseous_14-512x512.png",
|
||||
"/planetTextures/Gaseous/Gaseous_15-512x512.png",
|
||||
"/planetTextures/Gaseous/Gaseous_16-512x512.png",
|
||||
"/planetTextures/Gaseous/Gaseous_17-512x512.png",
|
||||
"/planetTextures/Gaseous/Gaseous_18-512x512.png",
|
||||
"/planetTextures/Gaseous/Gaseous_19-512x512.png",
|
||||
"/planetTextures/Gaseous/Gaseous_20-512x512.png",
|
||||
"/assets/materials/planetTextures/Gaseous/Gaseous_01-512x512.png",
|
||||
"/assets/materials/planetTextures/Gaseous/Gaseous_02-512x512.png",
|
||||
"/assets/materials/planetTextures/Gaseous/Gaseous_03-512x512.png",
|
||||
"/assets/materials/planetTextures/Gaseous/Gaseous_04-512x512.png",
|
||||
"/assets/materials/planetTextures/Gaseous/Gaseous_05-512x512.png",
|
||||
"/assets/materials/planetTextures/Gaseous/Gaseous_06-512x512.png",
|
||||
"/assets/materials/planetTextures/Gaseous/Gaseous_07-512x512.png",
|
||||
"/assets/materials/planetTextures/Gaseous/Gaseous_08-512x512.png",
|
||||
"/assets/materials/planetTextures/Gaseous/Gaseous_09-512x512.png",
|
||||
"/assets/materials/planetTextures/Gaseous/Gaseous_10-512x512.png",
|
||||
"/assets/materials/planetTextures/Gaseous/Gaseous_11-512x512.png",
|
||||
"/assets/materials/planetTextures/Gaseous/Gaseous_12-512x512.png",
|
||||
"/assets/materials/planetTextures/Gaseous/Gaseous_13-512x512.png",
|
||||
"/assets/materials/planetTextures/Gaseous/Gaseous_14-512x512.png",
|
||||
"/assets/materials/planetTextures/Gaseous/Gaseous_15-512x512.png",
|
||||
"/assets/materials/planetTextures/Gaseous/Gaseous_16-512x512.png",
|
||||
"/assets/materials/planetTextures/Gaseous/Gaseous_17-512x512.png",
|
||||
"/assets/materials/planetTextures/Gaseous/Gaseous_18-512x512.png",
|
||||
"/assets/materials/planetTextures/Gaseous/Gaseous_19-512x512.png",
|
||||
"/assets/materials/planetTextures/Gaseous/Gaseous_20-512x512.png",
|
||||
],
|
||||
grassland: [
|
||||
"/planetTextures/Grassland/Grassland_01-512x512.png",
|
||||
"/planetTextures/Grassland/Grassland_02-512x512.png",
|
||||
"/planetTextures/Grassland/Grassland_03-512x512.png",
|
||||
"/planetTextures/Grassland/Grassland_04-512x512.png",
|
||||
"/planetTextures/Grassland/Grassland_05-512x512.png",
|
||||
"/assets/materials/planetTextures/Grassland/Grassland_01-512x512.png",
|
||||
"/assets/materials/planetTextures/Grassland/Grassland_02-512x512.png",
|
||||
"/assets/materials/planetTextures/Grassland/Grassland_03-512x512.png",
|
||||
"/assets/materials/planetTextures/Grassland/Grassland_04-512x512.png",
|
||||
"/assets/materials/planetTextures/Grassland/Grassland_05-512x512.png",
|
||||
],
|
||||
jungle: [
|
||||
"/planetTextures/Jungle/Jungle_01-512x512.png",
|
||||
"/planetTextures/Jungle/Jungle_02-512x512.png",
|
||||
"/planetTextures/Jungle/Jungle_03-512x512.png",
|
||||
"/planetTextures/Jungle/Jungle_04-512x512.png",
|
||||
"/planetTextures/Jungle/Jungle_05-512x512.png",
|
||||
"/assets/materials/planetTextures/Jungle/Jungle_01-512x512.png",
|
||||
"/assets/materials/planetTextures/Jungle/Jungle_02-512x512.png",
|
||||
"/assets/materials/planetTextures/Jungle/Jungle_03-512x512.png",
|
||||
"/assets/materials/planetTextures/Jungle/Jungle_04-512x512.png",
|
||||
"/assets/materials/planetTextures/Jungle/Jungle_05-512x512.png",
|
||||
],
|
||||
marshy: [
|
||||
"/planetTextures/Marshy/Marshy_01-512x512.png",
|
||||
"/planetTextures/Marshy/Marshy_02-512x512.png",
|
||||
"/planetTextures/Marshy/Marshy_03-512x512.png",
|
||||
"/planetTextures/Marshy/Marshy_04-512x512.png",
|
||||
"/planetTextures/Marshy/Marshy_05-512x512.png",
|
||||
"/assets/materials/planetTextures/Marshy/Marshy_01-512x512.png",
|
||||
"/assets/materials/planetTextures/Marshy/Marshy_02-512x512.png",
|
||||
"/assets/materials/planetTextures/Marshy/Marshy_03-512x512.png",
|
||||
"/assets/materials/planetTextures/Marshy/Marshy_04-512x512.png",
|
||||
"/assets/materials/planetTextures/Marshy/Marshy_05-512x512.png",
|
||||
],
|
||||
martian: [
|
||||
"/planetTextures/Martian/Martian_01-512x512.png",
|
||||
"/planetTextures/Martian/Martian_02-512x512.png",
|
||||
"/planetTextures/Martian/Martian_03-512x512.png",
|
||||
"/planetTextures/Martian/Martian_04-512x512.png",
|
||||
"/planetTextures/Martian/Martian_05-512x512.png",
|
||||
"/assets/materials/planetTextures/Martian/Martian_01-512x512.png",
|
||||
"/assets/materials/planetTextures/Martian/Martian_02-512x512.png",
|
||||
"/assets/materials/planetTextures/Martian/Martian_03-512x512.png",
|
||||
"/assets/materials/planetTextures/Martian/Martian_04-512x512.png",
|
||||
"/assets/materials/planetTextures/Martian/Martian_05-512x512.png",
|
||||
],
|
||||
methane: [
|
||||
"/planetTextures/Methane/Methane_01-512x512.png",
|
||||
"/planetTextures/Methane/Methane_02-512x512.png",
|
||||
"/planetTextures/Methane/Methane_03-512x512.png",
|
||||
"/planetTextures/Methane/Methane_04-512x512.png",
|
||||
"/planetTextures/Methane/Methane_05-512x512.png",
|
||||
"/assets/materials/planetTextures/Methane/Methane_01-512x512.png",
|
||||
"/assets/materials/planetTextures/Methane/Methane_02-512x512.png",
|
||||
"/assets/materials/planetTextures/Methane/Methane_03-512x512.png",
|
||||
"/assets/materials/planetTextures/Methane/Methane_04-512x512.png",
|
||||
"/assets/materials/planetTextures/Methane/Methane_05-512x512.png",
|
||||
],
|
||||
sandy: [
|
||||
"/planetTextures/Sandy/Sandy_01-512x512.png",
|
||||
"/planetTextures/Sandy/Sandy_02-512x512.png",
|
||||
"/planetTextures/Sandy/Sandy_03-512x512.png",
|
||||
"/planetTextures/Sandy/Sandy_04-512x512.png",
|
||||
"/planetTextures/Sandy/Sandy_05-512x512.png",
|
||||
"/assets/materials/planetTextures/Sandy/Sandy_01-512x512.png",
|
||||
"/assets/materials/planetTextures/Sandy/Sandy_02-512x512.png",
|
||||
"/assets/materials/planetTextures/Sandy/Sandy_03-512x512.png",
|
||||
"/assets/materials/planetTextures/Sandy/Sandy_04-512x512.png",
|
||||
"/assets/materials/planetTextures/Sandy/Sandy_05-512x512.png",
|
||||
],
|
||||
snowy: [
|
||||
"/planetTextures/Snowy/Snowy_01-512x512.png",
|
||||
"/planetTextures/Snowy/Snowy_02-512x512.png",
|
||||
"/planetTextures/Snowy/Snowy_03-512x512.png",
|
||||
"/planetTextures/Snowy/Snowy_04-512x512.png",
|
||||
"/planetTextures/Snowy/Snowy_05-512x512.png",
|
||||
"/assets/materials/planetTextures/Snowy/Snowy_01-512x512.png",
|
||||
"/assets/materials/planetTextures/Snowy/Snowy_02-512x512.png",
|
||||
"/assets/materials/planetTextures/Snowy/Snowy_03-512x512.png",
|
||||
"/assets/materials/planetTextures/Snowy/Snowy_04-512x512.png",
|
||||
"/assets/materials/planetTextures/Snowy/Snowy_05-512x512.png",
|
||||
],
|
||||
tundra: [
|
||||
"/planetTextures/Tundra/Tundra_01-512x512.png",
|
||||
"/planetTextures/Tundra/Tundra_02-512x512.png",
|
||||
"/planetTextures/Tundra/Tundra_03-512x512.png",
|
||||
"/planetTextures/Tundra/Tundra_04-512x512.png",
|
||||
"/planetTextures/Tundra/Tundral-EQUIRECTANGULAR-5-512x512.png",
|
||||
"/assets/materials/planetTextures/Tundra/Tundra_01-512x512.png",
|
||||
"/assets/materials/planetTextures/Tundra/Tundra_02-512x512.png",
|
||||
"/assets/materials/planetTextures/Tundra/Tundra_03-512x512.png",
|
||||
"/assets/materials/planetTextures/Tundra/Tundra_04-512x512.png",
|
||||
"/assets/materials/planetTextures/Tundra/Tundral-EQUIRECTANGULAR-5-512x512.png",
|
||||
],
|
||||
};
|
||||
|
||||
|
||||
266
src/progression.ts
Normal file
@ -0,0 +1,266 @@
|
||||
/**
|
||||
* Progression tracking system for level completion and feature unlocks
|
||||
*/
|
||||
|
||||
export interface LevelProgress {
|
||||
levelName: string;
|
||||
completed: boolean;
|
||||
completedAt?: string; // ISO timestamp
|
||||
bestTime?: number; // Best completion time in seconds
|
||||
bestAccuracy?: number; // Best accuracy percentage
|
||||
playCount: number;
|
||||
}
|
||||
|
||||
export interface ProgressionData {
|
||||
version: string;
|
||||
completedLevels: Map<string, LevelProgress>;
|
||||
editorUnlocked: boolean;
|
||||
firstPlayDate?: string;
|
||||
lastPlayDate?: string;
|
||||
}
|
||||
|
||||
const STORAGE_KEY = 'space-game-progress';
|
||||
const PROGRESSION_VERSION = '1.0';
|
||||
const EDITOR_UNLOCK_REQUIREMENT = 3; // Complete 3 default levels to unlock editor
|
||||
|
||||
/**
|
||||
* Progression manager - tracks level completion and unlocks
|
||||
*/
|
||||
export class ProgressionManager {
|
||||
private static _instance: ProgressionManager;
|
||||
private _data: ProgressionData;
|
||||
|
||||
private constructor() {
|
||||
this._data = this.loadProgress();
|
||||
}
|
||||
|
||||
public static getInstance(): ProgressionManager {
|
||||
if (!ProgressionManager._instance) {
|
||||
ProgressionManager._instance = new ProgressionManager();
|
||||
}
|
||||
return ProgressionManager._instance;
|
||||
}
|
||||
|
||||
/**
|
||||
* Load progression data from localStorage
|
||||
*/
|
||||
private loadProgress(): ProgressionData {
|
||||
try {
|
||||
const stored = localStorage.getItem(STORAGE_KEY);
|
||||
if (stored) {
|
||||
const parsed = JSON.parse(stored);
|
||||
// Convert completedLevels array back to Map
|
||||
const completedLevels = new Map<string, LevelProgress>(
|
||||
parsed.completedLevels || []
|
||||
);
|
||||
return {
|
||||
version: parsed.version || PROGRESSION_VERSION,
|
||||
completedLevels,
|
||||
editorUnlocked: parsed.editorUnlocked || false,
|
||||
firstPlayDate: parsed.firstPlayDate,
|
||||
lastPlayDate: parsed.lastPlayDate
|
||||
};
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error loading progression data:', error);
|
||||
}
|
||||
|
||||
// Return fresh progression data
|
||||
return {
|
||||
version: PROGRESSION_VERSION,
|
||||
completedLevels: new Map(),
|
||||
editorUnlocked: false,
|
||||
firstPlayDate: new Date().toISOString()
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Save progression data to localStorage
|
||||
*/
|
||||
private saveProgress(): void {
|
||||
try {
|
||||
// Convert Map to array for JSON serialization
|
||||
const toSave = {
|
||||
...this._data,
|
||||
completedLevels: Array.from(this._data.completedLevels.entries()),
|
||||
lastPlayDate: new Date().toISOString()
|
||||
};
|
||||
localStorage.setItem(STORAGE_KEY, JSON.stringify(toSave));
|
||||
} catch (error) {
|
||||
console.error('Error saving progression data:', error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Mark a level as completed with optional stats
|
||||
*/
|
||||
public markLevelComplete(
|
||||
levelName: string,
|
||||
stats?: {
|
||||
completionTime?: number;
|
||||
accuracy?: number;
|
||||
}
|
||||
): void {
|
||||
const existing = this._data.completedLevels.get(levelName);
|
||||
const now = new Date().toISOString();
|
||||
|
||||
const progress: LevelProgress = {
|
||||
levelName,
|
||||
completed: true,
|
||||
completedAt: now,
|
||||
bestTime: stats?.completionTime,
|
||||
bestAccuracy: stats?.accuracy,
|
||||
playCount: (existing?.playCount || 0) + 1
|
||||
};
|
||||
|
||||
// Update best time if this is better
|
||||
if (existing?.bestTime && stats?.completionTime) {
|
||||
progress.bestTime = Math.min(existing.bestTime, stats.completionTime);
|
||||
}
|
||||
|
||||
// Update best accuracy if this is better
|
||||
if (existing?.bestAccuracy && stats?.accuracy) {
|
||||
progress.bestAccuracy = Math.max(existing.bestAccuracy, stats.accuracy);
|
||||
}
|
||||
|
||||
this._data.completedLevels.set(levelName, progress);
|
||||
|
||||
// Check if editor should be unlocked
|
||||
this.checkEditorUnlock();
|
||||
|
||||
this.saveProgress();
|
||||
}
|
||||
|
||||
/**
|
||||
* Record that a level was started (for play count)
|
||||
*/
|
||||
public recordLevelStart(levelName: string): void {
|
||||
const existing = this._data.completedLevels.get(levelName);
|
||||
if (!existing) {
|
||||
this._data.completedLevels.set(levelName, {
|
||||
levelName,
|
||||
completed: false,
|
||||
playCount: 1
|
||||
});
|
||||
}
|
||||
this.saveProgress();
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a level has been completed
|
||||
*/
|
||||
public isLevelComplete(levelName: string): boolean {
|
||||
return this._data.completedLevels.get(levelName)?.completed || false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get progress data for a specific level
|
||||
*/
|
||||
public getLevelProgress(levelName: string): LevelProgress | undefined {
|
||||
return this._data.completedLevels.get(levelName);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all completed default levels
|
||||
*/
|
||||
public getCompletedDefaultLevels(): string[] {
|
||||
const defaultLevels = this.getDefaultLevelNames();
|
||||
return defaultLevels.filter(name => this.isLevelComplete(name));
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the next incomplete default level
|
||||
*/
|
||||
public getNextLevel(): string | null {
|
||||
const defaultLevels = this.getDefaultLevelNames();
|
||||
for (const levelName of defaultLevels) {
|
||||
if (!this.isLevelComplete(levelName)) {
|
||||
return levelName;
|
||||
}
|
||||
}
|
||||
return null; // All levels completed
|
||||
}
|
||||
|
||||
/**
|
||||
* Get list of default level names in order
|
||||
*/
|
||||
private getDefaultLevelNames(): string[] {
|
||||
return [
|
||||
'Tutorial: Asteroid Field',
|
||||
'Rescue Mission',
|
||||
'Deep Space Patrol',
|
||||
'Enemy Territory',
|
||||
'The Gauntlet',
|
||||
'Final Challenge'
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if editor should be unlocked based on completion
|
||||
*/
|
||||
private checkEditorUnlock(): void {
|
||||
const completedCount = this.getCompletedDefaultLevels().length;
|
||||
if (completedCount >= EDITOR_UNLOCK_REQUIREMENT && !this._data.editorUnlocked) {
|
||||
this._data.editorUnlocked = true;
|
||||
console.log(`🎉 Editor unlocked! (${completedCount} levels completed)`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the level editor is unlocked
|
||||
*/
|
||||
public isEditorUnlocked(): boolean {
|
||||
return this._data.editorUnlocked;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get count of completed default levels
|
||||
*/
|
||||
public getCompletedCount(): number {
|
||||
return this.getCompletedDefaultLevels().length;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get total count of default levels
|
||||
*/
|
||||
public getTotalDefaultLevels(): number {
|
||||
return this.getDefaultLevelNames().length;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get completion percentage
|
||||
*/
|
||||
public getCompletionPercentage(): number {
|
||||
const total = this.getTotalDefaultLevels();
|
||||
const completed = this.getCompletedCount();
|
||||
return total > 0 ? (completed / total) * 100 : 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Reset all progression (for testing or user request)
|
||||
*/
|
||||
public reset(): void {
|
||||
this._data = {
|
||||
version: PROGRESSION_VERSION,
|
||||
completedLevels: new Map(),
|
||||
editorUnlocked: false,
|
||||
firstPlayDate: new Date().toISOString()
|
||||
};
|
||||
this.saveProgress();
|
||||
}
|
||||
|
||||
/**
|
||||
* Force unlock editor (admin/testing)
|
||||
*/
|
||||
public forceUnlockEditor(): void {
|
||||
this._data.editorUnlocked = true;
|
||||
this.saveProgress();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all progression data (for display/debugging)
|
||||
*/
|
||||
public getAllProgress(): ProgressionData {
|
||||
return { ...this._data };
|
||||
}
|
||||
}
|
||||
@ -98,12 +98,12 @@ export class RockFactory {
|
||||
debugLog(this._asteroidMesh);
|
||||
}
|
||||
|
||||
public static async createRock(i: number, position: Vector3, size: Vector3,
|
||||
public static async createRock(i: number, position: Vector3, scale: number,
|
||||
linearVelocitry: Vector3, angularVelocity: Vector3, score: Observable<ScoreEvent>): Promise<Rock> {
|
||||
|
||||
const rock = new InstancedMesh("asteroid-" +i, this._asteroidMesh as Mesh);
|
||||
debugLog(rock.id);
|
||||
rock.scaling = size;
|
||||
rock.scaling = new Vector3(scale, scale, scale);
|
||||
rock.position = position;
|
||||
//rock.material = this._rockMaterial;
|
||||
rock.name = "asteroid-" + i;
|
||||
|
||||
19
src/ship.ts
@ -314,7 +314,8 @@ export class Ship {
|
||||
this._gameStats,
|
||||
() => this.handleReplayRequest(),
|
||||
() => this.handleExitVR(),
|
||||
() => this.handleResume()
|
||||
() => this.handleResume(),
|
||||
() => this.handleNextLevel()
|
||||
);
|
||||
this._statusScreen.initialize(this._camera);
|
||||
}
|
||||
@ -345,6 +346,16 @@ export class Ship {
|
||||
this._controllerInput?.setEnabled(true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle next level button click from status screen
|
||||
*/
|
||||
private handleNextLevel(): void {
|
||||
debugLog('Next Level button clicked - navigating to level selector');
|
||||
// Navigate back to level selector (root route)
|
||||
window.location.hash = '#/';
|
||||
window.location.reload();
|
||||
}
|
||||
|
||||
/**
|
||||
* Check game-ending conditions and auto-show status screen
|
||||
* Conditions:
|
||||
@ -375,7 +386,7 @@ export class Ship {
|
||||
// Check condition 1: Death by hull damage (outside landing zone)
|
||||
if (!this._isInLandingZone && hull < 0.01) {
|
||||
debugLog('Game end condition met: Hull critical outside landing zone');
|
||||
this._statusScreen.show(true);
|
||||
this._statusScreen.show(true, false); // Game ended, not victory
|
||||
this._keyboardInput?.setEnabled(false);
|
||||
this._controllerInput?.setEnabled(false);
|
||||
this._statusScreenAutoShown = true;
|
||||
@ -385,7 +396,7 @@ export class Ship {
|
||||
// Check condition 2: Stranded (outside landing zone, no fuel, low velocity)
|
||||
if (!this._isInLandingZone && fuel < 0.01 && totalVelocity < 1) {
|
||||
debugLog('Game end condition met: Stranded (no fuel, low velocity)');
|
||||
this._statusScreen.show(true);
|
||||
this._statusScreen.show(true, false); // Game ended, not victory
|
||||
this._keyboardInput?.setEnabled(false);
|
||||
this._controllerInput?.setEnabled(false);
|
||||
this._statusScreenAutoShown = true;
|
||||
@ -395,7 +406,7 @@ export class Ship {
|
||||
// Check condition 3: Victory (all asteroids destroyed, inside landing zone)
|
||||
if (asteroidsRemaining <= 0 && this._isInLandingZone) {
|
||||
debugLog('Game end condition met: Victory (all asteroids destroyed)');
|
||||
this._statusScreen.show(true);
|
||||
this._statusScreen.show(true, true); // Game ended, VICTORY!
|
||||
this._keyboardInput?.setEnabled(false);
|
||||
this._controllerInput?.setEnabled(false);
|
||||
this._statusScreenAutoShown = true;
|
||||
|
||||
@ -10,6 +10,7 @@ import {DefaultScene} from "./defaultScene";
|
||||
import {GameConfig} from "./gameConfig";
|
||||
import debugLog from "./debug";
|
||||
import loadAsset from "./utils/loadAsset";
|
||||
import {Vector3Array} from "./levelConfig";
|
||||
|
||||
export interface StarBaseResult {
|
||||
baseMesh: AbstractMesh;
|
||||
@ -18,18 +19,30 @@ export interface StarBaseResult {
|
||||
|
||||
/**
|
||||
* Create and load the star base mesh
|
||||
* @param position - Position for the star base
|
||||
* @param position - Position for the star base (defaults to [0, 0, 0])
|
||||
* @param baseGlbPath - Path to the base GLB file (defaults to 'base.glb')
|
||||
* @returns Promise resolving to the loaded star base mesh and landing aggregate
|
||||
*/
|
||||
export default class StarBase {
|
||||
public static async buildStarBase(): Promise<StarBaseResult> {
|
||||
public static async buildStarBase(position?: Vector3Array, baseGlbPath: string = 'base.glb'): Promise<StarBaseResult> {
|
||||
const config = GameConfig.getInstance();
|
||||
const scene = DefaultScene.MainScene;
|
||||
const importMeshes = await loadAsset('base.glb');
|
||||
const importMeshes = await loadAsset(baseGlbPath);
|
||||
|
||||
const baseMesh = importMeshes.meshes.get('Base');
|
||||
const landingMesh = importMeshes.meshes.get('BaseLandingZone');
|
||||
|
||||
// Store the GLB path in metadata for serialization
|
||||
if (baseMesh) {
|
||||
baseMesh.metadata = baseMesh.metadata || {};
|
||||
baseMesh.metadata.baseGlbPath = baseGlbPath;
|
||||
}
|
||||
|
||||
// Apply position to both meshes (defaults to [0, 0, 0])
|
||||
const pos = position ? new Vector3(position[0], position[1], position[2]) : new Vector3(0, 0, 0);
|
||||
baseMesh.position = pos.clone();
|
||||
landingMesh.position = pos.clone();
|
||||
|
||||
let landingAgg: PhysicsAggregate | null = null;
|
||||
|
||||
if (config.physicsEnabled) {
|
||||
|
||||
@ -16,6 +16,7 @@ import {
|
||||
} from "@babylonjs/core";
|
||||
import { GameStats } from "./gameStats";
|
||||
import { DefaultScene } from "./defaultScene";
|
||||
import { ProgressionManager } from "./progression";
|
||||
|
||||
/**
|
||||
* Status screen that displays game statistics
|
||||
@ -41,21 +42,27 @@ export class StatusScreen {
|
||||
private _replayButton: Button;
|
||||
private _exitButton: Button;
|
||||
private _resumeButton: Button;
|
||||
private _nextLevelButton: Button;
|
||||
|
||||
// Callbacks
|
||||
private _onReplayCallback: (() => void) | null = null;
|
||||
private _onExitCallback: (() => void) | null = null;
|
||||
private _onResumeCallback: (() => void) | null = null;
|
||||
private _onNextLevelCallback: (() => void) | null = null;
|
||||
|
||||
// Track whether game has ended
|
||||
private _isGameEnded: boolean = false;
|
||||
|
||||
constructor(scene: Scene, gameStats: GameStats, onReplay?: () => void, onExit?: () => void, onResume?: () => void) {
|
||||
// Track current level name for progression
|
||||
private _currentLevelName: string | null = null;
|
||||
|
||||
constructor(scene: Scene, gameStats: GameStats, onReplay?: () => void, onExit?: () => void, onResume?: () => void, onNextLevel?: () => void) {
|
||||
this._scene = scene;
|
||||
this._gameStats = gameStats;
|
||||
this._onReplayCallback = onReplay || null;
|
||||
this._onExitCallback = onExit || null;
|
||||
this._onResumeCallback = onResume || null;
|
||||
this._onNextLevelCallback = onNextLevel || null;
|
||||
}
|
||||
|
||||
/**
|
||||
@ -154,8 +161,25 @@ export class StatusScreen {
|
||||
});
|
||||
buttonBar.addControl(this._resumeButton);
|
||||
|
||||
// Create Next Level button (only shown when game has ended and there's a next level)
|
||||
this._nextLevelButton = Button.CreateSimpleButton("nextLevelButton", "NEXT LEVEL");
|
||||
this._nextLevelButton.width = "300px";
|
||||
this._nextLevelButton.height = "60px";
|
||||
this._nextLevelButton.color = "white";
|
||||
this._nextLevelButton.background = "#0088ff";
|
||||
this._nextLevelButton.cornerRadius = 10;
|
||||
this._nextLevelButton.thickness = 0;
|
||||
this._nextLevelButton.fontSize = "30px";
|
||||
this._nextLevelButton.fontWeight = "bold";
|
||||
this._nextLevelButton.onPointerClickObservable.add(() => {
|
||||
if (this._onNextLevelCallback) {
|
||||
this._onNextLevelCallback();
|
||||
}
|
||||
});
|
||||
buttonBar.addControl(this._nextLevelButton);
|
||||
|
||||
// Create Replay button (only shown when game has ended)
|
||||
this._replayButton = Button.CreateSimpleButton("replayButton", "REPLAY LEVEL");
|
||||
this._replayButton = Button.CreateSimpleButton("replayButton", "REPLAY");
|
||||
this._replayButton.width = "300px";
|
||||
this._replayButton.height = "60px";
|
||||
this._replayButton.color = "white";
|
||||
@ -279,11 +303,19 @@ export class StatusScreen {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the current level name for progression tracking
|
||||
*/
|
||||
public setCurrentLevel(levelName: string): void {
|
||||
this._currentLevelName = levelName;
|
||||
}
|
||||
|
||||
/**
|
||||
* Show the status screen
|
||||
* @param isGameEnded - true if game has ended (death/stranded/victory), false if manually paused
|
||||
* @param victory - true if the level was completed successfully
|
||||
*/
|
||||
public show(isGameEnded: boolean = false): void {
|
||||
public show(isGameEnded: boolean = false, victory: boolean = false): void {
|
||||
if (!this._screenMesh) {
|
||||
return;
|
||||
}
|
||||
@ -291,6 +323,21 @@ export class StatusScreen {
|
||||
// Store game ended state
|
||||
this._isGameEnded = isGameEnded;
|
||||
|
||||
// Mark level as complete if victory and we have a level name
|
||||
const progression = ProgressionManager.getInstance();
|
||||
if (victory && this._currentLevelName) {
|
||||
const stats = this._gameStats.getStats();
|
||||
const gameTimeSeconds = this.parseGameTime(stats.gameTime);
|
||||
progression.markLevelComplete(this._currentLevelName, {
|
||||
completionTime: gameTimeSeconds,
|
||||
accuracy: stats.accuracy // Already a number from getAccuracy()
|
||||
});
|
||||
}
|
||||
|
||||
// Determine if there's a next level
|
||||
const nextLevel = progression.getNextLevel();
|
||||
const hasNextLevel = nextLevel !== null;
|
||||
|
||||
// Show/hide appropriate buttons based on whether game has ended
|
||||
if (this._resumeButton) {
|
||||
this._resumeButton.isVisible = !isGameEnded;
|
||||
@ -298,6 +345,10 @@ export class StatusScreen {
|
||||
if (this._replayButton) {
|
||||
this._replayButton.isVisible = isGameEnded;
|
||||
}
|
||||
if (this._nextLevelButton) {
|
||||
// Only show Next Level if game ended in victory and there's a next level
|
||||
this._nextLevelButton.isVisible = isGameEnded && victory && hasNextLevel;
|
||||
}
|
||||
|
||||
// Enable pointer selection for button interaction
|
||||
this.enablePointerSelection();
|
||||
@ -310,6 +361,19 @@ export class StatusScreen {
|
||||
this._isVisible = true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse game time string (MM:SS) to seconds
|
||||
*/
|
||||
private parseGameTime(timeString: string): number {
|
||||
const parts = timeString.split(':');
|
||||
if (parts.length === 2) {
|
||||
const minutes = parseInt(parts[0], 10);
|
||||
const seconds = parseInt(parts[1], 10);
|
||||
return minutes * 60 + seconds;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Hide the status screen
|
||||
*/
|
||||
|
||||