space-game/src/levels/config/levelSerializer.ts
Michael Mainguy 0dc3c9d68d
All checks were successful
Build / build (push) Successful in 1m20s
Restructure codebase into logical subdirectories
## Major Reorganization

Reorganized all 57 TypeScript files from flat src/ directory into logical subdirectories for improved maintainability and discoverability.

## New Directory Structure

```
src/
├── core/ (4 files)
│   └── Foundation modules: defaultScene, gameConfig, debug, router
│
├── ship/ (10 files)
│   ├── Ship coordination and subsystems
│   └── input/ - VR controller and keyboard input
│
├── levels/ (10 files)
│   ├── config/ - Level schema, serialization, deserialization
│   ├── generation/ - Level generator and editor
│   └── ui/ - Level selector
│
├── environment/ (11 files)
│   ├── asteroids/ - Rock factory and explosions
│   ├── celestial/ - Suns, planets, textures
│   ├── stations/ - Star base loading
│   └── background/ - Stars, mirror, radar
│
├── ui/ (9 files)
│   ├── hud/ - Scoreboard and status screen
│   ├── screens/ - Login, settings, preloader
│   └── widgets/ - Discord integration
│
├── replay/ (7 files)
│   ├── Replay system components
│   └── recording/ - Physics recording and storage
│
├── game/ (3 files)
│   └── Game systems: stats, progression, demo
│
├── services/ (2 files)
│   └── External integrations: auth, social
│
└── utils/ (5 files)
    └── Shared utilities and helpers
```

## Changes Made

### File Moves (57 files)
- Core modules: 4 files → core/
- Ship system: 10 files → ship/ + ship/input/
- Level system: 10 files → levels/ (+ 3 subdirs)
- Environment: 11 files → environment/ (+ 4 subdirs)
- UI components: 9 files → ui/ (+ 3 subdirs)
- Replay system: 7 files → replay/ + replay/recording/
- Game systems: 3 files → game/
- Services: 2 files → services/
- Utilities: 5 files → utils/

### Import Path Updates
- Updated ~200 import statements across all files
- Fixed relative paths based on new directory structure
- Fixed case-sensitive import issues (physicsRecorder, physicsStorage)
- Ensured consistent lowercase filenames for imports

## Benefits

1. **Easy Navigation** - Related code grouped together
2. **Clear Boundaries** - Logical separation of concerns
3. **Scalability** - Easy pattern for adding new features
4. **Discoverability** - Find ship code in /ship, levels in /levels, etc.
5. **Maintainability** - Isolated modules easier to update
6. **No Circular Dependencies** - Clean dependency graph maintained

## Testing

- All TypeScript compilation errors resolved
- Build succeeds with new structure
- Import paths verified and corrected
- Case-sensitivity issues fixed

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

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

487 lines
16 KiB
TypeScript

