- Create centralized logger module (src/core/logger.ts) - Replace all debugLog() calls with log.debug() - Replace console.log() with log.info() - Replace console.warn() with log.warn() - Replace console.error() with log.error() - Delete deprecated src/core/debug.ts - Configure log levels: debug for dev, warn for production - Add localStorage override for production debugging 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
284 lines
9.6 KiB
TypeScript
284 lines
9.6 KiB
TypeScript
import {
|
|
AbstractMesh,
|
|
Color3,
|
|
MeshBuilder,
|
|
Observable,
|
|
PBRMaterial,
|
|
PhysicsAggregate,
|
|
Texture,
|
|
Vector3,
|
|
} from "@babylonjs/core";
|
|
import { DefaultScene } from "../../core/defaultScene";
|
|
import { RockFactory } from "../../environment/asteroids/rockFactory";
|
|
import { ScoreEvent } from "../../ui/hud/scoreboard";
|
|
import {
|
|
LevelConfig,
|
|
ShipConfig,
|
|
Vector3Array,
|
|
validateLevelConfig
|
|
} from "./levelConfig";
|
|
import { FireProceduralTexture } from "@babylonjs/procedural-textures";
|
|
import { createSphereLightmap } from "../../environment/celestial/sphereLightmap";
|
|
import log from '../../core/logger';
|
|
import StarBase from "../../environment/stations/starBase";
|
|
import {LevelRegistry} from "../storage/levelRegistry";
|
|
|
|
/**
|
|
* Deserializes a LevelConfig JSON object and creates all entities in the scene
|
|
*/
|
|
export class LevelDeserializer {
|
|
private scene = DefaultScene.MainScene;
|
|
private 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
|
|
const validation = validateLevelConfig(config);
|
|
if (!validation.valid) {
|
|
throw new Error(`Invalid level config: ${validation.errors.join(', ')}`);
|
|
}
|
|
|
|
this.config = config;
|
|
}
|
|
|
|
/**
|
|
* Create all entities from the configuration
|
|
* @param scoreObservable - Observable for score events
|
|
*/
|
|
public async deserialize(
|
|
scoreObservable: Observable<ScoreEvent>
|
|
): Promise<{
|
|
startBase: AbstractMesh | null;
|
|
landingAggregate: PhysicsAggregate | null;
|
|
sun: AbstractMesh;
|
|
planets: AbstractMesh[];
|
|
asteroids: AbstractMesh[];
|
|
}> {
|
|
log.debug('Deserializing level:', this.config.difficulty);
|
|
|
|
const baseResult = await this.createStartBase();
|
|
const sun = this.createSun();
|
|
const planets = this.createPlanets();
|
|
const asteroids = await this.createAsteroids(scoreObservable);
|
|
|
|
return {
|
|
startBase: baseResult.baseMesh,
|
|
landingAggregate: baseResult.landingAggregate,
|
|
sun,
|
|
planets,
|
|
asteroids
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Create the start base from config
|
|
*/
|
|
private async createStartBase() {
|
|
const position = this.config.startBase?.position;
|
|
const baseGlbPath = this.config.startBase?.baseGlbPath || 'base.glb';
|
|
return await StarBase.buildStarBase(position, baseGlbPath);
|
|
}
|
|
|
|
/**
|
|
* Create the sun from config
|
|
*/
|
|
private createSun(): AbstractMesh {
|
|
const config = this.config.sun;
|
|
const sun = MeshBuilder.CreateSphere("sun", {
|
|
diameter: config.diameter,
|
|
segments: 32
|
|
}, this.scene);
|
|
sun.position = this.arrayToVector3(config.position);
|
|
|
|
// Create PBR sun material with fire texture
|
|
const material = new PBRMaterial("sunMaterial", this.scene);
|
|
material.emissiveTexture = new FireProceduralTexture("fire", 1024, this.scene);
|
|
material.albedoColor = Color3.Black();
|
|
material.emissiveColor = Color3.White();
|
|
material.disableLighting = true;
|
|
//material.emissiveColor.set(0.5, 0.5, 0.1);
|
|
material.unlit = true;
|
|
sun.material = material;
|
|
|
|
return sun;
|
|
}
|
|
|
|
/**
|
|
* Create planets from config
|
|
*/
|
|
private createPlanets(): AbstractMesh[] {
|
|
const planets: AbstractMesh[] = [];
|
|
const sunPosition = this.arrayToVector3(this.config.sun.position);
|
|
|
|
for (const planetConfig of this.config.planets) {
|
|
// Use fewer segments for better performance - planets are background objects
|
|
// 16 segments = ~256 vertices vs 32 segments = ~1024 vertices
|
|
const planet = MeshBuilder.CreateSphere(planetConfig.name, {
|
|
diameter: planetConfig.diameter,
|
|
segments: 12 // Reduced from 32 for performance
|
|
}, this.scene);
|
|
|
|
const planetPosition = this.arrayToVector3(planetConfig.position);
|
|
planet.position = planetPosition;
|
|
|
|
// Calculate direction from planet to sun
|
|
const toSun = sunPosition.subtract(planetPosition).normalize();
|
|
|
|
// Create PBR planet material
|
|
const material = new PBRMaterial(planetConfig.name + "-material", this.scene);
|
|
const texture = new Texture(planetConfig.texturePath, this.scene);
|
|
|
|
// Create lightmap with bright light pointing toward sun
|
|
const lightmap = createSphereLightmap(
|
|
planetConfig.name + "-lightmap",
|
|
256,
|
|
this.scene,
|
|
toSun,
|
|
1,
|
|
toSun.negate(),
|
|
0.3,
|
|
0.3
|
|
);
|
|
|
|
material.albedoTexture = texture;
|
|
material.lightmapTexture = lightmap;
|
|
material.useLightmapAsShadowmap = true;
|
|
material.roughness = 0.8;
|
|
material.metallic = 0;
|
|
material.unlit = true;
|
|
planet.material = material;
|
|
|
|
planets.push(planet);
|
|
}
|
|
|
|
log.debug(`Created ${planets.length} planets from config`);
|
|
return planets;
|
|
}
|
|
|
|
/**
|
|
* Create asteroids from config
|
|
*/
|
|
private async createAsteroids(
|
|
scoreObservable: Observable<ScoreEvent>
|
|
): Promise<AbstractMesh[]> {
|
|
const asteroids: AbstractMesh[] = [];
|
|
|
|
for (let i = 0; i < this.config.asteroids.length; i++) {
|
|
const asteroidConfig = this.config.asteroids[i];
|
|
|
|
log.debug(`[LevelDeserializer] Creating asteroid ${i} (${asteroidConfig.id}):`);
|
|
log.debug(`[LevelDeserializer] Position: [${asteroidConfig.position.join(', ')}]`);
|
|
log.debug(`[LevelDeserializer] Scale: ${asteroidConfig.scale}`);
|
|
log.debug(`[LevelDeserializer] Linear velocity: [${asteroidConfig.linearVelocity.join(', ')}]`);
|
|
log.debug(`[LevelDeserializer] Angular velocity: [${asteroidConfig.angularVelocity.join(', ')}]`);
|
|
|
|
// Use orbit constraints by default (true if not specified)
|
|
const useOrbitConstraints = this.config.useOrbitConstraints !== false;
|
|
log.debug(`[LevelDeserializer] Use orbit constraints: ${useOrbitConstraints}`);
|
|
|
|
// Use RockFactory to create the asteroid
|
|
const _rock = await RockFactory.createRock(
|
|
i,
|
|
this.arrayToVector3(asteroidConfig.position),
|
|
asteroidConfig.scale,
|
|
this.arrayToVector3(asteroidConfig.linearVelocity),
|
|
this.arrayToVector3(asteroidConfig.angularVelocity),
|
|
scoreObservable,
|
|
useOrbitConstraints
|
|
);
|
|
|
|
// Get the actual mesh from the Rock object
|
|
// The Rock class wraps the mesh, need to access it via position getter
|
|
const mesh = this.scene.getMeshByName(asteroidConfig.id);
|
|
if (mesh) {
|
|
asteroids.push(mesh);
|
|
}
|
|
}
|
|
|
|
log.debug(`Created ${asteroids.length} asteroids from config`);
|
|
return asteroids;
|
|
}
|
|
|
|
/**
|
|
* Get ship configuration (for external use to position ship)
|
|
*/
|
|
public getShipConfig(): ShipConfig {
|
|
return this.config.ship;
|
|
}
|
|
|
|
/**
|
|
* Helper to convert array to Vector3
|
|
*/
|
|
private arrayToVector3(arr: Vector3Array): Vector3 {
|
|
return new Vector3(arr[0], arr[1], arr[2]);
|
|
}
|
|
|
|
/**
|
|
* Static helper to load from JSON string
|
|
*/
|
|
public static fromJSON(json: string): LevelDeserializer {
|
|
const config = JSON.parse(json) as LevelConfig;
|
|
return new LevelDeserializer(config);
|
|
}
|
|
|
|
/**
|
|
* Static helper to load from JSON file URL
|
|
*/
|
|
public static async fromURL(url: string): Promise<LevelDeserializer> {
|
|
const response = await fetch(url);
|
|
if (!response.ok) {
|
|
throw new Error(`Failed to load level config from ${url}: ${response.statusText}`);
|
|
}
|
|
const json = await response.text();
|
|
return LevelDeserializer.fromJSON(json);
|
|
}
|
|
|
|
/**
|
|
* Static helper to load from uploaded file
|
|
*/
|
|
public static async fromFile(file: File): Promise<LevelDeserializer> {
|
|
return new Promise((resolve, reject) => {
|
|
const reader = new FileReader();
|
|
reader.onload = (e) => {
|
|
try {
|
|
const json = e.target?.result as string;
|
|
resolve(LevelDeserializer.fromJSON(json));
|
|
} catch (error) {
|
|
reject(error);
|
|
}
|
|
};
|
|
reader.onerror = () => reject(new Error('Failed to read file'));
|
|
reader.readAsText(file);
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Static helper to load from Level Registry by ID
|
|
* This is the preferred method for loading both default and custom levels
|
|
*/
|
|
public static async fromRegistry(levelId: string): Promise<LevelDeserializer> {
|
|
const registry = LevelRegistry.getInstance();
|
|
|
|
// Ensure registry is initialized
|
|
if (!registry.isInitialized()) {
|
|
await registry.initialize();
|
|
}
|
|
|
|
// Get level config from registry (loads if not already loaded)
|
|
const config = await registry.getLevel(levelId);
|
|
|
|
if (!config) {
|
|
throw new Error(`Level not found in registry: ${levelId}`);
|
|
}
|
|
|
|
return new LevelDeserializer(config);
|
|
}
|
|
}
|