Cleanup batch 1: Delete 11 unused source files

Removed files identified by knip as never imported:
- src/utils/scoreEvent.ts (duplicate type)
- src/components/shared/VectorInput.svelte
- src/levels/storage/ILevelStorageProvider.ts
- src/ship/shipEngine.ts
- src/levels/config/levelSerializer.ts
- src/levels/generation/levelEditor.ts
- src/levels/generation/levelGenerator.ts
- src/levels/stats/levelStats.ts
- src/ui/screens/controlsScreen.ts
- src/ui/screens/settingsScreen.ts
- src/environment/celestial/planetTextures.ts

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

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
Michael Mainguy 2025-11-28 17:41:02 -06:00
parent b4baa2beba
commit 8570c22a0c
12 changed files with 0 additions and 2909 deletions

BIN
public/HavokPhysics.wasm Normal file

Binary file not shown.

View File

@ -1,44 +0,0 @@
<script lang="ts">
import NumberInput from './NumberInput.svelte';
export let x: number = 0;
export let y: number = 0;
export let z: number = 0;
export let step: number = 1;
export let disabled: boolean = false;
</script>
<div class="vector-input">
<div class="vector-field">
<label>X</label>
<NumberInput bind:value={x} {step} {disabled} on:change />
</div>
<div class="vector-field">
<label>Y</label>
<NumberInput bind:value={y} {step} {disabled} on:change />
</div>
<div class="vector-field">
<label>Z</label>
<NumberInput bind:value={z} {step} {disabled} on:change />
</div>
</div>
<style>
.vector-input {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: var(--space-sm, 0.5rem);
}
.vector-field {
display: flex;
flex-direction: column;
gap: var(--space-xs, 0.25rem);
}
.vector-field label {
font-size: var(--font-size-sm, 0.875rem);
font-weight: 500;
color: var(--color-text-secondary, #666);
}
</style>

View File

@ -1,217 +0,0 @@
/**
* Planet texture paths for randomly generating planets
* All textures are 512x512 PNG files
*/
export const PLANET_TEXTURES = [
// Arid planets (5 textures)
"/assets/materials/planetTextures/Arid/Arid_01-512x512.png",
"/assets/materials/planetTextures/Arid/Arid_02-512x512.png",
"/assets/materials/planetTextures/Arid/Arid_03-512x512.png",
"/assets/materials/planetTextures/Arid/Arid_04-512x512.png",
"/assets/materials/planetTextures/Arid/Arid_05-512x512.png",
// Barren planets (5 textures)
"/assets/materials/planetTextures/Barren/Barren_01-512x512.png",
"/assets/materials/planetTextures/Barren/Barren_02-512x512.png",
"/assets/materials/planetTextures/Barren/Barren_03-512x512.png",
"/assets/materials/planetTextures/Barren/Barren_04-512x512.png",
"/assets/materials/planetTextures/Barren/Barren_05-512x512.png",
// Dusty planets (5 textures)
"/assets/materials/planetTextures/Dusty/Dusty_01-512x512.png",
"/assets/materials/planetTextures/Dusty/Dusty_02-512x512.png",
"/assets/materials/planetTextures/Dusty/Dusty_03-512x512.png",
"/assets/materials/planetTextures/Dusty/Dusty_04-512x512.png",
"/assets/materials/planetTextures/Dusty/Dusty_05-512x512.png",
// Gaseous planets (20 textures)
"/assets/materials/planetTextures/Gaseous/Gaseous_01-512x512.png",
"/assets/materials/planetTextures/Gaseous/Gaseous_02-512x512.png",
"/assets/materials/planetTextures/Gaseous/Gaseous_03-512x512.png",
"/assets/materials/planetTextures/Gaseous/Gaseous_04-512x512.png",
"/assets/materials/planetTextures/Gaseous/Gaseous_05-512x512.png",
"/assets/materials/planetTextures/Gaseous/Gaseous_06-512x512.png",
"/assets/materials/planetTextures/Gaseous/Gaseous_07-512x512.png",
"/assets/materials/planetTextures/Gaseous/Gaseous_08-512x512.png",
"/assets/materials/planetTextures/Gaseous/Gaseous_09-512x512.png",
"/assets/materials/planetTextures/Gaseous/Gaseous_10-512x512.png",
"/assets/materials/planetTextures/Gaseous/Gaseous_11-512x512.png",
"/assets/materials/planetTextures/Gaseous/Gaseous_12-512x512.png",
"/assets/materials/planetTextures/Gaseous/Gaseous_13-512x512.png",
"/assets/materials/planetTextures/Gaseous/Gaseous_14-512x512.png",
"/assets/materials/planetTextures/Gaseous/Gaseous_15-512x512.png",
"/assets/materials/planetTextures/Gaseous/Gaseous_16-512x512.png",
"/assets/materials/planetTextures/Gaseous/Gaseous_17-512x512.png",
"/assets/materials/planetTextures/Gaseous/Gaseous_18-512x512.png",
"/assets/materials/planetTextures/Gaseous/Gaseous_19-512x512.png",
"/assets/materials/planetTextures/Gaseous/Gaseous_20-512x512.png",
// Grassland planets (5 textures)
"/assets/materials/planetTextures/Grassland/Grassland_01-512x512.png",
"/assets/materials/planetTextures/Grassland/Grassland_02-512x512.png",
"/assets/materials/planetTextures/Grassland/Grassland_03-512x512.png",
"/assets/materials/planetTextures/Grassland/Grassland_04-512x512.png",
"/assets/materials/planetTextures/Grassland/Grassland_05-512x512.png",
// Jungle planets (5 textures)
"/assets/materials/planetTextures/Jungle/Jungle_01-512x512.png",
"/assets/materials/planetTextures/Jungle/Jungle_02-512x512.png",
"/assets/materials/planetTextures/Jungle/Jungle_03-512x512.png",
"/assets/materials/planetTextures/Jungle/Jungle_04-512x512.png",
"/assets/materials/planetTextures/Jungle/Jungle_05-512x512.png",
// Marshy planets (5 textures)
"/assets/materials/planetTextures/Marshy/Marshy_01-512x512.png",
"/assets/materials/planetTextures/Marshy/Marshy_02-512x512.png",
"/assets/materials/planetTextures/Marshy/Marshy_03-512x512.png",
"/assets/materials/planetTextures/Marshy/Marshy_04-512x512.png",
"/assets/materials/planetTextures/Marshy/Marshy_05-512x512.png",
// Martian planets (5 textures)
"/assets/materials/planetTextures/Martian/Martian_01-512x512.png",
"/assets/materials/planetTextures/Martian/Martian_02-512x512.png",
"/assets/materials/planetTextures/Martian/Martian_03-512x512.png",
"/assets/materials/planetTextures/Martian/Martian_04-512x512.png",
"/assets/materials/planetTextures/Martian/Martian_05-512x512.png",
// Methane planets (5 textures)
"/assets/materials/planetTextures/Methane/Methane_01-512x512.png",
"/assets/materials/planetTextures/Methane/Methane_02-512x512.png",
"/assets/materials/planetTextures/Methane/Methane_03-512x512.png",
"/assets/materials/planetTextures/Methane/Methane_04-512x512.png",
"/assets/materials/planetTextures/Methane/Methane_05-512x512.png",
// Sandy planets (5 textures)
"/assets/materials/planetTextures/Sandy/Sandy_01-512x512.png",
"/assets/materials/planetTextures/Sandy/Sandy_02-512x512.png",
"/assets/materials/planetTextures/Sandy/Sandy_03-512x512.png",
"/assets/materials/planetTextures/Sandy/Sandy_04-512x512.png",
"/assets/materials/planetTextures/Sandy/Sandy_05-512x512.png",
// Snowy planets (5 textures)
"/assets/materials/planetTextures/Snowy/Snowy_01-512x512.png",
"/assets/materials/planetTextures/Snowy/Snowy_02-512x512.png",
"/assets/materials/planetTextures/Snowy/Snowy_03-512x512.png",
"/assets/materials/planetTextures/Snowy/Snowy_04-512x512.png",
"/assets/materials/planetTextures/Snowy/Snowy_05-512x512.png",
// Tundra planets (5 textures)
"/assets/materials/planetTextures/Tundra/Tundra_01-512x512.png",
"/assets/materials/planetTextures/Tundra/Tundra_02-512x512.png",
"/assets/materials/planetTextures/Tundra/Tundra_03-512x512.png",
"/assets/materials/planetTextures/Tundra/Tundra_04-512x512.png",
"/assets/materials/planetTextures/Tundra/Tundral-EQUIRECTANGULAR-5-512x512.png",
];
/**
* Get a random planet texture path
*/
export function getRandomPlanetTexture(): string {
return PLANET_TEXTURES[Math.floor(Math.random() * PLANET_TEXTURES.length)];
}
/**
* Planet texture categories organized by type
*/
export const PLANET_TEXTURES_BY_TYPE = {
arid: [
"/assets/materials/planetTextures/Arid/Arid_01-512x512.png",
"/assets/materials/planetTextures/Arid/Arid_02-512x512.png",
"/assets/materials/planetTextures/Arid/Arid_03-512x512.png",
"/assets/materials/planetTextures/Arid/Arid_04-512x512.png",
"/assets/materials/planetTextures/Arid/Arid_05-512x512.png",
],
barren: [
"/assets/materials/planetTextures/Barren/Barren_01-512x512.png",
"/assets/materials/planetTextures/Barren/Barren_02-512x512.png",
"/assets/materials/planetTextures/Barren/Barren_03-512x512.png",
"/assets/materials/planetTextures/Barren/Barren_04-512x512.png",
"/assets/materials/planetTextures/Barren/Barren_05-512x512.png",
],
dusty: [
"/assets/materials/planetTextures/Dusty/Dusty_01-512x512.png",
"/assets/materials/planetTextures/Dusty/Dusty_02-512x512.png",
"/assets/materials/planetTextures/Dusty/Dusty_03-512x512.png",
"/assets/materials/planetTextures/Dusty/Dusty_04-512x512.png",
"/assets/materials/planetTextures/Dusty/Dusty_05-512x512.png",
],
gaseous: [
"/assets/materials/planetTextures/Gaseous/Gaseous_01-512x512.png",
"/assets/materials/planetTextures/Gaseous/Gaseous_02-512x512.png",
"/assets/materials/planetTextures/Gaseous/Gaseous_03-512x512.png",
"/assets/materials/planetTextures/Gaseous/Gaseous_04-512x512.png",
"/assets/materials/planetTextures/Gaseous/Gaseous_05-512x512.png",
"/assets/materials/planetTextures/Gaseous/Gaseous_06-512x512.png",
"/assets/materials/planetTextures/Gaseous/Gaseous_07-512x512.png",
"/assets/materials/planetTextures/Gaseous/Gaseous_08-512x512.png",
"/assets/materials/planetTextures/Gaseous/Gaseous_09-512x512.png",
"/assets/materials/planetTextures/Gaseous/Gaseous_10-512x512.png",
"/assets/materials/planetTextures/Gaseous/Gaseous_11-512x512.png",
"/assets/materials/planetTextures/Gaseous/Gaseous_12-512x512.png",
"/assets/materials/planetTextures/Gaseous/Gaseous_13-512x512.png",
"/assets/materials/planetTextures/Gaseous/Gaseous_14-512x512.png",
"/assets/materials/planetTextures/Gaseous/Gaseous_15-512x512.png",
"/assets/materials/planetTextures/Gaseous/Gaseous_16-512x512.png",
"/assets/materials/planetTextures/Gaseous/Gaseous_17-512x512.png",
"/assets/materials/planetTextures/Gaseous/Gaseous_18-512x512.png",
"/assets/materials/planetTextures/Gaseous/Gaseous_19-512x512.png",
"/assets/materials/planetTextures/Gaseous/Gaseous_20-512x512.png",
],
grassland: [
"/assets/materials/planetTextures/Grassland/Grassland_01-512x512.png",
"/assets/materials/planetTextures/Grassland/Grassland_02-512x512.png",
"/assets/materials/planetTextures/Grassland/Grassland_03-512x512.png",
"/assets/materials/planetTextures/Grassland/Grassland_04-512x512.png",
"/assets/materials/planetTextures/Grassland/Grassland_05-512x512.png",
],
jungle: [
"/assets/materials/planetTextures/Jungle/Jungle_01-512x512.png",
"/assets/materials/planetTextures/Jungle/Jungle_02-512x512.png",
"/assets/materials/planetTextures/Jungle/Jungle_03-512x512.png",
"/assets/materials/planetTextures/Jungle/Jungle_04-512x512.png",
"/assets/materials/planetTextures/Jungle/Jungle_05-512x512.png",
],
marshy: [
"/assets/materials/planetTextures/Marshy/Marshy_01-512x512.png",
"/assets/materials/planetTextures/Marshy/Marshy_02-512x512.png",
"/assets/materials/planetTextures/Marshy/Marshy_03-512x512.png",
"/assets/materials/planetTextures/Marshy/Marshy_04-512x512.png",
"/assets/materials/planetTextures/Marshy/Marshy_05-512x512.png",
],
martian: [
"/assets/materials/planetTextures/Martian/Martian_01-512x512.png",
"/assets/materials/planetTextures/Martian/Martian_02-512x512.png",
"/assets/materials/planetTextures/Martian/Martian_03-512x512.png",
"/assets/materials/planetTextures/Martian/Martian_04-512x512.png",
"/assets/materials/planetTextures/Martian/Martian_05-512x512.png",
],
methane: [
"/assets/materials/planetTextures/Methane/Methane_01-512x512.png",
"/assets/materials/planetTextures/Methane/Methane_02-512x512.png",
"/assets/materials/planetTextures/Methane/Methane_03-512x512.png",
"/assets/materials/planetTextures/Methane/Methane_04-512x512.png",
"/assets/materials/planetTextures/Methane/Methane_05-512x512.png",
],
sandy: [
"/assets/materials/planetTextures/Sandy/Sandy_01-512x512.png",
"/assets/materials/planetTextures/Sandy/Sandy_02-512x512.png",
"/assets/materials/planetTextures/Sandy/Sandy_03-512x512.png",
"/assets/materials/planetTextures/Sandy/Sandy_04-512x512.png",
"/assets/materials/planetTextures/Sandy/Sandy_05-512x512.png",
],
snowy: [
"/assets/materials/planetTextures/Snowy/Snowy_01-512x512.png",
"/assets/materials/planetTextures/Snowy/Snowy_02-512x512.png",
"/assets/materials/planetTextures/Snowy/Snowy_03-512x512.png",
"/assets/materials/planetTextures/Snowy/Snowy_04-512x512.png",
"/assets/materials/planetTextures/Snowy/Snowy_05-512x512.png",
],
tundra: [
"/assets/materials/planetTextures/Tundra/Tundra_01-512x512.png",
"/assets/materials/planetTextures/Tundra/Tundra_02-512x512.png",
"/assets/materials/planetTextures/Tundra/Tundra_03-512x512.png",
"/assets/materials/planetTextures/Tundra/Tundra_04-512x512.png",
"/assets/materials/planetTextures/Tundra/Tundral-EQUIRECTANGULAR-5-512x512.png",
],
};

View File

@ -1,486 +0,0 @@
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);
}
}

