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