space-game/scripts/generateDefaultLevels.cjs
Michael Mainguy 244a25fff5
All checks were successful
Build / build (push) Successful in 1m34s
Implement hybrid level storage system with JSON-based defaults and configurable orbit constraints
Major changes:
- Add LevelRegistry for managing default (JSON) and custom (localStorage) levels
- Default levels now load from /public/levels/*.json files
- Add 6 default level JSON files (rookie-training through final-challenge)
- Implement version-based automatic cache invalidation
- Add LevelVersionManager for tracking level updates
- Add LevelStatsManager for performance tracking (completion rate, best time, etc.)
- Add legacy migration tool for existing localStorage data
- Update level selector UI with stats display and version badges
- Add configurable orbit constraints per level (useOrbitConstraints flag)
- Hide copy button in level selector UI (TODO: re-enable later)
- Add extensive debug logging for velocity troubleshooting
- Add cloud sync infrastructure interfaces (future-ready)

Technical improvements:
- Hybrid storage: immutable defaults from JSON, editable custom levels in localStorage
- Automatic cache refresh when directory.json version changes
- Cache API for offline support
- Fresh start migration approach with export option
- Level loading now initializes before router starts

Physics configuration:
- Add useOrbitConstraints flag to LevelConfig
- Rookietraining.json uses constraints (velocities will create orbital motion)
- Debug logging added to verify velocity application

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-11 18:40:01 -06:00

216 lines
6.3 KiB
JavaScript

#!/usr/bin/env node
/**
* Script to generate default level JSON files
* Run with: node scripts/generateDefaultLevels.js
*/
const fs = require('fs');
const path = require('path');
// Helper function to generate random asteroid data
function generateAsteroid(id, config, shipPos = [0, 1, 0]) {
const { distanceMin, distanceMax, rockSizeMin, rockSizeMax, forceMultiplier } = config;
// Random spherical distribution
const theta = Math.random() * Math.PI * 2; // Azimuth angle
const phi = Math.acos(2 * Math.random() - 1); // Polar angle
const distance = distanceMin + Math.random() * (distanceMax - distanceMin);
const position = [
shipPos[0] + distance * Math.sin(phi) * Math.cos(theta),
shipPos[1] + distance * Math.sin(phi) * Math.sin(theta),
shipPos[2] + distance * Math.cos(phi)
];
const scale = rockSizeMin + Math.random() * (rockSizeMax - rockSizeMin);
// Random velocity toward ship
const speedMin = 15 * forceMultiplier;
const speedMax = 30 * forceMultiplier;
const speed = speedMin + Math.random() * (speedMax - speedMin);
const dirToShip = [
shipPos[0] - position[0],
shipPos[1] - position[1],
shipPos[2] - position[2]
];
const length = Math.sqrt(dirToShip[0]**2 + dirToShip[1]**2 + dirToShip[2]**2);
const normalized = dirToShip.map(v => v / length);
const linearVelocity = normalized.map(v => v * speed);
const angularVelocity = [
(Math.random() - 0.5) * 2,
(Math.random() - 0.5) * 2,
(Math.random() - 0.5) * 2
];
return {
id: `asteroid-${id}`,
position,
scale,
linearVelocity,
angularVelocity
};
}
// Level configurations matching LevelGenerator difficulty configs
const levels = [
{
filename: 'rookie-training.json',
difficulty: 'recruit',
difficultyConfig: {
rockCount: 5,
forceMultiplier: 0.8,
rockSizeMin: 10,
rockSizeMax: 15,
distanceMin: 220,
distanceMax: 250
},
metadata: {
author: 'System',
description: 'Learn the basics of ship control and asteroid destruction in a calm sector of space.',
estimatedTime: '3-5 minutes',
type: 'default'
}
},
{
filename: 'rescue-mission.json',
difficulty: 'pilot',
difficultyConfig: {
rockCount: 10,
forceMultiplier: 1.0,
rockSizeMin: 8,
rockSizeMax: 20,
distanceMin: 225,
distanceMax: 300
},
metadata: {
author: 'System',
description: 'Clear a path through moderate asteroid density to reach the stranded station.',
estimatedTime: '5-8 minutes',
type: 'default'
}
},
{
filename: 'deep-space-patrol.json',
difficulty: 'captain',
difficultyConfig: {
rockCount: 20,
forceMultiplier: 1.2,
rockSizeMin: 5,
rockSizeMax: 40,
distanceMin: 230,
distanceMax: 450
},
metadata: {
author: 'System',
description: 'Patrol a dangerous sector with heavy asteroid activity. Watch your fuel!',
estimatedTime: '8-12 minutes',
type: 'default'
}
},
{
filename: 'enemy-territory.json',
difficulty: 'commander',
difficultyConfig: {
rockCount: 50,
forceMultiplier: 1.3,
rockSizeMin: 2,
rockSizeMax: 8,
distanceMin: 90,
distanceMax: 280
},
metadata: {
author: 'System',
description: 'Navigate through hostile space with high-speed asteroids and limited resources.',
estimatedTime: '10-15 minutes',
type: 'default'
}
},
{
filename: 'the-gauntlet.json',
difficulty: 'commander',
difficultyConfig: {
rockCount: 50,
forceMultiplier: 1.3,
rockSizeMin: 2,
rockSizeMax: 8,
distanceMin: 90,
distanceMax: 280
},
metadata: {
author: 'System',
description: 'Face maximum asteroid density in this ultimate test of piloting skill.',
estimatedTime: '12-18 minutes',
type: 'default'
}
},
{
filename: 'final-challenge.json',
difficulty: 'commander',
difficultyConfig: {
rockCount: 50,
forceMultiplier: 1.3,
rockSizeMin: 2,
rockSizeMax: 8,
distanceMin: 90,
distanceMax: 280
},
metadata: {
author: 'System',
description: 'The ultimate challenge - survive the most chaotic asteroid field in known space.',
estimatedTime: '15-20 minutes',
type: 'default'
}
}
];
// Output directory
const outputDir = path.join(__dirname, '../public/levels');
// Ensure directory exists
if (!fs.existsSync(outputDir)) {
fs.mkdirSync(outputDir, { recursive: true });
}
// Generate each level
for (const level of levels) {
const asteroids = [];
for (let i = 0; i < level.difficultyConfig.rockCount; i++) {
asteroids.push(generateAsteroid(i, level.difficultyConfig));
}
const levelConfig = {
version: '1.0',
difficulty: level.difficulty,
timestamp: new Date().toISOString(),
metadata: level.metadata,
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: level.difficultyConfig
};
const outputPath = path.join(outputDir, level.filename);
fs.writeFileSync(outputPath, JSON.stringify(levelConfig, null, 2));
console.log(`Generated: ${level.filename} (${level.difficultyConfig.rockCount} asteroids)`);
}
console.log(`\nSuccessfully generated ${levels.length} default level files!`);