diff --git a/src/games/coloradodefense/ColoradoDefenseLogic.js b/src/games/coloradodefense/ColoradoDefenseLogic.js index d17798e..8dff9af 100644 --- a/src/games/coloradodefense/ColoradoDefenseLogic.js +++ b/src/games/coloradodefense/ColoradoDefenseLogic.js @@ -64,6 +64,11 @@ export const TUNE = { MIRV_RESPLIT_WAVE: 8, SPLIT_ZONE_1: [0.20, 0.333], SPLIT_ZONE_2: [0.45, 0.55], + // A MIRV's own children fly slower than a regular missile right when + // MIRVs are introduced, then quickly catch up to full speed. + MIRV_CHILD_SPEED_MULT_BASE: 0.65, + MIRV_CHILD_SPEED_MULT_GROWTH: 0.5, + MIRV_CHILD_SPEED_MULT_MAX: 1.0, BASE_AMMO: 10, BLAST_R_BASE: 90, BLAST_GROW_MS: 350, BLAST_HOLD_MS: 120, BLAST_FADE_MS: 200, INTERCEPTOR_SPEED: 900, @@ -118,6 +123,15 @@ export function mirvChance(wave) { return Math.min(TUNE.MIRV_CHANCE_MAX, TUNE.MIRV_CHANCE_BASE + TUNE.MIRV_CHANCE_GROWTH * (wave - TUNE.MIRV_WAVE)); } +// How fast a MIRV's split-off children fly, relative to a normal missile +// that wave — starts slow right when MIRVs are introduced, ramps to full speed. +export function mirvChildSpeedMult(wave) { + return Math.min( + TUNE.MIRV_CHILD_SPEED_MULT_MAX, + TUNE.MIRV_CHILD_SPEED_MULT_BASE + TUNE.MIRV_CHILD_SPEED_MULT_GROWTH * (wave - TUNE.MIRV_WAVE), + ); +} + // Automatic, level-driven palette progression (distinct from user-facing // pickers like 2048's) — advances every PALETTE_WAVES waves, clamped at the end. export const PALETTES = [ @@ -127,7 +141,7 @@ export const PALETTES = [ { bgTop: 0x1a140a, bgBottom: 0x301c0a, trail: 0xffb366, explosion: 0xff5555, accent: 0xff8a3c, ground: 0x2a1c0f, chain: 0xffee88 }, { bgTop: 0x120a1a, bgBottom: 0x260a30, trail: 0xc78dff, explosion: 0xff66c4, accent: 0xb84bff, ground: 0x1e0f2a, chain: 0x66ffcc }, ]; -export const PALETTE_WAVES = 4; +export const PALETTE_WAVES = 2; export function paletteIndexForWave(wave) { return Math.min(PALETTES.length - 1, Math.floor((wave - 1) / PALETTE_WAVES)); } @@ -216,13 +230,13 @@ export class Sim { // only a freshly-spawned, top-edge missile can randomly become one; a // MIRV's own children (fromOverride, no force) are always simple 'single's // so the split tree stays bounded. - spawnEnemyMissile(fromOverride, forceMirv = false) { + spawnEnemyMissile(fromOverride, forceMirv = false, speedMult = 1) { const target = this.pickTargetSlot(); if (!target) return null; const fromX = fromOverride ? fromOverride.x : 80 + this.rng() * (WIDTH - 160); const fromY = fromOverride ? fromOverride.y : 0; const dist = Math.hypot(target.x - fromX, target.y - fromY); - const speed = fallSpeed(this.wave); + const speed = fallSpeed(this.wave) * speedMult; const dur = Math.max(200, (dist / speed) * 1000); const isMirv = forceMirv || (!fromOverride && this.rng() < mirvChance(this.wave)); const kind = isMirv ? 'mirv' : 'single'; @@ -241,9 +255,10 @@ export class Sim { const options = TUNE.MIRV_CHILDREN; const n = options[Math.floor(this.rng() * options.length)]; const cur = lerpPos(parent); + const speedMult = mirvChildSpeedMult(this.wave); const children = []; for (let i = 0; i < n; i += 1) { - const m = this.spawnEnemyMissile(cur); + const m = this.spawnEnemyMissile(cur, false, speedMult); if (m) children.push(m); } return children; diff --git a/tools/verifyColoradoDefense.js b/tools/verifyColoradoDefense.js index 2ff0dea..b2b6ab9 100644 --- a/tools/verifyColoradoDefense.js +++ b/tools/verifyColoradoDefense.js @@ -7,6 +7,7 @@ // 3. Difficulty escalation monotonicity across waves 1-40 (bounded). // 4. MIRV step-unlock threshold. // 4b. MIRV split-zone geometry (top-third once, top-third + halfway from wave 8). +// 4c. MIRV child speed multiplier (slower right at introduction, ramps to full speed). // 5. Scripted explosion/collision fixture. // 5c. Bonus plane fixture (unlock wave, crossing speed, hit/score/debris). // 5d. Bonus UFO fixture (unlock wave, weaving/shots, hit/score/debris). @@ -16,7 +17,7 @@ import { BASE_SLOTS, CITY_SLOTS, GROUND_SLOTS, HEIGHT, WIDTH, GROUND_Y, slotX, mulberry32, pickCities, - DEFAULT_CITIES, TUNE, spawnInterval, fallSpeed, waveQuota, mirvChance, + DEFAULT_CITIES, TUNE, spawnInterval, fallSpeed, waveQuota, mirvChance, mirvChildSpeedMult, PALETTES, paletteIndexForWave, createGame, step, pickFiringBase, fireInterceptor, lerpPos, explosionRadius, } from '../src/games/coloradodefense/ColoradoDefenseLogic.js'; @@ -183,6 +184,41 @@ console.log('MIRV split-zone geometry'); check('its second split is scheduled in SPLIT_ZONE_2', sawZone2SplitY); } +// ── 4c. MIRV child speed multiplier ───────────────────────────────────────────── + +console.log('MIRV child speed multiplier'); +{ + check('multiplier at MIRV_WAVE is exactly the base value', + mirvChildSpeedMult(TUNE.MIRV_WAVE) === TUNE.MIRV_CHILD_SPEED_MULT_BASE); + let monotonic = true; + for (let w = TUNE.MIRV_WAVE + 1; w <= TUNE.MIRV_WAVE + 20; w += 1) { + if (mirvChildSpeedMult(w) < mirvChildSpeedMult(w - 1)) monotonic = false; + } + check('multiplier is monotonic non-decreasing with wave', monotonic); + check('multiplier never exceeds MIRV_CHILD_SPEED_MULT_MAX', + mirvChildSpeedMult(TUNE.MIRV_WAVE + 20) <= TUNE.MIRV_CHILD_SPEED_MULT_MAX); + check('a +0.5/wave growth step reaches the max within one wave of introduction', + mirvChildSpeedMult(TUNE.MIRV_WAVE + 1) === TUNE.MIRV_CHILD_SPEED_MULT_MAX); + + // A child spawned at the reduced multiplier takes proportionally longer to + // cover the SAME distance as one spawned at full speed. Force rng()=0 + // before each call so pickTargetSlot() deterministically picks the same + // target both times — otherwise a different target/distance would + // contaminate the dur comparison. + const sim = createGame(DEFAULT_CITIES, 60); + sim.wave = TUNE.MIRV_WAVE; + const from = { x: 500, y: 400 }; + sim.rng = () => 0; + const full = sim.spawnEnemyMissile(from, false, 1); + sim.rng = () => 0; + const slow = sim.spawnEnemyMissile(from, false, mirvChildSpeedMult(sim.wave)); + check('a slowed child (same distance) takes longer to arrive than a full-speed one', + slow.dur > full.dur, `full=${full.dur} slow=${slow.dur}`); + check('the slowdown ratio matches the configured base multiplier', + Math.abs(full.dur / slow.dur - TUNE.MIRV_CHILD_SPEED_MULT_BASE) < 1e-6, + `ratio=${full.dur / slow.dur}`); +} + // ── 5. Explosion/collision fixture ──────────────────────────────────────────── console.log('Explosion/collision fixture');