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>
264 lines
7.7 KiB
TypeScript
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
|
|
};
|
|
}
|