= new Map();
-
- constructor() {
- this.loadSavedLevels();
- this.setupEventListeners();
- this.loadPreset('captain'); // Default to captain difficulty
- this.renderSavedLevelsList();
- }
-
- private setupEventListeners() {
- // Preset buttons
- const presetButtons = document.querySelectorAll('.preset-btn');
- presetButtons.forEach(btn => {
- btn.addEventListener('click', (e) => {
- const difficulty = (e.target as HTMLButtonElement).dataset.difficulty;
- this.loadPreset(difficulty);
-
- // Update active state
- presetButtons.forEach(b => b.classList.remove('active'));
- (e.target as HTMLElement).classList.add('active');
- });
- });
-
- // Difficulty dropdown
- const difficultySelect = document.getElementById('difficulty') as HTMLSelectElement;
- difficultySelect.addEventListener('change', (e) => {
- this.loadPreset((e.target as HTMLSelectElement).value);
- });
-
- // Generate button - now saves to localStorage
- document.getElementById('generateBtn')?.addEventListener('click', () => {
- this.generateLevel();
- this.saveToLocalStorage();
- });
-
- // Download button
- document.getElementById('downloadBtn')?.addEventListener('click', () => {
- this.downloadJSON();
- });
-
- // Copy button
- document.getElementById('copyBtn')?.addEventListener('click', () => {
- this.copyToClipboard();
- });
-
- // Save edited JSON button
- document.getElementById('saveEditedJsonBtn')?.addEventListener('click', () => {
- this.saveEditedJSON();
- });
-
- // Validate JSON button
- document.getElementById('validateJsonBtn')?.addEventListener('click', () => {
- this.validateJSON();
- });
- }
-
- /**
- * Load saved levels from localStorage
- */
- private loadSavedLevels(): void {
- try {
- const stored = localStorage.getItem(STORAGE_KEY);
- if (stored) {
- const levelsArray: [string, LevelConfig][] = JSON.parse(stored);
- this.savedLevels = new Map(levelsArray);
- debugLog(`Loaded ${this.savedLevels.size} saved levels from localStorage`);
- }
- } catch (error) {
- console.error('Failed to load saved levels:', error);
- this.savedLevels = new Map();
- }
- }
-
- /**
- * Save current level to localStorage
- */
- private saveToLocalStorage(): void {
- if (!this.currentConfig) {
- alert('Please generate a level configuration first!');
- return;
- }
-
- const levelName = (document.getElementById('levelName') as HTMLInputElement).value ||
- `${this.currentConfig.difficulty}-${Date.now()}`;
-
- // Save to map
- this.savedLevels.set(levelName, this.currentConfig);
-
- // Convert Map to array for storage
- const levelsArray = Array.from(this.savedLevels.entries());
- localStorage.setItem(STORAGE_KEY, JSON.stringify(levelsArray));
-
- debugLog(`Saved level: ${levelName}`);
- this.renderSavedLevelsList();
-
- // Show feedback
- const feedback = document.createElement('div');
- feedback.textContent = `✓ Saved "${levelName}" to local storage`;
- feedback.style.cssText = `
- position: fixed;
- top: 20px;
- right: 20px;
- background: #4CAF50;
- color: white;
- padding: 15px 25px;
- border-radius: 5px;
- box-shadow: 0 4px 6px rgba(0,0,0,0.3);
- z-index: 10000;
- animation: slideIn 0.3s ease-out;
- `;
- document.body.appendChild(feedback);
- setTimeout(() => {
- feedback.remove();
- }, 3000);
- }
-
- /**
- * Delete a saved level
- */
- private deleteSavedLevel(levelName: string): void {
- if (confirm(`Delete "${levelName}"?`)) {
- this.savedLevels.delete(levelName);
- const levelsArray = Array.from(this.savedLevels.entries());
- localStorage.setItem(STORAGE_KEY, JSON.stringify(levelsArray));
- this.renderSavedLevelsList();
- debugLog(`Deleted level: ${levelName}`);
- }
- }
-
- /**
- * Load a saved level into the editor
- */
- private loadSavedLevel(levelName: string): void {
- const config = this.savedLevels.get(levelName);
- if (!config) {
- alert('Level not found!');
- return;
- }
-
- this.currentConfig = config;
-
- // Populate form with saved values
- (document.getElementById('levelName') as HTMLInputElement).value = levelName;
- (document.getElementById('difficulty') as HTMLSelectElement).value = config.difficulty;
-
- if (config.metadata?.author) {
- (document.getElementById('author') as HTMLInputElement).value = config.metadata.author;
- }
- if (config.metadata?.description) {
- (document.getElementById('description') as HTMLInputElement).value = config.metadata.description;
- }
-
- // Ship
- (document.getElementById('shipX') as HTMLInputElement).value = config.ship.position[0].toString();
- (document.getElementById('shipY') as HTMLInputElement).value = config.ship.position[1].toString();
- (document.getElementById('shipZ') as HTMLInputElement).value = config.ship.position[2].toString();
-
- // Start base
- (document.getElementById('baseX') as HTMLInputElement).value = config.startBase.position[0].toString();
- (document.getElementById('baseY') as HTMLInputElement).value = config.startBase.position[1].toString();
- (document.getElementById('baseZ') as HTMLInputElement).value = config.startBase.position[2].toString();
- (document.getElementById('baseGlbPath') as HTMLInputElement).value = config.startBase.baseGlbPath || 'base.glb';
-
- // Sun
- (document.getElementById('sunX') as HTMLInputElement).value = config.sun.position[0].toString();
- (document.getElementById('sunY') as HTMLInputElement).value = config.sun.position[1].toString();
- (document.getElementById('sunZ') as HTMLInputElement).value = config.sun.position[2].toString();
- (document.getElementById('sunDiameter') as HTMLInputElement).value = config.sun.diameter.toString();
-
- // Planets
- (document.getElementById('planetCount') as HTMLInputElement).value = config.planets.length.toString();
-
- // Asteroids (use difficulty config if available)
- if (config.difficultyConfig) {
- (document.getElementById('asteroidCount') as HTMLInputElement).value = config.difficultyConfig.rockCount.toString();
- (document.getElementById('forceMultiplier') as HTMLInputElement).value = config.difficultyConfig.forceMultiplier.toString();
- (document.getElementById('asteroidMinSize') as HTMLInputElement).value = config.difficultyConfig.rockSizeMin.toString();
- (document.getElementById('asteroidMaxSize') as HTMLInputElement).value = config.difficultyConfig.rockSizeMax.toString();
- (document.getElementById('asteroidMinDist') as HTMLInputElement).value = config.difficultyConfig.distanceMin.toString();
- (document.getElementById('asteroidMaxDist') as HTMLInputElement).value = config.difficultyConfig.distanceMax.toString();
- }
-
- // Display the JSON
- this.displayJSON();
-
- debugLog(`Loaded level: ${levelName}`);
- }
-
- /**
- * Render the list of saved levels
- */
- private renderSavedLevelsList(): void {
- const container = document.getElementById('savedLevelsList');
- if (!container) return;
-
- if (this.savedLevels.size === 0) {
- container.innerHTML = 'No saved levels yet. Generate a level to save it.
';
- return;
- }
-
- let html = '';
-
- for (const [name, config] of this.savedLevels.entries()) {
- const timestamp = config.timestamp ? new Date(config.timestamp).toLocaleString() : 'Unknown';
- html += `
-
-
-
${name}
-
- ${config.difficulty} • ${config.asteroids.length} asteroids • ${timestamp}
-
-
-
-
-
-
-
- `;
- }
-
- html += '
';
- container.innerHTML = html;
-
- // Add event listeners to load/delete buttons
- container.querySelectorAll('.load-level-btn').forEach(btn => {
- btn.addEventListener('click', (e) => {
- const levelName = (e.target as HTMLButtonElement).dataset.level;
- if (levelName) this.loadSavedLevel(levelName);
- });
- });
-
- container.querySelectorAll('.delete-level-btn').forEach(btn => {
- btn.addEventListener('click', (e) => {
- const levelName = (e.target as HTMLButtonElement).dataset.level;
- if (levelName) this.deleteSavedLevel(levelName);
- });
- });
- }
-
- /**
- * Load a difficulty preset into the form
- */
- private loadPreset(difficulty: string) {
- const difficultyConfig = this.getDifficultyConfig(difficulty);
-
- // Update difficulty dropdown
- (document.getElementById('difficulty') as HTMLSelectElement).value = difficulty;
-
- // Update asteroid settings based on difficulty
- (document.getElementById('asteroidCount') as HTMLInputElement).value = difficultyConfig.rockCount.toString();
- (document.getElementById('forceMultiplier') as HTMLInputElement).value = difficultyConfig.forceMultiplier.toString();
- (document.getElementById('asteroidMinSize') as HTMLInputElement).value = difficultyConfig.rockSizeMin.toString();
- (document.getElementById('asteroidMaxSize') as HTMLInputElement).value = difficultyConfig.rockSizeMax.toString();
- (document.getElementById('asteroidMinDist') as HTMLInputElement).value = difficultyConfig.distanceMin.toString();
- (document.getElementById('asteroidMaxDist') as HTMLInputElement).value = difficultyConfig.distanceMax.toString();
- }
-
- /**
- * Get difficulty configuration
- */
- private getDifficultyConfig(difficulty: string): DifficultyConfig {
- switch (difficulty) {
- case 'recruit':
- return {
- rockCount: 5,
- forceMultiplier: .5,
- rockSizeMin: 10,
- rockSizeMax: 15,
- distanceMin: 80,
- distanceMax: 100
- };
- case 'pilot':
- return {
- rockCount: 10,
- forceMultiplier: 1,
- rockSizeMin: 8,
- rockSizeMax: 12,
- distanceMin: 80,
- distanceMax: 150
- };
- case 'captain':
- return {
- rockCount: 20,
- forceMultiplier: 1.2,
- rockSizeMin: 2,
- rockSizeMax: 7,
- distanceMin: 100,
- distanceMax: 250
- };
- case 'commander':
- return {
- rockCount: 50,
- forceMultiplier: 1.3,
- rockSizeMin: 2,
- rockSizeMax: 8,
- distanceMin: 90,
- distanceMax: 280
- };
- case 'test':
- return {
- rockCount: 100,
- forceMultiplier: 0.3,
- rockSizeMin: 8,
- rockSizeMax: 15,
- distanceMin: 150,
- distanceMax: 200
- };
- default:
- return {
- rockCount: 5,
- forceMultiplier: 1.0,
- rockSizeMin: 4,
- rockSizeMax: 8,
- distanceMin: 170,
- distanceMax: 220
- };
- }
- }
-
- /**
- * Read form values and generate level configuration
- */
- private generateLevel() {
- const difficulty = (document.getElementById('difficulty') as HTMLSelectElement).value;
- const levelName = (document.getElementById('levelName') as HTMLInputElement).value || difficulty;
- const author = (document.getElementById('author') as HTMLInputElement).value;
- const description = (document.getElementById('description') as HTMLInputElement).value;
-
- // Create a custom generator with modified parameters
- const generator = new CustomLevelGenerator(difficulty);
-
- // Override ship position
- generator.shipPosition = [
- parseFloat((document.getElementById('shipX') as HTMLInputElement).value),
- parseFloat((document.getElementById('shipY') as HTMLInputElement).value),
- parseFloat((document.getElementById('shipZ') as HTMLInputElement).value)
- ];
-
- // Note: startBase is no longer generated by default
-
- // Override sun
- generator.sunPosition = [
- parseFloat((document.getElementById('sunX') as HTMLInputElement).value),
- parseFloat((document.getElementById('sunY') as HTMLInputElement).value),
- parseFloat((document.getElementById('sunZ') as HTMLInputElement).value)
- ];
- generator.sunDiameter = parseFloat((document.getElementById('sunDiameter') as HTMLInputElement).value);
-
- // Override planet generation params
- generator.planetCount = parseInt((document.getElementById('planetCount') as HTMLInputElement).value);
- generator.planetMinDiameter = parseFloat((document.getElementById('planetMinDiam') as HTMLInputElement).value);
- generator.planetMaxDiameter = parseFloat((document.getElementById('planetMaxDiam') as HTMLInputElement).value);
- generator.planetMinDistance = parseFloat((document.getElementById('planetMinDist') as HTMLInputElement).value);
- generator.planetMaxDistance = parseFloat((document.getElementById('planetMaxDist') as HTMLInputElement).value);
-
- // Override asteroid generation params
- const customDifficulty: DifficultyConfig = {
- rockCount: parseInt((document.getElementById('asteroidCount') as HTMLInputElement).value),
- forceMultiplier: parseFloat((document.getElementById('forceMultiplier') as HTMLInputElement).value),
- rockSizeMin: parseFloat((document.getElementById('asteroidMinSize') as HTMLInputElement).value),
- rockSizeMax: parseFloat((document.getElementById('asteroidMaxSize') as HTMLInputElement).value),
- distanceMin: parseFloat((document.getElementById('asteroidMinDist') as HTMLInputElement).value),
- distanceMax: parseFloat((document.getElementById('asteroidMaxDist') as HTMLInputElement).value)
- };
- generator.setDifficultyConfig(customDifficulty);
-
- // Generate the config
- this.currentConfig = generator.generate();
-
- // Add metadata
- if (author) {
- this.currentConfig.metadata = this.currentConfig.metadata || {};
- this.currentConfig.metadata.author = author;
- }
- if (description) {
- this.currentConfig.metadata = this.currentConfig.metadata || {};
- this.currentConfig.metadata.description = description;
- }
-
- // Display the JSON
- this.displayJSON();
- }
-
- /**
- * Display generated JSON in the output section
- */
- private displayJSON() {
- if (!this.currentConfig) return;
-
- const outputSection = document.getElementById('outputSection');
- const jsonEditor = document.getElementById('jsonEditor') as HTMLTextAreaElement;
-
- if (outputSection && jsonEditor) {
- const jsonString = JSON.stringify(this.currentConfig, null, 2);
- jsonEditor.value = jsonString;
- outputSection.style.display = 'block';
-
- // Scroll to output
- outputSection.scrollIntoView({ behavior: 'smooth' });
- }
- }
-
- /**
- * Validate the JSON in the editor
- */
- private validateJSON(): boolean {
- const jsonEditor = document.getElementById('jsonEditor') as HTMLTextAreaElement;
- const messageDiv = document.getElementById('jsonValidationMessage');
-
- if (!jsonEditor || !messageDiv) return false;
-
- try {
- const json = jsonEditor.value;
- const parsed = JSON.parse(json);
-
- // Validate against schema
- const validation = validateLevelConfig(parsed);
-
- if (validation.valid) {
- messageDiv.innerHTML = '✓ JSON is valid!
';
- return true;
- } else {
- messageDiv.innerHTML = `
- Validation Errors:
- ${validation.errors.map(e => `• ${e}`).join('
')}
-
`;
- return false;
- }
- } catch (error) {
- messageDiv.innerHTML = `
- JSON Parse Error:
- ${error.message}
-
`;
- return false;
- }
- }
-
- /**
- * Save edited JSON from the editor
- */
- private saveEditedJSON() {
- const jsonEditor = document.getElementById('jsonEditor') as HTMLTextAreaElement;
- const messageDiv = document.getElementById('jsonValidationMessage');
-
- if (!jsonEditor) {
- alert('JSON editor not found!');
- return;
- }
-
- // First validate
- if (!this.validateJSON()) {
- messageDiv.innerHTML += 'Please fix validation errors before saving.
';
- return;
- }
-
- try {
- const json = jsonEditor.value;
- const config = JSON.parse(json) as LevelConfig;
-
- // Update current config
- this.currentConfig = config;
-
- // Save to localStorage
- this.saveToLocalStorage();
-
- // Update message
- messageDiv.innerHTML = '✓ Edited JSON saved successfully!
';
-
- debugLog('Saved edited JSON');
- } catch (error) {
- alert(`Failed to save: ${error.message}`);
- }
- }
-
- /**
- * Download the current configuration as JSON file
- */
- private downloadJSON() {
- if (!this.currentConfig) {
- alert('Please generate a level configuration first!');
- return;
- }
-
- const levelName = (document.getElementById('levelName') as HTMLInputElement).value ||
- this.currentConfig.difficulty;
- const filename = `level-${levelName}-${Date.now()}.json`;
-
- const json = JSON.stringify(this.currentConfig, null, 2);
- const blob = new Blob([json], { type: 'application/json' });
- const url = URL.createObjectURL(blob);
-
- const a = document.createElement('a');
- a.href = url;
- a.download = filename;
- document.body.appendChild(a);
- a.click();
- document.body.removeChild(a);
- URL.revokeObjectURL(url);
-
- debugLog(`Downloaded: ${filename}`);
- }
-
- /**
- * Copy current configuration JSON to clipboard
- */
- private async copyToClipboard() {
- if (!this.currentConfig) {
- alert('Please generate a level configuration first!');
- return;
- }
-
- const json = JSON.stringify(this.currentConfig, null, 2);
-
- try {
- await navigator.clipboard.writeText(json);
- alert('JSON copied to clipboard!');
- } catch (err) {
- console.error('Failed to copy:', err);
- alert('Failed to copy to clipboard. Please copy manually from the output.');
- }
- }
-}
-
-/**
- * Custom level generator that allows overriding default values
- * Simply extends LevelGenerator - all properties are now public on the base class
- */
-class CustomLevelGenerator extends LevelGenerator {
- // No need to duplicate anything - just use the public properties from base class
- // Properties like shipPosition, startBasePosition, etc. are already defined and public in LevelGenerator
-}
-
-// Initialize the editor when this module is loaded
-if (!(window as any).__levelEditorInstance) {
- (window as any).__levelEditorInstance = new LevelEditor();
-}
-
-/**
- * Helper to get all saved levels from localStorage
- */
-export function getSavedLevels(): Map {
- try {
- const stored = localStorage.getItem(STORAGE_KEY);
- if (stored) {
- const levelsArray: [string, LevelConfig][] = JSON.parse(stored);
- return new Map(levelsArray);
- }
- } catch (error) {
- console.error('Failed to load saved levels:', error);
- }
- return new Map();
-}
-
-/**
- * Helper to get a specific saved level by name
- */
-export function getSavedLevel(name: string): LevelConfig | null {
- const levels = getSavedLevels();
- return levels.get(name) || null;
-}
-
-/**
- * Generate a simple rookie level with 4 asteroids
- * Asteroids at 100-200 distance with 20-100 tangential velocities
- */
-function generateSimpleRookieLevel(): void {
- debugLog('Creating simple rookie level with 4 asteroids...');
-
- const levelsMap = new Map();
-
- // Create base level structure
- const config: LevelConfig = {
- version: "1.0",
- difficulty: "rookie",
- timestamp: new Date().toISOString(),
- metadata: {
- author: 'System',
- description: 'Simple rookie training mission with 4 asteroids',
- type: 'default'
- },
- ship: {
- position: [0, 1, 0],
- rotation: [0, 0, 0],
- linearVelocity: [0, 0, 0],
- angularVelocity: [0, 0, 0]
- },
- startBase: {
- position: [0, 0, 0],
- baseGlbPath: 'base.glb'
- },
- sun: {
- position: [0, 0, 400],
- diameter: 50,
- intensity: 1000000
- },
- planets: [],
- asteroids: [],
- difficultyConfig: {
- rockCount: 4,
- forceMultiplier: 1.0,
- rockSizeMin: 3,
- rockSizeMax: 5,
- distanceMin: 100,
- distanceMax: 200
- }
- };
-
- // Generate 4 asteroids with tangential velocities
- const basePosition = [0, 0, 0]; // Start base position
-
- for (let i = 0; i < 4; i++) {
- // Random distance between 100-200
- const distance = 100 + Math.random() * 100;
-
- // Random angle around the base
- const angle = (Math.PI * 2 / 4) * i + (Math.random() - 0.5) * 0.5;
-
- // Position at distance and angle
- const x = basePosition[0] + distance * Math.cos(angle);
- const z = basePosition[2] + distance * Math.sin(angle);
- const y = basePosition[1] + (Math.random() - 0.5) * 20; // Some vertical variation
-
- // Calculate tangent direction (perpendicular to radial)
- const tangentX = -Math.sin(angle);
- const tangentZ = Math.cos(angle);
-
- // Random tangential speed between 20-100
- const speed = 20 + Math.random() * 80;
-
- const linearVelocity: Vector3Array = [
- tangentX * speed,
- (Math.random() - 0.5) * 10, // Small vertical velocity
- tangentZ * speed
- ];
-
- // Random size between min and max
- const scale = 3 + Math.random() * 2;
-
- // Random rotation
- const angularVelocity: Vector3Array = [
- (Math.random() - 0.5) * 2,
- (Math.random() - 0.5) * 2,
- (Math.random() - 0.5) * 2
- ];
-
- config.asteroids.push({
- id: `asteroid-${i}`,
- position: [x, y, z],
- scale,
- linearVelocity,
- angularVelocity
- });
- }
-
- levelsMap.set('Rookie Training', config);
- debugLog('Generated simple rookie level with 4 asteroids');
-
- // Save to localStorage
- const levelsArray = Array.from(levelsMap.entries());
- localStorage.setItem(STORAGE_KEY, JSON.stringify(levelsArray));
- debugLog('Simple rookie level saved to localStorage');
-}
-
-/**
- * Generate default levels if localStorage is empty
- * Creates either a simple rookie level or 6 themed levels based on progression flag
- */
-export function generateDefaultLevels(): void {
- const existing = getSavedLevels();
- if (existing.size > 0) {
- debugLog('Levels already exist in localStorage, skipping default generation');
- return;
- }
-
- // Check progression flag from GameConfig
- const GameConfig = (window as any).GameConfig;
- const progressionEnabled = GameConfig?.getInstance().progressionEnabled ?? false;
-
- if (!progressionEnabled) {
- debugLog('Progression disabled - generating simple rookie level...');
- generateSimpleRookieLevel();
- return;
- }
-
- debugLog('No saved levels found, generating 6 default levels...');
-
- // Define themed default levels with descriptions
- const defaultLevels = [
- {
- name: 'Tutorial: Asteroid Field',
- difficulty: 'recruit',
- description: 'Learn the basics of ship control and asteroid destruction in a calm sector of space.',
- estimatedTime: '3-5 minutes'
- },
- {
- name: 'Rescue Mission',
- difficulty: 'pilot',
- description: 'Clear a path through moderate asteroid density to reach the stranded station.',
- estimatedTime: '5-8 minutes'
- },
- {
- name: 'Deep Space Patrol',
- difficulty: 'captain',
- description: 'Patrol a dangerous sector with heavy asteroid activity. Watch your fuel!',
- estimatedTime: '8-12 minutes'
- },
- {
- name: 'Enemy Territory',
- difficulty: 'commander',
- description: 'Navigate through hostile space with high-speed asteroids and limited resources.',
- estimatedTime: '12-15 minutes'
- },
- {
- name: 'The Gauntlet',
- difficulty: 'commander',
- description: 'Face maximum asteroid density in this ultimate test of piloting skill.',
- estimatedTime: '15-20 minutes'
- },
- {
- name: 'Final Challenge',
- difficulty: 'commander',
- description: 'The ultimate challenge - survive the most chaotic asteroid field in known space.',
- estimatedTime: '20+ minutes'
- }
- ];
-
- const levelsMap = new Map();
-
- for (const level of defaultLevels) {
- const generator = new LevelGenerator(level.difficulty);
- const config = generator.generate();
-
- // Add rich metadata
- config.metadata = {
- author: 'System',
- description: level.description,
- estimatedTime: level.estimatedTime,
- type: 'default',
- difficulty: level.difficulty
- };
-
- levelsMap.set(level.name, config);
- debugLog(`Generated default level: ${level.name} (${level.difficulty})`);
- }
-
- // Save all levels to localStorage
- const levelsArray = Array.from(levelsMap.entries());
- localStorage.setItem(STORAGE_KEY, JSON.stringify(levelsArray));
-
- debugLog(`${defaultLevels.length} default levels saved to localStorage`);
-}
-
-// Export for manual initialization if needed
-export { LevelEditor, CustomLevelGenerator };
diff --git a/src/levels/generation/levelGenerator.ts b/src/levels/generation/levelGenerator.ts
deleted file mode 100644
index e393ec2..0000000
--- a/src/levels/generation/levelGenerator.ts
+++ /dev/null
@@ -1,281 +0,0 @@
-import {
- LevelConfig,
- ShipConfig,
- StartBaseConfig,
- SunConfig,
- PlanetConfig,
- AsteroidConfig,
- DifficultyConfig,
- Vector3Array
-} from "../config/levelConfig";
-import { getRandomPlanetTexture } from "../../environment/celestial/planetTextures";
-
-/**
- * Generates procedural level configurations matching the current Level1 generation logic
- */
-export class LevelGenerator {
- protected _difficulty: string;
- protected _difficultyConfig: DifficultyConfig;
-
- // Configurable properties (can be overridden by subclasses or set before generate())
- public shipPosition: Vector3Array = [0, 1, 0];
-
- public sunPosition: Vector3Array = [0, 0, 400];
- public sunDiameter = 50;
- public sunIntensity = 1000000;
-
- // Planet generation parameters
- public planetCount = 12;
- public planetMinDiameter = 100;
- public planetMaxDiameter = 200;
- public planetMinDistance = 1000;
- public planetMaxDistance = 2000;
-
- constructor(difficulty: string) {
- this._difficulty = difficulty;
- this._difficultyConfig = this.getDifficultyConfig(difficulty);
- }
-
- /**
- * Set custom difficulty configuration
- */
- public setDifficultyConfig(config: DifficultyConfig) {
- this._difficultyConfig = config;
- }
-
- /**
- * Generate a complete level configuration
- */
- public generate(): LevelConfig {
- const ship = this.generateShip();
- const sun = this.generateSun();
- const planets = this.generatePlanets();
- const asteroids = this.generateAsteroids();
-
- return {
- version: "1.0",
- difficulty: this._difficulty,
- timestamp: new Date().toISOString(),
- metadata: {
- generator: "LevelGenerator",
- description: `Procedurally generated ${this._difficulty} level`
- },
- ship,
- // startBase is now optional and not generated
- sun,
- planets,
- asteroids,
- difficultyConfig: this._difficultyConfig
- };
- }
-
- private generateShip(): ShipConfig {
- return {
- position: [...this.shipPosition],
- rotation: [0, 0, 0],
- linearVelocity: [0, 0, 0],
- angularVelocity: [0, 0, 0]
- };
- }
-
- private generateSun(): SunConfig {
- return {
- position: [...this.sunPosition],
- diameter: this.sunDiameter,
- intensity: this.sunIntensity
- };
- }
-
- /**
- * Generate planets in orbital pattern (matching createPlanetsOrbital logic)
- */
- private generatePlanets(): PlanetConfig[] {
- const planets: PlanetConfig[] = [];
-
- for (let i = 0; i < this.planetCount; i++) {
- // Random diameter between min and max
- const diameter = this.planetMinDiameter +
- Math.random() * (this.planetMaxDiameter - this.planetMinDiameter);
-
- // Random distance from sun
- const distance = this.planetMinDistance +
- Math.random() * (this.planetMaxDistance - this.planetMinDistance);
-
- // Random angle around Y axis (orbital plane)
- const angle = Math.random() * Math.PI * 2;
-
- // Small vertical variation (like a solar system)
- const y = (Math.random() - 0.5) * 400;
-
- const position: Vector3Array = [
- this.sunPosition[0] + distance * Math.cos(angle),
- this.sunPosition[1] + y,
- this.sunPosition[2] + distance * Math.sin(angle)
- ];
-
- planets.push({
- name: `planet-${i}`,
- position,
- diameter,
- texturePath: getRandomPlanetTexture(),
- rotation: [0, 0, 0]
- });
- }
-
- return planets;
- }
-
- /**
- * Generate asteroids distributed evenly around the base in a spherical pattern (all 3 axes)
- */
- private generateAsteroids(): AsteroidConfig[] {
- const asteroids: AsteroidConfig[] = [];
- const config = this._difficultyConfig;
-
- for (let i = 0; i < config.rockCount; i++) {
- // Random distance from start base
- const distRange = config.distanceMax - config.distanceMin;
- const dist = (Math.random() * distRange) + config.distanceMin;
-
- // Evenly distribute asteroids on a sphere using spherical coordinates
- // Azimuth angle (phi): rotation around Y axis
- const phi = (i / config.rockCount) * Math.PI * 2;
-
- // Elevation angle (theta): angle from top (0) to bottom (π)
- // Using equal area distribution: acos(1 - 2*u) where u is [0,1]
- const u = (i + 0.5) / config.rockCount;
- const theta = Math.acos(1 - 2 * u);
-
- // Add small random variations to prevent perfect spacing
- const phiVariation = (Math.random() - 0.5) * 0.3; // ±0.15 radians
- const thetaVariation = (Math.random() - 0.5) * 0.3; // ±0.15 radians
- const finalPhi = phi + phiVariation;
- const finalTheta = theta + thetaVariation;
-
- // Convert spherical to Cartesian coordinates
- const x = dist * Math.sin(finalTheta) * Math.cos(finalPhi);
- const y = dist * Math.cos(finalTheta);
- const z = dist * Math.sin(finalTheta) * Math.sin(finalPhi);
-
- const position: Vector3Array = [x, y, z];
-
- // Random size (uniform scale)
- const sizeRange = config.rockSizeMax - config.rockSizeMin;
- const scale = Math.random() * sizeRange + config.rockSizeMin;
-
- // Calculate initial velocity based on force applied in Level1
- // Velocity should be tangential to the sphere (perpendicular to radius)
- const forceMagnitude = 50000000 * config.forceMultiplier;
- const mass = 10000;
- const velocityMagnitude = forceMagnitude / mass / 100; // Approximation
-
- // Tangential velocity: use cross product of radius with an arbitrary vector
- // to get perpendicular direction, then rotate around radius
- // Simple approach: velocity perpendicular to radius in a tangent plane
- const vx = -velocityMagnitude * Math.sin(finalPhi);
- const vy = 0;
- const vz = velocityMagnitude * Math.cos(finalPhi);
-
- const linearVelocity: Vector3Array = [vx, vy, vz];
-
- asteroids.push({
- id: `asteroid-${i}`,
- position,
- scale,
- linearVelocity,
- angularVelocity: [0, 0, 0],
- mass
- });
- }
-
- return asteroids;
- }
-
- /**
- * Get difficulty configuration (matching Level1.getDifficultyConfig)
- */
- private getDifficultyConfig(difficulty: string): DifficultyConfig {
- switch (difficulty) {
- case 'recruit':
- return {
- rockCount: 5,
- forceMultiplier: .8,
- rockSizeMin: 10,
- rockSizeMax: 15,
- distanceMin: 220,
- distanceMax: 250
- };
- case 'pilot':
- return {
- rockCount: 10,
- forceMultiplier: 1,
- rockSizeMin: 8,
- rockSizeMax: 20,
- distanceMin: 225,
- distanceMax: 300
- };
- case 'captain':
- return {
- rockCount: 20,
- forceMultiplier: 1.2,
- rockSizeMin: 5,
- rockSizeMax: 40,
- distanceMin: 230,
- distanceMax: 450
- };
- case 'commander':
- return {
- rockCount: 50,
- forceMultiplier: 1.3,
- rockSizeMin: 2,
- rockSizeMax: 8,
- distanceMin: 90,
- distanceMax: 280
- };
- case 'test':
- return {
- rockCount: 100,
- forceMultiplier: 0.3,
- rockSizeMin: 8,
- rockSizeMax: 15,
- distanceMin: 150,
- distanceMax: 200
- };
- default:
- return {
- rockCount: 5,
- forceMultiplier: 1.0,
- rockSizeMin: 4,
- rockSizeMax: 8,
- distanceMin: 170,
- distanceMax: 220
- };
- }
- }
-
- /**
- * Static helper to generate and save a level to JSON string
- */
- public static generateJSON(difficulty: string): string {
- const generator = new LevelGenerator(difficulty);
- const config = generator.generate();
- return JSON.stringify(config, null, 2);
- }
-
- /**
- * Static helper to generate and trigger download of level JSON
- */
- public static downloadJSON(difficulty: string, filename?: string): void {
- const json = LevelGenerator.generateJSON(difficulty);
- const blob = new Blob([json], { type: 'application/json' });
- const url = URL.createObjectURL(blob);
-
- const a = document.createElement('a');
- a.href = url;
- a.download = filename || `level-${difficulty}-${Date.now()}.json`;
- document.body.appendChild(a);
- a.click();
- document.body.removeChild(a);
- URL.revokeObjectURL(url);
- }
-}
diff --git a/src/levels/stats/levelStats.ts b/src/levels/stats/levelStats.ts
deleted file mode 100644
index 0653f1d..0000000
--- a/src/levels/stats/levelStats.ts
+++ /dev/null
@@ -1,381 +0,0 @@
-/**
- * Completion record for a single play-through
- */
-export interface LevelCompletion {
- timestamp: Date;
- completionTimeSeconds: number;
- score?: number;
- survived: boolean; // false if player died/quit
-}
-
-/**
- * Aggregated statistics for a level
- */
-export interface LevelStatistics {
- levelId: string;
- firstPlayed?: Date;
- lastPlayed?: Date;
- completions: LevelCompletion[];
- totalAttempts: number; // Including incomplete attempts
- totalCompletions: number; // Only successful completions
- bestTimeSeconds?: number;
- averageTimeSeconds?: number;
- bestScore?: number;
- averageScore?: number;
- completionRate: number; // percentage (0-100)
- difficultyRating?: number; // 1-5 stars, user-submitted
-}
-
-const STATS_STORAGE_KEY = 'space-game-level-stats';
-
-/**
- * Manages level performance statistics and ratings
- */
-export class LevelStatsManager {
- private static instance: LevelStatsManager | null = null;
-
- private statsMap: Map = new Map();
-
- private constructor() {
- this.loadStats();
- }
-
- public static getInstance(): LevelStatsManager {
- if (!LevelStatsManager.instance) {
- LevelStatsManager.instance = new LevelStatsManager();
- }
- return LevelStatsManager.instance;
- }
-
- /**
- * Load stats from localStorage
- */
- private loadStats(): void {
- const stored = localStorage.getItem(STATS_STORAGE_KEY);
- if (!stored) {
- return;
- }
-
- try {
- const statsArray: [string, LevelStatistics][] = JSON.parse(stored);
-
- for (const [id, stats] of statsArray) {
- // Parse date strings back to Date objects
- if (stats.firstPlayed && typeof stats.firstPlayed === 'string') {
- stats.firstPlayed = new Date(stats.firstPlayed);
- }
- if (stats.lastPlayed && typeof stats.lastPlayed === 'string') {
- stats.lastPlayed = new Date(stats.lastPlayed);
- }
-
- // Parse completion timestamps
- stats.completions = stats.completions.map(c => ({
- ...c,
- timestamp: typeof c.timestamp === 'string' ? new Date(c.timestamp) : c.timestamp
- }));
-
- this.statsMap.set(id, stats);
- }
- } catch (error) {
- console.error('Failed to load level stats:', error);
- }
- }
-
- /**
- * Save stats to localStorage
- */
- private saveStats(): void {
- const statsArray = Array.from(this.statsMap.entries());
- localStorage.setItem(STATS_STORAGE_KEY, JSON.stringify(statsArray));
- }
-
- /**
- * Get statistics for a level
- */
- public getStats(levelId: string): LevelStatistics | undefined {
- return this.statsMap.get(levelId);
- }
-
- /**
- * Initialize stats for a level if not exists
- */
- private ensureStatsExist(levelId: string): LevelStatistics {
- let stats = this.statsMap.get(levelId);
- if (!stats) {
- stats = {
- levelId,
- completions: [],
- totalAttempts: 0,
- totalCompletions: 0,
- completionRate: 0
- };
- this.statsMap.set(levelId, stats);
- }
- return stats;
- }
-
- /**
- * Record that a level was started (attempt)
- */
- public recordAttempt(levelId: string): void {
- const stats = this.ensureStatsExist(levelId);
- stats.totalAttempts++;
-
- const now = new Date();
- if (!stats.firstPlayed) {
- stats.firstPlayed = now;
- }
- stats.lastPlayed = now;
-
- this.recalculateStats(stats);
- this.saveStats();
- }
-
- /**
- * Record a level completion
- */
- public recordCompletion(
- levelId: string,
- completionTimeSeconds: number,
- score?: number,
- survived: boolean = true
- ): void {
- const stats = this.ensureStatsExist(levelId);
-
- const completion: LevelCompletion = {
- timestamp: new Date(),
- completionTimeSeconds,
- score,
- survived
- };
-
- stats.completions.push(completion);
-
- if (survived) {
- stats.totalCompletions++;
- }
-
- const now = new Date();
- if (!stats.firstPlayed) {
- stats.firstPlayed = now;
- }
- stats.lastPlayed = now;
-
- this.recalculateStats(stats);
- this.saveStats();
- }
-
- /**
- * Set difficulty rating for a level (1-5 stars)
- */
- public setDifficultyRating(levelId: string, rating: number): void {
- if (rating < 1 || rating > 5) {
- console.warn('Rating must be between 1 and 5');
- return;
- }
-
- const stats = this.ensureStatsExist(levelId);
- stats.difficultyRating = rating;
- this.saveStats();
- }
-
- /**
- * Recalculate aggregated statistics
- */
- private recalculateStats(stats: LevelStatistics): void {
- const successfulCompletions = stats.completions.filter(c => c.survived);
-
- // Completion rate
- stats.completionRate = stats.totalAttempts > 0
- ? (stats.totalCompletions / stats.totalAttempts) * 100
- : 0;
-
- // Time statistics
- if (successfulCompletions.length > 0) {
- const times = successfulCompletions.map(c => c.completionTimeSeconds);
- stats.bestTimeSeconds = Math.min(...times);
- stats.averageTimeSeconds = times.reduce((a, b) => a + b, 0) / times.length;
- } else {
- stats.bestTimeSeconds = undefined;
- stats.averageTimeSeconds = undefined;
- }
-
- // Score statistics
- const completionsWithScore = successfulCompletions.filter(c => c.score !== undefined);
- if (completionsWithScore.length > 0) {
- const scores = completionsWithScore.map(c => c.score!);
- stats.bestScore = Math.max(...scores);
- stats.averageScore = scores.reduce((a, b) => a + b, 0) / scores.length;
- } else {
- stats.bestScore = undefined;
- stats.averageScore = undefined;
- }
- }
-
- /**
- * Get all stats
- */
- public getAllStats(): Map {
- return new Map(this.statsMap);
- }
-
- /**
- * Get stats for multiple levels
- */
- public getStatsForLevels(levelIds: string[]): Map {
- const result = new Map();
- for (const id of levelIds) {
- const stats = this.statsMap.get(id);
- if (stats) {
- result.set(id, stats);
- }
- }
- return result;
- }
-
- /**
- * Get top N fastest completions for a level
- */
- public getTopCompletions(levelId: string, limit: number = 10): LevelCompletion[] {
- const stats = this.statsMap.get(levelId);
- if (!stats) {
- return [];
- }
-
- return stats.completions
- .filter(c => c.survived)
- .sort((a, b) => a.completionTimeSeconds - b.completionTimeSeconds)
- .slice(0, limit);
- }
-
- /**
- * Get recent completions for a level
- */
- public getRecentCompletions(levelId: string, limit: number = 10): LevelCompletion[] {
- const stats = this.statsMap.get(levelId);
- if (!stats) {
- return [];
- }
-
- return [...stats.completions]
- .sort((a, b) => b.timestamp.getTime() - a.timestamp.getTime())
- .slice(0, limit);
- }
-
- /**
- * Delete stats for a level
- */
- public deleteStats(levelId: string): boolean {
- const deleted = this.statsMap.delete(levelId);
- if (deleted) {
- this.saveStats();
- }
- return deleted;
- }
-
- /**
- * Clear all stats (for testing/reset)
- */
- public clearAll(): void {
- this.statsMap.clear();
- localStorage.removeItem(STATS_STORAGE_KEY);
- }
-
- /**
- * Export stats as JSON
- */
- public exportStats(): string {
- const statsArray = Array.from(this.statsMap.entries());
- return JSON.stringify(statsArray, null, 2);
- }
-
- /**
- * Import stats from JSON
- */
- public importStats(jsonString: string): number {
- try {
- const statsArray: [string, LevelStatistics][] = JSON.parse(jsonString);
- let importCount = 0;
-
- for (const [id, stats] of statsArray) {
- // Parse dates
- if (stats.firstPlayed && typeof stats.firstPlayed === 'string') {
- stats.firstPlayed = new Date(stats.firstPlayed);
- }
- if (stats.lastPlayed && typeof stats.lastPlayed === 'string') {
- stats.lastPlayed = new Date(stats.lastPlayed);
- }
- stats.completions = stats.completions.map(c => ({
- ...c,
- timestamp: typeof c.timestamp === 'string' ? new Date(c.timestamp) : c.timestamp
- }));
-
- this.statsMap.set(id, stats);
- importCount++;
- }
-
- this.saveStats();
- return importCount;
- } catch (error) {
- console.error('Failed to import stats:', error);
- throw new Error('Invalid stats JSON format');
- }
- }
-
- /**
- * Get summary statistics across all levels
- */
- public getGlobalSummary(): {
- totalLevelsPlayed: number;
- totalAttempts: number;
- totalCompletions: number;
- averageCompletionRate: number;
- totalPlayTimeSeconds: number;
- } {
- let totalLevelsPlayed = 0;
- let totalAttempts = 0;
- let totalCompletions = 0;
- let totalPlayTimeSeconds = 0;
- let totalCompletionRates = 0;
-
- for (const stats of this.statsMap.values()) {
- if (stats.totalAttempts > 0) {
- totalLevelsPlayed++;
- totalAttempts += stats.totalAttempts;
- totalCompletions += stats.totalCompletions;
- totalCompletionRates += stats.completionRate;
-
- // Sum all completion times
- for (const completion of stats.completions) {
- if (completion.survived) {
- totalPlayTimeSeconds += completion.completionTimeSeconds;
- }
- }
- }
- }
-
- return {
- totalLevelsPlayed,
- totalAttempts,
- totalCompletions,
- averageCompletionRate: totalLevelsPlayed > 0 ? totalCompletionRates / totalLevelsPlayed : 0,
- totalPlayTimeSeconds
- };
- }
-
- /**
- * Format time in MM:SS format
- */
- public static formatTime(seconds: number): string {
- const mins = Math.floor(seconds / 60);
- const secs = Math.floor(seconds % 60);
- return `${mins}:${secs.toString().padStart(2, '0')}`;
- }
-
- /**
- * Format completion rate as percentage
- */
- public static formatCompletionRate(rate: number): string {
- return `${rate.toFixed(1)}%`;
- }
-}
diff --git a/src/levels/storage/ILevelStorageProvider.ts b/src/levels/storage/ILevelStorageProvider.ts
deleted file mode 100644
index c1f63e4..0000000
--- a/src/levels/storage/ILevelStorageProvider.ts
+++ /dev/null
@@ -1,241 +0,0 @@
-import {LevelConfig} from "../config/levelConfig";
-
-/**
- * Sync status for a level
- */
-export enum SyncStatus {
- NotSynced = 'not_synced',
- Syncing = 'syncing',
- Synced = 'synced',
- Conflict = 'conflict',
- Error = 'error'
-}
-
-/**
- * Metadata for synced levels
- */
-export interface SyncMetadata {
- lastSyncedAt?: Date;
- syncStatus: SyncStatus;
- cloudVersion?: string;
- localVersion?: string;
- syncError?: string;
-}
-
-/**
- * Interface for level storage providers (localStorage, cloud, etc.)
- */
-export interface ILevelStorageProvider {
- /**
- * Get a level by ID
- */
- getLevel(levelId: string): Promise;
-
- /**
- * Save a level
- */
- saveLevel(levelId: string, config: LevelConfig): Promise;
-
- /**
- * Delete a level
- */
- deleteLevel(levelId: string): Promise;
-
- /**
- * List all level IDs
- */
- listLevels(): Promise;
-
- /**
- * Check if provider is available/connected
- */
- isAvailable(): Promise;
-
- /**
- * Get sync metadata for a level (if supported)
- */
- getSyncMetadata?(levelId: string): Promise;
-}
-
-/**
- * LocalStorage implementation of level storage provider
- */
-export class LocalStorageProvider implements ILevelStorageProvider {
- private storageKey: string;
-
- constructor(storageKey: string = 'space-game-custom-levels') {
- this.storageKey = storageKey;
- }
-
- async getLevel(levelId: string): Promise {
- const stored = localStorage.getItem(this.storageKey);
- if (!stored) {
- return null;
- }
-
- try {
- const levelsArray: [string, LevelConfig][] = JSON.parse(stored);
- const found = levelsArray.find(([id]) => id === levelId);
- return found ? found[1] : null;
- } catch (error) {
- console.error('Failed to get level from localStorage:', error);
- return null;
- }
- }
-
- async saveLevel(levelId: string, config: LevelConfig): Promise {
- const stored = localStorage.getItem(this.storageKey);
- let levelsArray: [string, LevelConfig][] = [];
-
- if (stored) {
- try {
- levelsArray = JSON.parse(stored);
- } catch (error) {
- console.error('Failed to parse localStorage data:', error);
- }
- }
-
- // Update or add level
- const existingIndex = levelsArray.findIndex(([id]) => id === levelId);
- if (existingIndex >= 0) {
- levelsArray[existingIndex] = [levelId, config];
- } else {
- levelsArray.push([levelId, config]);
- }
-
- localStorage.setItem(this.storageKey, JSON.stringify(levelsArray));
- }
-
- async deleteLevel(levelId: string): Promise {
- const stored = localStorage.getItem(this.storageKey);
- if (!stored) {
- return false;
- }
-
- try {
- const levelsArray: [string, LevelConfig][] = JSON.parse(stored);
- const newArray = levelsArray.filter(([id]) => id !== levelId);
-
- if (newArray.length === levelsArray.length) {
- return false; // Level not found
- }
-
- localStorage.setItem(this.storageKey, JSON.stringify(newArray));
- return true;
- } catch (error) {
- console.error('Failed to delete level from localStorage:', error);
- return false;
- }
- }
-
- async listLevels(): Promise {
- const stored = localStorage.getItem(this.storageKey);
- if (!stored) {
- return [];
- }
-
- try {
- const levelsArray: [string, LevelConfig][] = JSON.parse(stored);
- return levelsArray.map(([id]) => id);
- } catch (error) {
- console.error('Failed to list levels from localStorage:', error);
- return [];
- }
- }
-
- async isAvailable(): Promise {
- try {
- const testKey = '_storage_test_';
- localStorage.setItem(testKey, 'test');
- localStorage.removeItem(testKey);
- return true;
- } catch {
- return false;
- }
- }
-}
-
-/**
- * Cloud storage provider (stub for future implementation)
- *
- * Future implementation could use:
- * - Firebase Firestore
- * - AWS S3 + DynamoDB
- * - Custom backend API
- * - IPFS for decentralized storage
- */
-export class CloudStorageProvider implements ILevelStorageProvider {
- private apiEndpoint: string;
- private authToken?: string;
-
- constructor(apiEndpoint: string, authToken?: string) {
- this.apiEndpoint = apiEndpoint;
- this.authToken = authToken;
- }
-
- async getLevel(_levelId: string): Promise {
- // TODO: Implement cloud fetch
- throw new Error('Cloud storage not yet implemented');
- }
-
- async saveLevel(_levelId: string, _config: LevelConfig): Promise {
- // TODO: Implement cloud save
- throw new Error('Cloud storage not yet implemented');
- }
-
- async deleteLevel(_levelId: string): Promise {
- // TODO: Implement cloud delete
- throw new Error('Cloud storage not yet implemented');
- }
-
- async listLevels(): Promise {
- // TODO: Implement cloud list
- throw new Error('Cloud storage not yet implemented');
- }
-
- async isAvailable(): Promise {
- // TODO: Implement cloud connectivity check
- return false;
- }
-
- async getSyncMetadata(_levelId: string): Promise {
- // TODO: Implement sync metadata fetch
- throw new Error('Cloud storage not yet implemented');
- }
-
- /**
- * Authenticate with cloud service
- */
- async authenticate(token: string): Promise {
- this.authToken = token;
- // TODO: Implement authentication
- return false;
- }
-
- /**
- * Sync local level to cloud
- */
- async syncToCloud(_levelId: string, _config: LevelConfig): Promise {
- // TODO: Implement sync to cloud
- throw new Error('Cloud storage not yet implemented');
- }
-
- /**
- * Sync cloud level to local
- */
- async syncFromCloud(_levelId: string): Promise {
- // TODO: Implement sync from cloud
- throw new Error('Cloud storage not yet implemented');
- }
-
- /**
- * Resolve sync conflicts
- */
- async resolveConflict(
- _levelId: string,
- _strategy: 'use_local' | 'use_cloud' | 'merge'
- ): Promise {
- // TODO: Implement conflict resolution
- throw new Error('Cloud storage not yet implemented');
- }
-}
diff --git a/src/ship/shipEngine.ts b/src/ship/shipEngine.ts
deleted file mode 100644
index 5e42de2..0000000
--- a/src/ship/shipEngine.ts
+++ /dev/null
@@ -1,80 +0,0 @@
-import {
- AbstractMesh, Color3, GlowLayer,
- MeshBuilder,
- ParticleSystem,
- StandardMaterial,
- Texture,
- TransformNode,
- Vector3
-} from "@babylonjs/core";
-import {DefaultScene} from "../core/defaultScene";
-
-type MainEngine = {
- transformNode: TransformNode;
- particleSystem: ParticleSystem;
-}
-export class ShipEngine {
- private _ship: TransformNode;
- private _leftMainEngine: MainEngine;
- private _rightMainEngine: MainEngine;
-
- constructor(ship: TransformNode) {
- this._ship = ship;
- this.initialize();
- }
-
- private initialize() {
-
- this._leftMainEngine = this.createEngine(new Vector3(-.44, .37, -1.1));
- this._rightMainEngine = this.createEngine(new Vector3(.44, .37, -1.1));
- }
- public idle() {
- this._leftMainEngine.particleSystem.emitRate = 1;
- this._rightMainEngine.particleSystem.emitRate = 1;
- }
- public forwardback(value: number) {
-
- if (Math.sign(value) > 0) {
- (this._leftMainEngine.particleSystem.emitter as AbstractMesh).rotation.y = 0;
- (this._rightMainEngine.particleSystem.emitter as AbstractMesh).rotation.y = 0;
- } else {
- (this._leftMainEngine.particleSystem.emitter as AbstractMesh).rotation.y = Math.PI;
- (this._rightMainEngine.particleSystem.emitter as AbstractMesh).rotation.y = Math.PI;
- }
- this._leftMainEngine.particleSystem.emitRate = Math.abs(value) * 10;
- this._rightMainEngine.particleSystem.emitRate = Math.abs(value) * 10;
- }
-
- private createEngine(position: Vector3) : MainEngine{
- const MAIN_ROTATION = Math.PI / 2;
- const engine = new TransformNode("engine", DefaultScene.MainScene);
- engine.parent = this._ship;
- engine.position = position;
- const leftDisc = MeshBuilder.CreateIcoSphere("engineSphere", {radius: .07}, DefaultScene.MainScene);
-
- const material = new StandardMaterial("material", DefaultScene.MainScene);
- material.emissiveColor = new Color3(.5, .5, .1);
- leftDisc.material = material;
- leftDisc.parent = engine;
- leftDisc.rotation.x = MAIN_ROTATION;
- const particleSystem = this.createParticleSystem(leftDisc);
- return {transformNode: engine, particleSystem: particleSystem};
- }
- private createParticleSystem(mesh: AbstractMesh): ParticleSystem {
- const myParticleSystem = new ParticleSystem("particles", 1000, DefaultScene.MainScene);
- myParticleSystem.emitRate = 1;
- //myParticleSystem.minEmitPower = 2;
- //myParticleSystem.maxEmitPower = 10;
-
- myParticleSystem.particleTexture = new Texture("/flare.png");
- myParticleSystem.emitter = mesh;
- const coneEmitter = myParticleSystem.createConeEmitter(0.1, Math.PI / 9);
- myParticleSystem.addSizeGradient(0, .01);
- myParticleSystem.addSizeGradient(1, .3);
- myParticleSystem.isLocal = true;
-
- myParticleSystem.start(); //S
- return myParticleSystem;
-
- }
-}
\ No newline at end of file
diff --git a/src/ui/screens/controlsScreen.ts b/src/ui/screens/controlsScreen.ts
deleted file mode 100644
index bd37195..0000000
--- a/src/ui/screens/controlsScreen.ts
+++ /dev/null
@@ -1,294 +0,0 @@
-import {
- ControllerMappingConfig,
- ControllerMapping,
- StickAction,
- ButtonAction
-} from '../../ship/input/controllerMapping';
-
-/**
- * Controller remapping screen
- * Allows users to customize VR controller button and stick mappings
- */
-export class ControlsScreen {
- private config: ControllerMappingConfig;
- private messageDiv: HTMLElement | null = null;
-
- constructor() {
- this.config = ControllerMappingConfig.getInstance();
- }
-
- /**
- * Initialize the controls screen
- * Set up event listeners and populate form with current configuration
- */
- public initialize(): void {
- console.log('[ControlsScreen] Initializing');
-
- // Get form elements
- this.messageDiv = document.getElementById('controlsMessage');
-
- // Populate dropdowns
- this.populateDropdowns();
-
- // Load current configuration into form
- this.loadCurrentMapping();
-
- // Set up event listeners
- this.setupEventListeners();
-
- console.log('[ControlsScreen] Initialized');
- }
-
- /**
- * Populate all dropdown select elements with available actions
- */
- private populateDropdowns(): void {
- // Stick action dropdowns
- const stickSelects = [
- 'leftStickX', 'leftStickY',
- 'rightStickX', 'rightStickY'
- ];
-
- const stickActions = ControllerMappingConfig.getAvailableStickActions();
-
- stickSelects.forEach(id => {
- const select = document.getElementById(id) as HTMLSelectElement;
- if (select) {
- select.innerHTML = '';
- stickActions.forEach(action => {
- const option = document.createElement('option');
- option.value = action;
- option.textContent = ControllerMappingConfig.getStickActionLabel(action);
- select.appendChild(option);
- });
- }
- });
-
- // Button action dropdowns
- const buttonSelects = [
- 'trigger', 'aButton', 'bButton',
- 'xButton', 'yButton', 'squeeze'
- ];
-
- const buttonActions = ControllerMappingConfig.getAvailableButtonActions();
-
- buttonSelects.forEach(id => {
- const select = document.getElementById(id) as HTMLSelectElement;
- if (select) {
- select.innerHTML = '';
- buttonActions.forEach(action => {
- const option = document.createElement('option');
- option.value = action;
- option.textContent = ControllerMappingConfig.getButtonActionLabel(action);
- select.appendChild(option);
- });
- }
- });
- }
-
- /**
- * Load current mapping configuration into form elements
- */
- private loadCurrentMapping(): void {
- const mapping = this.config.getMapping();
-
- // Stick mappings
- this.setSelectValue('leftStickX', mapping.leftStickX);
- this.setSelectValue('leftStickY', mapping.leftStickY);
- this.setSelectValue('rightStickX', mapping.rightStickX);
- this.setSelectValue('rightStickY', mapping.rightStickY);
-
- // Inversion checkboxes
- this.setCheckboxValue('invertLeftStickX', mapping.invertLeftStickX);
- this.setCheckboxValue('invertLeftStickY', mapping.invertLeftStickY);
- this.setCheckboxValue('invertRightStickX', mapping.invertRightStickX);
- this.setCheckboxValue('invertRightStickY', mapping.invertRightStickY);
-
- // Button mappings
- this.setSelectValue('trigger', mapping.trigger);
- this.setSelectValue('aButton', mapping.aButton);
- this.setSelectValue('bButton', mapping.bButton);
- this.setSelectValue('xButton', mapping.xButton);
- this.setSelectValue('yButton', mapping.yButton);
- this.setSelectValue('squeeze', mapping.squeeze);
-
- console.log('[ControlsScreen] Loaded current mapping into form');
- }
-
- /**
- * Set up event listeners for buttons
- */
- private setupEventListeners(): void {
- // Save button
- const saveBtn = document.getElementById('saveControlsBtn');
- if (saveBtn) {
- saveBtn.addEventListener('click', () => this.saveMapping());
- }
-
- // Reset button
- const resetBtn = document.getElementById('resetControlsBtn');
- if (resetBtn) {
- resetBtn.addEventListener('click', () => this.resetToDefault());
- }
-
- // Test button (shows current mapping preview)
- const testBtn = document.getElementById('testControlsBtn');
- if (testBtn) {
- testBtn.addEventListener('click', () => this.showTestPreview());
- }
- }
-
- /**
- * Save current form values to configuration
- */
- private saveMapping(): void {
- // Read all form values
- const mapping: ControllerMapping = {
- // Stick mappings
- leftStickX: this.getSelectValue('leftStickX') as StickAction,
- leftStickY: this.getSelectValue('leftStickY') as StickAction,
- rightStickX: this.getSelectValue('rightStickX') as StickAction,
- rightStickY: this.getSelectValue('rightStickY') as StickAction,
-
- // Inversions
- invertLeftStickX: this.getCheckboxValue('invertLeftStickX'),
- invertLeftStickY: this.getCheckboxValue('invertLeftStickY'),
- invertRightStickX: this.getCheckboxValue('invertRightStickX'),
- invertRightStickY: this.getCheckboxValue('invertRightStickY'),
-
- // Button mappings
- trigger: this.getSelectValue('trigger') as ButtonAction,
- aButton: this.getSelectValue('aButton') as ButtonAction,
- bButton: this.getSelectValue('bButton') as ButtonAction,
- xButton: this.getSelectValue('xButton') as ButtonAction,
- yButton: this.getSelectValue('yButton') as ButtonAction,
- squeeze: this.getSelectValue('squeeze') as ButtonAction,
- };
-
- // Validate
- this.config.setMapping(mapping);
- const warnings = this.config.validate();
-
- if (warnings.length > 0) {
- // Show warnings but still save
- this.showMessage(
- 'Configuration saved with warnings:\n' + warnings.join('\n'),
- 'warning'
- );
- } else {
- this.showMessage('Configuration saved successfully!', 'success');
- }
-
- // Save to localStorage
- this.config.save();
-
- console.log('[ControlsScreen] Saved mapping:', mapping);
- }
-
- /**
- * Reset form to default mapping
- */
- private resetToDefault(): void {
- if (confirm('Reset all controller mappings to default? This cannot be undone.')) {
- this.config.resetToDefault();
- this.config.save();
- this.loadCurrentMapping();
- this.showMessage('Reset to default configuration', 'success');
- console.log('[ControlsScreen] Reset to defaults');
- }
- }
-
- /**
- * Show test preview of current mapping
- */
- private showTestPreview(): void {
- const mapping = this.readCurrentFormValues();
-
- let preview = 'Current Controller Mapping:\n\n';
-
- preview += '📋 STICK MAPPINGS:\n';
- preview += ` Left Stick X: ${ControllerMappingConfig.getStickActionLabel(mapping.leftStickX)}`;
- preview += mapping.invertLeftStickX ? ' (Inverted)\n' : '\n';
- preview += ` Left Stick Y: ${ControllerMappingConfig.getStickActionLabel(mapping.leftStickY)}`;
- preview += mapping.invertLeftStickY ? ' (Inverted)\n' : '\n';
- preview += ` Right Stick X: ${ControllerMappingConfig.getStickActionLabel(mapping.rightStickX)}`;
- preview += mapping.invertRightStickX ? ' (Inverted)\n' : '\n';
- preview += ` Right Stick Y: ${ControllerMappingConfig.getStickActionLabel(mapping.rightStickY)}`;
- preview += mapping.invertRightStickY ? ' (Inverted)\n' : '\n';
-
- preview += '\n🎮 BUTTON MAPPINGS:\n';
- preview += ` Trigger: ${ControllerMappingConfig.getButtonActionLabel(mapping.trigger)}\n`;
- preview += ` A Button: ${ControllerMappingConfig.getButtonActionLabel(mapping.aButton)}\n`;
- preview += ` B Button: ${ControllerMappingConfig.getButtonActionLabel(mapping.bButton)}\n`;
- preview += ` X Button: ${ControllerMappingConfig.getButtonActionLabel(mapping.xButton)}\n`;
- preview += ` Y Button: ${ControllerMappingConfig.getButtonActionLabel(mapping.yButton)}\n`;
- preview += ` Squeeze/Grip: ${ControllerMappingConfig.getButtonActionLabel(mapping.squeeze)}\n`;
-
- alert(preview);
- }
-
- /**
- * Read current form values into a mapping object
- */
- private readCurrentFormValues(): ControllerMapping {
- return {
- leftStickX: this.getSelectValue('leftStickX') as StickAction,
- leftStickY: this.getSelectValue('leftStickY') as StickAction,
- rightStickX: this.getSelectValue('rightStickX') as StickAction,
- rightStickY: this.getSelectValue('rightStickY') as StickAction,
- invertLeftStickX: this.getCheckboxValue('invertLeftStickX'),
- invertLeftStickY: this.getCheckboxValue('invertLeftStickY'),
- invertRightStickX: this.getCheckboxValue('invertRightStickX'),
- invertRightStickY: this.getCheckboxValue('invertRightStickY'),
- trigger: this.getSelectValue('trigger') as ButtonAction,
- aButton: this.getSelectValue('aButton') as ButtonAction,
- bButton: this.getSelectValue('bButton') as ButtonAction,
- xButton: this.getSelectValue('xButton') as ButtonAction,
- yButton: this.getSelectValue('yButton') as ButtonAction,
- squeeze: this.getSelectValue('squeeze') as ButtonAction,
- };
- }
-
- /**
- * Show a message to the user
- */
- private showMessage(message: string, type: 'success' | 'error' | 'warning' = 'success'): void {
- if (this.messageDiv) {
- this.messageDiv.textContent = message;
- this.messageDiv.className = `controls-message ${type}`;
- this.messageDiv.style.display = 'block';
-
- // Hide after 5 seconds
- setTimeout(() => {
- if (this.messageDiv) {
- this.messageDiv.style.display = 'none';
- }
- }, 5000);
- }
- }
-
- // Helper methods for form manipulation
- private setSelectValue(id: string, value: string): void {
- const select = document.getElementById(id) as HTMLSelectElement;
- if (select) {
- select.value = value;
- }
- }
-
- private getSelectValue(id: string): string {
- const select = document.getElementById(id) as HTMLSelectElement;
- return select ? select.value : '';
- }
-
- private setCheckboxValue(id: string, checked: boolean): void {
- const checkbox = document.getElementById(id) as HTMLInputElement;
- if (checkbox) {
- checkbox.checked = checked;
- }
- }
-
- private getCheckboxValue(id: string): boolean {
- const checkbox = document.getElementById(id) as HTMLInputElement;
- return checkbox ? checkbox.checked : false;
- }
-}
diff --git a/src/ui/screens/settingsScreen.ts b/src/ui/screens/settingsScreen.ts
deleted file mode 100644
index 6172c4a..0000000
--- a/src/ui/screens/settingsScreen.ts
+++ /dev/null
@@ -1,91 +0,0 @@
-import { GameConfig } from "../../core/gameConfig";
-
-/**
- * Initialize the settings screen
- */
-export function initializeSettingsScreen(): void {
- const config = GameConfig.getInstance();
-
- // Get form elements
- const physicsEnabledCheckbox = document.getElementById('physicsEnabled') as HTMLInputElement;
- const debugEnabledCheckbox = document.getElementById('debugEnabled') as HTMLInputElement;
-
- // Ship physics inputs
- const maxLinearVelocityInput = document.getElementById('maxLinearVelocity') as HTMLInputElement;
- const maxAngularVelocityInput = document.getElementById('maxAngularVelocity') as HTMLInputElement;
- const linearForceMultiplierInput = document.getElementById('linearForceMultiplier') as HTMLInputElement;
- const angularForceMultiplierInput = document.getElementById('angularForceMultiplier') as HTMLInputElement;
-
- const saveBtn = document.getElementById('saveSettingsBtn');
- const resetBtn = document.getElementById('resetSettingsBtn');
- const messageDiv = document.getElementById('settingsMessage');
-
- // Load current settings
- loadSettings();
-
- // Save button handler
- saveBtn?.addEventListener('click', () => {
- saveSettings();
- showMessage('Settings saved successfully!', 'success');
- });
-
- // Reset button handler
- resetBtn?.addEventListener('click', () => {
- if (confirm('Are you sure you want to reset all settings to defaults?')) {
- config.reset();
- loadSettings();
- showMessage('Settings reset to defaults', 'info');
- }
- });
-
- /**
- * Load current settings into form
- */
- function loadSettings(): void {
- if (physicsEnabledCheckbox) physicsEnabledCheckbox.checked = config.physicsEnabled;
- if (debugEnabledCheckbox) debugEnabledCheckbox.checked = config.debug;
-
- // Load ship physics settings
- if (maxLinearVelocityInput) maxLinearVelocityInput.value = config.shipPhysics.maxLinearVelocity.toString();
- if (maxAngularVelocityInput) maxAngularVelocityInput.value = config.shipPhysics.maxAngularVelocity.toString();
- if (linearForceMultiplierInput) linearForceMultiplierInput.value = config.shipPhysics.linearForceMultiplier.toString();
- if (angularForceMultiplierInput) angularForceMultiplierInput.value = config.shipPhysics.angularForceMultiplier.toString();
- }
-
- /**
- * Save form settings to GameConfig
- */
- function saveSettings(): void {
- config.physicsEnabled = physicsEnabledCheckbox.checked;
- config.debug = debugEnabledCheckbox.checked;
-
- // Save ship physics settings
- config.shipPhysics.maxLinearVelocity = parseFloat(maxLinearVelocityInput.value);
- config.shipPhysics.maxAngularVelocity = parseFloat(maxAngularVelocityInput.value);
- config.shipPhysics.linearForceMultiplier = parseFloat(linearForceMultiplierInput.value);
- config.shipPhysics.angularForceMultiplier = parseFloat(angularForceMultiplierInput.value);
-
- config.save();
- }
-
- /**
- * Show a temporary message
- */
- function showMessage(message: string, type: 'success' | 'info' | 'warning'): void {
- if (!messageDiv) return;
-
- const colors = {
- success: '#4CAF50',
- info: '#2196F3',
- warning: '#FF9800'
- };
-
- messageDiv.textContent = message;
- messageDiv.style.color = colors[type];
- messageDiv.style.opacity = '1';
-
- setTimeout(() => {
- messageDiv.style.opacity = '0';
- }, 3000);
- }
-}
diff --git a/src/utils/scoreEvent.ts b/src/utils/scoreEvent.ts
deleted file mode 100644
index e58cdf1..0000000
--- a/src/utils/scoreEvent.ts
+++ /dev/null
@@ -1,4 +0,0 @@
-export type ScoreEvent = {
- score: number,
- message: string
-}
\ No newline at end of file