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
- 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

View File

@ -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>

View File

@ -1,6 +1,6 @@
{
"name": "space-game",
"deployHostname": "space.digital-experiment.com",
"deployHostname": "www.flatearthdefense.com",
"private": false,
"version": "0.0.1",
"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 {
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;

View File

@ -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,

View File

@ -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;
}

View File

@ -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`);

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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
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);
}
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;

View File

@ -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;

View File

@ -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) {

View File

@ -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
*/