Fix scoreboard, improve asteroid distribution, and eliminate code duplication
Some checks failed
Build / build (push) Failing after 19s

- Fix scoreboard remaining count initialization
  - Add setRemainingCount() method to Scoreboard class
  - Initialize count once with total asteroids in Level1
  - Remove incorrect per-asteroid notifications from deserializer

- Distribute asteroids evenly around base in circular pattern
  - Calculate positions using angle: (i / total) * 2π
  - Add small random variation (±0.15 radians) for natural spacing
  - Set tangential velocities for orbital motion
  - Apply to both LevelGenerator and CustomLevelGenerator

- Eliminate code duplication in level generation
  - Convert LevelGenerator static constants to public instance properties
  - Remove ~130 lines of duplicated code from CustomLevelGenerator
  - CustomLevelGenerator now just inherits from base class
  - Net reduction: 99 lines of code

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

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
Michael Mainguy 2025-10-29 08:44:55 -05:00
parent 12710b9a5c
commit 56488edd0b
5 changed files with 71 additions and 170 deletions

View File

@ -109,6 +109,10 @@ export class Level1 implements Level {
this._startBase = entities.startBase; this._startBase = entities.startBase;
// sun and planets are already created by deserializer // sun and planets are already created by deserializer
// Initialize scoreboard with total asteroid count
this._scoreboard.setRemainingCount(entities.asteroids.length);
console.log(`Initialized scoreboard with ${entities.asteroids.length} asteroids`);
// Position ship from config // Position ship from config
const shipConfig = this._deserializer.getShipConfig(); const shipConfig = this._deserializer.getShipConfig();
this._ship.position = new Vector3(shipConfig.position[0], shipConfig.position[1], shipConfig.position[2]); this._ship.position = new Vector3(shipConfig.position[0], shipConfig.position[1], shipConfig.position[2]);

View File

@ -201,13 +201,6 @@ export class LevelDeserializer {
if (mesh) { if (mesh) {
asteroids.push(mesh); asteroids.push(mesh);
} }
// Notify scoreboard of asteroid count
scoreObservable.notifyObservers({
score: 0,
remaining: i + 1,
message: "Loading from config"
});
} }
console.log(`Created ${asteroids.length} asteroids from config`); console.log(`Created ${asteroids.length} asteroids from config`);

View File

