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:
parent
b4baa2beba
commit
8570c22a0c
BIN
public/HavokPhysics.wasm
Normal file
BIN
public/HavokPhysics.wasm
Normal file
Binary file not shown.
@ -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>
|
||||
@ -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",
|
||||
],
|
||||
};
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
@ -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 };
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
@ -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)}%`;
|
||||
}
|
||||
}
|
||||
@ -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');
|
||||
}
|
||||
}
|
||||
@ -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;
|
||||
|
||||
}
|
||||
}
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
@ -1,4 +0,0 @@
|
||||
export type ScoreEvent = {
|
||||
score: number,
|
||||
message: string
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user