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>
This commit is contained in:
Michael Mainguy 2025-11-10 12:19:31 -06:00
parent dfec655b6c
commit ccc1745ed2
94 changed files with 1058 additions and 268 deletions

View File

@ -101,7 +101,7 @@ Located in `level1.ts:getDifficultyConfig()`
### Asset Loading ### Asset Loading
- 3D models: GLB format (cockpit, asteroids) - 3D models: GLB format (cockpit, asteroids)
- Particle systems: JSON format in `public/systems/` - 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 - Audio: MP3 format in public root
### Performance Considerations ### Performance Considerations
@ -128,7 +128,10 @@ src/
public/ public/
systems/ - Particle system definitions systems/ - Particle system definitions
assets/
materials/
planetTextures/ - Biome-based planet textures planetTextures/ - Biome-based planet textures
themes/ - Themed assets
cockpit*.glb - Ship interior models cockpit*.glb - Ship interior models
asteroid*.glb - Asteroid mesh variants asteroid*.glb - Asteroid mesh variants
*.mp3 - Audio assets *.mp3 - Audio assets

View File

@ -12,8 +12,6 @@
} }
}); });
</script> </script>
<link rel="prefetch" href="/background.mp3"/>
<link rel="prefetch" href="/8192.webp"/>
</head> </head>
<body> <body>
<!-- Game View --> <!-- Game View -->
@ -26,9 +24,32 @@
<div id="levelSelect"> <div id="levelSelect">
<!-- Controls Section --> <!-- Hero Section -->
<div class="controls-info"> <div style="text-align: center; margin-bottom: 40px;">
<h2>🎮 How to Play</h2> <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="controls-grid">
<div class="control-section"> <div class="control-section">
<h3>VR Controllers (Required for VR)</h3> <h3>VR Controllers (Required for VR)</h3>
@ -52,11 +73,7 @@
<p class="controls-note"> <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. ⚠️ <strong>Note:</strong> This game is designed for VR headsets with controllers. Desktop controls are provided for preview and testing purposes only.
</p> </p>
</div> </details>
<h1>Select Your Level</h1>
<div id="levelCardsContainer" class="card-container">
<!-- Level cards will be dynamically populated from localStorage -->
</div>
<div style="text-align: center; margin-top: 20px; display: none;"> <div style="text-align: center; margin-top: 20px; display: none;">
<button id="testLevelBtn" class="test-level-button"> <button id="testLevelBtn" class="test-level-button">
🧪 Test Scene (Debug) 🧪 Test Scene (Debug)
@ -165,12 +182,8 @@
</div> </div>
</div> </div>
<div class="form-group"> <div class="form-group">
<label for="baseDiameter">Diameter</label> <label>Base GLB Path</label>
<input type="number" id="baseDiameter" value="10" step="1" min="1"> <input type="text" id="baseGlbPath" value="base.glb" placeholder="base.glb">
</div>
<div class="form-group">
<label for="baseHeight">Height</label>
<input type="number" id="baseHeight" value="1" step="0.1" min="0.1">
</div> </div>
</div> </div>

View File