@ -566,129 +566,11 @@ class LevelEditor {
/** /**
* Custom level generator that allows overriding default values * Custom level generator that allows overriding default values
* Simply extends LevelGenerator - all properties are now public on the base class
*/ */
class CustomLevelGenerator extends LevelGenerator { class CustomLevelGenerator extends LevelGenerator {
public shipPosition: Vector3Array = [0, 1, 0]; // No need to duplicate anything - just use the public properties from base class
public startBasePosition: Vector3Array = [0, 0, 0]; // Properties like shipPosition, startBasePosition, etc. are already defined and public in LevelGenerator
public startBaseDiameter = 10;
public startBaseHeight = 1;
public sunPosition: Vector3Array = [0, 0, 400];
public sunDiameter = 50;
public planetCount = 12;
public planetMinDiameter = 100;
public planetMaxDiameter = 200;
public planetMinDistance = 1000;
public planetMaxDistance = 2000;
private customDifficultyConfig: DifficultyConfig | null = null;
public setDifficultyConfig(config: DifficultyConfig) {
this.customDifficultyConfig = config;
}
public generate(): LevelConfig {
const config = super.generate();
// Override with custom values
config.ship.position = [...this.shipPosition];
config.startBase.position = [...this.startBasePosition];
config.startBase.diameter = this.startBaseDiameter;
config.startBase.height = this.startBaseHeight;
config.sun.position = [...this.sunPosition];
config.sun.diameter = this.sunDiameter;
// Regenerate planets with custom params
if (this.planetCount !== 12 ||
this.planetMinDiameter !== 100 ||
this.planetMaxDiameter !== 200 ||
this.planetMinDistance !== 1000 ||
this.planetMaxDistance !== 2000) {
config.planets = this.generateCustomPlanets();
}
// Regenerate asteroids with custom params if provided
if (this.customDifficultyConfig) {
config.asteroids = this.generateCustomAsteroids(this.customDifficultyConfig);
config.difficultyConfig = this.customDifficultyConfig;
}
return config;
}
private generateCustomPlanets() {
const planets = [];
const sunPosition = this.sunPosition;
for (let i = 0; i < this.planetCount; i++) {
const diameter = this.planetMinDiameter +
Math.random() * (this.planetMaxDiameter - this.planetMinDiameter);
const distance = this.planetMinDistance +
Math.random() * (this.planetMaxDistance - this.planetMinDistance);
const angle = Math.random() * Math.PI * 2;
const y = (Math.random() - 0.5) * 100;
const position: Vector3Array = [
sunPosition[0] + distance * Math.cos(angle),
sunPosition[1] + y,
sunPosition[2] + distance * Math.sin(angle)
];
planets.push({
name: `planet-${i}`,
position,
diameter,
texturePath: this.getRandomPlanetTexture(),
rotation: [0, 0, 0] as Vector3Array
});
}
return planets;
}
private generateCustomAsteroids(config: DifficultyConfig) {
const asteroids = [];
for (let i = 0; i < config.rockCount; i++) {
const distRange = config.distanceMax - config.distanceMin;
const dist = (Math.random() * distRange) + config.distanceMin;
const position: Vector3Array = [0, 1, dist];
const sizeRange = config.rockSizeMax - config.rockSizeMin;
const size = Math.random() * sizeRange + config.rockSizeMin;
const scaling: Vector3Array = [size, size, size];
const forceMagnitude = 50000000 * config.forceMultiplier;
const mass = 10000;
const velocityMagnitude = forceMagnitude / mass / 100;
const linearVelocity: Vector3Array = [velocityMagnitude, 0, 0];
asteroids.push({
id: `asteroid-${i}`,
position,
scaling,
linearVelocity,
angularVelocity: [0, 0, 0] as Vector3Array,
mass
});
}
return asteroids;
}
private getRandomPlanetTexture(): string {
// Simple inline implementation to avoid circular dependency
const textures = [
"/planetTextures/Arid/Arid_01-512x512.png",
"/planetTextures/Barren/Barren_01-512x512.png",
"/planetTextures/Gaseous/Gaseous_01-512x512.png",
"/planetTextures/Grassland/Grassland_01-512x512.png"
];
return textures[Math.floor(Math.random() * textures.length)];
}
} }
// Initialize the editor when this module is loaded // Initialize the editor when this module is loaded

View File

@ -14,32 +14,39 @@ import { getRandomPlanetTexture } from "./planetTextures";
* Generates procedural level configurations matching the current Level1 generation logic * Generates procedural level configurations matching the current Level1 generation logic
*/ */
export class LevelGenerator { export class LevelGenerator {
private _difficulty: string; protected _difficulty: string;
private _difficultyConfig: DifficultyConfig; protected _difficultyConfig: DifficultyConfig;
// Constants matching Level1 defaults // Configurable properties (can be overridden by subclasses or set before generate())
private static readonly SHIP_POSITION: Vector3Array = [0, 1, 0]; public shipPosition: Vector3Array = [0, 1, 0];
private static readonly START_BASE_POSITION: Vector3Array = [0, 0, 0]; public startBasePosition: Vector3Array = [0, 0, 0];
private static readonly START_BASE_DIAMETER = 10; public startBaseDiameter = 10;
private static readonly START_BASE_HEIGHT = 1; public startBaseHeight = 1;
private static readonly START_BASE_COLOR: Vector3Array = [1, 1, 0]; // Yellow public startBaseColor: Vector3Array = [1, 1, 0]; // Yellow
private static readonly SUN_POSITION: Vector3Array = [0, 0, 400]; public sunPosition: Vector3Array = [0, 0, 400];
private static readonly SUN_DIAMETER = 50; public sunDiameter = 50;
private static readonly SUN_INTENSITY = 1000000; public sunIntensity = 1000000;
// Planet generation constants (matching createPlanetsOrbital call in Level1) // Planet generation parameters
private static readonly PLANET_COUNT = 12; public planetCount = 12;
private static readonly PLANET_MIN_DIAMETER = 100; public planetMinDiameter = 100;
private static readonly PLANET_MAX_DIAMETER = 200; public planetMaxDiameter = 200;
private static readonly PLANET_MIN_DISTANCE = 1000; public planetMinDistance = 1000;
private static readonly PLANET_MAX_DISTANCE = 2000; public planetMaxDistance = 2000;
constructor(difficulty: string) { constructor(difficulty: string) {
this._difficulty = difficulty; this._difficulty = difficulty;
this._difficultyConfig = this.getDifficultyConfig(difficulty); this._difficultyConfig = this.getDifficultyConfig(difficulty);
} }
/**
* Set custom difficulty configuration
*/
public setDifficultyConfig(config: DifficultyConfig) {
this._difficultyConfig = config;
}
/** /**
* Generate a complete level configuration * Generate a complete level configuration
*/ */
@ -69,7 +76,7 @@ export class LevelGenerator {
private generateShip(): ShipConfig { private generateShip(): ShipConfig {
return { return {
position: [...LevelGenerator.SHIP_POSITION], position: [...this.shipPosition],
rotation: [0, 0, 0], rotation: [0, 0, 0],
linearVelocity: [0, 0, 0], linearVelocity: [0, 0, 0],
angularVelocity: [0, 0, 0] angularVelocity: [0, 0, 0]
@ -78,18 +85,18 @@ export class LevelGenerator {
private generateStartBase(): StartBaseConfig { private generateStartBase(): StartBaseConfig {
return { return {
position: [...LevelGenerator.START_BASE_POSITION], position: [...this.startBasePosition],
diameter: LevelGenerator.START_BASE_DIAMETER, diameter: this.startBaseDiameter,
height: LevelGenerator.START_BASE_HEIGHT, height: this.startBaseHeight,
color: [...LevelGenerator.START_BASE_COLOR] color: [...this.startBaseColor]
}; };
} }
private generateSun(): SunConfig { private generateSun(): SunConfig {
return { return {
position: [...LevelGenerator.SUN_POSITION], position: [...this.sunPosition],
diameter: LevelGenerator.SUN_DIAMETER, diameter: this.sunDiameter,
intensity: LevelGenerator.SUN_INTENSITY intensity: this.sunIntensity
}; };
} }
@ -98,27 +105,26 @@ export class LevelGenerator {
*/ */
private generatePlanets(): PlanetConfig[] { private generatePlanets(): PlanetConfig[] {
const planets: PlanetConfig[] = []; const planets: PlanetConfig[] = [];
const sunPosition = LevelGenerator.SUN_POSITION;
for (let i = 0; i < LevelGenerator.PLANET_COUNT; i++) { for (let i = 0; i < this.planetCount; i++) {
// Random diameter between min and max // Random diameter between min and max
const diameter = LevelGenerator.PLANET_MIN_DIAMETER + const diameter = this.planetMinDiameter +
Math.random() * (LevelGenerator.PLANET_MAX_DIAMETER - LevelGenerator.PLANET_MIN_DIAMETER); Math.random() * (this.planetMaxDiameter - this.planetMinDiameter);
// Random distance from sun // Random distance from sun
const distance = LevelGenerator.PLANET_MIN_DISTANCE + const distance = this.planetMinDistance +
Math.random() * (LevelGenerator.PLANET_MAX_DISTANCE - LevelGenerator.PLANET_MIN_DISTANCE); Math.random() * (this.planetMaxDistance - this.planetMinDistance);
// Random angle around Y axis (orbital plane) // Random angle around Y axis (orbital plane)
const angle = Math.random() * Math.PI * 2; const angle = Math.random() * Math.PI * 2;
// Small vertical variation (like a solar system) // Small vertical variation (like a solar system)
const y = (Math.random() - 0.5) * 100; const y = (Math.random() - 0.5) * 400;
const position: Vector3Array = [ const position: Vector3Array = [
sunPosition[0] + distance * Math.cos(angle), this.sunPosition[0] + distance * Math.cos(angle),
sunPosition[1] + y, this.sunPosition[1] + y,
sunPosition[2] + distance * Math.sin(angle) this.sunPosition[2] + distance * Math.sin(angle)
]; ];
planets.push({ planets.push({
@ -134,7 +140,7 @@ export class LevelGenerator {
} }
/** /**
* Generate asteroids matching Level1.initialize() logic * Generate asteroids distributed evenly around the base in a circular pattern
*/ */
private generateAsteroids(): AsteroidConfig[] { private generateAsteroids(): AsteroidConfig[] {
const asteroids: AsteroidConfig[] = []; const asteroids: AsteroidConfig[] = [];
@ -145,8 +151,19 @@ export class LevelGenerator {
const distRange = config.distanceMax - config.distanceMin; const distRange = config.distanceMax - config.distanceMin;
const dist = (Math.random() * distRange) + config.distanceMin; const dist = (Math.random() * distRange) + config.distanceMin;
// Initial position (forward from start base) // Evenly distribute asteroids around a circle
const position: Vector3Array = [0, 1, dist]; const angle = (i / config.rockCount) * Math.PI * 2;
// Add small random variation to angle to prevent perfect spacing
const angleVariation = (Math.random() - 0.5) * 0.3; // ±0.15 radians variation
const finalAngle = angle + angleVariation;
// Calculate position in a circle around the base (XZ plane)
const x = dist * Math.cos(finalAngle);
const z = dist * Math.sin(finalAngle);
const y = 1; // Keep at same height as ship
const position: Vector3Array = [x, y, z];
// Random size // Random size
const sizeRange = config.rockSizeMax - config.rockSizeMin; const sizeRange = config.rockSizeMax - config.rockSizeMin;
@ -154,14 +171,16 @@ export class LevelGenerator {
const scaling: Vector3Array = [size, size, size]; const scaling: Vector3Array = [size, size, size];
// Calculate initial velocity based on force applied in Level1 // Calculate initial velocity based on force applied in Level1
// In Level1: rock.physicsBody.applyForce(new Vector3(50000000 * config.forceMultiplier, 0, 0), rock.position) // Velocity should be tangential to the circle (perpendicular to radius)
// For a body with mass 10000, force becomes velocity over time
// Simplified: velocity ≈ force / mass (ignoring physics timestep details)
const forceMagnitude = 50000000 * config.forceMultiplier; const forceMagnitude = 50000000 * config.forceMultiplier;
const mass = 10000; const mass = 10000;
const velocityMagnitude = forceMagnitude / mass / 100; // Approximation const velocityMagnitude = forceMagnitude / mass / 100; // Approximation
const linearVelocity: Vector3Array = [velocityMagnitude, 0, 0]; // Tangential velocity (perpendicular to the radius vector)
const vx = -velocityMagnitude * Math.sin(finalAngle);
const vz = velocityMagnitude * Math.cos(finalAngle);
const linearVelocity: Vector3Array = [vx, 0, vz];
asteroids.push({ asteroids.push({
id: `asteroid-${i}`, id: `asteroid-${i}`,

View File

@ -33,6 +33,9 @@ export class Scoreboard {
public set done(value: boolean) { public set done(value: boolean) {
this._done = value; this._done = value;
} }
public setRemainingCount(count: number) {
this._remaining = count;
}
private initialize() { private initialize() {
const scene = DefaultScene.MainScene; const scene = DefaultScene.MainScene;