- Difficulty: ${config.difficulty}
+
+
Progress
+
+ ${completedCount} of ${totalCount} default levels completed (${completionPercent.toFixed(0)}%)
-
${description}
- ${timestamp ? `
${timestamp}
` : ''}
-
+
+ ${nextLevel ? `
Next: ${nextLevel}
` : ''}
`;
}
+ // Default levels section - show all levels if progression disabled, or current/next if enabled
+ if (defaultLevels.size > 0) {
+ html += `
+
+
Available Levels
+
+ `;
+
+ // 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 += `
+
+
${name}
+
+ Difficulty: ${config.difficulty}${estimatedTime ? ` • ${estimatedTime}` : ''}
+
+
${description}
+
+
+ `;
+ }
+ } 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 += `
+
+
+
${nextLevelName}
+
START HERE
+
+
+ Difficulty: ${config.difficulty}${estimatedTime ? ` • ${estimatedTime}` : ''}
+
+
${description}
+
+
+ `;
+ }
+ } 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 += `
+
+
+
${currentLevelName}
+
✓
+
+
+ Difficulty: ${config.difficulty}${estimatedTime ? ` • ${estimatedTime}` : ''}
+
+
${description}
+ ${levelProgress?.playCount ? `
Played ${levelProgress.playCount} time${levelProgress.playCount > 1 ? 's' : ''}
` : ''}
+
+
+ `;
+ }
+ }
+
+ // 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 += `
+
+
+
${nextLevelName}
+
NEXT
+
+
+ Difficulty: ${config.difficulty}${estimatedTime ? ` • ${estimatedTime}` : ''}
+
+
${description}
+
+
+ `;
+ }
+ }
+ }
+
+ // 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 += `
+
+
✦ ✦ ✦
+
+ ${remainingCount} more level${remainingCount > 1 ? 's' : ''} beyond...
+
+
+ Complete challenges to unlock new missions
+
+
+ `;
+ }
+ } // End of progressionEnabled else block
+ }
+
+ // Custom levels section
+ if (customLevels.size > 0) {
+ html += `
+
+
Custom Levels
+
+ `;
+
+ for (const [name, config] of customLevels.entries()) {
+ const timestamp = config.timestamp ? new Date(config.timestamp).toLocaleDateString() : '';
+ const description = config.metadata?.description || `${config.asteroids.length} asteroids • ${config.planets.length} planets`;
+ const author = config.metadata?.author || 'Unknown';
+
+ html += `
+
+
${name}
+
+ Difficulty: ${config.difficulty} • By ${author}
+
+
${description}
+ ${timestamp ? `
Created ${timestamp}
` : ''}
+
+
+ `;
+ }
+ }
+
+ // Editor unlock button (always unlocked if progression disabled)
+ const isEditorUnlocked = !progressionEnabled || progression.isEditorUnlocked();
+ const completedCount = progression.getCompletedCount();
+
+ html += `
+
+ ${isEditorUnlocked ? `
+
+ 🎨 Create Custom Level
+
+ ` : `
+
+ 🔒 Level Editor (Complete ${3 - completedCount} more level${(3 - completedCount) !== 1 ? 's' : ''})
+
+ `}
+
+ `;
+
container.innerHTML = html;
// Add event listeners to level buttons
diff --git a/src/levelSerializer.ts b/src/levelSerializer.ts
index 0a3c28b..75a6c00 100644
--- a/src/levelSerializer.ts
+++ b/src/levelSerializer.ts
@@ -100,7 +100,7 @@ export class LevelSerializer {
}
/**
- * Serialize start base state
+ * Serialize start base state (position and GLB paths)
*/
private serializeStartBase(): StartBaseConfig {
const startBase = this.scene.getMeshByName("startBase");
@@ -109,31 +109,18 @@ export class LevelSerializer {
console.warn("Start base not found, using defaults");
return {
position: [0, 0, 0],
- diameter: 10,
- height: 1,
- color: [1, 1, 0]
+ baseGlbPath: 'base.glb'
};
}
const position = this.vector3ToArray(startBase.position);
- // Try to extract diameter and height from scaling or metadata
- // Assuming cylinder was created with specific dimensions
- const diameter = 10; // Default from Level1
- const height = 1; // Default from Level1
-
- // Get color from material if available
- let color: Vector3Array = [1, 1, 0]; // Default yellow
- if (startBase.material && (startBase.material as any).diffuseColor) {
- const diffuseColor = (startBase.material as any).diffuseColor;
- color = [diffuseColor.r, diffuseColor.g, diffuseColor.b];
- }
+ // Capture GLB path from metadata if available, otherwise use default
+ const baseGlbPath = startBase.metadata?.baseGlbPath || 'base.glb';
return {
position,
- diameter,
- height,
- color
+ baseGlbPath
};
}
@@ -191,7 +178,7 @@ export class LevelSerializer {
const diameter = boundingInfo.boundingSphere.radiusWorld * 2;
// Get texture path from material
- let texturePath = "/planetTextures/Arid/Arid_01-512x512.png"; // Default
+ let texturePath = "/assets/materials/planetTextures/Arid/Arid_01-512x512.png"; // Default
if (mesh.material && (mesh.material as any).diffuseTexture) {
const texture = (mesh.material as any).diffuseTexture;
texturePath = texture.url || texturePath;
@@ -222,7 +209,8 @@ export class LevelSerializer {
for (const mesh of asteroidMeshes) {
const position = this.vector3ToArray(mesh.position);
- const scaling = this.vector3ToArray(mesh.scaling);
+ // Use uniform scale (assume uniform scaling, take x component)
+ const scale = parseFloat(mesh.scaling.x.toFixed(3));
// Get velocities from physics body
let linearVelocity: Vector3Array = [0, 0, 0];
@@ -238,7 +226,7 @@ export class LevelSerializer {
asteroids.push({
id: mesh.name,
position,
- scaling,
+ scale,
linearVelocity,
angularVelocity,
mass
diff --git a/src/main.ts b/src/main.ts
index d28b387..667b8d4 100644
--- a/src/main.ts
+++ b/src/main.ts
@@ -118,6 +118,12 @@ export class Main {
// Listen for replay requests from the ship
if (ship) {
+ // Set current level name for progression tracking
+ if (ship._statusScreen) {
+ ship._statusScreen.setCurrentLevel(levelName);
+ debugLog(`Set current level for progression: ${levelName}`);
+ }
+
ship.onReplayRequestObservable.add(() => {
debugLog('Replay requested - reloading page');
window.location.reload();
@@ -472,16 +478,10 @@ export class Main {
// Setup router
router.on('/', () => {
- // Check if there are saved levels
- if (!hasSavedLevels()) {
- debugLog('No saved levels found, redirecting to editor');
- router.navigate('/editor');
- return;
- }
-
+ // Always show game view with level selector (no editor redirect)
showView('game');
- // Populate level selector
+ // Populate level selector (will show default levels if no custom levels)
populateLevelSelector();
// Initialize game if not in debug mode
diff --git a/src/planetTextures.ts b/src/planetTextures.ts
index d25d671..3537c0b 100644
--- a/src/planetTextures.ts
+++ b/src/planetTextures.ts
@@ -5,103 +5,103 @@
export const PLANET_TEXTURES = [
// Arid planets (5 textures)
- "/planetTextures/Arid/Arid_01-512x512.png",
- "/planetTextures/Arid/Arid_02-512x512.png",
- "/planetTextures/Arid/Arid_03-512x512.png",
- "/planetTextures/Arid/Arid_04-512x512.png",
- "/planetTextures/Arid/Arid_05-512x512.png",
+ "/assets/materials/planetTextures/Arid/Arid_01-512x512.png",
+ "/assets/materials/planetTextures/Arid/Arid_02-512x512.png",
+ "/assets/materials/planetTextures/Arid/Arid_03-512x512.png",
+ "/assets/materials/planetTextures/Arid/Arid_04-512x512.png",
+ "/assets/materials/planetTextures/Arid/Arid_05-512x512.png",
// Barren planets (5 textures)
- "/planetTextures/Barren/Barren_01-512x512.png",
- "/planetTextures/Barren/Barren_02-512x512.png",
- "/planetTextures/Barren/Barren_03-512x512.png",
- "/planetTextures/Barren/Barren_04-512x512.png",
- "/planetTextures/Barren/Barren_05-512x512.png",
+ "/assets/materials/planetTextures/Barren/Barren_01-512x512.png",
+ "/assets/materials/planetTextures/Barren/Barren_02-512x512.png",
+ "/assets/materials/planetTextures/Barren/Barren_03-512x512.png",
+ "/assets/materials/planetTextures/Barren/Barren_04-512x512.png",
+ "/assets/materials/planetTextures/Barren/Barren_05-512x512.png",
// Dusty planets (5 textures)
- "/planetTextures/Dusty/Dusty_01-512x512.png",
- "/planetTextures/Dusty/Dusty_02-512x512.png",
- "/planetTextures/Dusty/Dusty_03-512x512.png",
- "/planetTextures/Dusty/Dusty_04-512x512.png",
- "/planetTextures/Dusty/Dusty_05-512x512.png",
+ "/assets/materials/planetTextures/Dusty/Dusty_01-512x512.png",
+ "/assets/materials/planetTextures/Dusty/Dusty_02-512x512.png",
+ "/assets/materials/planetTextures/Dusty/Dusty_03-512x512.png",
+ "/assets/materials/planetTextures/Dusty/Dusty_04-512x512.png",
+ "/assets/materials/planetTextures/Dusty/Dusty_05-512x512.png",
// Gaseous planets (20 textures)
- "/planetTextures/Gaseous/Gaseous_01-512x512.png",
- "/planetTextures/Gaseous/Gaseous_02-512x512.png",
- "/planetTextures/Gaseous/Gaseous_03-512x512.png",
- "/planetTextures/Gaseous/Gaseous_04-512x512.png",
- "/planetTextures/Gaseous/Gaseous_05-512x512.png",
- "/planetTextures/Gaseous/Gaseous_06-512x512.png",
- "/planetTextures/Gaseous/Gaseous_07-512x512.png",
- "/planetTextures/Gaseous/Gaseous_08-512x512.png",
- "/planetTextures/Gaseous/Gaseous_09-512x512.png",
- "/planetTextures/Gaseous/Gaseous_10-512x512.png",
- "/planetTextures/Gaseous/Gaseous_11-512x512.png",
- "/planetTextures/Gaseous/Gaseous_12-512x512.png",
- "/planetTextures/Gaseous/Gaseous_13-512x512.png",
- "/planetTextures/Gaseous/Gaseous_14-512x512.png",
- "/planetTextures/Gaseous/Gaseous_15-512x512.png",
- "/planetTextures/Gaseous/Gaseous_16-512x512.png",
- "/planetTextures/Gaseous/Gaseous_17-512x512.png",
- "/planetTextures/Gaseous/Gaseous_18-512x512.png",
- "/planetTextures/Gaseous/Gaseous_19-512x512.png",
- "/planetTextures/Gaseous/Gaseous_20-512x512.png",
+ "/assets/materials/planetTextures/Gaseous/Gaseous_01-512x512.png",
+ "/assets/materials/planetTextures/Gaseous/Gaseous_02-512x512.png",
+ "/assets/materials/planetTextures/Gaseous/Gaseous_03-512x512.png",
+ "/assets/materials/planetTextures/Gaseous/Gaseous_04-512x512.png",
+ "/assets/materials/planetTextures/Gaseous/Gaseous_05-512x512.png",
+ "/assets/materials/planetTextures/Gaseous/Gaseous_06-512x512.png",
+ "/assets/materials/planetTextures/Gaseous/Gaseous_07-512x512.png",
+ "/assets/materials/planetTextures/Gaseous/Gaseous_08-512x512.png",
+ "/assets/materials/planetTextures/Gaseous/Gaseous_09-512x512.png",
+ "/assets/materials/planetTextures/Gaseous/Gaseous_10-512x512.png",
+ "/assets/materials/planetTextures/Gaseous/Gaseous_11-512x512.png",
+ "/assets/materials/planetTextures/Gaseous/Gaseous_12-512x512.png",
+ "/assets/materials/planetTextures/Gaseous/Gaseous_13-512x512.png",
+ "/assets/materials/planetTextures/Gaseous/Gaseous_14-512x512.png",
+ "/assets/materials/planetTextures/Gaseous/Gaseous_15-512x512.png",
+ "/assets/materials/planetTextures/Gaseous/Gaseous_16-512x512.png",
+ "/assets/materials/planetTextures/Gaseous/Gaseous_17-512x512.png",
+ "/assets/materials/planetTextures/Gaseous/Gaseous_18-512x512.png",
+ "/assets/materials/planetTextures/Gaseous/Gaseous_19-512x512.png",
+ "/assets/materials/planetTextures/Gaseous/Gaseous_20-512x512.png",
// Grassland planets (5 textures)
- "/planetTextures/Grassland/Grassland_01-512x512.png",
- "/planetTextures/Grassland/Grassland_02-512x512.png",
- "/planetTextures/Grassland/Grassland_03-512x512.png",
- "/planetTextures/Grassland/Grassland_04-512x512.png",
- "/planetTextures/Grassland/Grassland_05-512x512.png",
+ "/assets/materials/planetTextures/Grassland/Grassland_01-512x512.png",
+ "/assets/materials/planetTextures/Grassland/Grassland_02-512x512.png",
+ "/assets/materials/planetTextures/Grassland/Grassland_03-512x512.png",
+ "/assets/materials/planetTextures/Grassland/Grassland_04-512x512.png",
+ "/assets/materials/planetTextures/Grassland/Grassland_05-512x512.png",
// Jungle planets (5 textures)
- "/planetTextures/Jungle/Jungle_01-512x512.png",
- "/planetTextures/Jungle/Jungle_02-512x512.png",
- "/planetTextures/Jungle/Jungle_03-512x512.png",
- "/planetTextures/Jungle/Jungle_04-512x512.png",
- "/planetTextures/Jungle/Jungle_05-512x512.png",
+ "/assets/materials/planetTextures/Jungle/Jungle_01-512x512.png",
+ "/assets/materials/planetTextures/Jungle/Jungle_02-512x512.png",
+ "/assets/materials/planetTextures/Jungle/Jungle_03-512x512.png",
+ "/assets/materials/planetTextures/Jungle/Jungle_04-512x512.png",
+ "/assets/materials/planetTextures/Jungle/Jungle_05-512x512.png",
// Marshy planets (5 textures)
- "/planetTextures/Marshy/Marshy_01-512x512.png",
- "/planetTextures/Marshy/Marshy_02-512x512.png",
- "/planetTextures/Marshy/Marshy_03-512x512.png",
- "/planetTextures/Marshy/Marshy_04-512x512.png",
- "/planetTextures/Marshy/Marshy_05-512x512.png",
+ "/assets/materials/planetTextures/Marshy/Marshy_01-512x512.png",
+ "/assets/materials/planetTextures/Marshy/Marshy_02-512x512.png",
+ "/assets/materials/planetTextures/Marshy/Marshy_03-512x512.png",
+ "/assets/materials/planetTextures/Marshy/Marshy_04-512x512.png",
+ "/assets/materials/planetTextures/Marshy/Marshy_05-512x512.png",
// Martian planets (5 textures)
- "/planetTextures/Martian/Martian_01-512x512.png",
- "/planetTextures/Martian/Martian_02-512x512.png",
- "/planetTextures/Martian/Martian_03-512x512.png",
- "/planetTextures/Martian/Martian_04-512x512.png",
- "/planetTextures/Martian/Martian_05-512x512.png",
+ "/assets/materials/planetTextures/Martian/Martian_01-512x512.png",
+ "/assets/materials/planetTextures/Martian/Martian_02-512x512.png",
+ "/assets/materials/planetTextures/Martian/Martian_03-512x512.png",
+ "/assets/materials/planetTextures/Martian/Martian_04-512x512.png",
+ "/assets/materials/planetTextures/Martian/Martian_05-512x512.png",
// Methane planets (5 textures)
- "/planetTextures/Methane/Methane_01-512x512.png",
- "/planetTextures/Methane/Methane_02-512x512.png",
- "/planetTextures/Methane/Methane_03-512x512.png",
- "/planetTextures/Methane/Methane_04-512x512.png",
- "/planetTextures/Methane/Methane_05-512x512.png",
+ "/assets/materials/planetTextures/Methane/Methane_01-512x512.png",
+ "/assets/materials/planetTextures/Methane/Methane_02-512x512.png",
+ "/assets/materials/planetTextures/Methane/Methane_03-512x512.png",
+ "/assets/materials/planetTextures/Methane/Methane_04-512x512.png",
+ "/assets/materials/planetTextures/Methane/Methane_05-512x512.png",
// Sandy planets (5 textures)
- "/planetTextures/Sandy/Sandy_01-512x512.png",
- "/planetTextures/Sandy/Sandy_02-512x512.png",
- "/planetTextures/Sandy/Sandy_03-512x512.png",
- "/planetTextures/Sandy/Sandy_04-512x512.png",
- "/planetTextures/Sandy/Sandy_05-512x512.png",
+ "/assets/materials/planetTextures/Sandy/Sandy_01-512x512.png",
+ "/assets/materials/planetTextures/Sandy/Sandy_02-512x512.png",
+ "/assets/materials/planetTextures/Sandy/Sandy_03-512x512.png",
+ "/assets/materials/planetTextures/Sandy/Sandy_04-512x512.png",
+ "/assets/materials/planetTextures/Sandy/Sandy_05-512x512.png",
// Snowy planets (5 textures)
- "/planetTextures/Snowy/Snowy_01-512x512.png",
- "/planetTextures/Snowy/Snowy_02-512x512.png",
- "/planetTextures/Snowy/Snowy_03-512x512.png",
- "/planetTextures/Snowy/Snowy_04-512x512.png",
- "/planetTextures/Snowy/Snowy_05-512x512.png",
+ "/assets/materials/planetTextures/Snowy/Snowy_01-512x512.png",
+ "/assets/materials/planetTextures/Snowy/Snowy_02-512x512.png",
+ "/assets/materials/planetTextures/Snowy/Snowy_03-512x512.png",
+ "/assets/materials/planetTextures/Snowy/Snowy_04-512x512.png",
+ "/assets/materials/planetTextures/Snowy/Snowy_05-512x512.png",
// Tundra planets (5 textures)
- "/planetTextures/Tundra/Tundra_01-512x512.png",
- "/planetTextures/Tundra/Tundra_02-512x512.png",
- "/planetTextures/Tundra/Tundra_03-512x512.png",
- "/planetTextures/Tundra/Tundra_04-512x512.png",
- "/planetTextures/Tundra/Tundral-EQUIRECTANGULAR-5-512x512.png",
+ "/assets/materials/planetTextures/Tundra/Tundra_01-512x512.png",
+ "/assets/materials/planetTextures/Tundra/Tundra_02-512x512.png",
+ "/assets/materials/planetTextures/Tundra/Tundra_03-512x512.png",
+ "/assets/materials/planetTextures/Tundra/Tundra_04-512x512.png",
+ "/assets/materials/planetTextures/Tundra/Tundral-EQUIRECTANGULAR-5-512x512.png",
];
/**
@@ -116,103 +116,103 @@ export function getRandomPlanetTexture(): string {
*/
export const PLANET_TEXTURES_BY_TYPE = {
arid: [
- "/planetTextures/Arid/Arid_01-512x512.png",
- "/planetTextures/Arid/Arid_02-512x512.png",
- "/planetTextures/Arid/Arid_03-512x512.png",
- "/planetTextures/Arid/Arid_04-512x512.png",
- "/planetTextures/Arid/Arid_05-512x512.png",
+ "/assets/materials/planetTextures/Arid/Arid_01-512x512.png",
+ "/assets/materials/planetTextures/Arid/Arid_02-512x512.png",
+ "/assets/materials/planetTextures/Arid/Arid_03-512x512.png",
+ "/assets/materials/planetTextures/Arid/Arid_04-512x512.png",
+ "/assets/materials/planetTextures/Arid/Arid_05-512x512.png",
],
barren: [
- "/planetTextures/Barren/Barren_01-512x512.png",
- "/planetTextures/Barren/Barren_02-512x512.png",
- "/planetTextures/Barren/Barren_03-512x512.png",
- "/planetTextures/Barren/Barren_04-512x512.png",
- "/planetTextures/Barren/Barren_05-512x512.png",
+ "/assets/materials/planetTextures/Barren/Barren_01-512x512.png",
+ "/assets/materials/planetTextures/Barren/Barren_02-512x512.png",
+ "/assets/materials/planetTextures/Barren/Barren_03-512x512.png",
+ "/assets/materials/planetTextures/Barren/Barren_04-512x512.png",
+ "/assets/materials/planetTextures/Barren/Barren_05-512x512.png",
],
dusty: [
- "/planetTextures/Dusty/Dusty_01-512x512.png",
- "/planetTextures/Dusty/Dusty_02-512x512.png",
- "/planetTextures/Dusty/Dusty_03-512x512.png",
- "/planetTextures/Dusty/Dusty_04-512x512.png",
- "/planetTextures/Dusty/Dusty_05-512x512.png",
+ "/assets/materials/planetTextures/Dusty/Dusty_01-512x512.png",
+ "/assets/materials/planetTextures/Dusty/Dusty_02-512x512.png",
+ "/assets/materials/planetTextures/Dusty/Dusty_03-512x512.png",
+ "/assets/materials/planetTextures/Dusty/Dusty_04-512x512.png",
+ "/assets/materials/planetTextures/Dusty/Dusty_05-512x512.png",
],
gaseous: [
- "/planetTextures/Gaseous/Gaseous_01-512x512.png",
- "/planetTextures/Gaseous/Gaseous_02-512x512.png",
- "/planetTextures/Gaseous/Gaseous_03-512x512.png",
- "/planetTextures/Gaseous/Gaseous_04-512x512.png",
- "/planetTextures/Gaseous/Gaseous_05-512x512.png",
- "/planetTextures/Gaseous/Gaseous_06-512x512.png",
- "/planetTextures/Gaseous/Gaseous_07-512x512.png",
- "/planetTextures/Gaseous/Gaseous_08-512x512.png",
- "/planetTextures/Gaseous/Gaseous_09-512x512.png",
- "/planetTextures/Gaseous/Gaseous_10-512x512.png",
- "/planetTextures/Gaseous/Gaseous_11-512x512.png",
- "/planetTextures/Gaseous/Gaseous_12-512x512.png",
- "/planetTextures/Gaseous/Gaseous_13-512x512.png",
- "/planetTextures/Gaseous/Gaseous_14-512x512.png",
- "/planetTextures/Gaseous/Gaseous_15-512x512.png",
- "/planetTextures/Gaseous/Gaseous_16-512x512.png",
- "/planetTextures/Gaseous/Gaseous_17-512x512.png",
- "/planetTextures/Gaseous/Gaseous_18-512x512.png",
- "/planetTextures/Gaseous/Gaseous_19-512x512.png",
- "/planetTextures/Gaseous/Gaseous_20-512x512.png",
+ "/assets/materials/planetTextures/Gaseous/Gaseous_01-512x512.png",
+ "/assets/materials/planetTextures/Gaseous/Gaseous_02-512x512.png",
+ "/assets/materials/planetTextures/Gaseous/Gaseous_03-512x512.png",
+ "/assets/materials/planetTextures/Gaseous/Gaseous_04-512x512.png",
+ "/assets/materials/planetTextures/Gaseous/Gaseous_05-512x512.png",
+ "/assets/materials/planetTextures/Gaseous/Gaseous_06-512x512.png",
+ "/assets/materials/planetTextures/Gaseous/Gaseous_07-512x512.png",
+ "/assets/materials/planetTextures/Gaseous/Gaseous_08-512x512.png",
+ "/assets/materials/planetTextures/Gaseous/Gaseous_09-512x512.png",
+ "/assets/materials/planetTextures/Gaseous/Gaseous_10-512x512.png",
+ "/assets/materials/planetTextures/Gaseous/Gaseous_11-512x512.png",
+ "/assets/materials/planetTextures/Gaseous/Gaseous_12-512x512.png",
+ "/assets/materials/planetTextures/Gaseous/Gaseous_13-512x512.png",
+ "/assets/materials/planetTextures/Gaseous/Gaseous_14-512x512.png",
+ "/assets/materials/planetTextures/Gaseous/Gaseous_15-512x512.png",
+ "/assets/materials/planetTextures/Gaseous/Gaseous_16-512x512.png",
+ "/assets/materials/planetTextures/Gaseous/Gaseous_17-512x512.png",
+ "/assets/materials/planetTextures/Gaseous/Gaseous_18-512x512.png",
+ "/assets/materials/planetTextures/Gaseous/Gaseous_19-512x512.png",
+ "/assets/materials/planetTextures/Gaseous/Gaseous_20-512x512.png",
],
grassland: [
- "/planetTextures/Grassland/Grassland_01-512x512.png",
- "/planetTextures/Grassland/Grassland_02-512x512.png",
- "/planetTextures/Grassland/Grassland_03-512x512.png",
- "/planetTextures/Grassland/Grassland_04-512x512.png",
- "/planetTextures/Grassland/Grassland_05-512x512.png",
+ "/assets/materials/planetTextures/Grassland/Grassland_01-512x512.png",
+ "/assets/materials/planetTextures/Grassland/Grassland_02-512x512.png",
+ "/assets/materials/planetTextures/Grassland/Grassland_03-512x512.png",
+ "/assets/materials/planetTextures/Grassland/Grassland_04-512x512.png",
+ "/assets/materials/planetTextures/Grassland/Grassland_05-512x512.png",
],
jungle: [
- "/planetTextures/Jungle/Jungle_01-512x512.png",
- "/planetTextures/Jungle/Jungle_02-512x512.png",
- "/planetTextures/Jungle/Jungle_03-512x512.png",
- "/planetTextures/Jungle/Jungle_04-512x512.png",
- "/planetTextures/Jungle/Jungle_05-512x512.png",
+ "/assets/materials/planetTextures/Jungle/Jungle_01-512x512.png",
+ "/assets/materials/planetTextures/Jungle/Jungle_02-512x512.png",
+ "/assets/materials/planetTextures/Jungle/Jungle_03-512x512.png",
+ "/assets/materials/planetTextures/Jungle/Jungle_04-512x512.png",
+ "/assets/materials/planetTextures/Jungle/Jungle_05-512x512.png",
],
marshy: [
- "/planetTextures/Marshy/Marshy_01-512x512.png",
- "/planetTextures/Marshy/Marshy_02-512x512.png",
- "/planetTextures/Marshy/Marshy_03-512x512.png",
- "/planetTextures/Marshy/Marshy_04-512x512.png",
- "/planetTextures/Marshy/Marshy_05-512x512.png",
+ "/assets/materials/planetTextures/Marshy/Marshy_01-512x512.png",
+ "/assets/materials/planetTextures/Marshy/Marshy_02-512x512.png",
+ "/assets/materials/planetTextures/Marshy/Marshy_03-512x512.png",
+ "/assets/materials/planetTextures/Marshy/Marshy_04-512x512.png",
+ "/assets/materials/planetTextures/Marshy/Marshy_05-512x512.png",
],
martian: [
- "/planetTextures/Martian/Martian_01-512x512.png",
- "/planetTextures/Martian/Martian_02-512x512.png",
- "/planetTextures/Martian/Martian_03-512x512.png",
- "/planetTextures/Martian/Martian_04-512x512.png",
- "/planetTextures/Martian/Martian_05-512x512.png",
+ "/assets/materials/planetTextures/Martian/Martian_01-512x512.png",
+ "/assets/materials/planetTextures/Martian/Martian_02-512x512.png",
+ "/assets/materials/planetTextures/Martian/Martian_03-512x512.png",
+ "/assets/materials/planetTextures/Martian/Martian_04-512x512.png",
+ "/assets/materials/planetTextures/Martian/Martian_05-512x512.png",
],
methane: [
- "/planetTextures/Methane/Methane_01-512x512.png",
- "/planetTextures/Methane/Methane_02-512x512.png",
- "/planetTextures/Methane/Methane_03-512x512.png",
- "/planetTextures/Methane/Methane_04-512x512.png",
- "/planetTextures/Methane/Methane_05-512x512.png",
+ "/assets/materials/planetTextures/Methane/Methane_01-512x512.png",
+ "/assets/materials/planetTextures/Methane/Methane_02-512x512.png",
+ "/assets/materials/planetTextures/Methane/Methane_03-512x512.png",
+ "/assets/materials/planetTextures/Methane/Methane_04-512x512.png",
+ "/assets/materials/planetTextures/Methane/Methane_05-512x512.png",
],
sandy: [
- "/planetTextures/Sandy/Sandy_01-512x512.png",
- "/planetTextures/Sandy/Sandy_02-512x512.png",
- "/planetTextures/Sandy/Sandy_03-512x512.png",
- "/planetTextures/Sandy/Sandy_04-512x512.png",
- "/planetTextures/Sandy/Sandy_05-512x512.png",
+ "/assets/materials/planetTextures/Sandy/Sandy_01-512x512.png",
+ "/assets/materials/planetTextures/Sandy/Sandy_02-512x512.png",
+ "/assets/materials/planetTextures/Sandy/Sandy_03-512x512.png",
+ "/assets/materials/planetTextures/Sandy/Sandy_04-512x512.png",
+ "/assets/materials/planetTextures/Sandy/Sandy_05-512x512.png",
],
snowy: [
- "/planetTextures/Snowy/Snowy_01-512x512.png",
- "/planetTextures/Snowy/Snowy_02-512x512.png",
- "/planetTextures/Snowy/Snowy_03-512x512.png",
- "/planetTextures/Snowy/Snowy_04-512x512.png",
- "/planetTextures/Snowy/Snowy_05-512x512.png",
+ "/assets/materials/planetTextures/Snowy/Snowy_01-512x512.png",
+ "/assets/materials/planetTextures/Snowy/Snowy_02-512x512.png",
+ "/assets/materials/planetTextures/Snowy/Snowy_03-512x512.png",
+ "/assets/materials/planetTextures/Snowy/Snowy_04-512x512.png",
+ "/assets/materials/planetTextures/Snowy/Snowy_05-512x512.png",
],
tundra: [
- "/planetTextures/Tundra/Tundra_01-512x512.png",
- "/planetTextures/Tundra/Tundra_02-512x512.png",
- "/planetTextures/Tundra/Tundra_03-512x512.png",
- "/planetTextures/Tundra/Tundra_04-512x512.png",
- "/planetTextures/Tundra/Tundral-EQUIRECTANGULAR-5-512x512.png",
+ "/assets/materials/planetTextures/Tundra/Tundra_01-512x512.png",
+ "/assets/materials/planetTextures/Tundra/Tundra_02-512x512.png",
+ "/assets/materials/planetTextures/Tundra/Tundra_03-512x512.png",
+ "/assets/materials/planetTextures/Tundra/Tundra_04-512x512.png",
+ "/assets/materials/planetTextures/Tundra/Tundral-EQUIRECTANGULAR-5-512x512.png",
],
};
diff --git a/src/progression.ts b/src/progression.ts
new file mode 100644
index 0000000..bc75997
--- /dev/null
+++ b/src/progression.ts
@@ -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
;
+ 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(
+ 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 };
+ }
+}
diff --git a/src/rockFactory.ts b/src/rockFactory.ts
index 34e771d..052cefb 100644
--- a/src/rockFactory.ts
+++ b/src/rockFactory.ts
@@ -98,12 +98,12 @@ export class RockFactory {
debugLog(this._asteroidMesh);
}
- public static async createRock(i: number, position: Vector3, size: Vector3,
+ public static async createRock(i: number, position: Vector3, scale: number,
linearVelocitry: Vector3, angularVelocity: Vector3, score: Observable): Promise {
const rock = new InstancedMesh("asteroid-" +i, this._asteroidMesh as Mesh);
debugLog(rock.id);
- rock.scaling = size;
+ rock.scaling = new Vector3(scale, scale, scale);
rock.position = position;
//rock.material = this._rockMaterial;
rock.name = "asteroid-" + i;
diff --git a/src/ship.ts b/src/ship.ts
index db5f1fa..2f8e125 100644
--- a/src/ship.ts
+++ b/src/ship.ts
@@ -314,7 +314,8 @@ export class Ship {
this._gameStats,
() => this.handleReplayRequest(),
() => this.handleExitVR(),
- () => this.handleResume()
+ () => this.handleResume(),
+ () => this.handleNextLevel()
);
this._statusScreen.initialize(this._camera);
}
@@ -345,6 +346,16 @@ export class Ship {
this._controllerInput?.setEnabled(true);
}
+ /**
+ * Handle next level button click from status screen
+ */
+ private handleNextLevel(): void {
+ debugLog('Next Level button clicked - navigating to level selector');
+ // Navigate back to level selector (root route)
+ window.location.hash = '#/';
+ window.location.reload();
+ }
+
/**
* Check game-ending conditions and auto-show status screen
* Conditions:
@@ -375,7 +386,7 @@ export class Ship {
// Check condition 1: Death by hull damage (outside landing zone)
if (!this._isInLandingZone && hull < 0.01) {
debugLog('Game end condition met: Hull critical outside landing zone');
- this._statusScreen.show(true);
+ this._statusScreen.show(true, false); // Game ended, not victory
this._keyboardInput?.setEnabled(false);
this._controllerInput?.setEnabled(false);
this._statusScreenAutoShown = true;
@@ -385,7 +396,7 @@ export class Ship {
// Check condition 2: Stranded (outside landing zone, no fuel, low velocity)
if (!this._isInLandingZone && fuel < 0.01 && totalVelocity < 1) {
debugLog('Game end condition met: Stranded (no fuel, low velocity)');
- this._statusScreen.show(true);
+ this._statusScreen.show(true, false); // Game ended, not victory
this._keyboardInput?.setEnabled(false);
this._controllerInput?.setEnabled(false);
this._statusScreenAutoShown = true;
@@ -395,7 +406,7 @@ export class Ship {
// Check condition 3: Victory (all asteroids destroyed, inside landing zone)
if (asteroidsRemaining <= 0 && this._isInLandingZone) {
debugLog('Game end condition met: Victory (all asteroids destroyed)');
- this._statusScreen.show(true);
+ this._statusScreen.show(true, true); // Game ended, VICTORY!
this._keyboardInput?.setEnabled(false);
this._controllerInput?.setEnabled(false);
this._statusScreenAutoShown = true;
diff --git a/src/starBase.ts b/src/starBase.ts
index 4ec5850..98ac583 100644
--- a/src/starBase.ts
+++ b/src/starBase.ts
@@ -10,6 +10,7 @@ import {DefaultScene} from "./defaultScene";
import {GameConfig} from "./gameConfig";
import debugLog from "./debug";
import loadAsset from "./utils/loadAsset";
+import {Vector3Array} from "./levelConfig";
export interface StarBaseResult {
baseMesh: AbstractMesh;
@@ -18,18 +19,30 @@ export interface StarBaseResult {
/**
* Create and load the star base mesh
- * @param position - Position for the star base
+ * @param position - Position for the star base (defaults to [0, 0, 0])
+ * @param baseGlbPath - Path to the base GLB file (defaults to 'base.glb')
* @returns Promise resolving to the loaded star base mesh and landing aggregate
*/
export default class StarBase {
- public static async buildStarBase(): Promise {
+ public static async buildStarBase(position?: Vector3Array, baseGlbPath: string = 'base.glb'): Promise {
const config = GameConfig.getInstance();
const scene = DefaultScene.MainScene;
- const importMeshes = await loadAsset('base.glb');
+ const importMeshes = await loadAsset(baseGlbPath);
const baseMesh = importMeshes.meshes.get('Base');
const landingMesh = importMeshes.meshes.get('BaseLandingZone');
+ // Store the GLB path in metadata for serialization
+ if (baseMesh) {
+ baseMesh.metadata = baseMesh.metadata || {};
+ baseMesh.metadata.baseGlbPath = baseGlbPath;
+ }
+
+ // Apply position to both meshes (defaults to [0, 0, 0])
+ const pos = position ? new Vector3(position[0], position[1], position[2]) : new Vector3(0, 0, 0);
+ baseMesh.position = pos.clone();
+ landingMesh.position = pos.clone();
+
let landingAgg: PhysicsAggregate | null = null;
if (config.physicsEnabled) {
diff --git a/src/statusScreen.ts b/src/statusScreen.ts
index d98ad9c..4da5217 100644
--- a/src/statusScreen.ts
+++ b/src/statusScreen.ts
@@ -16,6 +16,7 @@ import {
} from "@babylonjs/core";
import { GameStats } from "./gameStats";
import { DefaultScene } from "./defaultScene";
+import { ProgressionManager } from "./progression";
/**
* Status screen that displays game statistics
@@ -41,21 +42,27 @@ export class StatusScreen {
private _replayButton: Button;
private _exitButton: Button;
private _resumeButton: Button;
+ private _nextLevelButton: Button;
// Callbacks
private _onReplayCallback: (() => void) | null = null;
private _onExitCallback: (() => void) | null = null;
private _onResumeCallback: (() => void) | null = null;
+ private _onNextLevelCallback: (() => void) | null = null;
// Track whether game has ended
private _isGameEnded: boolean = false;
- constructor(scene: Scene, gameStats: GameStats, onReplay?: () => void, onExit?: () => void, onResume?: () => void) {
+ // Track current level name for progression
+ private _currentLevelName: string | null = null;
+
+ constructor(scene: Scene, gameStats: GameStats, onReplay?: () => void, onExit?: () => void, onResume?: () => void, onNextLevel?: () => void) {
this._scene = scene;
this._gameStats = gameStats;
this._onReplayCallback = onReplay || null;
this._onExitCallback = onExit || null;
this._onResumeCallback = onResume || null;
+ this._onNextLevelCallback = onNextLevel || null;
}
/**
@@ -154,8 +161,25 @@ export class StatusScreen {
});
buttonBar.addControl(this._resumeButton);
+ // Create Next Level button (only shown when game has ended and there's a next level)
+ this._nextLevelButton = Button.CreateSimpleButton("nextLevelButton", "NEXT LEVEL");
+ this._nextLevelButton.width = "300px";
+ this._nextLevelButton.height = "60px";
+ this._nextLevelButton.color = "white";
+ this._nextLevelButton.background = "#0088ff";
+ this._nextLevelButton.cornerRadius = 10;
+ this._nextLevelButton.thickness = 0;
+ this._nextLevelButton.fontSize = "30px";
+ this._nextLevelButton.fontWeight = "bold";
+ this._nextLevelButton.onPointerClickObservable.add(() => {
+ if (this._onNextLevelCallback) {
+ this._onNextLevelCallback();
+ }
+ });
+ buttonBar.addControl(this._nextLevelButton);
+
// Create Replay button (only shown when game has ended)
- this._replayButton = Button.CreateSimpleButton("replayButton", "REPLAY LEVEL");
+ this._replayButton = Button.CreateSimpleButton("replayButton", "REPLAY");
this._replayButton.width = "300px";
this._replayButton.height = "60px";
this._replayButton.color = "white";
@@ -279,11 +303,19 @@ export class StatusScreen {
}
}
+ /**
+ * Set the current level name for progression tracking
+ */
+ public setCurrentLevel(levelName: string): void {
+ this._currentLevelName = levelName;
+ }
+
/**
* Show the status screen
* @param isGameEnded - true if game has ended (death/stranded/victory), false if manually paused
+ * @param victory - true if the level was completed successfully
*/
- public show(isGameEnded: boolean = false): void {
+ public show(isGameEnded: boolean = false, victory: boolean = false): void {
if (!this._screenMesh) {
return;
}
@@ -291,6 +323,21 @@ export class StatusScreen {
// Store game ended state
this._isGameEnded = isGameEnded;
+ // Mark level as complete if victory and we have a level name
+ const progression = ProgressionManager.getInstance();
+ if (victory && this._currentLevelName) {
+ const stats = this._gameStats.getStats();
+ const gameTimeSeconds = this.parseGameTime(stats.gameTime);
+ progression.markLevelComplete(this._currentLevelName, {
+ completionTime: gameTimeSeconds,
+ accuracy: stats.accuracy // Already a number from getAccuracy()
+ });
+ }
+
+ // Determine if there's a next level
+ const nextLevel = progression.getNextLevel();
+ const hasNextLevel = nextLevel !== null;
+
// Show/hide appropriate buttons based on whether game has ended
if (this._resumeButton) {
this._resumeButton.isVisible = !isGameEnded;
@@ -298,6 +345,10 @@ export class StatusScreen {
if (this._replayButton) {
this._replayButton.isVisible = isGameEnded;
}
+ if (this._nextLevelButton) {
+ // Only show Next Level if game ended in victory and there's a next level
+ this._nextLevelButton.isVisible = isGameEnded && victory && hasNextLevel;
+ }
// Enable pointer selection for button interaction
this.enablePointerSelection();
@@ -310,6 +361,19 @@ export class StatusScreen {
this._isVisible = true;
}
+ /**
+ * Parse game time string (MM:SS) to seconds
+ */
+ private parseGameTime(timeString: string): number {
+ const parts = timeString.split(':');
+ if (parts.length === 2) {
+ const minutes = parseInt(parts[0], 10);
+ const seconds = parseInt(parts[1], 10);
+ return minutes * 60 + seconds;
+ }
+ return 0;
+ }
+
/**
* Hide the status screen
*/