@ -1,6 +1,6 @@
{ {
"name": "space-game", "name": "space-game",
"deployHostname": "space.digital-experiment.com", "deployHostname": "www.flatearthdefense.com",
"private": false, "private": false,
"version": "0.0.1", "version": "0.0.1",
"type": "module", "type": "module",

View File

Before

Width:  |  Height:  |  Size: 573 KiB

After

Width:  |  Height:  |  Size: 573 KiB

View File

Before

Width:  |  Height:  |  Size: 565 KiB

After

Width:  |  Height:  |  Size: 565 KiB

View File

Before

Width:  |  Height:  |  Size: 585 KiB

After

Width:  |  Height:  |  Size: 585 KiB

View File

Before

Width:  |  Height:  |  Size: 560 KiB

After

Width:  |  Height:  |  Size: 560 KiB

View File

Before

Width:  |  Height:  |  Size: 570 KiB

After

Width:  |  Height:  |  Size: 570 KiB

View File

Before

Width:  |  Height:  |  Size: 352 KiB

After

Width:  |  Height:  |  Size: 352 KiB

View File

Before

Width:  |  Height:  |  Size: 360 KiB

After

Width:  |  Height:  |  Size: 360 KiB

View File

Before

Width:  |  Height:  |  Size: 347 KiB

After

Width:  |  Height:  |  Size: 347 KiB

View File

Before

Width:  |  Height:  |  Size: 359 KiB

After

Width:  |  Height:  |  Size: 359 KiB

View File

Before

Width:  |  Height:  |  Size: 374 KiB

After

Width:  |  Height:  |  Size: 374 KiB

View File

Before

Width:  |  Height:  |  Size: 567 KiB

After

Width:  |  Height:  |  Size: 567 KiB

View File

Before

Width:  |  Height:  |  Size: 564 KiB

After

Width:  |  Height:  |  Size: 564 KiB

View File

Before

Width:  |  Height:  |  Size: 572 KiB

After

Width:  |  Height:  |  Size: 572 KiB

View File

Before

Width:  |  Height:  |  Size: 573 KiB

After

Width:  |  Height:  |  Size: 573 KiB

View File

Before

Width:  |  Height:  |  Size: 560 KiB

After

Width:  |  Height:  |  Size: 560 KiB

View File

Before

Width:  |  Height:  |  Size: 98 KiB

After

Width:  |  Height:  |  Size: 98 KiB

View File

Before

Width:  |  Height:  |  Size: 142 KiB

After

Width:  |  Height:  |  Size: 142 KiB

View File

Before

Width:  |  Height:  |  Size: 136 KiB

After

Width:  |  Height:  |  Size: 136 KiB

View File

Before

Width:  |  Height:  |  Size: 171 KiB

After

Width:  |  Height:  |  Size: 171 KiB

View File

Before

Width:  |  Height:  |  Size: 168 KiB

After

Width:  |  Height:  |  Size: 168 KiB

View File

Before

Width:  |  Height:  |  Size: 188 KiB

After

Width:  |  Height:  |  Size: 188 KiB

View File

Before

Width:  |  Height:  |  Size: 132 KiB

After

Width:  |  Height:  |  Size: 132 KiB

View File

Before

Width:  |  Height:  |  Size: 139 KiB

After

Width:  |  Height:  |  Size: 139 KiB

View File

Before

Width:  |  Height:  |  Size: 118 KiB

After

Width:  |  Height:  |  Size: 118 KiB

View File

Before

Width:  |  Height:  |  Size: 118 KiB

After

Width:  |  Height:  |  Size: 118 KiB

View File

Before

Width:  |  Height:  |  Size: 131 KiB

After

Width:  |  Height:  |  Size: 131 KiB

View File

Before

Width:  |  Height:  |  Size: 201 KiB

After

Width:  |  Height:  |  Size: 201 KiB

View File

Before

Width:  |  Height:  |  Size: 134 KiB

After

Width:  |  Height:  |  Size: 134 KiB

View File

Before

Width:  |  Height:  |  Size: 165 KiB

After

Width:  |  Height:  |  Size: 165 KiB

View File

Before

Width:  |  Height:  |  Size: 166 KiB

After

Width:  |  Height:  |  Size: 166 KiB

View File

Before

Width:  |  Height:  |  Size: 218 KiB

After

Width:  |  Height:  |  Size: 218 KiB

View File

Before

Width:  |  Height:  |  Size: 139 KiB

After

Width:  |  Height:  |  Size: 139 KiB

View File

Before

Width:  |  Height:  |  Size: 144 KiB

After

Width:  |  Height:  |  Size: 144 KiB

View File

Before

Width:  |  Height:  |  Size: 145 KiB

After

Width:  |  Height:  |  Size: 145 KiB

View File

Before

Width:  |  Height:  |  Size: 205 KiB

After

Width:  |  Height:  |  Size: 205 KiB

View File

Before

Width:  |  Height:  |  Size: 678 KiB

After

Width:  |  Height:  |  Size: 678 KiB

View File

Before

Width:  |  Height:  |  Size: 626 KiB

After

Width:  |  Height:  |  Size: 626 KiB

View File

Before

Width:  |  Height:  |  Size: 670 KiB

After

Width:  |  Height:  |  Size: 670 KiB

View File

Before

Width:  |  Height:  |  Size: 675 KiB

After

Width:  |  Height:  |  Size: 675 KiB

View File

Before

Width:  |  Height:  |  Size: 666 KiB

After

Width:  |  Height:  |  Size: 666 KiB

View File

Before

Width:  |  Height:  |  Size: 634 KiB

After

Width:  |  Height:  |  Size: 634 KiB

View File

Before

Width:  |  Height:  |  Size: 602 KiB

After

Width:  |  Height:  |  Size: 602 KiB

View File

Before

Width:  |  Height:  |  Size: 614 KiB

After

Width:  |  Height:  |  Size: 614 KiB

View File

Before

Width:  |  Height:  |  Size: 600 KiB

After

Width:  |  Height:  |  Size: 600 KiB

View File

Before

Width:  |  Height:  |  Size: 656 KiB

After

Width:  |  Height:  |  Size: 656 KiB

View File

Before

Width:  |  Height:  |  Size: 710 KiB

After

Width:  |  Height:  |  Size: 710 KiB

View File

Before

Width:  |  Height:  |  Size: 714 KiB

After

Width:  |  Height:  |  Size: 714 KiB

View File

Before

Width:  |  Height:  |  Size: 713 KiB

After

Width:  |  Height:  |  Size: 713 KiB

View File

Before

Width:  |  Height:  |  Size: 727 KiB

After

Width:  |  Height:  |  Size: 727 KiB

View File

Before

Width:  |  Height:  |  Size: 713 KiB

After

Width:  |  Height:  |  Size: 713 KiB

View File

Before

Width:  |  Height:  |  Size: 517 KiB

After

Width:  |  Height:  |  Size: 517 KiB

View File

Before

Width:  |  Height:  |  Size: 515 KiB

After

Width:  |  Height:  |  Size: 515 KiB

View File

Before

Width:  |  Height:  |  Size: 526 KiB

After

Width:  |  Height:  |  Size: 526 KiB

View File

Before

Width:  |  Height:  |  Size: 517 KiB

After

Width:  |  Height:  |  Size: 517 KiB

View File

Before

Width:  |  Height:  |  Size: 526 KiB

After

Width:  |  Height:  |  Size: 526 KiB

View File

Before

Width:  |  Height:  |  Size: 656 KiB

After

Width:  |  Height:  |  Size: 656 KiB

View File

Before

Width:  |  Height:  |  Size: 663 KiB

After

Width:  |  Height:  |  Size: 663 KiB

View File

Before

Width:  |  Height:  |  Size: 648 KiB

After

Width:  |  Height:  |  Size: 648 KiB

View File

Before

Width:  |  Height:  |  Size: 665 KiB

After

Width:  |  Height:  |  Size: 665 KiB

View File

Before

Width:  |  Height:  |  Size: 670 KiB

After

Width:  |  Height:  |  Size: 670 KiB

View File

Before

Width:  |  Height:  |  Size: 384 KiB

After

Width:  |  Height:  |  Size: 384 KiB

View File

Before

Width:  |  Height:  |  Size: 327 KiB

After

Width:  |  Height:  |  Size: 327 KiB

View File

Before

Width:  |  Height:  |  Size: 336 KiB

After

Width:  |  Height:  |  Size: 336 KiB

View File

Before

Width:  |  Height:  |  Size: 325 KiB

After

Width:  |  Height:  |  Size: 325 KiB

View File

Before

Width:  |  Height:  |  Size: 421 KiB

After

Width:  |  Height:  |  Size: 421 KiB

View File

Before

Width:  |  Height:  |  Size: 576 KiB

After

Width:  |  Height:  |  Size: 576 KiB

View File

Before

Width:  |  Height:  |  Size: 589 KiB

After

Width:  |  Height:  |  Size: 589 KiB

View File

Before

Width:  |  Height:  |  Size: 591 KiB

After

Width:  |  Height:  |  Size: 591 KiB

View File

Before

Width:  |  Height:  |  Size: 605 KiB

After

Width:  |  Height:  |  Size: 605 KiB

View File

Before

Width:  |  Height:  |  Size: 585 KiB

After

Width:  |  Height:  |  Size: 585 KiB

View File

Before

Width:  |  Height:  |  Size: 373 KiB

After

Width:  |  Height:  |  Size: 373 KiB

View File

Before

Width:  |  Height:  |  Size: 374 KiB

After

Width:  |  Height:  |  Size: 374 KiB

View File

Before

Width:  |  Height:  |  Size: 389 KiB

After

Width:  |  Height:  |  Size: 389 KiB

View File

Before

Width:  |  Height:  |  Size: 399 KiB

After

Width:  |  Height:  |  Size: 399 KiB

View File

@ -36,7 +36,7 @@ export function createSun() : AbstractMesh {
export function createPlanet(position: Vector3, diameter: number, name: string) : AbstractMesh { export function createPlanet(position: Vector3, diameter: number, name: string) : AbstractMesh {
const planet = MeshBuilder.CreateSphere(name, {diameter: diameter, segments: 32}, DefaultScene.MainScene); const planet = MeshBuilder.CreateSphere(name, {diameter: diameter, segments: 32}, DefaultScene.MainScene);
const material = new StandardMaterial(name + "-material", 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.diffuseTexture = texture;
material.ambientTexture = texture; material.ambientTexture = texture;
material.roughness = 1; material.roughness = 1;

View File

@ -9,6 +9,9 @@ export class GameConfig {
// Physics settings // Physics settings
public physicsEnabled: boolean = true; public physicsEnabled: boolean = true;
// Feature flags
public progressionEnabled: boolean = false; // Set to false for simple rookie level
// Ship physics tuning parameters // Ship physics tuning parameters
public shipPhysics = { public shipPhysics = {
maxLinearVelocity: 200, maxLinearVelocity: 200,
@ -42,6 +45,7 @@ export class GameConfig {
const config = { const config = {
physicsEnabled: this.physicsEnabled, physicsEnabled: this.physicsEnabled,
debug: this.debug, debug: this.debug,
progressionEnabled: this.progressionEnabled,
shipPhysics: this.shipPhysics shipPhysics: this.shipPhysics
}; };
localStorage.setItem('game-config', JSON.stringify(config)); localStorage.setItem('game-config', JSON.stringify(config));
@ -57,6 +61,7 @@ export class GameConfig {
const config = JSON.parse(stored); const config = JSON.parse(stored);
this.physicsEnabled = config.physicsEnabled ?? true; this.physicsEnabled = config.physicsEnabled ?? true;
this.debug = config.debug ?? false; this.debug = config.debug ?? false;
this.progressionEnabled = config.progressionEnabled ?? false;
// Load ship physics with fallback to defaults // Load ship physics with fallback to defaults
if (config.shipPhysics) { if (config.shipPhysics) {
@ -81,6 +86,7 @@ export class GameConfig {
public reset(): void { public reset(): void {
this.physicsEnabled = true; this.physicsEnabled = true;
this.debug = false; this.debug = false;
this.progressionEnabled = false;
this.shipPhysics = { this.shipPhysics = {
maxLinearVelocity: 200, maxLinearVelocity: 200,
maxAngularVelocity: 1.4, maxAngularVelocity: 1.4,

View File

@ -100,13 +100,15 @@ export class KeyboardInput {
document.onkeydown = (ev) => { document.onkeydown = (ev) => {
// Always allow inspector and camera toggle, even when disabled // Always allow inspector and camera toggle, even when disabled
if (ev.key === 'i') { if (ev.key === 'i') {
// Open Babylon Inspector // Toggle Babylon Inspector
import("@babylonjs/inspector").then((inspector) => { if (this._scene.debugLayer.isVisible()) {
inspector.Inspector.Show(this._scene, { this._scene.debugLayer.hide();
} else {
this._scene.debugLayer.show({
overlay: true, overlay: true,
showExplorer: true, showExplorer: true,
}); });
}); }
return; return;
} }

View File

@ -74,10 +74,9 @@ export interface ShipConfig {
* All fields optional to allow levels without start bases * All fields optional to allow levels without start bases
*/ */
export interface StartBaseConfig { export interface StartBaseConfig {
position?: Vector3Array; position?: Vector3Array; // Defaults to [0, 0, 0] if not specified
diameter?: number; baseGlbPath?: string; // Path to base GLB model (defaults to 'base.glb')
height?: number; landingGlbPath?: string; // Path to landing zone GLB model (uses same file as base, different mesh name)
color?: Vector3Array; // RGB values 0-1
} }
/** /**
@ -106,7 +105,7 @@ export interface PlanetConfig {
export interface AsteroidConfig { export interface AsteroidConfig {
id: string; id: string;
position: Vector3Array; position: Vector3Array;
scaling: Vector3Array; scale: number; // Uniform scale applied to all axes
linearVelocity: Vector3Array; linearVelocity: Vector3Array;
angularVelocity?: Vector3Array; angularVelocity?: Vector3Array;
mass?: number; 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)) { 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'); 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 // Check sun
@ -236,6 +229,17 @@ export function validateLevelConfig(config: any): ValidationResult {
if (!Array.isArray(config.asteroids)) { if (!Array.isArray(config.asteroids)) {
errors.push('Missing or invalid asteroids array'); errors.push('Missing or invalid asteroids array');
} else { } 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) => { config.asteroids.forEach((asteroid: any, idx: number) => {
if (!asteroid.id || typeof asteroid.id !== 'string') { if (!asteroid.id || typeof asteroid.id !== 'string') {
errors.push(`Asteroid ${idx}: missing or invalid id`); 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) { if (!Array.isArray(asteroid.position) || asteroid.position.length !== 3) {
errors.push(`Asteroid ${idx}: invalid position - must be [x, y, z] array`); errors.push(`Asteroid ${idx}: invalid position - must be [x, y, z] array`);
} }
if (!Array.isArray(asteroid.scaling) || asteroid.scaling.length !== 3) { if (typeof asteroid.scale !== 'number' || asteroid.scale <= 0) {
errors.push(`Asteroid ${idx}: invalid scaling - must be [x, y, z] array`); errors.push(`Asteroid ${idx}: invalid scale - must be a positive number`);
} }
if (!Array.isArray(asteroid.linearVelocity) || asteroid.linearVelocity.length !== 3) { if (!Array.isArray(asteroid.linearVelocity) || asteroid.linearVelocity.length !== 3) {
errors.push(`Asteroid ${idx}: invalid linearVelocity - must be [x, y, z] array`); errors.push(`Asteroid ${idx}: invalid linearVelocity - must be [x, y, z] array`);

View File

@ -30,6 +30,14 @@ export class LevelDeserializer {
private config: LevelConfig; private config: LevelConfig;
constructor(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 // Validate config first
const validation = validateLevelConfig(config); const validation = validateLevelConfig(config);
if (!validation.valid) { if (!validation.valid) {
@ -72,7 +80,9 @@ export class LevelDeserializer {
* Create the start base from config * Create the start base from config
*/ */
private async createStartBase() { 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( const rock = await RockFactory.createRock(
i, i,
this.arrayToVector3(asteroidConfig.position), this.arrayToVector3(asteroidConfig.position),
this.arrayToVector3(asteroidConfig.scaling), asteroidConfig.scale,
this.arrayToVector3(asteroidConfig.linearVelocity), this.arrayToVector3(asteroidConfig.linearVelocity),
this.arrayToVector3(asteroidConfig.angularVelocity), this.arrayToVector3(asteroidConfig.angularVelocity),
scoreObservable scoreObservable

View File

@ -1,5 +1,5 @@
import { LevelGenerator } from "./levelGenerator"; import { LevelGenerator } from "./levelGenerator";
import { LevelConfig, DifficultyConfig, validateLevelConfig } from "./levelConfig"; import { LevelConfig, DifficultyConfig, validateLevelConfig, Vector3Array } from "./levelConfig";
import debugLog from './debug'; import debugLog from './debug';
const STORAGE_KEY = 'space-game-levels'; 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('baseX') as HTMLInputElement).value = config.startBase.position[0].toString();
(document.getElementById('baseY') as HTMLInputElement).value = config.startBase.position[1].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('baseZ') as HTMLInputElement).value = config.startBase.position[2].toString();
(document.getElementById('baseDiameter') as HTMLInputElement).value = config.startBase.diameter.toString(); (document.getElementById('baseGlbPath') as HTMLInputElement).value = config.startBase.baseGlbPath || 'base.glb';
(document.getElementById('baseHeight') as HTMLInputElement).value = config.startBase.height.toString();
// Sun // Sun
(document.getElementById('sunX') as HTMLInputElement).value = config.sun.position[0].toString(); (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; 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 * 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 { export function generateDefaultLevels(): void {
const existing = getSavedLevels(); const existing = getSavedLevels();
@ -607,30 +708,82 @@ export function generateDefaultLevels(): void {
return; 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>(); const levelsMap = new Map<string, LevelConfig>();
for (const difficulty of difficulties) { for (const level of defaultLevels) {
const generator = new LevelGenerator(difficulty); const generator = new LevelGenerator(level.difficulty);
const config = generator.generate(); const config = generator.generate();
// Add metadata // Add rich metadata
config.metadata = { config.metadata = {
author: 'System', author: 'System',
description: `Default ${difficulty} level` description: level.description,
estimatedTime: level.estimatedTime,
type: 'default',
difficulty: level.difficulty
}; };
levelsMap.set(difficulty, config); levelsMap.set(level.name, config);
debugLog(`Generated default level: ${difficulty}`); debugLog(`Generated default level: ${level.name} (${level.difficulty})`);
} }
// Save all levels to localStorage // Save all levels to localStorage
const levelsArray = Array.from(levelsMap.entries()); const levelsArray = Array.from(levelsMap.entries());
localStorage.setItem(STORAGE_KEY, JSON.stringify(levelsArray)); 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 // Export for manual initialization if needed

View File

@ -159,10 +159,9 @@ export class LevelGenerator {
const position: Vector3Array = [x, y, z]; const position: Vector3Array = [x, y, z];
// Random size // Random size (uniform scale)
const sizeRange = config.rockSizeMax - config.rockSizeMin; const sizeRange = config.rockSizeMax - config.rockSizeMin;
const size = Math.random() * sizeRange + config.rockSizeMin; const scale = Math.random() * sizeRange + config.rockSizeMin;
const scaling: Vector3Array = [size, size, size];
// Calculate initial velocity based on force applied in Level1 // Calculate initial velocity based on force applied in Level1
// Velocity should be tangential to the sphere (perpendicular to radius) // Velocity should be tangential to the sphere (perpendicular to radius)
@ -182,7 +181,7 @@ export class LevelGenerator {
asteroids.push({ asteroids.push({
id: `asteroid-${i}`, id: `asteroid-${i}`,
position, position,
scaling, scale,
linearVelocity, linearVelocity,
angularVelocity: [0, 0, 0], angularVelocity: [0, 0, 0],
mass mass

View File

@ -1,11 +1,14 @@
import { getSavedLevels } from "./levelEditor"; import { getSavedLevels } from "./levelEditor";
import { LevelConfig } from "./levelConfig"; import { LevelConfig } from "./levelConfig";
import { ProgressionManager } from "./progression";
import { GameConfig } from "./gameConfig";
import debugLog from './debug'; import debugLog from './debug';
const SELECTED_LEVEL_KEY = 'space-game-selected-level'; const SELECTED_LEVEL_KEY = 'space-game-selected-level';
/** /**
* Populate the level selection screen with saved levels * Populate the level selection screen with saved levels
* Shows default levels and custom levels with progression tracking
*/ */
export function populateLevelSelector(): boolean { export function populateLevelSelector(): boolean {
const container = document.getElementById('levelCardsContainer'); const container = document.getElementById('levelCardsContainer');
@ -15,16 +18,11 @@ export function populateLevelSelector(): boolean {
} }
const savedLevels = getSavedLevels(); const savedLevels = getSavedLevels();
const gameConfig = GameConfig.getInstance();
const progressionEnabled = gameConfig.progressionEnabled;
const progression = ProgressionManager.getInstance();
// Filter to only show recruit and pilot difficulty levels if (savedLevels.size === 0) {
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) {
container.innerHTML = ` container.innerHTML = `
<div style=" <div style="
grid-column: 1 / -1; grid-column: 1 / -1;
@ -33,7 +31,7 @@ export function populateLevelSelector(): boolean {
color: #ccc; color: #ccc;
"> ">
<h2 style="margin-bottom: 20px;">No Levels Found</h2> <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=" <a href="#/editor" style="
display: inline-block; display: inline-block;
padding: 15px 30px; padding: 15px 30px;
@ -49,24 +47,284 @@ export function populateLevelSelector(): boolean {
return false; 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 = ''; let html = '';
for (const [name, config] of filteredLevels.entries()) {
// 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 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>
<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 timestamp = config.timestamp ? new Date(config.timestamp).toLocaleDateString() : '';
const description = config.metadata?.description || `${config.asteroids.length} asteroids • ${config.planets.length} planets`; const description = config.metadata?.description || `${config.asteroids.length} asteroids • ${config.planets.length} planets`;
const author = config.metadata?.author || 'Unknown';
html += ` html += `
<div class="level-card"> <div class="level-card">
<h2>${name}</h2> <h2>${name}</h2>
<div style="font-size: 0.9em; color: #aaa; margin: 10px 0;"> <div style="font-size: 0.9em; color: #aaa; margin: 10px 0;">
Difficulty: ${config.difficulty} Difficulty: ${config.difficulty} By ${author}
</div> </div>
<p>${description}</p> <p>${description}</p>
${timestamp ? `<div style="font-size: 0.8em; color: #888; margin-bottom: 10px;">${timestamp}</div>` : ''} ${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> <button class="level-button" data-level="${name}">Play Level</button>
</div> </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; container.innerHTML = html;

View File

@ -100,7 +100,7 @@ export class LevelSerializer {
} }
/** /**
* Serialize start base state * Serialize start base state (position and GLB paths)
*/ */
private serializeStartBase(): StartBaseConfig { private serializeStartBase(): StartBaseConfig {
const startBase = this.scene.getMeshByName("startBase"); const startBase = this.scene.getMeshByName("startBase");
@ -109,31 +109,18 @@ export class LevelSerializer {
console.warn("Start base not found, using defaults"); console.warn("Start base not found, using defaults");
return { return {
position: [0, 0, 0], position: [0, 0, 0],
diameter: 10, baseGlbPath: 'base.glb'
height: 1,
color: [1, 1, 0]
}; };
} }
const position = this.vector3ToArray(startBase.position); const position = this.vector3ToArray(startBase.position);
// Try to extract diameter and height from scaling or metadata // Capture GLB path from metadata if available, otherwise use default
// Assuming cylinder was created with specific dimensions const baseGlbPath = startBase.metadata?.baseGlbPath || 'base.glb';
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];
}
return { return {
position, position,
diameter, baseGlbPath
height,
color
}; };
} }
@ -191,7 +178,7 @@ export class LevelSerializer {
const diameter = boundingInfo.boundingSphere.radiusWorld * 2; const diameter = boundingInfo.boundingSphere.radiusWorld * 2;
// Get texture path from material // 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) { if (mesh.material && (mesh.material as any).diffuseTexture) {
const texture = (mesh.material as any).diffuseTexture; const texture = (mesh.material as any).diffuseTexture;
texturePath = texture.url || texturePath; texturePath = texture.url || texturePath;
@ -222,7 +209,8 @@ export class LevelSerializer {
for (const mesh of asteroidMeshes) { for (const mesh of asteroidMeshes) {
const position = this.vector3ToArray(mesh.position); 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 // Get velocities from physics body
let linearVelocity: Vector3Array = [0, 0, 0]; let linearVelocity: Vector3Array = [0, 0, 0];
@ -238,7 +226,7 @@ export class LevelSerializer {
asteroids.push({ asteroids.push({
id: mesh.name, id: mesh.name,
position, position,
scaling, scale,
linearVelocity, linearVelocity,
angularVelocity, angularVelocity,
mass mass

View File

@ -118,6 +118,12 @@ export class Main {
// Listen for replay requests from the ship // Listen for replay requests from the ship
if (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(() => { ship.onReplayRequestObservable.add(() => {
debugLog('Replay requested - reloading page'); debugLog('Replay requested - reloading page');
window.location.reload(); window.location.reload();
@ -472,16 +478,10 @@ export class Main {
// Setup router // Setup router
router.on('/', () => { router.on('/', () => {
// Check if there are saved levels // Always show game view with level selector (no editor redirect)
if (!hasSavedLevels()) {
debugLog('No saved levels found, redirecting to editor');
router.navigate('/editor');
return;
}
showView('game'); showView('game');
// Populate level selector // Populate level selector (will show default levels if no custom levels)
populateLevelSelector(); populateLevelSelector();
// Initialize game if not in debug mode // Initialize game if not in debug mode

View File

@ -5,103 +5,103 @@
export const PLANET_TEXTURES = [ export const PLANET_TEXTURES = [
// Arid planets (5 textures) // Arid planets (5 textures)
"/planetTextures/Arid/Arid_01-512x512.png", "/assets/materials/planetTextures/Arid/Arid_01-512x512.png",
"/planetTextures/Arid/Arid_02-512x512.png", "/assets/materials/planetTextures/Arid/Arid_02-512x512.png",
"/planetTextures/Arid/Arid_03-512x512.png", "/assets/materials/planetTextures/Arid/Arid_03-512x512.png",
"/planetTextures/Arid/Arid_04-512x512.png", "/assets/materials/planetTextures/Arid/Arid_04-512x512.png",
"/planetTextures/Arid/Arid_05-512x512.png", "/assets/materials/planetTextures/Arid/Arid_05-512x512.png",
// Barren planets (5 textures) // Barren planets (5 textures)
"/planetTextures/Barren/Barren_01-512x512.png", "/assets/materials/planetTextures/Barren/Barren_01-512x512.png",
"/planetTextures/Barren/Barren_02-512x512.png", "/assets/materials/planetTextures/Barren/Barren_02-512x512.png",
"/planetTextures/Barren/Barren_03-512x512.png", "/assets/materials/planetTextures/Barren/Barren_03-512x512.png",
"/planetTextures/Barren/Barren_04-512x512.png", "/assets/materials/planetTextures/Barren/Barren_04-512x512.png",
"/planetTextures/Barren/Barren_05-512x512.png", "/assets/materials/planetTextures/Barren/Barren_05-512x512.png",
// Dusty planets (5 textures) // Dusty planets (5 textures)
"/planetTextures/Dusty/Dusty_01-512x512.png", "/assets/materials/planetTextures/Dusty/Dusty_01-512x512.png",
"/planetTextures/Dusty/Dusty_02-512x512.png", "/assets/materials/planetTextures/Dusty/Dusty_02-512x512.png",
"/planetTextures/Dusty/Dusty_03-512x512.png", "/assets/materials/planetTextures/Dusty/Dusty_03-512x512.png",
"/planetTextures/Dusty/Dusty_04-512x512.png", "/assets/materials/planetTextures/Dusty/Dusty_04-512x512.png",
"/planetTextures/Dusty/Dusty_05-512x512.png", "/assets/materials/planetTextures/Dusty/Dusty_05-512x512.png",
// Gaseous planets (20 textures) // Gaseous planets (20 textures)
"/planetTextures/Gaseous/Gaseous_01-512x512.png", "/assets/materials/planetTextures/Gaseous/Gaseous_01-512x512.png",
"/planetTextures/Gaseous/Gaseous_02-512x512.png", "/assets/materials/planetTextures/Gaseous/Gaseous_02-512x512.png",
"/planetTextures/Gaseous/Gaseous_03-512x512.png", "/assets/materials/planetTextures/Gaseous/Gaseous_03-512x512.png",
"/planetTextures/Gaseous/Gaseous_04-512x512.png", "/assets/materials/planetTextures/Gaseous/Gaseous_04-512x512.png",
"/planetTextures/Gaseous/Gaseous_05-512x512.png", "/assets/materials/planetTextures/Gaseous/Gaseous_05-512x512.png",
"/planetTextures/Gaseous/Gaseous_06-512x512.png", "/assets/materials/planetTextures/Gaseous/Gaseous_06-512x512.png",
"/planetTextures/Gaseous/Gaseous_07-512x512.png", "/assets/materials/planetTextures/Gaseous/Gaseous_07-512x512.png",
"/planetTextures/Gaseous/Gaseous_08-512x512.png", "/assets/materials/planetTextures/Gaseous/Gaseous_08-512x512.png",
"/planetTextures/Gaseous/Gaseous_09-512x512.png", "/assets/materials/planetTextures/Gaseous/Gaseous_09-512x512.png",
"/planetTextures/Gaseous/Gaseous_10-512x512.png", "/assets/materials/planetTextures/Gaseous/Gaseous_10-512x512.png",
"/planetTextures/Gaseous/Gaseous_11-512x512.png", "/assets/materials/planetTextures/Gaseous/Gaseous_11-512x512.png",
"/planetTextures/Gaseous/Gaseous_12-512x512.png", "/assets/materials/planetTextures/Gaseous/Gaseous_12-512x512.png",
"/planetTextures/Gaseous/Gaseous_13-512x512.png", "/assets/materials/planetTextures/Gaseous/Gaseous_13-512x512.png",
"/planetTextures/Gaseous/Gaseous_14-512x512.png", "/assets/materials/planetTextures/Gaseous/Gaseous_14-512x512.png",
"/planetTextures/Gaseous/Gaseous_15-512x512.png", "/assets/materials/planetTextures/Gaseous/Gaseous_15-512x512.png",
"/planetTextures/Gaseous/Gaseous_16-512x512.png", "/assets/materials/planetTextures/Gaseous/Gaseous_16-512x512.png",
"/planetTextures/Gaseous/Gaseous_17-512x512.png", "/assets/materials/planetTextures/Gaseous/Gaseous_17-512x512.png",
"/planetTextures/Gaseous/Gaseous_18-512x512.png", "/assets/materials/planetTextures/Gaseous/Gaseous_18-512x512.png",
"/planetTextures/Gaseous/Gaseous_19-512x512.png", "/assets/materials/planetTextures/Gaseous/Gaseous_19-512x512.png",
"/planetTextures/Gaseous/Gaseous_20-512x512.png", "/assets/materials/planetTextures/Gaseous/Gaseous_20-512x512.png",
// Grassland planets (5 textures) // Grassland planets (5 textures)
"/planetTextures/Grassland/Grassland_01-512x512.png", "/assets/materials/planetTextures/Grassland/Grassland_01-512x512.png",
"/planetTextures/Grassland/Grassland_02-512x512.png", "/assets/materials/planetTextures/Grassland/Grassland_02-512x512.png",
"/planetTextures/Grassland/Grassland_03-512x512.png", "/assets/materials/planetTextures/Grassland/Grassland_03-512x512.png",
"/planetTextures/Grassland/Grassland_04-512x512.png", "/assets/materials/planetTextures/Grassland/Grassland_04-512x512.png",
"/planetTextures/Grassland/Grassland_05-512x512.png", "/assets/materials/planetTextures/Grassland/Grassland_05-512x512.png",
// Jungle planets (5 textures) // Jungle planets (5 textures)
"/planetTextures/Jungle/Jungle_01-512x512.png", "/assets/materials/planetTextures/Jungle/Jungle_01-512x512.png",
"/planetTextures/Jungle/Jungle_02-512x512.png", "/assets/materials/planetTextures/Jungle/Jungle_02-512x512.png",
"/planetTextures/Jungle/Jungle_03-512x512.png", "/assets/materials/planetTextures/Jungle/Jungle_03-512x512.png",
"/planetTextures/Jungle/Jungle_04-512x512.png", "/assets/materials/planetTextures/Jungle/Jungle_04-512x512.png",
"/planetTextures/Jungle/Jungle_05-512x512.png", "/assets/materials/planetTextures/Jungle/Jungle_05-512x512.png",
// Marshy planets (5 textures) // Marshy planets (5 textures)
"/planetTextures/Marshy/Marshy_01-512x512.png", "/assets/materials/planetTextures/Marshy/Marshy_01-512x512.png",
"/planetTextures/Marshy/Marshy_02-512x512.png", "/assets/materials/planetTextures/Marshy/Marshy_02-512x512.png",
"/planetTextures/Marshy/Marshy_03-512x512.png", "/assets/materials/planetTextures/Marshy/Marshy_03-512x512.png",
"/planetTextures/Marshy/Marshy_04-512x512.png", "/assets/materials/planetTextures/Marshy/Marshy_04-512x512.png",
"/planetTextures/Marshy/Marshy_05-512x512.png", "/assets/materials/planetTextures/Marshy/Marshy_05-512x512.png",
// Martian planets (5 textures) // Martian planets (5 textures)
"/planetTextures/Martian/Martian_01-512x512.png", "/assets/materials/planetTextures/Martian/Martian_01-512x512.png",
"/planetTextures/Martian/Martian_02-512x512.png", "/assets/materials/planetTextures/Martian/Martian_02-512x512.png",
"/planetTextures/Martian/Martian_03-512x512.png", "/assets/materials/planetTextures/Martian/Martian_03-512x512.png",
"/planetTextures/Martian/Martian_04-512x512.png", "/assets/materials/planetTextures/Martian/Martian_04-512x512.png",
"/planetTextures/Martian/Martian_05-512x512.png", "/assets/materials/planetTextures/Martian/Martian_05-512x512.png",
// Methane planets (5 textures) // Methane planets (5 textures)
"/planetTextures/Methane/Methane_01-512x512.png", "/assets/materials/planetTextures/Methane/Methane_01-512x512.png",
"/planetTextures/Methane/Methane_02-512x512.png", "/assets/materials/planetTextures/Methane/Methane_02-512x512.png",
"/planetTextures/Methane/Methane_03-512x512.png", "/assets/materials/planetTextures/Methane/Methane_03-512x512.png",
"/planetTextures/Methane/Methane_04-512x512.png", "/assets/materials/planetTextures/Methane/Methane_04-512x512.png",
"/planetTextures/Methane/Methane_05-512x512.png", "/assets/materials/planetTextures/Methane/Methane_05-512x512.png",
// Sandy planets (5 textures) // Sandy planets (5 textures)
"/planetTextures/Sandy/Sandy_01-512x512.png", "/assets/materials/planetTextures/Sandy/Sandy_01-512x512.png",
"/planetTextures/Sandy/Sandy_02-512x512.png", "/assets/materials/planetTextures/Sandy/Sandy_02-512x512.png",
"/planetTextures/Sandy/Sandy_03-512x512.png", "/assets/materials/planetTextures/Sandy/Sandy_03-512x512.png",
"/planetTextures/Sandy/Sandy_04-512x512.png", "/assets/materials/planetTextures/Sandy/Sandy_04-512x512.png",
"/planetTextures/Sandy/Sandy_05-512x512.png", "/assets/materials/planetTextures/Sandy/Sandy_05-512x512.png",
// Snowy planets (5 textures) // Snowy planets (5 textures)
"/planetTextures/Snowy/Snowy_01-512x512.png", "/assets/materials/planetTextures/Snowy/Snowy_01-512x512.png",
"/planetTextures/Snowy/Snowy_02-512x512.png", "/assets/materials/planetTextures/Snowy/Snowy_02-512x512.png",
"/planetTextures/Snowy/Snowy_03-512x512.png", "/assets/materials/planetTextures/Snowy/Snowy_03-512x512.png",
"/planetTextures/Snowy/Snowy_04-512x512.png", "/assets/materials/planetTextures/Snowy/Snowy_04-512x512.png",
"/planetTextures/Snowy/Snowy_05-512x512.png", "/assets/materials/planetTextures/Snowy/Snowy_05-512x512.png",
// Tundra planets (5 textures) // Tundra planets (5 textures)
"/planetTextures/Tundra/Tundra_01-512x512.png", "/assets/materials/planetTextures/Tundra/Tundra_01-512x512.png",
"/planetTextures/Tundra/Tundra_02-512x512.png", "/assets/materials/planetTextures/Tundra/Tundra_02-512x512.png",
"/planetTextures/Tundra/Tundra_03-512x512.png", "/assets/materials/planetTextures/Tundra/Tundra_03-512x512.png",
"/planetTextures/Tundra/Tundra_04-512x512.png", "/assets/materials/planetTextures/Tundra/Tundra_04-512x512.png",
"/planetTextures/Tundra/Tundral-EQUIRECTANGULAR-5-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 = { export const PLANET_TEXTURES_BY_TYPE = {
arid: [ arid: [
"/planetTextures/Arid/Arid_01-512x512.png", "/assets/materials/planetTextures/Arid/Arid_01-512x512.png",
"/planetTextures/Arid/Arid_02-512x512.png", "/assets/materials/planetTextures/Arid/Arid_02-512x512.png",
"/planetTextures/Arid/Arid_03-512x512.png", "/assets/materials/planetTextures/Arid/Arid_03-512x512.png",
"/planetTextures/Arid/Arid_04-512x512.png", "/assets/materials/planetTextures/Arid/Arid_04-512x512.png",
"/planetTextures/Arid/Arid_05-512x512.png", "/assets/materials/planetTextures/Arid/Arid_05-512x512.png",
], ],
barren: [ barren: [
"/planetTextures/Barren/Barren_01-512x512.png", "/assets/materials/planetTextures/Barren/Barren_01-512x512.png",
"/planetTextures/Barren/Barren_02-512x512.png", "/assets/materials/planetTextures/Barren/Barren_02-512x512.png",
"/planetTextures/Barren/Barren_03-512x512.png", "/assets/materials/planetTextures/Barren/Barren_03-512x512.png",
"/planetTextures/Barren/Barren_04-512x512.png", "/assets/materials/planetTextures/Barren/Barren_04-512x512.png",
"/planetTextures/Barren/Barren_05-512x512.png", "/assets/materials/planetTextures/Barren/Barren_05-512x512.png",
], ],
dusty: [ dusty: [
"/planetTextures/Dusty/Dusty_01-512x512.png", "/assets/materials/planetTextures/Dusty/Dusty_01-512x512.png",
"/planetTextures/Dusty/Dusty_02-512x512.png", "/assets/materials/planetTextures/Dusty/Dusty_02-512x512.png",
"/planetTextures/Dusty/Dusty_03-512x512.png", "/assets/materials/planetTextures/Dusty/Dusty_03-512x512.png",
"/planetTextures/Dusty/Dusty_04-512x512.png", "/assets/materials/planetTextures/Dusty/Dusty_04-512x512.png",
"/planetTextures/Dusty/Dusty_05-512x512.png", "/assets/materials/planetTextures/Dusty/Dusty_05-512x512.png",
], ],
gaseous: [ gaseous: [
"/planetTextures/Gaseous/Gaseous_01-512x512.png", "/assets/materials/planetTextures/Gaseous/Gaseous_01-512x512.png",
"/planetTextures/Gaseous/Gaseous_02-512x512.png", "/assets/materials/planetTextures/Gaseous/Gaseous_02-512x512.png",
"/planetTextures/Gaseous/Gaseous_03-512x512.png", "/assets/materials/planetTextures/Gaseous/Gaseous_03-512x512.png",
"/planetTextures/Gaseous/Gaseous_04-512x512.png", "/assets/materials/planetTextures/Gaseous/Gaseous_04-512x512.png",
"/planetTextures/Gaseous/Gaseous_05-512x512.png", "/assets/materials/planetTextures/Gaseous/Gaseous_05-512x512.png",
"/planetTextures/Gaseous/Gaseous_06-512x512.png", "/assets/materials/planetTextures/Gaseous/Gaseous_06-512x512.png",
"/planetTextures/Gaseous/Gaseous_07-512x512.png", "/assets/materials/planetTextures/Gaseous/Gaseous_07-512x512.png",
"/planetTextures/Gaseous/Gaseous_08-512x512.png", "/assets/materials/planetTextures/Gaseous/Gaseous_08-512x512.png",
"/planetTextures/Gaseous/Gaseous_09-512x512.png", "/assets/materials/planetTextures/Gaseous/Gaseous_09-512x512.png",
"/planetTextures/Gaseous/Gaseous_10-512x512.png", "/assets/materials/planetTextures/Gaseous/Gaseous_10-512x512.png",
"/planetTextures/Gaseous/Gaseous_11-512x512.png", "/assets/materials/planetTextures/Gaseous/Gaseous_11-512x512.png",
"/planetTextures/Gaseous/Gaseous_12-512x512.png", "/assets/materials/planetTextures/Gaseous/Gaseous_12-512x512.png",
"/planetTextures/Gaseous/Gaseous_13-512x512.png", "/assets/materials/planetTextures/Gaseous/Gaseous_13-512x512.png",
"/planetTextures/Gaseous/Gaseous_14-512x512.png", "/assets/materials/planetTextures/Gaseous/Gaseous_14-512x512.png",
"/planetTextures/Gaseous/Gaseous_15-512x512.png", "/assets/materials/planetTextures/Gaseous/Gaseous_15-512x512.png",
"/planetTextures/Gaseous/Gaseous_16-512x512.png", "/assets/materials/planetTextures/Gaseous/Gaseous_16-512x512.png",
"/planetTextures/Gaseous/Gaseous_17-512x512.png", "/assets/materials/planetTextures/Gaseous/Gaseous_17-512x512.png",
"/planetTextures/Gaseous/Gaseous_18-512x512.png", "/assets/materials/planetTextures/Gaseous/Gaseous_18-512x512.png",
"/planetTextures/Gaseous/Gaseous_19-512x512.png", "/assets/materials/planetTextures/Gaseous/Gaseous_19-512x512.png",
"/planetTextures/Gaseous/Gaseous_20-512x512.png", "/assets/materials/planetTextures/Gaseous/Gaseous_20-512x512.png",
], ],
grassland: [ grassland: [
"/planetTextures/Grassland/Grassland_01-512x512.png", "/assets/materials/planetTextures/Grassland/Grassland_01-512x512.png",
"/planetTextures/Grassland/Grassland_02-512x512.png", "/assets/materials/planetTextures/Grassland/Grassland_02-512x512.png",
"/planetTextures/Grassland/Grassland_03-512x512.png", "/assets/materials/planetTextures/Grassland/Grassland_03-512x512.png",
"/planetTextures/Grassland/Grassland_04-512x512.png", "/assets/materials/planetTextures/Grassland/Grassland_04-512x512.png",
"/planetTextures/Grassland/Grassland_05-512x512.png", "/assets/materials/planetTextures/Grassland/Grassland_05-512x512.png",
], ],
jungle: [ jungle: [
"/planetTextures/Jungle/Jungle_01-512x512.png", "/assets/materials/planetTextures/Jungle/Jungle_01-512x512.png",
"/planetTextures/Jungle/Jungle_02-512x512.png", "/assets/materials/planetTextures/Jungle/Jungle_02-512x512.png",
"/planetTextures/Jungle/Jungle_03-512x512.png", "/assets/materials/planetTextures/Jungle/Jungle_03-512x512.png",
"/planetTextures/Jungle/Jungle_04-512x512.png", "/assets/materials/planetTextures/Jungle/Jungle_04-512x512.png",
"/planetTextures/Jungle/Jungle_05-512x512.png", "/assets/materials/planetTextures/Jungle/Jungle_05-512x512.png",
], ],
marshy: [ marshy: [
"/planetTextures/Marshy/Marshy_01-512x512.png", "/assets/materials/planetTextures/Marshy/Marshy_01-512x512.png",
"/planetTextures/Marshy/Marshy_02-512x512.png", "/assets/materials/planetTextures/Marshy/Marshy_02-512x512.png",
"/planetTextures/Marshy/Marshy_03-512x512.png", "/assets/materials/planetTextures/Marshy/Marshy_03-512x512.png",
"/planetTextures/Marshy/Marshy_04-512x512.png", "/assets/materials/planetTextures/Marshy/Marshy_04-512x512.png",
"/planetTextures/Marshy/Marshy_05-512x512.png", "/assets/materials/planetTextures/Marshy/Marshy_05-512x512.png",
], ],
martian: [ martian: [
"/planetTextures/Martian/Martian_01-512x512.png", "/assets/materials/planetTextures/Martian/Martian_01-512x512.png",
"/planetTextures/Martian/Martian_02-512x512.png", "/assets/materials/planetTextures/Martian/Martian_02-512x512.png",
"/planetTextures/Martian/Martian_03-512x512.png", "/assets/materials/planetTextures/Martian/Martian_03-512x512.png",
"/planetTextures/Martian/Martian_04-512x512.png", "/assets/materials/planetTextures/Martian/Martian_04-512x512.png",
"/planetTextures/Martian/Martian_05-512x512.png", "/assets/materials/planetTextures/Martian/Martian_05-512x512.png",
], ],
methane: [ methane: [
"/planetTextures/Methane/Methane_01-512x512.png", "/assets/materials/planetTextures/Methane/Methane_01-512x512.png",
"/planetTextures/Methane/Methane_02-512x512.png", "/assets/materials/planetTextures/Methane/Methane_02-512x512.png",
"/planetTextures/Methane/Methane_03-512x512.png", "/assets/materials/planetTextures/Methane/Methane_03-512x512.png",
"/planetTextures/Methane/Methane_04-512x512.png", "/assets/materials/planetTextures/Methane/Methane_04-512x512.png",
"/planetTextures/Methane/Methane_05-512x512.png", "/assets/materials/planetTextures/Methane/Methane_05-512x512.png",
], ],
sandy: [ sandy: [
"/planetTextures/Sandy/Sandy_01-512x512.png", "/assets/materials/planetTextures/Sandy/Sandy_01-512x512.png",
"/planetTextures/Sandy/Sandy_02-512x512.png", "/assets/materials/planetTextures/Sandy/Sandy_02-512x512.png",
"/planetTextures/Sandy/Sandy_03-512x512.png", "/assets/materials/planetTextures/Sandy/Sandy_03-512x512.png",
"/planetTextures/Sandy/Sandy_04-512x512.png", "/assets/materials/planetTextures/Sandy/Sandy_04-512x512.png",
"/planetTextures/Sandy/Sandy_05-512x512.png", "/assets/materials/planetTextures/Sandy/Sandy_05-512x512.png",
], ],
snowy: [ snowy: [
"/planetTextures/Snowy/Snowy_01-512x512.png", "/assets/materials/planetTextures/Snowy/Snowy_01-512x512.png",
"/planetTextures/Snowy/Snowy_02-512x512.png", "/assets/materials/planetTextures/Snowy/Snowy_02-512x512.png",
"/planetTextures/Snowy/Snowy_03-512x512.png", "/assets/materials/planetTextures/Snowy/Snowy_03-512x512.png",
"/planetTextures/Snowy/Snowy_04-512x512.png", "/assets/materials/planetTextures/Snowy/Snowy_04-512x512.png",
"/planetTextures/Snowy/Snowy_05-512x512.png", "/assets/materials/planetTextures/Snowy/Snowy_05-512x512.png",
], ],
tundra: [ tundra: [
"/planetTextures/Tundra/Tundra_01-512x512.png", "/assets/materials/planetTextures/Tundra/Tundra_01-512x512.png",
"/planetTextures/Tundra/Tundra_02-512x512.png", "/assets/materials/planetTextures/Tundra/Tundra_02-512x512.png",
"/planetTextures/Tundra/Tundra_03-512x512.png", "/assets/materials/planetTextures/Tundra/Tundra_03-512x512.png",
"/planetTextures/Tundra/Tundra_04-512x512.png", "/assets/materials/planetTextures/Tundra/Tundra_04-512x512.png",
"/planetTextures/Tundra/Tundral-EQUIRECTANGULAR-5-512x512.png", "/assets/materials/planetTextures/Tundra/Tundral-EQUIRECTANGULAR-5-512x512.png",
], ],
}; };

266
src/progression.ts Normal file
View 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 };
}
}

View File

@ -98,12 +98,12 @@ export class RockFactory {
debugLog(this._asteroidMesh); 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> { linearVelocitry: Vector3, angularVelocity: Vector3, score: Observable<ScoreEvent>): Promise<Rock> {
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 = size; rock.scaling = new Vector3(scale, scale, scale);
rock.position = position; rock.position = position;
//rock.material = this._rockMaterial; //rock.material = this._rockMaterial;
rock.name = "asteroid-" + i; rock.name = "asteroid-" + i;

View File

@ -314,7 +314,8 @@ export class Ship {
this._gameStats, this._gameStats,
() => this.handleReplayRequest(), () => this.handleReplayRequest(),
() => this.handleExitVR(), () => this.handleExitVR(),
() => this.handleResume() () => this.handleResume(),
() => this.handleNextLevel()
); );
this._statusScreen.initialize(this._camera); this._statusScreen.initialize(this._camera);
} }
@ -345,6 +346,16 @@ export class Ship {
this._controllerInput?.setEnabled(true); 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 * Check game-ending conditions and auto-show status screen
* Conditions: * Conditions:
@ -375,7 +386,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); this._statusScreen.show(true, false); // Game ended, not victory
this._keyboardInput?.setEnabled(false); this._keyboardInput?.setEnabled(false);
this._controllerInput?.setEnabled(false); this._controllerInput?.setEnabled(false);
this._statusScreenAutoShown = true; this._statusScreenAutoShown = true;
@ -385,7 +396,7 @@ 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 < 1) { if (!this._isInLandingZone && fuel < 0.01 && totalVelocity < 1) {
debugLog('Game end condition met: Stranded (no fuel, low velocity)'); 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._keyboardInput?.setEnabled(false);
this._controllerInput?.setEnabled(false); this._controllerInput?.setEnabled(false);
this._statusScreenAutoShown = true; this._statusScreenAutoShown = true;
@ -395,7 +406,7 @@ export class Ship {
// Check condition 3: Victory (all asteroids destroyed, inside landing zone) // Check condition 3: Victory (all asteroids destroyed, inside landing zone)
if (asteroidsRemaining <= 0 && this._isInLandingZone) { if (asteroidsRemaining <= 0 && this._isInLandingZone) {
debugLog('Game end condition met: Victory (all asteroids destroyed)'); 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._keyboardInput?.setEnabled(false);
this._controllerInput?.setEnabled(false); this._controllerInput?.setEnabled(false);
this._statusScreenAutoShown = true; this._statusScreenAutoShown = true;

View File

@ -10,6 +10,7 @@ import {DefaultScene} from "./defaultScene";
import {GameConfig} from "./gameConfig"; import {GameConfig} from "./gameConfig";
import debugLog from "./debug"; import debugLog from "./debug";
import loadAsset from "./utils/loadAsset"; import loadAsset from "./utils/loadAsset";
import {Vector3Array} from "./levelConfig";
export interface StarBaseResult { export interface StarBaseResult {
baseMesh: AbstractMesh; baseMesh: AbstractMesh;
@ -18,18 +19,30 @@ export interface StarBaseResult {
/** /**
* Create and load the star base mesh * 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 * @returns Promise resolving to the loaded star base mesh and landing aggregate
*/ */
export default class StarBase { 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 config = GameConfig.getInstance();
const scene = DefaultScene.MainScene; const scene = DefaultScene.MainScene;
const importMeshes = await loadAsset('base.glb'); const importMeshes = await loadAsset(baseGlbPath);
const baseMesh = importMeshes.meshes.get('Base'); const baseMesh = importMeshes.meshes.get('Base');
const landingMesh = importMeshes.meshes.get('BaseLandingZone'); 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; let landingAgg: PhysicsAggregate | null = null;
if (config.physicsEnabled) { if (config.physicsEnabled) {

View File

@ -16,6 +16,7 @@ import {
} from "@babylonjs/core"; } from "@babylonjs/core";
import { GameStats } from "./gameStats"; import { GameStats } from "./gameStats";
import { DefaultScene } from "./defaultScene"; import { DefaultScene } from "./defaultScene";
import { ProgressionManager } from "./progression";
/** /**
* Status screen that displays game statistics * Status screen that displays game statistics
@ -41,21 +42,27 @@ export class StatusScreen {
private _replayButton: Button; private _replayButton: Button;
private _exitButton: Button; private _exitButton: Button;
private _resumeButton: Button; private _resumeButton: Button;
private _nextLevelButton: Button;
// Callbacks // Callbacks
private _onReplayCallback: (() => void) | null = null; private _onReplayCallback: (() => void) | null = null;
private _onExitCallback: (() => void) | null = null; private _onExitCallback: (() => void) | null = null;
private _onResumeCallback: (() => void) | null = null; private _onResumeCallback: (() => void) | null = null;
private _onNextLevelCallback: (() => void) | null = null;
// Track whether game has ended // Track whether game has ended
private _isGameEnded: boolean = false; 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._scene = scene;
this._gameStats = gameStats; this._gameStats = gameStats;
this._onReplayCallback = onReplay || null; this._onReplayCallback = onReplay || null;
this._onExitCallback = onExit || null; this._onExitCallback = onExit || null;
this._onResumeCallback = onResume || null; this._onResumeCallback = onResume || null;
this._onNextLevelCallback = onNextLevel || null;
} }
/** /**
@ -154,8 +161,25 @@ export class StatusScreen {
}); });
buttonBar.addControl(this._resumeButton); 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) // 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.width = "300px";
this._replayButton.height = "60px"; this._replayButton.height = "60px";
this._replayButton.color = "white"; 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 * 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
*/ */
public show(isGameEnded: boolean = false): void { public show(isGameEnded: boolean = false, victory: boolean = false): void {
if (!this._screenMesh) { if (!this._screenMesh) {
return; return;
} }
@ -291,6 +323,21 @@ export class StatusScreen {
// Store game ended state // Store game ended state
this._isGameEnded = isGameEnded; 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 // Show/hide appropriate buttons based on whether game has ended
if (this._resumeButton) { if (this._resumeButton) {
this._resumeButton.isVisible = !isGameEnded; this._resumeButton.isVisible = !isGameEnded;
@ -298,6 +345,10 @@ export class StatusScreen {
if (this._replayButton) { if (this._replayButton) {
this._replayButton.isVisible = isGameEnded; 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 // Enable pointer selection for button interaction
this.enablePointerSelection(); this.enablePointerSelection();
@ -310,6 +361,19 @@ export class StatusScreen {
this._isVisible = true; 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 * Hide the status screen
*/ */