Further Tweaks to Colorado Defense

This commit is contained in:
Brian Fertig 2026-07-14 13:19:07 -06:00
parent bfa2359ea2
commit 8399cc69db
2 changed files with 56 additions and 5 deletions

View File

@ -64,6 +64,11 @@ export const TUNE = {
MIRV_RESPLIT_WAVE: 8, MIRV_RESPLIT_WAVE: 8,
SPLIT_ZONE_1: [0.20, 0.333], SPLIT_ZONE_1: [0.20, 0.333],
SPLIT_ZONE_2: [0.45, 0.55], 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, BASE_AMMO: 10,
BLAST_R_BASE: 90, BLAST_GROW_MS: 350, BLAST_HOLD_MS: 120, BLAST_FADE_MS: 200, BLAST_R_BASE: 90, BLAST_GROW_MS: 350, BLAST_HOLD_MS: 120, BLAST_FADE_MS: 200,
INTERCEPTOR_SPEED: 900, 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)); 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 // Automatic, level-driven palette progression (distinct from user-facing
// pickers like 2048's) — advances every PALETTE_WAVES waves, clamped at the end. // pickers like 2048's) — advances every PALETTE_WAVES waves, clamped at the end.
export const PALETTES = [ 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: 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 }, { 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) { export function paletteIndexForWave(wave) {
return Math.min(PALETTES.length - 1, Math.floor((wave - 1) / PALETTE_WAVES)); 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 // only a freshly-spawned, top-edge missile can randomly become one; a
// MIRV's own children (fromOverride, no force) are always simple 'single's // MIRV's own children (fromOverride, no force) are always simple 'single's
// so the split tree stays bounded. // so the split tree stays bounded.
spawnEnemyMissile(fromOverride, forceMirv = false) { spawnEnemyMissile(fromOverride, forceMirv = false, speedMult = 1) {
const target = this.pickTargetSlot(); const target = this.pickTargetSlot();
if (!target) return null; if (!target) return null;
const fromX = fromOverride ? fromOverride.x : 80 + this.rng() * (WIDTH - 160); const fromX = fromOverride ? fromOverride.x : 80 + this.rng() * (WIDTH - 160);
const fromY = fromOverride ? fromOverride.y : 0; const fromY = fromOverride ? fromOverride.y : 0;
const dist = Math.hypot(target.x - fromX, target.y - fromY); 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 dur = Math.max(200, (dist / speed) * 1000);
const isMirv = forceMirv || (!fromOverride && this.rng() < mirvChance(this.wave)); const isMirv = forceMirv || (!fromOverride && this.rng() < mirvChance(this.wave));
const kind = isMirv ? 'mirv' : 'single'; const kind = isMirv ? 'mirv' : 'single';
@ -241,9 +255,10 @@ export class Sim {
const options = TUNE.MIRV_CHILDREN; const options = TUNE.MIRV_CHILDREN;
const n = options[Math.floor(this.rng() * options.length)]; const n = options[Math.floor(this.rng() * options.length)];
const cur = lerpPos(parent); const cur = lerpPos(parent);
const speedMult = mirvChildSpeedMult(this.wave);
const children = []; const children = [];
for (let i = 0; i < n; i += 1) { 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); if (m) children.push(m);
} }
return children; return children;

View File

@ -7,6 +7,7 @@
// 3. Difficulty escalation monotonicity across waves 1-40 (bounded). // 3. Difficulty escalation monotonicity across waves 1-40 (bounded).
// 4. MIRV step-unlock threshold. // 4. MIRV step-unlock threshold.
// 4b. MIRV split-zone geometry (top-third once, top-third + halfway from wave 8). // 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. // 5. Scripted explosion/collision fixture.
// 5c. Bonus plane fixture (unlock wave, crossing speed, hit/score/debris). // 5c. Bonus plane fixture (unlock wave, crossing speed, hit/score/debris).
// 5d. Bonus UFO fixture (unlock wave, weaving/shots, hit/score/debris). // 5d. Bonus UFO fixture (unlock wave, weaving/shots, hit/score/debris).
@ -16,7 +17,7 @@
import { import {
BASE_SLOTS, CITY_SLOTS, GROUND_SLOTS, HEIGHT, WIDTH, GROUND_Y, slotX, mulberry32, pickCities, 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, PALETTES, paletteIndexForWave, createGame, step, pickFiringBase,
fireInterceptor, lerpPos, explosionRadius, fireInterceptor, lerpPos, explosionRadius,
} from '../src/games/coloradodefense/ColoradoDefenseLogic.js'; } 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); 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 ──────────────────────────────────────────── // ── 5. Explosion/collision fixture ────────────────────────────────────────────
console.log('Explosion/collision fixture'); console.log('Explosion/collision fixture');