Refactor asteroid scaling and reorganize assets
All checks were successful
Build / build (push) Successful in 1m21s
Major changes: - Change asteroid config to use single scale number instead of Vector3 - Move planetTextures to public/assets/materials/planetTextures - Add GLB path configuration for start base - Fix inspector toggle to work bidirectionally - Add progression system support Asteroid Scaling Changes: - Update AsteroidConfig interface to use 'scale: number' instead of 'scaling: Vector3Array' - Modify RockFactory.createRock() to accept single scale parameter - Update level serializer/deserializer to use uniform scale - Simplify level generation code in levelEditor and levelGenerator - Update validation to check for positive number instead of 3-element array Asset Organization: - Move public/planetTextures → public/assets/materials/planetTextures - Update all texture path references in planetTextures.ts (210 paths) - Update default texture paths in createSun.ts and levelSerializer.ts - Update CLAUDE.md documentation with new asset structure Start Base Improvements: - Add baseGlbPath and landingGlbPath to StartBaseConfig - Update StarBase.buildStarBase() to accept GLB path parameter - Add position parameter support to StarBase - Store GLB path in mesh metadata for serialization - Add UI field in level editor for base GLB path Inspector Toggle: - Fix 'i' key to toggle inspector on/off instead of only on - Use scene.debugLayer.isVisible() for state checking - Consistent with ReplayManager implementation 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
@ -101,7 +101,7 @@ Located in `level1.ts:getDifficultyConfig()`
|
|||||||
### Asset Loading
|
### 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
|
||||||
|
|||||||
45
index.html
@ -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>
|
||||||
|
|
||||||
|
|||||||
@ -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",
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 573 KiB After Width: | Height: | Size: 573 KiB |
|
Before Width: | Height: | Size: 565 KiB After Width: | Height: | Size: 565 KiB |
|
Before Width: | Height: | Size: 585 KiB After Width: | Height: | Size: 585 KiB |
|
Before Width: | Height: | Size: 560 KiB After Width: | Height: | Size: 560 KiB |
|
Before Width: | Height: | Size: 570 KiB After Width: | Height: | Size: 570 KiB |
|
Before Width: | Height: | Size: 352 KiB After Width: | Height: | Size: 352 KiB |
|
Before Width: | Height: | Size: 360 KiB After Width: | Height: | Size: 360 KiB |
|
Before Width: | Height: | Size: 347 KiB After Width: | Height: | Size: 347 KiB |
|
Before Width: | Height: | Size: 359 KiB After Width: | Height: | Size: 359 KiB |
|
Before Width: | Height: | Size: 374 KiB After Width: | Height: | Size: 374 KiB |
|
Before Width: | Height: | Size: 567 KiB After Width: | Height: | Size: 567 KiB |
|
Before Width: | Height: | Size: 564 KiB After Width: | Height: | Size: 564 KiB |
|
Before Width: | Height: | Size: 572 KiB After Width: | Height: | Size: 572 KiB |
|
Before Width: | Height: | Size: 573 KiB After Width: | Height: | Size: 573 KiB |
|
Before Width: | Height: | Size: 560 KiB After Width: | Height: | Size: 560 KiB |
|
Before Width: | Height: | Size: 98 KiB After Width: | Height: | Size: 98 KiB |
|
Before Width: | Height: | Size: 142 KiB After Width: | Height: | Size: 142 KiB |
|
Before Width: | Height: | Size: 136 KiB After Width: | Height: | Size: 136 KiB |
|
Before Width: | Height: | Size: 171 KiB After Width: | Height: | Size: 171 KiB |
|
Before Width: | Height: | Size: 168 KiB After Width: | Height: | Size: 168 KiB |
|
Before Width: | Height: | Size: 188 KiB After Width: | Height: | Size: 188 KiB |
|
Before Width: | Height: | Size: 132 KiB After Width: | Height: | Size: 132 KiB |
|
Before Width: | Height: | Size: 139 KiB After Width: | Height: | Size: 139 KiB |
|
Before Width: | Height: | Size: 118 KiB After Width: | Height: | Size: 118 KiB |
|
Before Width: | Height: | Size: 118 KiB After Width: | Height: | Size: 118 KiB |
|
Before Width: | Height: | Size: 131 KiB After Width: | Height: | Size: 131 KiB |
|
Before Width: | Height: | Size: 201 KiB After Width: | Height: | Size: 201 KiB |
|
Before Width: | Height: | Size: 134 KiB After Width: | Height: | Size: 134 KiB |
|
Before Width: | Height: | Size: 165 KiB After Width: | Height: | Size: 165 KiB |
|
Before Width: | Height: | Size: 166 KiB After Width: | Height: | Size: 166 KiB |
|
Before Width: | Height: | Size: 218 KiB After Width: | Height: | Size: 218 KiB |
|
Before Width: | Height: | Size: 139 KiB After Width: | Height: | Size: 139 KiB |
|
Before Width: | Height: | Size: 144 KiB After Width: | Height: | Size: 144 KiB |
|
Before Width: | Height: | Size: 145 KiB After Width: | Height: | Size: 145 KiB |
|
Before Width: | Height: | Size: 205 KiB After Width: | Height: | Size: 205 KiB |
|
Before Width: | Height: | Size: 678 KiB After Width: | Height: | Size: 678 KiB |
|
Before Width: | Height: | Size: 626 KiB After Width: | Height: | Size: 626 KiB |
|
Before Width: | Height: | Size: 670 KiB After Width: | Height: | Size: 670 KiB |
|
Before Width: | Height: | Size: 675 KiB After Width: | Height: | Size: 675 KiB |
|
Before Width: | Height: | Size: 666 KiB After Width: | Height: | Size: 666 KiB |
|
Before Width: | Height: | Size: 634 KiB After Width: | Height: | Size: 634 KiB |
|
Before Width: | Height: | Size: 602 KiB After Width: | Height: | Size: 602 KiB |
|
Before Width: | Height: | Size: 614 KiB After Width: | Height: | Size: 614 KiB |
|
Before Width: | Height: | Size: 600 KiB After Width: | Height: | Size: 600 KiB |
|
Before Width: | Height: | Size: 656 KiB After Width: | Height: | Size: 656 KiB |
|
Before Width: | Height: | Size: 710 KiB After Width: | Height: | Size: 710 KiB |
|
Before Width: | Height: | Size: 714 KiB After Width: | Height: | Size: 714 KiB |
|
Before Width: | Height: | Size: 713 KiB After Width: | Height: | Size: 713 KiB |
|
Before Width: | Height: | Size: 727 KiB After Width: | Height: | Size: 727 KiB |
|
Before Width: | Height: | Size: 713 KiB After Width: | Height: | Size: 713 KiB |
|
Before Width: | Height: | Size: 517 KiB After Width: | Height: | Size: 517 KiB |
|
Before Width: | Height: | Size: 515 KiB After Width: | Height: | Size: 515 KiB |
|
Before Width: | Height: | Size: 526 KiB After Width: | Height: | Size: 526 KiB |
|
Before Width: | Height: | Size: 517 KiB After Width: | Height: | Size: 517 KiB |
|
Before Width: | Height: | Size: 526 KiB After Width: | Height: | Size: 526 KiB |
|
Before Width: | Height: | Size: 656 KiB After Width: | Height: | Size: 656 KiB |
|
Before Width: | Height: | Size: 663 KiB After Width: | Height: | Size: 663 KiB |
|
Before Width: | Height: | Size: 648 KiB After Width: | Height: | Size: 648 KiB |
|
Before Width: | Height: | Size: 665 KiB After Width: | Height: | Size: 665 KiB |
|
Before Width: | Height: | Size: 670 KiB After Width: | Height: | Size: 670 KiB |
|
Before Width: | Height: | Size: 384 KiB After Width: | Height: | Size: 384 KiB |
|
Before Width: | Height: | Size: 327 KiB After Width: | Height: | Size: 327 KiB |
|
Before Width: | Height: | Size: 336 KiB After Width: | Height: | Size: 336 KiB |
|
Before Width: | Height: | Size: 325 KiB After Width: | Height: | Size: 325 KiB |
|
Before Width: | Height: | Size: 421 KiB After Width: | Height: | Size: 421 KiB |
|
Before Width: | Height: | Size: 576 KiB After Width: | Height: | Size: 576 KiB |
|
Before Width: | Height: | Size: 589 KiB After Width: | Height: | Size: 589 KiB |
|
Before Width: | Height: | Size: 591 KiB After Width: | Height: | Size: 591 KiB |
|
Before Width: | Height: | Size: 605 KiB After Width: | Height: | Size: 605 KiB |
|
Before Width: | Height: | Size: 585 KiB After Width: | Height: | Size: 585 KiB |
|
Before Width: | Height: | Size: 373 KiB After Width: | Height: | Size: 373 KiB |
|
Before Width: | Height: | Size: 374 KiB After Width: | Height: | Size: 374 KiB |
|
Before Width: | Height: | Size: 389 KiB After Width: | Height: | Size: 389 KiB |
|
Before Width: | Height: | Size: 399 KiB After Width: | Height: | Size: 399 KiB |
|
Before Width: | Height: | Size: 396 KiB After Width: | Height: | Size: 396 KiB |
@ -36,7 +36,7 @@ export function createSun() : AbstractMesh {
|
|||||||
export function createPlanet(position: Vector3, diameter: number, name: string) : AbstractMesh {
|
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;
|
||||||
|
|||||||
@ -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,
|
||||||
|
|||||||
@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -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`);
|
||||||
|
|||||||
@ -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
|
||||||
|
|||||||
@ -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
|
||||||
|
|||||||
@ -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
|
||||||
|
|||||||
@ -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;
|
||||||
|
|
||||||
|
|||||||
@ -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
|
||||||
|
|||||||
16
src/main.ts
@ -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
|
||||||
|
|||||||
@ -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
@ -0,0 +1,266 @@
|
|||||||
|
/**
|
||||||
|
* Progression tracking system for level completion and feature unlocks
|
||||||
|
*/
|
||||||
|
|
||||||
|
export interface LevelProgress {
|
||||||
|
levelName: string;
|
||||||
|
completed: boolean;
|
||||||
|
completedAt?: string; // ISO timestamp
|
||||||
|
bestTime?: number; // Best completion time in seconds
|
||||||
|
bestAccuracy?: number; // Best accuracy percentage
|
||||||
|
playCount: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ProgressionData {
|
||||||
|
version: string;
|
||||||
|
completedLevels: Map<string, LevelProgress>;
|
||||||
|
editorUnlocked: boolean;
|
||||||
|
firstPlayDate?: string;
|
||||||
|
lastPlayDate?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const STORAGE_KEY = 'space-game-progress';
|
||||||
|
const PROGRESSION_VERSION = '1.0';
|
||||||
|
const EDITOR_UNLOCK_REQUIREMENT = 3; // Complete 3 default levels to unlock editor
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Progression manager - tracks level completion and unlocks
|
||||||
|
*/
|
||||||
|
export class ProgressionManager {
|
||||||
|
private static _instance: ProgressionManager;
|
||||||
|
private _data: ProgressionData;
|
||||||
|
|
||||||
|
private constructor() {
|
||||||
|
this._data = this.loadProgress();
|
||||||
|
}
|
||||||
|
|
||||||
|
public static getInstance(): ProgressionManager {
|
||||||
|
if (!ProgressionManager._instance) {
|
||||||
|
ProgressionManager._instance = new ProgressionManager();
|
||||||
|
}
|
||||||
|
return ProgressionManager._instance;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Load progression data from localStorage
|
||||||
|
*/
|
||||||
|
private loadProgress(): ProgressionData {
|
||||||
|
try {
|
||||||
|
const stored = localStorage.getItem(STORAGE_KEY);
|
||||||
|
if (stored) {
|
||||||
|
const parsed = JSON.parse(stored);
|
||||||
|
// Convert completedLevels array back to Map
|
||||||
|
const completedLevels = new Map<string, LevelProgress>(
|
||||||
|
parsed.completedLevels || []
|
||||||
|
);
|
||||||
|
return {
|
||||||
|
version: parsed.version || PROGRESSION_VERSION,
|
||||||
|
completedLevels,
|
||||||
|
editorUnlocked: parsed.editorUnlocked || false,
|
||||||
|
firstPlayDate: parsed.firstPlayDate,
|
||||||
|
lastPlayDate: parsed.lastPlayDate
|
||||||
|
};
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error loading progression data:', error);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Return fresh progression data
|
||||||
|
return {
|
||||||
|
version: PROGRESSION_VERSION,
|
||||||
|
completedLevels: new Map(),
|
||||||
|
editorUnlocked: false,
|
||||||
|
firstPlayDate: new Date().toISOString()
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Save progression data to localStorage
|
||||||
|
*/
|
||||||
|
private saveProgress(): void {
|
||||||
|
try {
|
||||||
|
// Convert Map to array for JSON serialization
|
||||||
|
const toSave = {
|
||||||
|
...this._data,
|
||||||
|
completedLevels: Array.from(this._data.completedLevels.entries()),
|
||||||
|
lastPlayDate: new Date().toISOString()
|
||||||
|
};
|
||||||
|
localStorage.setItem(STORAGE_KEY, JSON.stringify(toSave));
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error saving progression data:', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Mark a level as completed with optional stats
|
||||||
|
*/
|
||||||
|
public markLevelComplete(
|
||||||
|
levelName: string,
|
||||||
|
stats?: {
|
||||||
|
completionTime?: number;
|
||||||
|
accuracy?: number;
|
||||||
|
}
|
||||||
|
): void {
|
||||||
|
const existing = this._data.completedLevels.get(levelName);
|
||||||
|
const now = new Date().toISOString();
|
||||||
|
|
||||||
|
const progress: LevelProgress = {
|
||||||
|
levelName,
|
||||||
|
completed: true,
|
||||||
|
completedAt: now,
|
||||||
|
bestTime: stats?.completionTime,
|
||||||
|
bestAccuracy: stats?.accuracy,
|
||||||
|
playCount: (existing?.playCount || 0) + 1
|
||||||
|
};
|
||||||
|
|
||||||
|
// Update best time if this is better
|
||||||
|
if (existing?.bestTime && stats?.completionTime) {
|
||||||
|
progress.bestTime = Math.min(existing.bestTime, stats.completionTime);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update best accuracy if this is better
|
||||||
|
if (existing?.bestAccuracy && stats?.accuracy) {
|
||||||
|
progress.bestAccuracy = Math.max(existing.bestAccuracy, stats.accuracy);
|
||||||
|
}
|
||||||
|
|
||||||
|
this._data.completedLevels.set(levelName, progress);
|
||||||
|
|
||||||
|
// Check if editor should be unlocked
|
||||||
|
this.checkEditorUnlock();
|
||||||
|
|
||||||
|
this.saveProgress();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Record that a level was started (for play count)
|
||||||
|
*/
|
||||||
|
public recordLevelStart(levelName: string): void {
|
||||||
|
const existing = this._data.completedLevels.get(levelName);
|
||||||
|
if (!existing) {
|
||||||
|
this._data.completedLevels.set(levelName, {
|
||||||
|
levelName,
|
||||||
|
completed: false,
|
||||||
|
playCount: 1
|
||||||
|
});
|
||||||
|
}
|
||||||
|
this.saveProgress();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if a level has been completed
|
||||||
|
*/
|
||||||
|
public isLevelComplete(levelName: string): boolean {
|
||||||
|
return this._data.completedLevels.get(levelName)?.completed || false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get progress data for a specific level
|
||||||
|
*/
|
||||||
|
public getLevelProgress(levelName: string): LevelProgress | undefined {
|
||||||
|
return this._data.completedLevels.get(levelName);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get all completed default levels
|
||||||
|
*/
|
||||||
|
public getCompletedDefaultLevels(): string[] {
|
||||||
|
const defaultLevels = this.getDefaultLevelNames();
|
||||||
|
return defaultLevels.filter(name => this.isLevelComplete(name));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the next incomplete default level
|
||||||
|
*/
|
||||||
|
public getNextLevel(): string | null {
|
||||||
|
const defaultLevels = this.getDefaultLevelNames();
|
||||||
|
for (const levelName of defaultLevels) {
|
||||||
|
if (!this.isLevelComplete(levelName)) {
|
||||||
|
return levelName;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null; // All levels completed
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get list of default level names in order
|
||||||
|
*/
|
||||||
|
private getDefaultLevelNames(): string[] {
|
||||||
|
return [
|
||||||
|
'Tutorial: Asteroid Field',
|
||||||
|
'Rescue Mission',
|
||||||
|
'Deep Space Patrol',
|
||||||
|
'Enemy Territory',
|
||||||
|
'The Gauntlet',
|
||||||
|
'Final Challenge'
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if editor should be unlocked based on completion
|
||||||
|
*/
|
||||||
|
private checkEditorUnlock(): void {
|
||||||
|
const completedCount = this.getCompletedDefaultLevels().length;
|
||||||
|
if (completedCount >= EDITOR_UNLOCK_REQUIREMENT && !this._data.editorUnlocked) {
|
||||||
|
this._data.editorUnlocked = true;
|
||||||
|
console.log(`🎉 Editor unlocked! (${completedCount} levels completed)`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if the level editor is unlocked
|
||||||
|
*/
|
||||||
|
public isEditorUnlocked(): boolean {
|
||||||
|
return this._data.editorUnlocked;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get count of completed default levels
|
||||||
|
*/
|
||||||
|
public getCompletedCount(): number {
|
||||||
|
return this.getCompletedDefaultLevels().length;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get total count of default levels
|
||||||
|
*/
|
||||||
|
public getTotalDefaultLevels(): number {
|
||||||
|
return this.getDefaultLevelNames().length;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get completion percentage
|
||||||
|
*/
|
||||||
|
public getCompletionPercentage(): number {
|
||||||
|
const total = this.getTotalDefaultLevels();
|
||||||
|
const completed = this.getCompletedCount();
|
||||||
|
return total > 0 ? (completed / total) * 100 : 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reset all progression (for testing or user request)
|
||||||
|
*/
|
||||||
|
public reset(): void {
|
||||||
|
this._data = {
|
||||||
|
version: PROGRESSION_VERSION,
|
||||||
|
completedLevels: new Map(),
|
||||||
|
editorUnlocked: false,
|
||||||
|
firstPlayDate: new Date().toISOString()
|
||||||
|
};
|
||||||
|
this.saveProgress();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Force unlock editor (admin/testing)
|
||||||
|
*/
|
||||||
|
public forceUnlockEditor(): void {
|
||||||
|
this._data.editorUnlocked = true;
|
||||||
|
this.saveProgress();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get all progression data (for display/debugging)
|
||||||
|
*/
|
||||||
|
public getAllProgress(): ProgressionData {
|
||||||
|
return { ...this._data };
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -98,12 +98,12 @@ export class RockFactory {
|
|||||||
debugLog(this._asteroidMesh);
|
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;
|
||||||
|
|||||||
19
src/ship.ts
@ -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;
|
||||||
|
|||||||
@ -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) {
|
||||||
|
|||||||
@ -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
|
||||||
*/
|
*/
|
||||||
|
|||||||