import { Rng } from '../../shared/rng'; import { ENEMY_DEFINITIONS } from '../combat/enemy-definitions'; import type { MapNode, RunMap, EncounterType } from './run-map.types'; const NORMAL_ENEMIES = ['slime', 'cultist', 'jaw_worm']; const ELITE_ENEMIES = ['brute']; const BOSS_ENEMIES = ['brute']; /** * Generates a linear run map from a seed. Deterministic: the same seed always * yields the same path. Replaces the old platformer DungeonSeeder. */ export class MapGenerator { generate(seed: number, length = 10): RunMap { const rng = new Rng(seed); const nodes: MapNode[] = []; for (let floor = 1; floor <= length; floor++) { const type = this.pickType(floor, length, rng); nodes.push({ id: `node-${floor}`, floor, type, enemyIds: this.pickEnemies(type, floor, rng), }); } return { seed, nodes }; } private pickType(floor: number, length: number, rng: Rng): EncounterType { if (floor === length) return 'boss'; if (floor === 1) return 'combat'; if (floor % 4 === 0) return 'rest'; if (rng.next() > 0.8) return 'elite'; return 'combat'; } private pickEnemies(type: EncounterType, floor: number, rng: Rng): string[] { switch (type) { case 'rest': return []; case 'boss': return [rng.pick(BOSS_ENEMIES)]; case 'elite': return [rng.pick(ELITE_ENEMIES)]; case 'combat': { // Scale group size gently with depth. const count = 1 + (floor > 5 && rng.next() > 0.5 ? 1 : 0); return Array.from({ length: count }, () => { const id = rng.pick(NORMAL_ENEMIES); // Guard against a bad id sneaking in from future edits. return ENEMY_DEFINITIONS[id] ? id : 'slime'; }); } } } }