import { Vector3, Quaternion, Material, PBRMaterial, StandardMaterial, AbstractMesh, TransformNode } from "@babylonjs/core";
import { DefaultScene } from "../../core/defaultScene";
import {
LevelConfig,
ShipConfig,
StartBaseConfig,
SunConfig,
PlanetConfig,
AsteroidConfig,
Vector3Array,
QuaternionArray,
Color4Array,
MaterialConfig,
SceneNodeConfig
} from "./levelConfig";
import debugLog from '../../core/debug';
/**
* Serializes the current runtime state of a level to JSON configuration
*/
export class LevelSerializer {
private scene = DefaultScene.MainScene;
/**
* Serialize the current level state to a LevelConfig object
* @param difficulty - Difficulty level string
* @param includeFullScene - If true, serialize complete scene (materials, hierarchy, assets)
*/
public serialize(difficulty: string = 'custom', includeFullScene: boolean = true): LevelConfig {
const ship = this.serializeShip();
const startBase = this.serializeStartBase();
const sun = this.serializeSun();
const planets = this.serializePlanets();
const asteroids = this.serializeAsteroids();
const config: LevelConfig = {
version: "1.0",
difficulty,
timestamp: new Date().toISOString(),
metadata: {
generator: "LevelSerializer",
description: `Captured level state at ${new Date().toLocaleString()}`,
captureTime: performance.now(),
babylonVersion: "8.32.0"
},
ship,
startBase,
sun,
planets,
asteroids
};
// Include full scene serialization if requested
if (includeFullScene) {
config.materials = this.serializeMaterials();
config.sceneHierarchy = this.serializeSceneHierarchy();
config.assetReferences = this.serializeAssetReferences();
debugLog(`LevelSerializer: Serialized ${config.materials.length} materials, ${config.sceneHierarchy.length} scene nodes`);
}
return config;
}
/**
* Serialize ship state
*/
private serializeShip(): ShipConfig {
// Find the ship transform node
const shipNode = this.scene.getTransformNodeByName("ship");
if (!shipNode) {
console.warn("Ship not found, using default position");
return {
position: [0, 1, 0],
rotation: [0, 0, 0],
linearVelocity: [0, 0, 0],
angularVelocity: [0, 0, 0]
};
}
const position = this.vector3ToArray(shipNode.position);
const rotation = this.vector3ToArray(shipNode.rotation);
// Get physics body velocities if available
let linearVelocity: Vector3Array = [0, 0, 0];
let angularVelocity: Vector3Array = [0, 0, 0];
if (shipNode.physicsBody) {
linearVelocity = this.vector3ToArray(shipNode.physicsBody.getLinearVelocity());
angularVelocity = this.vector3ToArray(shipNode.physicsBody.getAngularVelocity());
}
return {
position,
rotation,
linearVelocity,
angularVelocity
};
}
/**
* Serialize start base state (position and GLB paths)
*/
private serializeStartBase(): StartBaseConfig {
const startBase = this.scene.getMeshByName("startBase");
if (!startBase) {
console.warn("Start base not found, using defaults");
return {
position: [0, 0, 0],
baseGlbPath: 'base.glb'
};
}
const position = this.vector3ToArray(startBase.position);
// Capture GLB path from metadata if available, otherwise use default
const baseGlbPath = startBase.metadata?.baseGlbPath || 'base.glb';
return {
position,
baseGlbPath
};
}
/**
* Serialize sun state
*/
private serializeSun(): SunConfig {
const sun = this.scene.getMeshByName("sun");
if (!sun) {
console.warn("Sun not found, using defaults");
return {
position: [0, 0, 400],
diameter: 50,
intensity: 1000000
};
}
const position = this.vector3ToArray(sun.position);
// Get diameter from scaling (assuming uniform scaling)
const diameter = 50; // Default from createSun
// Try to find the sun's light for intensity
let intensity = 1000000;
const sunLight = this.scene.getLightByName("light");
if (sunLight) {
intensity = sunLight.intensity;
}
return {
position,
diameter,
intensity
};
}
/**
* Serialize all planets
*/
private serializePlanets(): PlanetConfig[] {
const planets: PlanetConfig[] = [];
// Find all meshes that start with "planet-"
const planetMeshes = this.scene.meshes.filter(mesh =>
mesh.name.startsWith('planet-')
);
for (const mesh of planetMeshes) {
const position = this.vector3ToArray(mesh.position);
const rotation = this.vector3ToArray(mesh.rotation);
// Get diameter from bounding info
const boundingInfo = mesh.getBoundingInfo();
const diameter = boundingInfo.boundingSphere.radiusWorld * 2;
// Get texture path from material
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;
}
planets.push({
name: mesh.name,
position,
diameter,
texturePath,
rotation
});
}
return planets;
}
/**
* Serialize all asteroids
*/
private serializeAsteroids(): AsteroidConfig[] {
const asteroids: AsteroidConfig[] = [];
// Find all meshes that start with "asteroid-"
const asteroidMeshes = this.scene.meshes.filter(mesh =>
mesh.name.startsWith('asteroid-') && mesh.metadata?.type === 'asteroid'
);
for (const mesh of asteroidMeshes) {
const position = this.vector3ToArray(mesh.position);
// 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];
let angularVelocity: Vector3Array = [0, 0, 0];
let mass = 10000; // Default
if (mesh.physicsBody) {
linearVelocity = this.vector3ToArray(mesh.physicsBody.getLinearVelocity());
angularVelocity = this.vector3ToArray(mesh.physicsBody.getAngularVelocity());
mass = mesh.physicsBody.getMassProperties().mass;
}
asteroids.push({
id: mesh.name,
position,
scale,
linearVelocity,
angularVelocity,
mass
});
}
return asteroids;
}
/**
* Serialize all materials in the scene
*/
private serializeMaterials(): MaterialConfig[] {
const materials: MaterialConfig[] = [];
const seenIds = new Set<string>();
for (const material of this.scene.materials) {
// Skip duplicates
if (seenIds.has(material.id)) {
continue;
}
seenIds.add(material.id);
const materialConfig: MaterialConfig = {
id: material.id,
name: material.name,
type: "Basic",
alpha: material.alpha,
backFaceCulling: material.backFaceCulling
};
// Handle PBR materials
if (material instanceof PBRMaterial) {
materialConfig.type = "PBR";
if (material.albedoColor) {
materialConfig.albedoColor = [
material.albedoColor.r,
material.albedoColor.g,
material.albedoColor.b
];
}
materialConfig.metallic = material.metallic;
materialConfig.roughness = material.roughness;
if (material.emissiveColor) {
materialConfig.emissiveColor = [
material.emissiveColor.r,
material.emissiveColor.g,
material.emissiveColor.b
];
}
materialConfig.emissiveIntensity = material.emissiveIntensity;
// Capture texture references (not data)
materialConfig.textures = {};
if (material.albedoTexture) {
materialConfig.textures.albedo = material.albedoTexture.name;
}
if (material.bumpTexture) {
materialConfig.textures.normal = material.bumpTexture.name;
}
if (material.metallicTexture) {
materialConfig.textures.metallic = material.metallicTexture.name;
}
if (material.emissiveTexture) {
materialConfig.textures.emissive = material.emissiveTexture.name;
}
}
// Handle Standard materials
else if (material instanceof StandardMaterial) {
materialConfig.type = "Standard";
if (material.diffuseColor) {
materialConfig.albedoColor = [
material.diffuseColor.r,
material.diffuseColor.g,
material.diffuseColor.b
];
}
if (material.emissiveColor) {
materialConfig.emissiveColor = [
material.emissiveColor.r,
material.emissiveColor.g,
material.emissiveColor.b
];
}
}
materials.push(materialConfig);
}
return materials;
}
/**
* Serialize scene hierarchy (all transform nodes and meshes)
*/
private serializeSceneHierarchy(): SceneNodeConfig[] {
const nodes: SceneNodeConfig[] = [];
const seenIds = new Set<string>();
// Serialize all transform nodes
for (const node of this.scene.transformNodes) {
if (seenIds.has(node.id)) continue;
seenIds.add(node.id);
const nodeConfig: SceneNodeConfig = {
id: node.id,
name: node.name,
type: "TransformNode",
position: this.vector3ToArray(node.position),
rotation: this.vector3ToArray(node.rotation),
scaling: this.vector3ToArray(node.scaling),
isEnabled: node.isEnabled(),
metadata: node.metadata
};
// Capture quaternion if present
if (node.rotationQuaternion) {
nodeConfig.rotationQuaternion = this.quaternionToArray(node.rotationQuaternion);
}
// Capture parent reference
if (node.parent) {
nodeConfig.parentId = node.parent.id;
}
nodes.push(nodeConfig);
}
// Serialize all meshes
for (const mesh of this.scene.meshes) {
if (seenIds.has(mesh.id)) continue;
seenIds.add(mesh.id);
const nodeConfig: SceneNodeConfig = {
id: mesh.id,
name: mesh.name,
type: mesh.getClassName() === "InstancedMesh" ? "InstancedMesh" : "Mesh",
position: this.vector3ToArray(mesh.position),
rotation: this.vector3ToArray(mesh.rotation),
scaling: this.vector3ToArray(mesh.scaling),
isVisible: mesh.isVisible,
isEnabled: mesh.isEnabled(),
metadata: mesh.metadata
};
// Capture quaternion if present
if (mesh.rotationQuaternion) {
nodeConfig.rotationQuaternion = this.quaternionToArray(mesh.rotationQuaternion);
}
// Capture parent reference
if (mesh.parent) {
nodeConfig.parentId = mesh.parent.id;
}
// Capture material reference
if (mesh.material) {
nodeConfig.materialId = mesh.material.id;
}
// Determine asset reference from mesh source (use full paths)
if (mesh.metadata?.source) {
nodeConfig.assetReference = mesh.metadata.source;
} else if (mesh.name.includes("ship") || mesh.name.includes("Ship")) {
nodeConfig.assetReference = "assets/themes/default/models/ship.glb";
} else if (mesh.name.includes("asteroid") || mesh.name.includes("Asteroid")) {
nodeConfig.assetReference = "assets/themes/default/models/asteroid.glb";
} else if (mesh.name.includes("base") || mesh.name.includes("Base")) {
nodeConfig.assetReference = "assets/themes/default/models/base.glb";
}
nodes.push(nodeConfig);
}
return nodes;
}
/**
* Serialize asset references (mesh ID -> GLB file path)
*/
private serializeAssetReferences(): { [key: string]: string } {
const assetRefs: { [key: string]: string } = {};
// Map common mesh patterns to their source assets (use full paths as keys)
for (const mesh of this.scene.meshes) {
if (mesh.metadata?.source) {
assetRefs[mesh.id] = mesh.metadata.source;
} else if (mesh.name.toLowerCase().includes("ship")) {
assetRefs[mesh.id] = "assets/themes/default/models/ship.glb";
} else if (mesh.name.toLowerCase().includes("asteroid")) {
assetRefs[mesh.id] = "assets/themes/default/models/asteroid.glb";
} else if (mesh.name.toLowerCase().includes("base")) {
assetRefs[mesh.id] = "assets/themes/default/models/base.glb";
}
}
return assetRefs;
}
/**
* Helper to convert Vector3 to array
*/
private vector3ToArray(vector: Vector3): Vector3Array {
return [
parseFloat(vector.x.toFixed(3)),
parseFloat(vector.y.toFixed(3)),
parseFloat(vector.z.toFixed(3))
];
}
/**
* Helper to convert Quaternion to array
*/
private quaternionToArray(quat: Quaternion): QuaternionArray {
return [
parseFloat(quat.x.toFixed(4)),
parseFloat(quat.y.toFixed(4)),
parseFloat(quat.z.toFixed(4)),
parseFloat(quat.w.toFixed(4))
];
}
/**
* Export current level to JSON string
*/
public serializeToJSON(difficulty: string = 'custom'): string {
const config = this.serialize(difficulty);
return JSON.stringify(config, null, 2);
}
/**
* Download current level as JSON file
*/
public downloadJSON(difficulty: string = 'custom', filename?: string): void {
const json = this.serializeToJSON(difficulty);
const blob = new Blob([json], { type: 'application/json' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = filename || `level-captured-${difficulty}-${Date.now()}.json`;
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
URL.revokeObjectURL(url);
debugLog(`Downloaded level state: ${a.download}`);
}
/**
* Static helper to serialize and download current level
*/
public static export(difficulty: string = 'custom', filename?: string): void {
const serializer = new LevelSerializer();
serializer.downloadJSON(difficulty, filename);
}
}