diff --git a/public/HavokPhysics.wasm b/public/HavokPhysics.wasm new file mode 100644 index 0000000..6e5c1f2 Binary files /dev/null and b/public/HavokPhysics.wasm differ diff --git a/src/components/shared/VectorInput.svelte b/src/components/shared/VectorInput.svelte deleted file mode 100644 index cc2cb6b..0000000 --- a/src/components/shared/VectorInput.svelte +++ /dev/null @@ -1,44 +0,0 @@ - - -
-
- - -
-
- - -
-
- - -
-
- - diff --git a/src/environment/celestial/planetTextures.ts b/src/environment/celestial/planetTextures.ts deleted file mode 100644 index bbe1dd8..0000000 --- a/src/environment/celestial/planetTextures.ts +++ /dev/null @@ -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", - ], -}; \ No newline at end of file diff --git a/src/levels/config/levelSerializer.ts b/src/levels/config/levelSerializer.ts deleted file mode 100644 index 7ef4ded..0000000 --- a/src/levels/config/levelSerializer.ts +++ /dev/null @@ -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(); - - 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(); - - // 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); - } -} diff --git a/src/levels/generation/levelEditor.ts b/src/levels/generation/levelEditor.ts deleted file mode 100644 index fdd9ead..0000000 --- a/src/levels/generation/levelEditor.ts +++ /dev/null @@ -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 = 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 = '

No saved levels yet. Generate a level to save it.

'; - return; - } - - let html = '
'; - - for (const [name, config] of this.savedLevels.entries()) { - const timestamp = config.timestamp ? new Date(config.timestamp).toLocaleString() : 'Unknown'; - html += ` -
-
-
${name}
-
- ${config.difficulty} • ${config.asteroids.length} asteroids • ${timestamp} -
-
-
- - -
-
- `; - } - - html += '
'; - 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 = '
✓ JSON is valid!
'; - return true; - } else { - messageDiv.innerHTML = `
- Validation Errors:
- ${validation.errors.map(e => `• ${e}`).join('
')} -
`; - return false; - } - } catch (error) { - messageDiv.innerHTML = `
- JSON Parse Error:
- ${error.message} -
`; - 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 += '
Please fix validation errors before saving.
'; - 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 = '
✓ Edited JSON saved successfully!
'; - - 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 { - 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(); - - // 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(); - - 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 }; diff --git a/src/levels/generation/levelGenerator.ts b/src/levels/generation/levelGenerator.ts deleted file mode 100644 index e393ec2..0000000 --- a/src/levels/generation/levelGenerator.ts +++ /dev/null @@ -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); - } -} diff --git a/src/levels/stats/levelStats.ts b/src/levels/stats/levelStats.ts deleted file mode 100644 index 0653f1d..0000000 --- a/src/levels/stats/levelStats.ts +++ /dev/null @@ -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 = 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 { - return new Map(this.statsMap); - } - - /** - * Get stats for multiple levels - */ - public getStatsForLevels(levelIds: string[]): Map { - const result = new Map(); - 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)}%`; - } -} diff --git a/src/levels/storage/ILevelStorageProvider.ts b/src/levels/storage/ILevelStorageProvider.ts deleted file mode 100644 index c1f63e4..0000000 --- a/src/levels/storage/ILevelStorageProvider.ts +++ /dev/null @@ -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; - - /** - * Save a level - */ - saveLevel(levelId: string, config: LevelConfig): Promise; - - /** - * Delete a level - */ - deleteLevel(levelId: string): Promise; - - /** - * List all level IDs - */ - listLevels(): Promise; - - /** - * Check if provider is available/connected - */ - isAvailable(): Promise; - - /** - * Get sync metadata for a level (if supported) - */ - getSyncMetadata?(levelId: string): Promise; -} - -/** - * 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 { - 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 { - 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 { - 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 { - 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 { - 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 { - // TODO: Implement cloud fetch - throw new Error('Cloud storage not yet implemented'); - } - - async saveLevel(_levelId: string, _config: LevelConfig): Promise { - // TODO: Implement cloud save - throw new Error('Cloud storage not yet implemented'); - } - - async deleteLevel(_levelId: string): Promise { - // TODO: Implement cloud delete - throw new Error('Cloud storage not yet implemented'); - } - - async listLevels(): Promise { - // TODO: Implement cloud list - throw new Error('Cloud storage not yet implemented'); - } - - async isAvailable(): Promise { - // TODO: Implement cloud connectivity check - return false; - } - - async getSyncMetadata(_levelId: string): Promise { - // TODO: Implement sync metadata fetch - throw new Error('Cloud storage not yet implemented'); - } - - /** - * Authenticate with cloud service - */ - async authenticate(token: string): Promise { - this.authToken = token; - // TODO: Implement authentication - return false; - } - - /** - * Sync local level to cloud - */ - async syncToCloud(_levelId: string, _config: LevelConfig): Promise { - // TODO: Implement sync to cloud - throw new Error('Cloud storage not yet implemented'); - } - - /** - * Sync cloud level to local - */ - async syncFromCloud(_levelId: string): Promise { - // 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 { - // TODO: Implement conflict resolution - throw new Error('Cloud storage not yet implemented'); - } -} diff --git a/src/ship/shipEngine.ts b/src/ship/shipEngine.ts deleted file mode 100644 index 5e42de2..0000000 --- a/src/ship/shipEngine.ts +++ /dev/null @@ -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; - - } -} \ No newline at end of file diff --git a/src/ui/screens/controlsScreen.ts b/src/ui/screens/controlsScreen.ts deleted file mode 100644 index bd37195..0000000 --- a/src/ui/screens/controlsScreen.ts +++ /dev/null @@ -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; - } -} diff --git a/src/ui/screens/settingsScreen.ts b/src/ui/screens/settingsScreen.ts deleted file mode 100644 index 6172c4a..0000000 --- a/src/ui/screens/settingsScreen.ts +++ /dev/null @@ -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); - } -} diff --git a/src/utils/scoreEvent.ts b/src/utils/scoreEvent.ts deleted file mode 100644 index e58cdf1..0000000 --- a/src/utils/scoreEvent.ts +++ /dev/null @@ -1,4 +0,0 @@ -export type ScoreEvent = { - score: number, - message: string -} \ No newline at end of file