space-game/src/levelConfig.ts
Michael Mainguy ccc1745ed2
All checks were successful
Build / build (push) Successful in 1m21s
Refactor asteroid scaling and reorganize assets
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>
2025-11-10 12:19:31 -06:00

264 lines
7.7 KiB
TypeScript

/**
* Level configuration schema for serializing and deserializing game levels
*/
/**
* 3D vector stored as array [x, y, z]
*/
export type Vector3Array = [number, number, number];
/**
* 4D quaternion stored as array [x, y, z, w]
*/
export type QuaternionArray = [number, number, number, number];
/**
* 4D color stored as array [r, g, b, a] (0-1 range)
*/
export type Color4Array = [number, number, number, number];
/**
* Material configuration for PBR materials
*/
export interface MaterialConfig {
id: string;
name: string;
type: "PBR" | "Standard" | "Basic";
albedoColor?: Vector3Array; // RGB color (Color3)
metallic?: number;
roughness?: number;
emissiveColor?: Vector3Array;
emissiveIntensity?: number;
alpha?: number;
backFaceCulling?: boolean;
textures?: {
albedo?: string; // Asset reference or data URL
normal?: string;
metallic?: string;
roughness?: string;
emissive?: string;
};
}
/**
* Scene hierarchy node (TransformNode or Mesh)
*/
export interface SceneNodeConfig {
id: string;
name: string;
type: "TransformNode" | "Mesh" | "InstancedMesh";
position: Vector3Array;
rotation?: Vector3Array;
rotationQuaternion?: QuaternionArray;
scaling?: Vector3Array;
parentId?: string; // Reference to parent node
materialId?: string; // Reference to material
assetReference?: string; // For meshes loaded from GLB
isVisible?: boolean;
isEnabled?: boolean;
metadata?: any;
}
/**
* Ship configuration
*/
export interface ShipConfig {
position: Vector3Array;
rotation?: Vector3Array;
linearVelocity?: Vector3Array;
angularVelocity?: Vector3Array;
}
/**
* Start base configuration (yellow cylinder where asteroids are constrained to)
* All fields optional to allow levels without start bases
*/
export interface StartBaseConfig {
position?: Vector3Array; // Defaults to [0, 0, 0] if not specified
baseGlbPath?: string; // Path to base GLB model (defaults to 'base.glb')
landingGlbPath?: string; // Path to landing zone GLB model (uses same file as base, different mesh name)
}
/**
* Sun configuration
*/
export interface SunConfig {
position: Vector3Array;
diameter: number;
intensity?: number; // Light intensity
}
/**
* Individual planet configuration
*/
export interface PlanetConfig {
name: string;
position: Vector3Array;
diameter: number;
texturePath: string;
rotation?: Vector3Array;
}
/**
* Individual asteroid configuration
*/
export interface AsteroidConfig {
id: string;
position: Vector3Array;
scale: number; // Uniform scale applied to all axes
linearVelocity: Vector3Array;
angularVelocity?: Vector3Array;
mass?: number;
}
/**
* Difficulty configuration settings
*/
export interface DifficultyConfig {
rockCount: number;
forceMultiplier: number;
rockSizeMin: number;
rockSizeMax: number;
distanceMin: number;
distanceMax: number;
}
/**
* Complete level configuration
*/
export interface LevelConfig {
version: string;
difficulty: string;
timestamp?: string; // ISO date string
metadata?: {
author?: string;
description?: string;
babylonVersion?: string;
captureTime?: number;
[key: string]: any;
};
ship: ShipConfig;
startBase?: StartBaseConfig;
sun: SunConfig;
planets: PlanetConfig[];
asteroids: AsteroidConfig[];
// Optional: include original difficulty config for reference
difficultyConfig?: DifficultyConfig;
// New fields for full scene serialization
materials?: MaterialConfig[];
sceneHierarchy?: SceneNodeConfig[];
assetReferences?: { [key: string]: string }; // mesh id -> asset path (e.g., "ship" -> "ship.glb")
}
/**
* Validation result
*/
export interface ValidationResult {
valid: boolean;
errors: string[];
}
/**
* Validates a level configuration object
*/
export function validateLevelConfig(config: any): ValidationResult {
const errors: string[] = [];
// Check version
if (!config.version || typeof config.version !== 'string') {
errors.push('Missing or invalid version field');
}
// Check difficulty
if (!config.difficulty || typeof config.difficulty !== 'string') {
errors.push('Missing or invalid difficulty field');
}
// Check ship
if (!config.ship) {
errors.push('Missing ship configuration');
} else {
if (!Array.isArray(config.ship.position) || config.ship.position.length !== 3) {
errors.push('Invalid ship.position - must be [x, y, z] array');
}
}
// Check startBase (optional)
if (config.startBase) {
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');
}
}
// Check sun
if (!config.sun) {
errors.push('Missing sun configuration');
} else {
if (!Array.isArray(config.sun.position) || config.sun.position.length !== 3) {
errors.push('Invalid sun.position - must be [x, y, z] array');
}
if (typeof config.sun.diameter !== 'number') {
errors.push('Invalid sun.diameter - must be a number');
}
}
// Check planets
if (!Array.isArray(config.planets)) {
errors.push('Missing or invalid planets array');
} else {
config.planets.forEach((planet: any, idx: number) => {
if (!planet.name || typeof planet.name !== 'string') {
errors.push(`Planet ${idx}: missing or invalid name`);
}
if (!Array.isArray(planet.position) || planet.position.length !== 3) {
errors.push(`Planet ${idx}: invalid position - must be [x, y, z] array`);
}
if (typeof planet.diameter !== 'number') {
errors.push(`Planet ${idx}: invalid diameter - must be a number`);
}
if (!planet.texturePath || typeof planet.texturePath !== 'string') {
errors.push(`Planet ${idx}: missing or invalid texturePath`);
}
});
}
// Check asteroids
if (!Array.isArray(config.asteroids)) {
errors.push('Missing or invalid asteroids array');
} else {
// HYBRID MIGRATION NOTE: If we need to support legacy localStorage data,
// add migration here before validation:
//
// config.asteroids = config.asteroids.map((a, i) => ({
// ...a,
// id: a.id || `asteroid-${i}`, // Auto-generate missing ids
// scale: a.scale || a.scaling?.[0] || a.size || 1 // Migrate from old formats
// }));
//
// This would auto-heal old data with "scaling" array or "size" property
config.asteroids.forEach((asteroid: any, idx: number) => {
if (!asteroid.id || typeof asteroid.id !== 'string') {
errors.push(`Asteroid ${idx}: missing or invalid id`);
}
if (!Array.isArray(asteroid.position) || asteroid.position.length !== 3) {
errors.push(`Asteroid ${idx}: invalid position - must be [x, y, z] array`);
}
if (typeof asteroid.scale !== 'number' || asteroid.scale <= 0) {
errors.push(`Asteroid ${idx}: invalid scale - must be a positive number`);
}
if (!Array.isArray(asteroid.linearVelocity) || asteroid.linearVelocity.length !== 3) {
errors.push(`Asteroid ${idx}: invalid linearVelocity - must be [x, y, z] array`);
}
});
}
return {
valid: errors.length === 0,
errors
};
}