View File

@ -1,790 +0,0 @@
import { LevelGenerator } from "./levelGenerator";
import { LevelConfig, DifficultyConfig, validateLevelConfig, Vector3Array } from "../config/levelConfig";
import debugLog from '../../core/debug';
const STORAGE_KEY = 'space-game-levels';
/**
* Level Editor UI Controller
* Handles the level editor interface and configuration generation
*/
class LevelEditor {
private currentConfig: LevelConfig | null = null;
private savedLevels: Map<string, LevelConfig> = new Map();
constructor() {
this.loadSavedLevels();
this.setupEventListeners();
this.loadPreset('captain'); // Default to captain difficulty
this.renderSavedLevelsList();
}
private setupEventListeners() {
// Preset buttons
const presetButtons = document.querySelectorAll('.preset-btn');
presetButtons.forEach(btn => {
btn.addEventListener('click', (e) => {
const difficulty = (e.target as HTMLButtonElement).dataset.difficulty;
this.loadPreset(difficulty);
// Update active state
presetButtons.forEach(b => b.classList.remove('active'));
(e.target as HTMLElement).classList.add('active');
});
});
// Difficulty dropdown
const difficultySelect = document.getElementById('difficulty') as HTMLSelectElement;
difficultySelect.addEventListener('change', (e) => {
this.loadPreset((e.target as HTMLSelectElement).value);
});
// Generate button - now saves to localStorage
document.getElementById('generateBtn')?.addEventListener('click', () => {
this.generateLevel();
this.saveToLocalStorage();
});
// Download button
document.getElementById('downloadBtn')?.addEventListener('click', () => {
this.downloadJSON();
});
// Copy button
document.getElementById('copyBtn')?.addEventListener('click', () => {
this.copyToClipboard();
});
// Save edited JSON button
document.getElementById('saveEditedJsonBtn')?.addEventListener('click', () => {
this.saveEditedJSON();
});
// Validate JSON button
document.getElementById('validateJsonBtn')?.addEventListener('click', () => {
this.validateJSON();
});
}
/**
* Load saved levels from localStorage
*/
private loadSavedLevels(): void {
try {
const stored = localStorage.getItem(STORAGE_KEY);
if (stored) {
const levelsArray: [string, LevelConfig][] = JSON.parse(stored);
this.savedLevels = new Map(levelsArray);
debugLog(`Loaded ${this.savedLevels.size} saved levels from localStorage`);
}
} catch (error) {
console.error('Failed to load saved levels:', error);
this.savedLevels = new Map();
}
}
/**
* Save current level to localStorage
*/
private saveToLocalStorage(): void {
if (!this.currentConfig) {
alert('Please generate a level configuration first!');
return;
}
const levelName = (document.getElementById('levelName') as HTMLInputElement).value ||
`${this.currentConfig.difficulty}-${Date.now()}`;
// Save to map
this.savedLevels.set(levelName, this.currentConfig);
// Convert Map to array for storage
const levelsArray = Array.from(this.savedLevels.entries());
localStorage.setItem(STORAGE_KEY, JSON.stringify(levelsArray));
debugLog(`Saved level: ${levelName}`);
this.renderSavedLevelsList();
// Show feedback
const feedback = document.createElement('div');
feedback.textContent = `✓ Saved "${levelName}" to local storage`;
feedback.style.cssText = `
position: fixed;
top: 20px;
right: 20px;
background: #4CAF50;
color: white;
padding: 15px 25px;
border-radius: 5px;
box-shadow: 0 4px 6px rgba(0,0,0,0.3);
z-index: 10000;
animation: slideIn 0.3s ease-out;
`;
document.body.appendChild(feedback);
setTimeout(() => {
feedback.remove();
}, 3000);
}
/**
* Delete a saved level
*/
private deleteSavedLevel(levelName: string): void {
if (confirm(`Delete "${levelName}"?`)) {
this.savedLevels.delete(levelName);
const levelsArray = Array.from(this.savedLevels.entries());
localStorage.setItem(STORAGE_KEY, JSON.stringify(levelsArray));
this.renderSavedLevelsList();
debugLog(`Deleted level: ${levelName}`);
}
}
/**
* Load a saved level into the editor
*/
private loadSavedLevel(levelName: string): void {
const config = this.savedLevels.get(levelName);
if (!config) {
alert('Level not found!');
return;
}
this.currentConfig = config;
// Populate form with saved values
(document.getElementById('levelName') as HTMLInputElement).value = levelName;
(document.getElementById('difficulty') as HTMLSelectElement).value = config.difficulty;
if (config.metadata?.author) {
(document.getElementById('author') as HTMLInputElement).value = config.metadata.author;
}
if (config.metadata?.description) {
(document.getElementById('description') as HTMLInputElement).value = config.metadata.description;
}
// Ship
(document.getElementById('shipX') as HTMLInputElement).value = config.ship.position[0].toString();
(document.getElementById('shipY') as HTMLInputElement).value = config.ship.position[1].toString();
(document.getElementById('shipZ') as HTMLInputElement).value = config.ship.position[2].toString();
// Start base
(document.getElementById('baseX') as HTMLInputElement).value = config.startBase.position[0].toString();
(document.getElementById('baseY') as HTMLInputElement).value = config.startBase.position[1].toString();
(document.getElementById('baseZ') as HTMLInputElement).value = config.startBase.position[2].toString();
(document.getElementById('baseGlbPath') as HTMLInputElement).value = config.startBase.baseGlbPath || 'base.glb';
// Sun
(document.getElementById('sunX') as HTMLInputElement).value = config.sun.position[0].toString();
(document.getElementById('sunY') as HTMLInputElement).value = config.sun.position[1].toString();
(document.getElementById('sunZ') as HTMLInputElement).value = config.sun.position[2].toString();
(document.getElementById('sunDiameter') as HTMLInputElement).value = config.sun.diameter.toString();
// Planets
(document.getElementById('planetCount') as HTMLInputElement).value = config.planets.length.toString();
// Asteroids (use difficulty config if available)
if (config.difficultyConfig) {
(document.getElementById('asteroidCount') as HTMLInputElement).value = config.difficultyConfig.rockCount.toString();
(document.getElementById('forceMultiplier') as HTMLInputElement).value = config.difficultyConfig.forceMultiplier.toString();
(document.getElementById('asteroidMinSize') as HTMLInputElement).value = config.difficultyConfig.rockSizeMin.toString();
(document.getElementById('asteroidMaxSize') as HTMLInputElement).value = config.difficultyConfig.rockSizeMax.toString();
(document.getElementById('asteroidMinDist') as HTMLInputElement).value = config.difficultyConfig.distanceMin.toString();
(document.getElementById('asteroidMaxDist') as HTMLInputElement).value = config.difficultyConfig.distanceMax.toString();
}
// Display the JSON
this.displayJSON();
debugLog(`Loaded level: ${levelName}`);
}
/**
* Render the list of saved levels
*/
private renderSavedLevelsList(): void {
const container = document.getElementById('savedLevelsList');
if (!container) return;
if (this.savedLevels.size === 0) {
container.innerHTML = '<p style="color: #888; font-style: italic;">No saved levels yet. Generate a level to save it.</p>';
return;
}
let html = '<div style="display: grid; gap: 10px;">';
for (const [name, config] of this.savedLevels.entries()) {
const timestamp = config.timestamp ? new Date(config.timestamp).toLocaleString() : 'Unknown';
html += `
<div style="
background: rgba(255, 255, 255, 0.08);
border: 1px solid rgba(255, 255, 255, 0.2);
border-radius: 5px;
padding: 12px;
display: flex;
justify-content: space-between;
align-items: center;
">
<div style="flex: 1;">
<div style="font-weight: bold; color: #fff; margin-bottom: 4px;">${name}</div>
<div style="font-size: 0.85em; color: #aaa;">
${config.difficulty} ${config.asteroids.length} asteroids ${timestamp}
</div>
</div>
<div style="display: flex; gap: 8px;">
<button class="load-level-btn" data-level="${name}" style="
padding: 6px 12px;
background: #4CAF50;
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
font-size: 0.9em;
">Load</button>
<button class="delete-level-btn" data-level="${name}" style="
padding: 6px 12px;
background: #f44336;
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
font-size: 0.9em;
">Delete</button>
</div>
</div>
`;
}
html += '</div>';
container.innerHTML = html;
// Add event listeners to load/delete buttons
container.querySelectorAll('.load-level-btn').forEach(btn => {
btn.addEventListener('click', (e) => {
const levelName = (e.target as HTMLButtonElement).dataset.level;
if (levelName) this.loadSavedLevel(levelName);
});
});
container.querySelectorAll('.delete-level-btn').forEach(btn => {
btn.addEventListener('click', (e) => {
const levelName = (e.target as HTMLButtonElement).dataset.level;
if (levelName) this.deleteSavedLevel(levelName);
});
});
}
/**
* Load a difficulty preset into the form
*/
private loadPreset(difficulty: string) {
const difficultyConfig = this.getDifficultyConfig(difficulty);
// Update difficulty dropdown
(document.getElementById('difficulty') as HTMLSelectElement).value = difficulty;
// Update asteroid settings based on difficulty
(document.getElementById('asteroidCount') as HTMLInputElement).value = difficultyConfig.rockCount.toString();
(document.getElementById('forceMultiplier') as HTMLInputElement).value = difficultyConfig.forceMultiplier.toString();
(document.getElementById('asteroidMinSize') as HTMLInputElement).value = difficultyConfig.rockSizeMin.toString();
(document.getElementById('asteroidMaxSize') as HTMLInputElement).value = difficultyConfig.rockSizeMax.toString();
(document.getElementById('asteroidMinDist') as HTMLInputElement).value = difficultyConfig.distanceMin.toString();
(document.getElementById('asteroidMaxDist') as HTMLInputElement).value = difficultyConfig.distanceMax.toString();
}
/**
* Get difficulty configuration
*/
private getDifficultyConfig(difficulty: string): DifficultyConfig {
switch (difficulty) {
case 'recruit':
return {
rockCount: 5,
forceMultiplier: .5,
rockSizeMin: 10,
rockSizeMax: 15,
distanceMin: 80,
distanceMax: 100
};
case 'pilot':
return {
rockCount: 10,
forceMultiplier: 1,
rockSizeMin: 8,
rockSizeMax: 12,
distanceMin: 80,
distanceMax: 150
};
case 'captain':
return {
rockCount: 20,
forceMultiplier: 1.2,
rockSizeMin: 2,
rockSizeMax: 7,
distanceMin: 100,
distanceMax: 250
};
case 'commander':
return {
rockCount: 50,
forceMultiplier: 1.3,
rockSizeMin: 2,
rockSizeMax: 8,
distanceMin: 90,
distanceMax: 280
};
case 'test':
return {
rockCount: 100,
forceMultiplier: 0.3,
rockSizeMin: 8,
rockSizeMax: 15,
distanceMin: 150,
distanceMax: 200
};
default:
return {
rockCount: 5,
forceMultiplier: 1.0,
rockSizeMin: 4,
rockSizeMax: 8,
distanceMin: 170,
distanceMax: 220
};
}
}
/**
* Read form values and generate level configuration
*/
private generateLevel() {
const difficulty = (document.getElementById('difficulty') as HTMLSelectElement).value;
const levelName = (document.getElementById('levelName') as HTMLInputElement).value || difficulty;
const author = (document.getElementById('author') as HTMLInputElement).value;
const description = (document.getElementById('description') as HTMLInputElement).value;
// Create a custom generator with modified parameters
const generator = new CustomLevelGenerator(difficulty);
// Override ship position
generator.shipPosition = [
parseFloat((document.getElementById('shipX') as HTMLInputElement).value),
parseFloat((document.getElementById('shipY') as HTMLInputElement).value),
parseFloat((document.getElementById('shipZ') as HTMLInputElement).value)
];
// Note: startBase is no longer generated by default
// Override sun
generator.sunPosition = [
parseFloat((document.getElementById('sunX') as HTMLInputElement).value),
parseFloat((document.getElementById('sunY') as HTMLInputElement).value),
parseFloat((document.getElementById('sunZ') as HTMLInputElement).value)
];
generator.sunDiameter = parseFloat((document.getElementById('sunDiameter') as HTMLInputElement).value);
// Override planet generation params
generator.planetCount = parseInt((document.getElementById('planetCount') as HTMLInputElement).value);
generator.planetMinDiameter = parseFloat((document.getElementById('planetMinDiam') as HTMLInputElement).value);
generator.planetMaxDiameter = parseFloat((document.getElementById('planetMaxDiam') as HTMLInputElement).value);
generator.planetMinDistance = parseFloat((document.getElementById('planetMinDist') as HTMLInputElement).value);
generator.planetMaxDistance = parseFloat((document.getElementById('planetMaxDist') as HTMLInputElement).value);
// Override asteroid generation params
const customDifficulty: DifficultyConfig = {
rockCount: parseInt((document.getElementById('asteroidCount') as HTMLInputElement).value),
forceMultiplier: parseFloat((document.getElementById('forceMultiplier') as HTMLInputElement).value),
rockSizeMin: parseFloat((document.getElementById('asteroidMinSize') as HTMLInputElement).value),
rockSizeMax: parseFloat((document.getElementById('asteroidMaxSize') as HTMLInputElement).value),
distanceMin: parseFloat((document.getElementById('asteroidMinDist') as HTMLInputElement).value),
distanceMax: parseFloat((document.getElementById('asteroidMaxDist') as HTMLInputElement).value)
};
generator.setDifficultyConfig(customDifficulty);
// Generate the config
this.currentConfig = generator.generate();
// Add metadata
if (author) {
this.currentConfig.metadata = this.currentConfig.metadata || {};
this.currentConfig.metadata.author = author;
}
if (description) {
this.currentConfig.metadata = this.currentConfig.metadata || {};
this.currentConfig.metadata.description = description;
}
// Display the JSON
this.displayJSON();
}
/**
* Display generated JSON in the output section
*/
private displayJSON() {
if (!this.currentConfig) return;
const outputSection = document.getElementById('outputSection');
const jsonEditor = document.getElementById('jsonEditor') as HTMLTextAreaElement;
if (outputSection && jsonEditor) {
const jsonString = JSON.stringify(this.currentConfig, null, 2);
jsonEditor.value = jsonString;
outputSection.style.display = 'block';
// Scroll to output
outputSection.scrollIntoView({ behavior: 'smooth' });
}
}
/**
* Validate the JSON in the editor
*/
private validateJSON(): boolean {
const jsonEditor = document.getElementById('jsonEditor') as HTMLTextAreaElement;
const messageDiv = document.getElementById('jsonValidationMessage');
if (!jsonEditor || !messageDiv) return false;
try {
const json = jsonEditor.value;
const parsed = JSON.parse(json);
// Validate against schema
const validation = validateLevelConfig(parsed);
if (validation.valid) {
messageDiv.innerHTML = '<div style="color: #4CAF50; padding: 10px; background: rgba(76, 175, 80, 0.1); border-radius: 5px;">✓ JSON is valid!</div>';
return true;
} else {
messageDiv.innerHTML = `<div style="color: #f44336; padding: 10px; background: rgba(244, 67, 54, 0.1); border-radius: 5px;">
<strong>Validation Errors:</strong><br>
${validation.errors.map(e => `${e}`).join('<br>')}
</div>`;
return false;
}
} catch (error) {
messageDiv.innerHTML = `<div style="color: #f44336; padding: 10px; background: rgba(244, 67, 54, 0.1); border-radius: 5px;">
<strong>JSON Parse Error:</strong><br>
${error.message}
</div>`;
return false;
}
}
/**
* Save edited JSON from the editor
*/
private saveEditedJSON() {
const jsonEditor = document.getElementById('jsonEditor') as HTMLTextAreaElement;
const messageDiv = document.getElementById('jsonValidationMessage');
if (!jsonEditor) {
alert('JSON editor not found!');
return;
}
// First validate
if (!this.validateJSON()) {
messageDiv.innerHTML += '<div style="color: #ff9800; margin-top: 10px;">Please fix validation errors before saving.</div>';
return;
}
try {
const json = jsonEditor.value;
const config = JSON.parse(json) as LevelConfig;
// Update current config
this.currentConfig = config;
// Save to localStorage
this.saveToLocalStorage();
// Update message
messageDiv.innerHTML = '<div style="color: #4CAF50; padding: 10px; background: rgba(76, 175, 80, 0.1); border-radius: 5px;">✓ Edited JSON saved successfully!</div>';
debugLog('Saved edited JSON');
} catch (error) {
alert(`Failed to save: ${error.message}`);
}
}
/**
* Download the current configuration as JSON file
*/
private downloadJSON() {
if (!this.currentConfig) {
alert('Please generate a level configuration first!');
return;
}
const levelName = (document.getElementById('levelName') as HTMLInputElement).value ||
this.currentConfig.difficulty;
const filename = `level-${levelName}-${Date.now()}.json`;
const json = JSON.stringify(this.currentConfig, null, 2);
const blob = new Blob([json], { type: 'application/json' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = filename;
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
URL.revokeObjectURL(url);
debugLog(`Downloaded: ${filename}`);
}
/**
* Copy current configuration JSON to clipboard
*/
private async copyToClipboard() {
if (!this.currentConfig) {
alert('Please generate a level configuration first!');
return;
}
const json = JSON.stringify(this.currentConfig, null, 2);
try {
await navigator.clipboard.writeText(json);
alert('JSON copied to clipboard!');
} catch (err) {
console.error('Failed to copy:', err);
alert('Failed to copy to clipboard. Please copy manually from the output.');
}
}
}
/**
* Custom level generator that allows overriding default values
* Simply extends LevelGenerator - all properties are now public on the base class
*/
class CustomLevelGenerator extends LevelGenerator {
// No need to duplicate anything - just use the public properties from base class
// Properties like shipPosition, startBasePosition, etc. are already defined and public in LevelGenerator
}
// Initialize the editor when this module is loaded
if (!(window as any).__levelEditorInstance) {
(window as any).__levelEditorInstance = new LevelEditor();
}
/**
* Helper to get all saved levels from localStorage
*/
export function getSavedLevels(): Map<string, LevelConfig> {
try {
const stored = localStorage.getItem(STORAGE_KEY);
if (stored) {
const levelsArray: [string, LevelConfig][] = JSON.parse(stored);
return new Map(levelsArray);
}
} catch (error) {
console.error('Failed to load saved levels:', error);
}
return new Map();
}
/**
* Helper to get a specific saved level by name
*/
export function getSavedLevel(name: string): LevelConfig | null {
const levels = getSavedLevels();
return levels.get(name) || null;
}
/**
* Generate a simple rookie level with 4 asteroids
* Asteroids at 100-200 distance with 20-100 tangential velocities
*/
function generateSimpleRookieLevel(): void {
debugLog('Creating simple rookie level with 4 asteroids...');
const levelsMap = new Map<string, LevelConfig>();
// Create base level structure
const config: LevelConfig = {
version: "1.0",
difficulty: "rookie",
timestamp: new Date().toISOString(),
metadata: {
author: 'System',
description: 'Simple rookie training mission with 4 asteroids',
type: 'default'
},
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: {
rockCount: 4,
forceMultiplier: 1.0,
rockSizeMin: 3,
rockSizeMax: 5,
distanceMin: 100,
distanceMax: 200
}
};
// Generate 4 asteroids with tangential velocities
const basePosition = [0, 0, 0]; // Start base position
for (let i = 0; i < 4; i++) {
// Random distance between 100-200
const distance = 100 + Math.random() * 100;
// Random angle around the base
const angle = (Math.PI * 2 / 4) * i + (Math.random() - 0.5) * 0.5;
// Position at distance and angle
const x = basePosition[0] + distance * Math.cos(angle);
const z = basePosition[2] + distance * Math.sin(angle);
const y = basePosition[1] + (Math.random() - 0.5) * 20; // Some vertical variation
// Calculate tangent direction (perpendicular to radial)
const tangentX = -Math.sin(angle);
const tangentZ = Math.cos(angle);
// Random tangential speed between 20-100
const speed = 20 + Math.random() * 80;
const linearVelocity: Vector3Array = [
tangentX * speed,
(Math.random() - 0.5) * 10, // Small vertical velocity
tangentZ * speed
];
// Random size between min and max
const scale = 3 + Math.random() * 2;
// Random rotation
const angularVelocity: Vector3Array = [
(Math.random() - 0.5) * 2,
(Math.random() - 0.5) * 2,
(Math.random() - 0.5) * 2
];
config.asteroids.push({
id: `asteroid-${i}`,
position: [x, y, z],
scale,
linearVelocity,
angularVelocity
});
}
levelsMap.set('Rookie Training', config);
debugLog('Generated simple rookie level with 4 asteroids');
// Save to localStorage
const levelsArray = Array.from(levelsMap.entries());
localStorage.setItem(STORAGE_KEY, JSON.stringify(levelsArray));
debugLog('Simple rookie level saved to localStorage');
}
/**
* Generate default levels if localStorage is empty
* Creates either a simple rookie level or 6 themed levels based on progression flag
*/
export function generateDefaultLevels(): void {
const existing = getSavedLevels();
if (existing.size > 0) {
debugLog('Levels already exist in localStorage, skipping default generation');
return;
}
// Check progression flag from GameConfig
const GameConfig = (window as any).GameConfig;
const progressionEnabled = GameConfig?.getInstance().progressionEnabled ?? false;
if (!progressionEnabled) {
debugLog('Progression disabled - generating simple rookie level...');
generateSimpleRookieLevel();
return;
}
debugLog('No saved levels found, generating 6 default levels...');
// Define themed default levels with descriptions
const defaultLevels = [
{
name: 'Tutorial: Asteroid Field',
difficulty: 'recruit',
description: 'Learn the basics of ship control and asteroid destruction in a calm sector of space.',
estimatedTime: '3-5 minutes'
},
{
name: 'Rescue Mission',
difficulty: 'pilot',
description: 'Clear a path through moderate asteroid density to reach the stranded station.',
estimatedTime: '5-8 minutes'
},
{
name: 'Deep Space Patrol',
difficulty: 'captain',
description: 'Patrol a dangerous sector with heavy asteroid activity. Watch your fuel!',
estimatedTime: '8-12 minutes'
},
{
name: 'Enemy Territory',
difficulty: 'commander',
description: 'Navigate through hostile space with high-speed asteroids and limited resources.',
estimatedTime: '12-15 minutes'
},
{
name: 'The Gauntlet',
difficulty: 'commander',
description: 'Face maximum asteroid density in this ultimate test of piloting skill.',
estimatedTime: '15-20 minutes'
},
{
name: 'Final Challenge',
difficulty: 'commander',
description: 'The ultimate challenge - survive the most chaotic asteroid field in known space.',
estimatedTime: '20+ minutes'
}
];
const levelsMap = new Map<string, LevelConfig>();
for (const level of defaultLevels) {
const generator = new LevelGenerator(level.difficulty);
const config = generator.generate();
// Add rich metadata
config.metadata = {
author: 'System',
description: level.description,
estimatedTime: level.estimatedTime,
type: 'default',
difficulty: level.difficulty
};
levelsMap.set(level.name, config);
debugLog(`Generated default level: ${level.name} (${level.difficulty})`);
}
// Save all levels to localStorage
const levelsArray = Array.from(levelsMap.entries());
localStorage.setItem(STORAGE_KEY, JSON.stringify(levelsArray));
debugLog(`${defaultLevels.length} default levels saved to localStorage`);
}
// Export for manual initialization if needed
export { LevelEditor, CustomLevelGenerator };

View File

@ -1,281 +0,0 @@
import {
LevelConfig,
ShipConfig,
StartBaseConfig,
SunConfig,
PlanetConfig,
AsteroidConfig,
DifficultyConfig,
Vector3Array
} from "../config/levelConfig";
import { getRandomPlanetTexture } from "../../environment/celestial/planetTextures";
/**
* Generates procedural level configurations matching the current Level1 generation logic
*/
export class LevelGenerator {
protected _difficulty: string;
protected _difficultyConfig: DifficultyConfig;
// Configurable properties (can be overridden by subclasses or set before generate())
public shipPosition: Vector3Array = [0, 1, 0];
public sunPosition: Vector3Array = [0, 0, 400];
public sunDiameter = 50;
public sunIntensity = 1000000;
// Planet generation parameters
public planetCount = 12;
public planetMinDiameter = 100;
public planetMaxDiameter = 200;
public planetMinDistance = 1000;
public planetMaxDistance = 2000;
constructor(difficulty: string) {
this._difficulty = difficulty;
this._difficultyConfig = this.getDifficultyConfig(difficulty);
}
/**
* Set custom difficulty configuration
*/
public setDifficultyConfig(config: DifficultyConfig) {
this._difficultyConfig = config;
}
/**
* Generate a complete level configuration
*/
public generate(): LevelConfig {
const ship = this.generateShip();
const sun = this.generateSun();
const planets = this.generatePlanets();
const asteroids = this.generateAsteroids();
return {
version: "1.0",
difficulty: this._difficulty,
timestamp: new Date().toISOString(),
metadata: {
generator: "LevelGenerator",
description: `Procedurally generated ${this._difficulty} level`
},
ship,
// startBase is now optional and not generated
sun,
planets,
asteroids,
difficultyConfig: this._difficultyConfig
};
}
private generateShip(): ShipConfig {
return {
position: [...this.shipPosition],
rotation: [0, 0, 0],
linearVelocity: [0, 0, 0],
angularVelocity: [0, 0, 0]
};
}
private generateSun(): SunConfig {
return {
position: [...this.sunPosition],
diameter: this.sunDiameter,
intensity: this.sunIntensity
};
}
/**
* Generate planets in orbital pattern (matching createPlanetsOrbital logic)
*/
private generatePlanets(): PlanetConfig[] {
const planets: PlanetConfig[] = [];
for (let i = 0; i < this.planetCount; i++) {
// Random diameter between min and max
const diameter = this.planetMinDiameter +
Math.random() * (this.planetMaxDiameter - this.planetMinDiameter);
// Random distance from sun
const distance = this.planetMinDistance +
Math.random() * (this.planetMaxDistance - this.planetMinDistance);
// Random angle around Y axis (orbital plane)
const angle = Math.random() * Math.PI * 2;
// Small vertical variation (like a solar system)
const y = (Math.random() - 0.5) * 400;
const position: Vector3Array = [
this.sunPosition[0] + distance * Math.cos(angle),
this.sunPosition[1] + y,
this.sunPosition[2] + distance * Math.sin(angle)
];
planets.push({
name: `planet-${i}`,
position,
diameter,
texturePath: getRandomPlanetTexture(),
rotation: [0, 0, 0]
});
}
return planets;
}
/**
* Generate asteroids distributed evenly around the base in a spherical pattern (all 3 axes)
*/
private generateAsteroids(): AsteroidConfig[] {
const asteroids: AsteroidConfig[] = [];
const config = this._difficultyConfig;
for (let i = 0; i < config.rockCount; i++) {
// Random distance from start base
const distRange = config.distanceMax - config.distanceMin;
const dist = (Math.random() * distRange) + config.distanceMin;
// Evenly distribute asteroids on a sphere using spherical coordinates
// Azimuth angle (phi): rotation around Y axis
const phi = (i / config.rockCount) * Math.PI * 2;
// Elevation angle (theta): angle from top (0) to bottom (π)
// Using equal area distribution: acos(1 - 2*u) where u is [0,1]
const u = (i + 0.5) / config.rockCount;
const theta = Math.acos(1 - 2 * u);
// Add small random variations to prevent perfect spacing
const phiVariation = (Math.random() - 0.5) * 0.3; // ±0.15 radians
const thetaVariation = (Math.random() - 0.5) * 0.3; // ±0.15 radians
const finalPhi = phi + phiVariation;
const finalTheta = theta + thetaVariation;
// Convert spherical to Cartesian coordinates
const x = dist * Math.sin(finalTheta) * Math.cos(finalPhi);
const y = dist * Math.cos(finalTheta);
const z = dist * Math.sin(finalTheta) * Math.sin(finalPhi);
const position: Vector3Array = [x, y, z];
// Random size (uniform scale)
const sizeRange = config.rockSizeMax - config.rockSizeMin;
const scale = Math.random() * sizeRange + config.rockSizeMin;
// Calculate initial velocity based on force applied in Level1
// Velocity should be tangential to the sphere (perpendicular to radius)
const forceMagnitude = 50000000 * config.forceMultiplier;
const mass = 10000;
const velocityMagnitude = forceMagnitude / mass / 100; // Approximation
// Tangential velocity: use cross product of radius with an arbitrary vector
// to get perpendicular direction, then rotate around radius
// Simple approach: velocity perpendicular to radius in a tangent plane
const vx = -velocityMagnitude * Math.sin(finalPhi);
const vy = 0;
const vz = velocityMagnitude * Math.cos(finalPhi);
const linearVelocity: Vector3Array = [vx, vy, vz];
asteroids.push({
id: `asteroid-${i}`,
position,
scale,
linearVelocity,
angularVelocity: [0, 0, 0],
mass
});
}
return asteroids;
}
/**
* Get difficulty configuration (matching Level1.getDifficultyConfig)
*/
private getDifficultyConfig(difficulty: string): DifficultyConfig {
switch (difficulty) {
case 'recruit':
return {
rockCount: 5,
forceMultiplier: .8,
rockSizeMin: 10,
rockSizeMax: 15,
distanceMin: 220,
distanceMax: 250
};
case 'pilot':
return {
rockCount: 10,
forceMultiplier: 1,
rockSizeMin: 8,
rockSizeMax: 20,
distanceMin: 225,
distanceMax: 300
};
case 'captain':
return {
rockCount: 20,
forceMultiplier: 1.2,
rockSizeMin: 5,
rockSizeMax: 40,
distanceMin: 230,
distanceMax: 450
};
case 'commander':
return {
rockCount: 50,
forceMultiplier: 1.3,
rockSizeMin: 2,
rockSizeMax: 8,
distanceMin: 90,
distanceMax: 280
};
case 'test':
return {
rockCount: 100,
forceMultiplier: 0.3,
rockSizeMin: 8,
rockSizeMax: 15,
distanceMin: 150,
distanceMax: 200
};
default:
return {
rockCount: 5,
forceMultiplier: 1.0,
rockSizeMin: 4,
rockSizeMax: 8,
distanceMin: 170,
distanceMax: 220
};
}
}
/**
* Static helper to generate and save a level to JSON string
*/
public static generateJSON(difficulty: string): string {
const generator = new LevelGenerator(difficulty);
const config = generator.generate();
return JSON.stringify(config, null, 2);
}
/**
* Static helper to generate and trigger download of level JSON
*/
public static downloadJSON(difficulty: string, filename?: string): void {
const json = LevelGenerator.generateJSON(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-${difficulty}-${Date.now()}.json`;
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
URL.revokeObjectURL(url);
}
}

View File

@ -1,381 +0,0 @@
/**
* Completion record for a single play-through
*/
export interface LevelCompletion {
timestamp: Date;
completionTimeSeconds: number;
score?: number;
survived: boolean; // false if player died/quit
}
/**
* Aggregated statistics for a level
*/
export interface LevelStatistics {
levelId: string;
firstPlayed?: Date;
lastPlayed?: Date;
completions: LevelCompletion[];
totalAttempts: number; // Including incomplete attempts
totalCompletions: number; // Only successful completions
bestTimeSeconds?: number;
averageTimeSeconds?: number;
bestScore?: number;
averageScore?: number;
completionRate: number; // percentage (0-100)
difficultyRating?: number; // 1-5 stars, user-submitted
}
const STATS_STORAGE_KEY = 'space-game-level-stats';
/**
* Manages level performance statistics and ratings
*/
export class LevelStatsManager {
private static instance: LevelStatsManager | null = null;
private statsMap: Map<string, LevelStatistics> = new Map();
private constructor() {
this.loadStats();
}
public static getInstance(): LevelStatsManager {
if (!LevelStatsManager.instance) {
LevelStatsManager.instance = new LevelStatsManager();
}
return LevelStatsManager.instance;
}
/**
* Load stats from localStorage
*/
private loadStats(): void {
const stored = localStorage.getItem(STATS_STORAGE_KEY);
if (!stored) {
return;
}
try {
const statsArray: [string, LevelStatistics][] = JSON.parse(stored);
for (const [id, stats] of statsArray) {
// Parse date strings back to Date objects
if (stats.firstPlayed && typeof stats.firstPlayed === 'string') {
stats.firstPlayed = new Date(stats.firstPlayed);
}
if (stats.lastPlayed && typeof stats.lastPlayed === 'string') {
stats.lastPlayed = new Date(stats.lastPlayed);
}
// Parse completion timestamps
stats.completions = stats.completions.map(c => ({
...c,
timestamp: typeof c.timestamp === 'string' ? new Date(c.timestamp) : c.timestamp
}));
this.statsMap.set(id, stats);
}
} catch (error) {
console.error('Failed to load level stats:', error);
}
}
/**
* Save stats to localStorage
*/
private saveStats(): void {
const statsArray = Array.from(this.statsMap.entries());
localStorage.setItem(STATS_STORAGE_KEY, JSON.stringify(statsArray));
}
/**
* Get statistics for a level
*/
public getStats(levelId: string): LevelStatistics | undefined {
return this.statsMap.get(levelId);
}
/**
* Initialize stats for a level if not exists
*/
private ensureStatsExist(levelId: string): LevelStatistics {
let stats = this.statsMap.get(levelId);
if (!stats) {
stats = {
levelId,
completions: [],
totalAttempts: 0,
totalCompletions: 0,
completionRate: 0
};
this.statsMap.set(levelId, stats);
}
return stats;
}
/**
* Record that a level was started (attempt)
*/
public recordAttempt(levelId: string): void {
const stats = this.ensureStatsExist(levelId);
stats.totalAttempts++;
const now = new Date();
if (!stats.firstPlayed) {
stats.firstPlayed = now;
}
stats.lastPlayed = now;
this.recalculateStats(stats);
this.saveStats();
}
/**
* Record a level completion
*/
public recordCompletion(
levelId: string,
completionTimeSeconds: number,
score?: number,
survived: boolean = true
): void {
const stats = this.ensureStatsExist(levelId);
const completion: LevelCompletion = {
timestamp: new Date(),
completionTimeSeconds,
score,
survived
};
stats.completions.push(completion);
if (survived) {
stats.totalCompletions++;
}
const now = new Date();
if (!stats.firstPlayed) {
stats.firstPlayed = now;
}
stats.lastPlayed = now;
this.recalculateStats(stats);
this.saveStats();
}
/**
* Set difficulty rating for a level (1-5 stars)
*/
public setDifficultyRating(levelId: string, rating: number): void {
if (rating < 1 || rating > 5) {
console.warn('Rating must be between 1 and 5');
return;
}
const stats = this.ensureStatsExist(levelId);
stats.difficultyRating = rating;
this.saveStats();
}
/**
* Recalculate aggregated statistics
*/
private recalculateStats(stats: LevelStatistics): void {
const successfulCompletions = stats.completions.filter(c => c.survived);
// Completion rate
stats.completionRate = stats.totalAttempts > 0
? (stats.totalCompletions / stats.totalAttempts) * 100
: 0;
// Time statistics
if (successfulCompletions.length > 0) {
const times = successfulCompletions.map(c => c.completionTimeSeconds);
stats.bestTimeSeconds = Math.min(...times);
stats.averageTimeSeconds = times.reduce((a, b) => a + b, 0) / times.length;
} else {
stats.bestTimeSeconds = undefined;
stats.averageTimeSeconds = undefined;
}
// Score statistics
const completionsWithScore = successfulCompletions.filter(c => c.score !== undefined);
if (completionsWithScore.length > 0) {
const scores = completionsWithScore.map(c => c.score!);
stats.bestScore = Math.max(...scores);
stats.averageScore = scores.reduce((a, b) => a + b, 0) / scores.length;
} else {
stats.bestScore = undefined;
stats.averageScore = undefined;
}
}
/**
* Get all stats
*/
public getAllStats(): Map<string, LevelStatistics> {
return new Map(this.statsMap);
}
/**
* Get stats for multiple levels
*/
public getStatsForLevels(levelIds: string[]): Map<string, LevelStatistics> {
const result = new Map<string, LevelStatistics>();
for (const id of levelIds) {
const stats = this.statsMap.get(id);
if (stats) {
result.set(id, stats);
}
}
return result;
}
/**
* Get top N fastest completions for a level
*/
public getTopCompletions(levelId: string, limit: number = 10): LevelCompletion[] {
const stats = this.statsMap.get(levelId);
if (!stats) {
return [];
}
return stats.completions
.filter(c => c.survived)
.sort((a, b) => a.completionTimeSeconds - b.completionTimeSeconds)
.slice(0, limit);
}
/**
* Get recent completions for a level
*/
public getRecentCompletions(levelId: string, limit: number = 10): LevelCompletion[] {
const stats = this.statsMap.get(levelId);
if (!stats) {
return [];
}
return [...stats.completions]
.sort((a, b) => b.timestamp.getTime() - a.timestamp.getTime())
.slice(0, limit);
}
/**
* Delete stats for a level
*/
public deleteStats(levelId: string): boolean {
const deleted = this.statsMap.delete(levelId);
if (deleted) {
this.saveStats();
}
return deleted;
}
/**
* Clear all stats (for testing/reset)
*/
public clearAll(): void {
this.statsMap.clear();
localStorage.removeItem(STATS_STORAGE_KEY);
}
/**
* Export stats as JSON
*/
public exportStats(): string {
const statsArray = Array.from(this.statsMap.entries());
return JSON.stringify(statsArray, null, 2);
}
/**
* Import stats from JSON
*/
public importStats(jsonString: string): number {
try {
const statsArray: [string, LevelStatistics][] = JSON.parse(jsonString);
let importCount = 0;
for (const [id, stats] of statsArray) {
// Parse dates
if (stats.firstPlayed && typeof stats.firstPlayed === 'string') {
stats.firstPlayed = new Date(stats.firstPlayed);
}
if (stats.lastPlayed && typeof stats.lastPlayed === 'string') {
stats.lastPlayed = new Date(stats.lastPlayed);
}
stats.completions = stats.completions.map(c => ({
...c,
timestamp: typeof c.timestamp === 'string' ? new Date(c.timestamp) : c.timestamp
}));
this.statsMap.set(id, stats);
importCount++;
}
this.saveStats();
return importCount;
} catch (error) {
console.error('Failed to import stats:', error);
throw new Error('Invalid stats JSON format');
}
}
/**
* Get summary statistics across all levels
*/
public getGlobalSummary(): {
totalLevelsPlayed: number;
totalAttempts: number;
totalCompletions: number;
averageCompletionRate: number;
totalPlayTimeSeconds: number;
} {
let totalLevelsPlayed = 0;
let totalAttempts = 0;
let totalCompletions = 0;
let totalPlayTimeSeconds = 0;
let totalCompletionRates = 0;
for (const stats of this.statsMap.values()) {
if (stats.totalAttempts > 0) {
totalLevelsPlayed++;
totalAttempts += stats.totalAttempts;
totalCompletions += stats.totalCompletions;
totalCompletionRates += stats.completionRate;
// Sum all completion times
for (const completion of stats.completions) {
if (completion.survived) {
totalPlayTimeSeconds += completion.completionTimeSeconds;
}
}
}
}
return {
totalLevelsPlayed,
totalAttempts,
totalCompletions,
averageCompletionRate: totalLevelsPlayed > 0 ? totalCompletionRates / totalLevelsPlayed : 0,
totalPlayTimeSeconds
};
}
/**
* Format time in MM:SS format
*/
public static formatTime(seconds: number): string {
const mins = Math.floor(seconds / 60);
const secs = Math.floor(seconds % 60);
return `${mins}:${secs.toString().padStart(2, '0')}`;
}
/**
* Format completion rate as percentage
*/
public static formatCompletionRate(rate: number): string {
return `${rate.toFixed(1)}%`;
}
}

View File

@ -1,241 +0,0 @@
import {LevelConfig} from "../config/levelConfig";
/**
* Sync status for a level
*/
export enum SyncStatus {
NotSynced = 'not_synced',
Syncing = 'syncing',
Synced = 'synced',
Conflict = 'conflict',
Error = 'error'
}
/**
* Metadata for synced levels
*/
export interface SyncMetadata {
lastSyncedAt?: Date;
syncStatus: SyncStatus;
cloudVersion?: string;
localVersion?: string;
syncError?: string;
}
/**
* Interface for level storage providers (localStorage, cloud, etc.)
*/
export interface ILevelStorageProvider {
/**
* Get a level by ID
*/
getLevel(levelId: string): Promise<LevelConfig | null>;
/**
* Save a level
*/
saveLevel(levelId: string, config: LevelConfig): Promise<void>;
/**
* Delete a level
*/
deleteLevel(levelId: string): Promise<boolean>;
/**
* List all level IDs
*/
listLevels(): Promise<string[]>;
/**
* Check if provider is available/connected
*/
isAvailable(): Promise<boolean>;
/**
* Get sync metadata for a level (if supported)
*/
getSyncMetadata?(levelId: string): Promise<SyncMetadata | null>;
}
/**
* LocalStorage implementation of level storage provider
*/
export class LocalStorageProvider implements ILevelStorageProvider {
private storageKey: string;
constructor(storageKey: string = 'space-game-custom-levels') {
this.storageKey = storageKey;
}
async getLevel(levelId: string): Promise<LevelConfig | null> {
const stored = localStorage.getItem(this.storageKey);
if (!stored) {
return null;
}
try {
const levelsArray: [string, LevelConfig][] = JSON.parse(stored);
const found = levelsArray.find(([id]) => id === levelId);
return found ? found[1] : null;
} catch (error) {
console.error('Failed to get level from localStorage:', error);
return null;
}
}
async saveLevel(levelId: string, config: LevelConfig): Promise<void> {
const stored = localStorage.getItem(this.storageKey);
let levelsArray: [string, LevelConfig][] = [];
if (stored) {
try {
levelsArray = JSON.parse(stored);
} catch (error) {
console.error('Failed to parse localStorage data:', error);
}
}
// Update or add level
const existingIndex = levelsArray.findIndex(([id]) => id === levelId);
if (existingIndex >= 0) {
levelsArray[existingIndex] = [levelId, config];
} else {
levelsArray.push([levelId, config]);
}
localStorage.setItem(this.storageKey, JSON.stringify(levelsArray));
}
async deleteLevel(levelId: string): Promise<boolean> {
const stored = localStorage.getItem(this.storageKey);
if (!stored) {
return false;
}
try {
const levelsArray: [string, LevelConfig][] = JSON.parse(stored);
const newArray = levelsArray.filter(([id]) => id !== levelId);
if (newArray.length === levelsArray.length) {
return false; // Level not found
}
localStorage.setItem(this.storageKey, JSON.stringify(newArray));
return true;
} catch (error) {
console.error('Failed to delete level from localStorage:', error);
return false;
}
}
async listLevels(): Promise<string[]> {
const stored = localStorage.getItem(this.storageKey);
if (!stored) {
return [];
}
try {
const levelsArray: [string, LevelConfig][] = JSON.parse(stored);
return levelsArray.map(([id]) => id);
} catch (error) {
console.error('Failed to list levels from localStorage:', error);
return [];
}
}
async isAvailable(): Promise<boolean> {
try {
const testKey = '_storage_test_';
localStorage.setItem(testKey, 'test');
localStorage.removeItem(testKey);
return true;
} catch {
return false;
}
}
}
/**
* Cloud storage provider (stub for future implementation)
*
* Future implementation could use:
* - Firebase Firestore
* - AWS S3 + DynamoDB
* - Custom backend API
* - IPFS for decentralized storage
*/
export class CloudStorageProvider implements ILevelStorageProvider {
private apiEndpoint: string;
private authToken?: string;
constructor(apiEndpoint: string, authToken?: string) {
this.apiEndpoint = apiEndpoint;
this.authToken = authToken;
}
async getLevel(_levelId: string): Promise<LevelConfig | null> {
// TODO: Implement cloud fetch
throw new Error('Cloud storage not yet implemented');
}
async saveLevel(_levelId: string, _config: LevelConfig): Promise<void> {
// TODO: Implement cloud save
throw new Error('Cloud storage not yet implemented');
}
async deleteLevel(_levelId: string): Promise<boolean> {
// TODO: Implement cloud delete
throw new Error('Cloud storage not yet implemented');
}
async listLevels(): Promise<string[]> {
// TODO: Implement cloud list
throw new Error('Cloud storage not yet implemented');
}
async isAvailable(): Promise<boolean> {
// TODO: Implement cloud connectivity check
return false;
}
async getSyncMetadata(_levelId: string): Promise<SyncMetadata | null> {
// TODO: Implement sync metadata fetch
throw new Error('Cloud storage not yet implemented');
}
/**
* Authenticate with cloud service
*/
async authenticate(token: string): Promise<boolean> {
this.authToken = token;
// TODO: Implement authentication
return false;
}
/**
* Sync local level to cloud
*/
async syncToCloud(_levelId: string, _config: LevelConfig): Promise<SyncMetadata> {
// TODO: Implement sync to cloud
throw new Error('Cloud storage not yet implemented');
}
/**
* Sync cloud level to local
*/
async syncFromCloud(_levelId: string): Promise<LevelConfig> {
// TODO: Implement sync from cloud
throw new Error('Cloud storage not yet implemented');
}
/**
* Resolve sync conflicts
*/
async resolveConflict(
_levelId: string,
_strategy: 'use_local' | 'use_cloud' | 'merge'
): Promise<LevelConfig> {
// TODO: Implement conflict resolution
throw new Error('Cloud storage not yet implemented');
}
}

View File

@ -1,80 +0,0 @@
import {
AbstractMesh, Color3, GlowLayer,
MeshBuilder,
ParticleSystem,
StandardMaterial,
Texture,
TransformNode,
Vector3
} from "@babylonjs/core";
import {DefaultScene} from "../core/defaultScene";
type MainEngine = {
transformNode: TransformNode;
particleSystem: ParticleSystem;
}
export class ShipEngine {
private _ship: TransformNode;
private _leftMainEngine: MainEngine;
private _rightMainEngine: MainEngine;
constructor(ship: TransformNode) {
this._ship = ship;
this.initialize();
}
private initialize() {
this._leftMainEngine = this.createEngine(new Vector3(-.44, .37, -1.1));
this._rightMainEngine = this.createEngine(new Vector3(.44, .37, -1.1));
}
public idle() {
this._leftMainEngine.particleSystem.emitRate = 1;
this._rightMainEngine.particleSystem.emitRate = 1;
}
public forwardback(value: number) {
if (Math.sign(value) > 0) {
(this._leftMainEngine.particleSystem.emitter as AbstractMesh).rotation.y = 0;
(this._rightMainEngine.particleSystem.emitter as AbstractMesh).rotation.y = 0;
} else {
(this._leftMainEngine.particleSystem.emitter as AbstractMesh).rotation.y = Math.PI;
(this._rightMainEngine.particleSystem.emitter as AbstractMesh).rotation.y = Math.PI;
}
this._leftMainEngine.particleSystem.emitRate = Math.abs(value) * 10;
this._rightMainEngine.particleSystem.emitRate = Math.abs(value) * 10;
}
private createEngine(position: Vector3) : MainEngine{
const MAIN_ROTATION = Math.PI / 2;
const engine = new TransformNode("engine", DefaultScene.MainScene);
engine.parent = this._ship;
engine.position = position;
const leftDisc = MeshBuilder.CreateIcoSphere("engineSphere", {radius: .07}, DefaultScene.MainScene);
const material = new StandardMaterial("material", DefaultScene.MainScene);
material.emissiveColor = new Color3(.5, .5, .1);
leftDisc.material = material;
leftDisc.parent = engine;
leftDisc.rotation.x = MAIN_ROTATION;
const particleSystem = this.createParticleSystem(leftDisc);
return {transformNode: engine, particleSystem: particleSystem};
}
private createParticleSystem(mesh: AbstractMesh): ParticleSystem {
const myParticleSystem = new ParticleSystem("particles", 1000, DefaultScene.MainScene);
myParticleSystem.emitRate = 1;
//myParticleSystem.minEmitPower = 2;
//myParticleSystem.maxEmitPower = 10;
myParticleSystem.particleTexture = new Texture("/flare.png");
myParticleSystem.emitter = mesh;
const coneEmitter = myParticleSystem.createConeEmitter(0.1, Math.PI / 9);
myParticleSystem.addSizeGradient(0, .01);
myParticleSystem.addSizeGradient(1, .3);
myParticleSystem.isLocal = true;
myParticleSystem.start(); //S
return myParticleSystem;
}
}

View File

@ -1,294 +0,0 @@
import {
ControllerMappingConfig,
ControllerMapping,
StickAction,
ButtonAction
} from '../../ship/input/controllerMapping';
/**
* Controller remapping screen
* Allows users to customize VR controller button and stick mappings
*/
export class ControlsScreen {
private config: ControllerMappingConfig;
private messageDiv: HTMLElement | null = null;
constructor() {
this.config = ControllerMappingConfig.getInstance();
}
/**
* Initialize the controls screen
* Set up event listeners and populate form with current configuration
*/
public initialize(): void {
console.log('[ControlsScreen] Initializing');
// Get form elements
this.messageDiv = document.getElementById('controlsMessage');
// Populate dropdowns
this.populateDropdowns();
// Load current configuration into form
this.loadCurrentMapping();
// Set up event listeners
this.setupEventListeners();
console.log('[ControlsScreen] Initialized');
}
/**
* Populate all dropdown select elements with available actions
*/
private populateDropdowns(): void {
// Stick action dropdowns
const stickSelects = [
'leftStickX', 'leftStickY',
'rightStickX', 'rightStickY'
];
const stickActions = ControllerMappingConfig.getAvailableStickActions();
stickSelects.forEach(id => {
const select = document.getElementById(id) as HTMLSelectElement;
if (select) {
select.innerHTML = '';
stickActions.forEach(action => {
const option = document.createElement('option');
option.value = action;
option.textContent = ControllerMappingConfig.getStickActionLabel(action);
select.appendChild(option);
});
}
});
// Button action dropdowns
const buttonSelects = [
'trigger', 'aButton', 'bButton',
'xButton', 'yButton', 'squeeze'
];
const buttonActions = ControllerMappingConfig.getAvailableButtonActions();
buttonSelects.forEach(id => {
const select = document.getElementById(id) as HTMLSelectElement;
if (select) {
select.innerHTML = '';
buttonActions.forEach(action => {
const option = document.createElement('option');
option.value = action;
option.textContent = ControllerMappingConfig.getButtonActionLabel(action);
select.appendChild(option);
});
}
});
}
/**
* Load current mapping configuration into form elements
*/
private loadCurrentMapping(): void {
const mapping = this.config.getMapping();
// Stick mappings
this.setSelectValue('leftStickX', mapping.leftStickX);
this.setSelectValue('leftStickY', mapping.leftStickY);
this.setSelectValue('rightStickX', mapping.rightStickX);
this.setSelectValue('rightStickY', mapping.rightStickY);
// Inversion checkboxes
this.setCheckboxValue('invertLeftStickX', mapping.invertLeftStickX);
this.setCheckboxValue('invertLeftStickY', mapping.invertLeftStickY);
this.setCheckboxValue('invertRightStickX', mapping.invertRightStickX);
this.setCheckboxValue('invertRightStickY', mapping.invertRightStickY);
// Button mappings
this.setSelectValue('trigger', mapping.trigger);
this.setSelectValue('aButton', mapping.aButton);
this.setSelectValue('bButton', mapping.bButton);
this.setSelectValue('xButton', mapping.xButton);
this.setSelectValue('yButton', mapping.yButton);
this.setSelectValue('squeeze', mapping.squeeze);
console.log('[ControlsScreen] Loaded current mapping into form');
}
/**
* Set up event listeners for buttons
*/
private setupEventListeners(): void {
// Save button
const saveBtn = document.getElementById('saveControlsBtn');
if (saveBtn) {
saveBtn.addEventListener('click', () => this.saveMapping());
}
// Reset button
const resetBtn = document.getElementById('resetControlsBtn');
if (resetBtn) {
resetBtn.addEventListener('click', () => this.resetToDefault());
}
// Test button (shows current mapping preview)
const testBtn = document.getElementById('testControlsBtn');
if (testBtn) {
testBtn.addEventListener('click', () => this.showTestPreview());
}
}
/**
* Save current form values to configuration
*/
private saveMapping(): void {
// Read all form values
const mapping: ControllerMapping = {
// Stick mappings
leftStickX: this.getSelectValue('leftStickX') as StickAction,
leftStickY: this.getSelectValue('leftStickY') as StickAction,
rightStickX: this.getSelectValue('rightStickX') as StickAction,
rightStickY: this.getSelectValue('rightStickY') as StickAction,
// Inversions
invertLeftStickX: this.getCheckboxValue('invertLeftStickX'),
invertLeftStickY: this.getCheckboxValue('invertLeftStickY'),
invertRightStickX: this.getCheckboxValue('invertRightStickX'),
invertRightStickY: this.getCheckboxValue('invertRightStickY'),
// Button mappings
trigger: this.getSelectValue('trigger') as ButtonAction,
aButton: this.getSelectValue('aButton') as ButtonAction,
bButton: this.getSelectValue('bButton') as ButtonAction,
xButton: this.getSelectValue('xButton') as ButtonAction,
yButton: this.getSelectValue('yButton') as ButtonAction,
squeeze: this.getSelectValue('squeeze') as ButtonAction,
};
// Validate
this.config.setMapping(mapping);
const warnings = this.config.validate();
if (warnings.length > 0) {
// Show warnings but still save
this.showMessage(
'Configuration saved with warnings:\n' + warnings.join('\n'),
'warning'
);
} else {
this.showMessage('Configuration saved successfully!', 'success');
}
// Save to localStorage
this.config.save();
console.log('[ControlsScreen] Saved mapping:', mapping);
}
/**
* Reset form to default mapping
*/
private resetToDefault(): void {
if (confirm('Reset all controller mappings to default? This cannot be undone.')) {
this.config.resetToDefault();
this.config.save();
this.loadCurrentMapping();
this.showMessage('Reset to default configuration', 'success');
console.log('[ControlsScreen] Reset to defaults');
}
}
/**
* Show test preview of current mapping
*/
private showTestPreview(): void {
const mapping = this.readCurrentFormValues();
let preview = 'Current Controller Mapping:\n\n';
preview += '📋 STICK MAPPINGS:\n';
preview += ` Left Stick X: ${ControllerMappingConfig.getStickActionLabel(mapping.leftStickX)}`;
preview += mapping.invertLeftStickX ? ' (Inverted)\n' : '\n';
preview += ` Left Stick Y: ${ControllerMappingConfig.getStickActionLabel(mapping.leftStickY)}`;
preview += mapping.invertLeftStickY ? ' (Inverted)\n' : '\n';
preview += ` Right Stick X: ${ControllerMappingConfig.getStickActionLabel(mapping.rightStickX)}`;
preview += mapping.invertRightStickX ? ' (Inverted)\n' : '\n';
preview += ` Right Stick Y: ${ControllerMappingConfig.getStickActionLabel(mapping.rightStickY)}`;
preview += mapping.invertRightStickY ? ' (Inverted)\n' : '\n';
preview += '\n🎮 BUTTON MAPPINGS:\n';
preview += ` Trigger: ${ControllerMappingConfig.getButtonActionLabel(mapping.trigger)}\n`;
preview += ` A Button: ${ControllerMappingConfig.getButtonActionLabel(mapping.aButton)}\n`;
preview += ` B Button: ${ControllerMappingConfig.getButtonActionLabel(mapping.bButton)}\n`;
preview += ` X Button: ${ControllerMappingConfig.getButtonActionLabel(mapping.xButton)}\n`;
preview += ` Y Button: ${ControllerMappingConfig.getButtonActionLabel(mapping.yButton)}\n`;
preview += ` Squeeze/Grip: ${ControllerMappingConfig.getButtonActionLabel(mapping.squeeze)}\n`;
alert(preview);
}
/**
* Read current form values into a mapping object
*/
private readCurrentFormValues(): ControllerMapping {
return {
leftStickX: this.getSelectValue('leftStickX') as StickAction,
leftStickY: this.getSelectValue('leftStickY') as StickAction,
rightStickX: this.getSelectValue('rightStickX') as StickAction,
rightStickY: this.getSelectValue('rightStickY') as StickAction,
invertLeftStickX: this.getCheckboxValue('invertLeftStickX'),
invertLeftStickY: this.getCheckboxValue('invertLeftStickY'),
invertRightStickX: this.getCheckboxValue('invertRightStickX'),
invertRightStickY: this.getCheckboxValue('invertRightStickY'),
trigger: this.getSelectValue('trigger') as ButtonAction,
aButton: this.getSelectValue('aButton') as ButtonAction,
bButton: this.getSelectValue('bButton') as ButtonAction,
xButton: this.getSelectValue('xButton') as ButtonAction,
yButton: this.getSelectValue('yButton') as ButtonAction,
squeeze: this.getSelectValue('squeeze') as ButtonAction,
};
}
/**
* Show a message to the user
*/
private showMessage(message: string, type: 'success' | 'error' | 'warning' = 'success'): void {
if (this.messageDiv) {
this.messageDiv.textContent = message;
this.messageDiv.className = `controls-message ${type}`;
this.messageDiv.style.display = 'block';
// Hide after 5 seconds
setTimeout(() => {
if (this.messageDiv) {
this.messageDiv.style.display = 'none';
}
}, 5000);
}
}
// Helper methods for form manipulation
private setSelectValue(id: string, value: string): void {
const select = document.getElementById(id) as HTMLSelectElement;
if (select) {
select.value = value;
}
}
private getSelectValue(id: string): string {
const select = document.getElementById(id) as HTMLSelectElement;
return select ? select.value : '';
}
private setCheckboxValue(id: string, checked: boolean): void {
const checkbox = document.getElementById(id) as HTMLInputElement;
if (checkbox) {
checkbox.checked = checked;
}
}
private getCheckboxValue(id: string): boolean {
const checkbox = document.getElementById(id) as HTMLInputElement;
return checkbox ? checkbox.checked : false;
}
}

View File

@ -1,91 +0,0 @@
import { GameConfig } from "../../core/gameConfig";
/**
* Initialize the settings screen
*/
export function initializeSettingsScreen(): void {
const config = GameConfig.getInstance();
// Get form elements
const physicsEnabledCheckbox = document.getElementById('physicsEnabled') as HTMLInputElement;
const debugEnabledCheckbox = document.getElementById('debugEnabled') as HTMLInputElement;
// Ship physics inputs
const maxLinearVelocityInput = document.getElementById('maxLinearVelocity') as HTMLInputElement;
const maxAngularVelocityInput = document.getElementById('maxAngularVelocity') as HTMLInputElement;
const linearForceMultiplierInput = document.getElementById('linearForceMultiplier') as HTMLInputElement;
const angularForceMultiplierInput = document.getElementById('angularForceMultiplier') as HTMLInputElement;
const saveBtn = document.getElementById('saveSettingsBtn');
const resetBtn = document.getElementById('resetSettingsBtn');
const messageDiv = document.getElementById('settingsMessage');
// Load current settings
loadSettings();
// Save button handler
saveBtn?.addEventListener('click', () => {
saveSettings();
showMessage('Settings saved successfully!', 'success');
});
// Reset button handler
resetBtn?.addEventListener('click', () => {
if (confirm('Are you sure you want to reset all settings to defaults?')) {
config.reset();
loadSettings();
showMessage('Settings reset to defaults', 'info');
}
});
/**
* Load current settings into form
*/
function loadSettings(): void {
if (physicsEnabledCheckbox) physicsEnabledCheckbox.checked = config.physicsEnabled;
if (debugEnabledCheckbox) debugEnabledCheckbox.checked = config.debug;
// Load ship physics settings
if (maxLinearVelocityInput) maxLinearVelocityInput.value = config.shipPhysics.maxLinearVelocity.toString();
if (maxAngularVelocityInput) maxAngularVelocityInput.value = config.shipPhysics.maxAngularVelocity.toString();
if (linearForceMultiplierInput) linearForceMultiplierInput.value = config.shipPhysics.linearForceMultiplier.toString();
if (angularForceMultiplierInput) angularForceMultiplierInput.value = config.shipPhysics.angularForceMultiplier.toString();
}
/**
* Save form settings to GameConfig
*/
function saveSettings(): void {
config.physicsEnabled = physicsEnabledCheckbox.checked;
config.debug = debugEnabledCheckbox.checked;
// Save ship physics settings
config.shipPhysics.maxLinearVelocity = parseFloat(maxLinearVelocityInput.value);
config.shipPhysics.maxAngularVelocity = parseFloat(maxAngularVelocityInput.value);
config.shipPhysics.linearForceMultiplier = parseFloat(linearForceMultiplierInput.value);
config.shipPhysics.angularForceMultiplier = parseFloat(angularForceMultiplierInput.value);
config.save();
}
/**
* Show a temporary message
*/
function showMessage(message: string, type: 'success' | 'info' | 'warning'): void {
if (!messageDiv) return;
const colors = {
success: '#4CAF50',
info: '#2196F3',
warning: '#FF9800'
};
messageDiv.textContent = message;
messageDiv.style.color = colors[type];
messageDiv.style.opacity = '1';
setTimeout(() => {
messageDiv.style.opacity = '0';
}, 3000);
}
}

View File

@ -1,4 +0,0 @@
export type ScoreEvent = {
score: number,
message: string
}