From d0d412cea9c17e0ecb610482f0b5682a25263abc Mon Sep 17 00:00:00 2001 From: Brian Fertig Date: Sun, 4 Oct 2026 19:56:07 -0600 Subject: [PATCH] Add Geometry Wars arcade shooter with warp grid and neon bloom - Headless sim (GeometryWarsLogic) with evolved and pacifism modes, seeded RNG for deterministic replays, and a fixed-tick accumulator loop - Spring-lattice warp grid that reacts to bullets, explosions and black holes - Additive neon bloom post pipeline plus a particle field for streaks/rings - Vector font, title screen with mode cards, HUD, pause and game-over panels - Register the game in the registry, scene dispatch, soundtrack and assets - tools/verifyGeometryWars.js: headless checks incl. a bot soak across seeds --- assets/images/game-icons.png | Bin 90937 -> 86753 bytes assets/images/game-icons.psd | Bin 966270 -> 974751 bytes src/data/assetManifest.js | 6 + src/data/gamesRegistry.js | 1 + src/games/geometrywars/GeometryWarsBloom.js | 91 ++ src/games/geometrywars/GeometryWarsFx.js | 182 +++ src/games/geometrywars/GeometryWarsGame.js | 1300 +++++++++++++++++ src/games/geometrywars/GeometryWarsGrid.js | 249 ++++ src/games/geometrywars/GeometryWarsLogic.js | 1071 ++++++++++++++ .../geometrywars/GeometryWarsVectorFont.js | 117 ++ src/main.js | 2 + src/scenes/GameRoomScene.js | 2 +- src/services/soundtrack.js | 1 + tools/verifyGeometryWars.js | 650 +++++++++ 14 files changed, 3671 insertions(+), 1 deletion(-) create mode 100644 src/games/geometrywars/GeometryWarsBloom.js create mode 100644 src/games/geometrywars/GeometryWarsFx.js create mode 100644 src/games/geometrywars/GeometryWarsGame.js create mode 100644 src/games/geometrywars/GeometryWarsGrid.js create mode 100644 src/games/geometrywars/GeometryWarsLogic.js create mode 100644 src/games/geometrywars/GeometryWarsVectorFont.js create mode 100644 tools/verifyGeometryWars.js diff --git a/assets/images/game-icons.png b/assets/images/game-icons.png index 7e3d4ce0ecb75a9c06d9f8ea62d20d6a816ed476..1683f56e76cdaa3e07e2314d5112d7fa1df9dfbf 100644 GIT binary patch literal 86753 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 literal 90937 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

*hlk}b6d=WW?8 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�SoJPfd9_5=Y6 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 YbOmFvF|Y8X;cr+RUVwm=^)5j8H^)xPQ~&?~ delta 14947 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 diff --git a/src/data/assetManifest.js b/src/data/assetManifest.js index de0fa17..0741d09 100644 --- a/src/data/assetManifest.js +++ b/src/data/assetManifest.js @@ -488,6 +488,12 @@ export const MANIFEST = { // so its audio only downloads once Defender is actually entered. (scene) => musicFrom(scene, 'arcadedark-music'), ], + geometrywars: [ + // arcadedark soundtrack (see services/soundtrack.js) — lazy-loaded here + // so its audio only downloads once Geometry Wars is actually entered. + // Everything else in the game is drawn procedurally; no image assets. + (scene) => musicFrom(scene, 'arcadedark-music'), + ], mastermind: [ // hacker soundtrack (see services/soundtrack.js) — lazy-loaded here so // its audio only downloads once Mastermind is actually entered. diff --git a/src/data/gamesRegistry.js b/src/data/gamesRegistry.js index 4cbe37a..d5174c6 100644 --- a/src/data/gamesRegistry.js +++ b/src/data/gamesRegistry.js @@ -125,3 +125,4 @@ registerGame({ slug: 'pipepuzzle', name: 'Pipe Puzzle', category: 'logic', minPl registerGame({ slug: 'tents', name: 'Tents & Trees', category: 'logic', minPlayers: 1, maxPlayers: 1, minOpponents: 0, maxOpponents: 0, iconFrame: 95 }); registerGame({ slug: 'jigsaw', name: 'Jigsaw', category: 'logic', minPlayers: 1, maxPlayers: 1, minOpponents: 0, maxOpponents: 0, iconFrame: 96 }); registerGame({ slug: 'defender', name: 'Defender', category: 'arcade-console-pc', minPlayers: 1, maxPlayers: 1, minOpponents: 0, maxOpponents: 0, iconFrame: 97 }); +registerGame({ slug: 'geometrywars', name: 'Geometry Wars', category: 'arcade-console-pc', minPlayers: 1, maxPlayers: 1, minOpponents: 0, maxOpponents: 0, iconFrame: 98 }); diff --git a/src/games/geometrywars/GeometryWarsBloom.js b/src/games/geometrywars/GeometryWarsBloom.js new file mode 100644 index 0000000..6fa7a2d --- /dev/null +++ b/src/games/geometrywars/GeometryWarsBloom.js @@ -0,0 +1,91 @@ +// Additive neon bloom for Geometry Wars' world camera. +// +// Phaser's built-in Bloom FX finishes with a 50/50 linear mix of the sharp +// frame and the blurred one, which halves the brightness of every thin line +// — exactly the wrong trade for a game made of 2px neon strokes. This is the +// same ping-pong Gaussian blur (same onDraw shape as Phaser's +// BloomFXPipeline) but composited with blendFramesAdditive, so cores stay at +// full brightness and the glow is added on top. +// +// Canvas renderer gets a no-op stub. PostFXPipeline convention follows +// src/ui/ArcadeCurvePipeline.js. + +import * as Phaser from 'phaser'; + +const FRAG = ` +precision mediump float; +uniform sampler2D uMainSampler; +uniform vec2 offset; +varying vec2 outTexCoord; +void main(void) { + vec4 sum = texture2D(uMainSampler, outTexCoord) * 0.2270270270; + sum += texture2D(uMainSampler, outTexCoord + offset * 1.3846153846) * 0.3162162162; + sum += texture2D(uMainSampler, outTexCoord - offset * 1.3846153846) * 0.3162162162; + sum += texture2D(uMainSampler, outTexCoord + offset * 3.2307692308) * 0.0702702703; + sum += texture2D(uMainSampler, outTexCoord - offset * 3.2307692308) * 0.0702702703; + gl_FragColor = sum; +} +`; + +// Tuning knobs — radius is in ~2px units per blur tap, strength is how much +// of the blurred frame gets added back on top. +export const GLOW_LEVELS = { + off: null, + low: { radius: 1.2, strength: 0.75, steps: 3 }, + high: { radius: 1.8, strength: 1.2, steps: 4 }, +}; + +export class NeonBloomPipeline extends Phaser.Renderer.WebGL.Pipelines.PostFXPipeline { + constructor(game) { + super({ game, name: 'GWNeonBloom', fragShader: FRAG }); + this.radius = GLOW_LEVELS.high.radius; + this.strength = GLOW_LEVELS.high.strength; + this.steps = GLOW_LEVELS.high.steps; + } + + onDraw(target1) { + const target2 = this.fullFrame1; + const target3 = this.fullFrame2; + this.copyFrame(target1, target3); + const x = (2 / target1.width) * this.radius; + const y = (2 / target1.height) * this.radius; + // set2f only rebinds this pipeline's program when the value changes; + // alternating (x,0)/(0,y) guarantees it does after copyFrame bound the + // utility shader (same reliance as Phaser's own BloomFXPipeline). + for (let i = 0; i < this.steps; i += 1) { + this.set2f('offset', x, 0); + this.copySprite(target1, target2); + this.set2f('offset', 0, y); + this.copySprite(target2, target1); + } + this.blendFramesAdditive(target3, target1, target2, this.strength); + this.copyToGame(target2); + } +} + +export function attachNeonBloom(scene, camera, level = 'high') { + if (!scene.renderer || scene.renderer.type !== Phaser.WEBGL) { + return { setLevel() {}, destroy() {} }; + } + scene.renderer.pipelines.addPostPipeline('GWNeonBloom', NeonBloomPipeline); + let attached = false; + const setLevel = (lvl) => { + const cfg = GLOW_LEVELS[lvl] ?? null; + if (!cfg) { + if (attached) { camera.removePostPipeline(NeonBloomPipeline); attached = false; } + return; + } + if (!attached) { camera.setPostPipeline(NeonBloomPipeline); attached = true; } + const got = camera.getPostPipeline(NeonBloomPipeline); + const inst = Array.isArray(got) ? got[0] : got; + if (inst) Object.assign(inst, cfg); + }; + setLevel(level); + return { + setLevel, + destroy() { + if (attached) { try { camera.removePostPipeline(NeonBloomPipeline); } catch (_) { /* camera gone */ } } + attached = false; + }, + }; +} diff --git a/src/games/geometrywars/GeometryWarsFx.js b/src/games/geometrywars/GeometryWarsFx.js new file mode 100644 index 0000000..9135273 --- /dev/null +++ b/src/games/geometrywars/GeometryWarsFx.js @@ -0,0 +1,182 @@ +// Particle field + shockwave rings for Geometry Wars. +// +// Particles are a fixed-size struct-of-arrays pool (oldest slot reused when +// full) drawn as velocity-aligned streaks — the look of a Geometry Wars +// explosion is thousands of short neon dashes slowing down, bouncing off the +// arena walls and getting sucked into black holes. Drawing is batched by +// (colour, alpha band, width) so a full screen costs a few dozen +// lineStyle switches rather than one per particle. +// +// Phaser-free: draw() only calls Graphics methods on the object it is given. + +export const FX_MAX = 2800; +const ALPHA_BANDS = 4; +const WIDTHS = [2, 3.4]; + +export default class ParticleField { + constructor(bounds) { + this.bounds = bounds; // { w, h } + const n = FX_MAX; + this.x = new Float32Array(n); this.y = new Float32Array(n); + this.vx = new Float32Array(n); this.vy = new Float32Array(n); + this.life = new Float32Array(n); this.maxLife = new Float32Array(n); + this.len = new Float32Array(n); this.damp = new Float32Array(n); + this.col = new Uint8Array(n); this.wid = new Uint8Array(n); + this.cursor = 0; + this.palette = []; + this.paletteIndex = new Map(); + this.rings = []; + this.buckets = new Map(); + } + + colorId(color) { + let id = this.paletteIndex.get(color); + if (id === undefined) { + id = this.palette.length; + if (id >= 255) return 0; + this.palette.push(color); + this.paletteIndex.set(color, id); + } + return id; + } + + liveCount() { + let n = 0; + for (let i = 0; i < FX_MAX; i += 1) if (this.life[i] > 0) n += 1; + return n; + } + + clear() { + this.life.fill(0); + this.rings.length = 0; + } + + emit(x, y, vx, vy, lifeMs, color, opts = {}) { + const i = this.cursor; + this.cursor = (i + 1) % FX_MAX; + this.x[i] = x; this.y[i] = y; + this.vx[i] = vx; this.vy[i] = vy; + this.life[i] = lifeMs; this.maxLife[i] = lifeMs; + this.len[i] = opts.len ?? 1; + this.damp[i] = opts.damp ?? 0.955; + this.col[i] = this.colorId(color); + this.wid[i] = opts.wide ? 1 : 0; + } + + // Classic radial burst. Speeds follow 1 - 1/rand so most sparks are fast + // and a few linger near the centre, which reads as a "hot core". + explosion(x, y, color, opts = {}) { + const count = opts.count ?? 60; + const speed = opts.speed ?? 1000; + const life = opts.life ?? 1100; + const color2 = opts.color2 ?? color; + for (let k = 0; k < count; k += 1) { + const a = Math.random() * Math.PI * 2; + const sp = speed * (1 - 1 / (1 + Math.random() * 9)); + const c = Math.random() < 0.5 ? color : color2; + this.emit(x, y, Math.cos(a) * sp, Math.sin(a) * sp, life * (0.55 + Math.random() * 0.6), c, + { len: opts.len ?? 1, wide: opts.wide && Math.random() < 0.5 }); + } + } + + // Sparks thrown back off a wall hit, reflected about the wall normal. + wallSparks(x, y, angle, color, count = 18) { + const { w, h } = this.bounds; + let nx = 0; let ny = 0; + if (x <= 1) nx = 1; else if (x >= w - 1) nx = -1; + if (y <= 1) ny = 1; else if (y >= h - 1) ny = -1; + let dx = Math.cos(angle); let dy = Math.sin(angle); + if (nx) dx = -dx; + if (ny) dy = -dy; + const base = Math.atan2(dy, dx); + for (let k = 0; k < count; k += 1) { + const a = base + (Math.random() - 0.5) * 1.6; + const sp = 200 + Math.random() * 700; + this.emit(x + nx * 2, y + ny * 2, Math.cos(a) * sp, Math.sin(a) * sp, 380 + Math.random() * 380, color); + } + } + + ring(x, y, maxR, lifeMs, color, width = 3, startR = 0) { + if (this.rings.length > 60) this.rings.shift(); + this.rings.push({ x, y, r: startR, startR, maxR, life: lifeMs, maxLife: lifeMs, color, width }); + } + + // holes: [{x, y, r}] — active black holes pull sparks in and spin them. + update(dtMs, holes = []) { + const dt = dtMs / 1000; + if (dt <= 0) return; + const { w, h } = this.bounds; + const f60 = dt * 60; + for (let i = 0; i < FX_MAX; i += 1) { + if (this.life[i] <= 0) continue; + this.life[i] -= dtMs; + if (this.life[i] <= 0) continue; + let vx = this.vx[i]; let vy = this.vy[i]; + for (let k = 0; k < holes.length; k += 1) { + const hole = holes[k]; + const dx = hole.x - this.x[i]; const dy = hole.y - this.y[i]; + const d2 = dx * dx + dy * dy; + if (d2 > 160000) continue; + const d = Math.sqrt(d2) || 1; + if (d < hole.r * 0.6) { this.life[i] = 0; break; } + const pull = 2600 * (1 - d / 400) * dt; + vx += (dx / d) * pull - (dy / d) * pull * 0.9; + vy += (dy / d) * pull + (dx / d) * pull * 0.9; + } + const damp = Math.pow(this.damp[i], f60); + vx *= damp; vy *= damp; + let x = this.x[i] + vx * dt; let y = this.y[i] + vy * dt; + if (x < 0) { x = -x; vx = -vx * 0.6; } else if (x > w) { x = 2 * w - x; vx = -vx * 0.6; } + if (y < 0) { y = -y; vy = -vy * 0.6; } else if (y > h) { y = 2 * h - y; vy = -vy * 0.6; } + this.x[i] = x; this.y[i] = y; this.vx[i] = vx; this.vy[i] = vy; + } + for (const r of this.rings) { + r.life -= dtMs; + const t = 1 - Math.max(0, r.life) / r.maxLife; + r.r = r.startR + (r.maxR - r.startR) * (1 - (1 - t) * (1 - t)); + } + if (this.rings.length && this.rings[0].life <= 0) this.rings = this.rings.filter((r) => r.life > 0); + } + + draw(g, view) { + const x0 = view.x - 40; const x1 = view.x + view.w + 40; + const y0 = view.y - 40; const y1 = view.y + view.h + 40; + for (const list of this.buckets.values()) list.length = 0; + for (let i = 0; i < FX_MAX; i += 1) { + if (this.life[i] <= 0) continue; + const x = this.x[i]; const y = this.y[i]; + if (x < x0 || x > x1 || y < y0 || y > y1) continue; + const f = this.life[i] / this.maxLife[i]; + const band = Math.min(ALPHA_BANDS - 1, Math.floor(Math.min(1, f * 1.6) * ALPHA_BANDS)); + const key = (this.col[i] * ALPHA_BANDS + band) * 2 + this.wid[i]; + let list = this.buckets.get(key); + if (!list) { list = []; this.buckets.set(key, list); } + list.push(i); + } + for (const [key, list] of this.buckets) { + if (list.length === 0) continue; + const wid = key % 2; + const band = Math.floor(key / 2) % ALPHA_BANDS; + const col = Math.floor(key / 2 / ALPHA_BANDS); + g.lineStyle(WIDTHS[wid], this.palette[col], (band + 1) / ALPHA_BANDS); + g.beginPath(); + for (const i of list) { + const vx = this.vx[i]; const vy = this.vy[i]; + const sp = Math.sqrt(vx * vx + vy * vy); + const L = Math.min(28, Math.max(2.5, sp * 0.02)) * this.len[i]; + const ux = sp > 1 ? vx / sp : 1; const uy = sp > 1 ? vy / sp : 0; + g.moveTo(this.x[i] - ux * L, this.y[i] - uy * L); + g.lineTo(this.x[i], this.y[i]); + } + g.strokePath(); + } + for (const r of this.rings) { + if (r.life <= 0) continue; + const f = Math.max(0, r.life) / r.maxLife; + g.lineStyle(r.width * 3, r.color, 0.18 * f); + g.strokeCircle(r.x, r.y, r.r); + g.lineStyle(r.width, r.color, 0.85 * f); + g.strokeCircle(r.x, r.y, r.r); + } + } +} diff --git a/src/games/geometrywars/GeometryWarsGame.js b/src/games/geometrywars/GeometryWarsGame.js new file mode 100644 index 0000000..6c35e30 --- /dev/null +++ b/src/games/geometrywars/GeometryWarsGame.js @@ -0,0 +1,1300 @@ +import * as Phaser from 'phaser'; +import { GAME_WIDTH, GAME_HEIGHT } from '../../config.js'; +import { Button } from '../../ui/Button.js'; +import { MusicPlayer } from '../../ui/MusicPlayer.js'; +import { getGameSoundtrack } from '../../services/soundtrack.js'; +import { playSoundEx, SFX } from '../../ui/Sounds.js'; +import { api } from '../../services/api.js'; +import { + STEP_MS, ARENA_W, ARENA_H, TUNE, ENEMY_TYPES, WEAPONS, + createGame, setInput, step, snakeSegments, gateEndpoints, +} from './GeometryWarsLogic.js'; +import WarpGrid from './GeometryWarsGrid.js'; +import ParticleField from './GeometryWarsFx.js'; +import { attachNeonBloom } from './GeometryWarsBloom.js'; +import { drawVectorText, formatScore } from './GeometryWarsVectorFont.js'; + +// Rendering model: the sim (GeometryWarsLogic) owns gameplay; this scene +// redraws a stack of Graphics layers from sim state every frame (no +// persistent per-entity GameObjects), same idiom as Tempest/Defender. +// +// Two cameras: the main camera scrolls over the arena (slightly larger than +// the screen) and carries the additive neon bloom; uiCam is fixed and +// bloom-free so HUD text, buttons and the music player stay crisp. +// routeCameras() assigns every display-list object to exactly one of them: +// anything registered in worldSet is world, everything else is UI. + +const C = { + bg: 0x02030a, + player: 0xffffff, + playerGlow: 0x8fe8ff, + bullet: 0xfff07a, + bulletGlow: 0xff8a1a, + geom: 0xc8ff3a, + border: 0x6f8dff, + hudLabel: 0x7f8fd8, + gold: 0xffd23a, + gate: 0xfff3b0, + gateEnd: 0xff9a2a, + danger: 0xff4f7a, +}; + +const ENEMY_COLOR = { + wanderer: 0xc65bff, + grunt: 0x38b8ff, + weaver: 0x46ff7a, + splitter: 0xff47d6, + mini: 0xff8ff0, + snake: 0xffd23a, + blackhole: 0xff3b3b, + proton: 0x9ff6ff, + rocket: 0xff7a1a, +}; +const SNAKE_TAIL = 0xff9f1c; + +const GRID_STYLE = { + minor: 0x1a2a8a, minorAlpha: 0.42, minorWidth: 1.2, + major: 0x2c45d8, majorAlpha: 0.5, majorWidth: 2, + hot: 0x5d7cff, hotAlpha: 0.5, + hotter: 0xb8c8ff, hotterAlpha: 0.7, + majorEvery: 4, +}; + +const MODE_INFO = { + evolved: { + title: 'EVOLVED', color: 0x5ff2ff, + desc: 'Shoot everything. Kills drop geoms; collect them to raise your multiplier and power up your gun. Three ships, three bombs.', + }, + pacifism: { + title: 'PACIFISM', color: 0xb6ff4a, + desc: 'No guns and one ship. Fly through the middle of a gate to detonate it and take every nearby enemy with the blast. The posts are deadly.', + }, +}; + +const GUIDE = ['wanderer', 'grunt', 'weaver', 'splitter', 'snake', 'rocket', 'blackhole']; + +const GLOW_KEY = 'geometrywars-glow'; +const GLOW_ORDER = ['high', 'low', 'off']; +const bestKey = (mode) => `geometrywars-best-${mode}`; + +const CAM_MARGIN = 90; +const TITLE_SCROLL_X = (ARENA_W - GAME_WIDTH) / 2; +const TITLE_SCROLL_Y = (ARENA_H - GAME_HEIGHT) / 2; +const SLOWMO_MS = 1300; +const UI_FONT = 'Righteous, "Julius Sans One", sans-serif'; + +// Player ship: a claw with its mouth open forward (nose = +x), unit space. +const SHIP = [1.05, -0.45, -0.1, -0.95, -0.8, -0.6, -0.45, 0, -0.8, 0.6, -0.1, 0.95, 1.05, 0.45, 0.25, 0.3, 0, 0, 0.25, -0.3, 1.05, -0.45]; +const SHIP_CORE = [-0.38, -0.26, 0.04, 0, -0.38, 0.26]; +const ROCKET = [1.25, 0, -0.9, -0.78, -0.4, 0, -0.9, 0.78, 1.25, 0]; +const SNAKE_HEAD = [1.25, 0, 0.3, -0.8, -0.85, -0.55, -0.85, 0.55, 0.3, 0.8, 1.25, 0]; + +function clamp(v, lo, hi) { return v < lo ? lo : (v > hi ? hi : v); } + +function lighten(color, f) { + const r = (color >> 16) & 255; const g = (color >> 8) & 255; const b = color & 255; + return (Math.round(r + (255 - r) * f) << 16) | (Math.round(g + (255 - g) * f) << 8) | Math.round(b + (255 - b) * f); +} + +function readStore(key, fallback) { + try { const v = localStorage.getItem(key); return v == null ? fallback : v; } catch (_) { return fallback; } +} +function writeStore(key, value) { + try { localStorage.setItem(key, String(value)); } catch (_) { /* private mode */ } +} + +// ── Shape builders: each pushes closed flat polylines [x0,y0,x1,y1,...] ───── + +function tpoly(out, local, x, y, ang, s) { + const c = Math.cos(ang) * s; const n = Math.sin(ang) * s; + const p = new Array(local.length); + for (let i = 0; i < local.length; i += 2) { + p[i] = x + local[i] * c - local[i + 1] * n; + p[i + 1] = y + local[i] * n + local[i + 1] * c; + } + out.push(p); +} + +function ngon(out, x, y, R, sides, rot) { + const p = []; + for (let i = 0; i <= sides; i += 1) { + const a = rot + (i / sides) * Math.PI * 2; + p.push(x + Math.cos(a) * R, y + Math.sin(a) * R); + } + out.push(p); +} + +function enemyShape(out, e, x, y, s, time) { + const r = (e.radius ?? ENEMY_TYPES[e.type].radius) * s; + switch (e.type) { + case 'wanderer': + for (let i = 0; i < 4; i += 1) { + const a = e.spin + i * Math.PI / 2; + out.push([x, y, x + Math.cos(a) * r, y + Math.sin(a) * r, + x + Math.cos(a + 0.85) * r * 0.62, y + Math.sin(a + 0.85) * r * 0.62, x, y]); + } + break; + case 'grunt': { + const k = 1 + 0.16 * Math.sin((e.age ?? time) * 0.009); + const a = r * 1.08 * k; const b = r * 1.08 / k; + out.push([x + a, y, x, y + b, x - a, y, x, y - b, x + a, y]); + out.push([x + a * 0.45, y, x, y + b * 0.45, x - a * 0.45, y, x, y - b * 0.45, x + a * 0.45, y]); + break; + } + case 'weaver': + ngon(out, x, y, r * 1.12, 4, e.spin + Math.PI / 4); + ngon(out, x, y, r * 1.12 * 0.7071, 4, e.spin); + break; + case 'splitter': + ngon(out, x, y, r * 1.15, 4, e.spin); + ngon(out, x, y, r * 0.8, 4, -e.spin * 0.6 + Math.PI / 4); + break; + case 'mini': + ngon(out, x, y, r * 1.2, 4, e.spin); + break; + case 'proton': + for (let i = 0; i < 3; i += 1) { + const a = e.spin + i * Math.PI / 3; + out.push([x - Math.cos(a) * r * 1.3, y - Math.sin(a) * r * 1.3, x + Math.cos(a) * r * 1.3, y + Math.sin(a) * r * 1.3]); + } + ngon(out, x, y, r * 0.55, 6, e.spin); + break; + case 'rocket': + tpoly(out, ROCKET, x, y, Math.atan2(e.vy || 0, e.vx || 1), r); + break; + case 'snake': + tpoly(out, SNAKE_HEAD, x, y, e.heading ?? 0, r); + break; + default: + ngon(out, x, y, r, 6, 0); + } +} + +export default class GeometryWarsGame extends Phaser.Scene { + constructor() { super('GeometryWarsGame'); } + + init(data) { + this.gameDef = data.game ?? { slug: 'geometrywars', name: 'Geometry Wars' }; + this.startMode = data.startMode ?? null; + this.mode = 'title'; // 'title' | 'playing' | 'paused' | 'gameover' + this.sim = null; + this.playMode = 'evolved'; + this.selectedCard = 0; + this.timeScale = 1; + this.slowMoMs = 0; + this.gridAcc = 0; + this.exhaustAcc = 0; + this.banner = null; + this.popups = []; + this.borderHits = []; + this.sfxLast = {}; + this.multPulse = 0; + this.titleDeco = []; + this.titleHole = null; + this.fireworkMs = 600; + this.decoPopMs = 1500; + this.endPanel = null; + this.pauseUi = null; + this.titleUi = null; + } + + create() { + this.input.mouse?.disableContextMenu(); + const main = this.cameras.main; + main.setBackgroundColor(C.bg); + this.uiCam = this.cameras.add(0, 0, GAME_WIDTH, GAME_HEIGHT); + this.worldSet = new Set(); + + const world = (depth, additive = true) => { + const g = this.add.graphics().setDepth(depth); + if (additive) g.setBlendMode(Phaser.BlendModes.ADD); + this.worldSet.add(g); + return g; + }; + this.gridG = world(0); + this.borderG = world(1); + this.holeG = world(2, false); + this.fxG = world(3); + this.geomG = world(4); + this.gateG = world(5); + this.enemyG = world(6); + this.bulletG = world(7); + this.playerG = world(8); + this.popupG = world(9); + this.titleG = world(10); + + this.hudG = this.add.graphics().setDepth(50); + this.bannerG = this.add.graphics().setDepth(52); + this.crossG = this.add.graphics().setDepth(95); + this.flash = this.add.rectangle(GAME_WIDTH / 2, GAME_HEIGHT / 2, GAME_WIDTH, GAME_HEIGHT, 0xffffff, 1) + .setDepth(80).setAlpha(0).setBlendMode(Phaser.BlendModes.ADD); + + try { + const { tracks, volume } = getGameSoundtrack(this); + if (tracks.length) this.music = new MusicPlayer(this, tracks, volume); + } catch (_) { /* optional */ } + + this.grid = new WarpGrid(ARENA_W, ARENA_H, 40); + this.fx = new ParticleField({ w: ARENA_W, h: ARENA_H }); + + this.glowLevel = readStore(GLOW_KEY, 'high'); + if (!GLOW_ORDER.includes(this.glowLevel)) this.glowLevel = 'high'; + this.bloom = attachNeonBloom(this, main, this.glowLevel); + + this.bindInput(); + this.game.events.on(Phaser.Core.Events.BLUR, this.onBlur, this); + this.events.once('shutdown', () => this.cleanup()); + + if (this.startMode) this.startRun(this.startMode); + else this.showTitle(); + this.routeCameras(); + } + + cleanup() { + this.input.setDefaultCursor('default'); + this.game.events.off(Phaser.Core.Events.BLUR, this.onBlur, this); + this.bloom?.destroy(); + } + + routeCameras() { + const main = this.cameras.main; + for (const obj of this.children.list) { + if (obj.__gwCam) continue; + obj.__gwCam = true; + if (this.worldSet.has(obj)) this.uiCam.ignore(obj); + else main.ignore(obj); + } + } + + // ── Input ───────────────────────────────────────────────────────────────── + + bindInput() { + this.keys = this.input.keyboard.addKeys('W,A,S,D,UP,DOWN,LEFT,RIGHT,SPACE,SHIFT,E,Q'); + this.input.keyboard.on('keydown', (ev) => { + const code = ev.code; + if (this.mode === 'title') { + if (code === 'ArrowLeft' || code === 'KeyA') this.selectCard(0); + else if (code === 'ArrowRight' || code === 'KeyD') this.selectCard(1); + else if (code === 'Enter' || code === 'Space') this.startRun(this.selectedCard === 0 ? 'evolved' : 'pacifism'); + } else if (this.mode === 'playing') { + if (code === 'Escape' || code === 'KeyP') this.pauseGame(); + } else if (this.mode === 'paused') { + if (code === 'Escape' || code === 'KeyP') this.resumeGame(); + } else if (this.mode === 'gameover' && this.endPanel) { + if (code === 'Enter') this.restartRun(); + } + }); + } + + readInput() { + const k = this.keys; + const ptr = this.input.activePointer; + const wp = this.cameras.main.getWorldPoint(ptr.x, ptr.y); + return { + moveX: ((k.D.isDown || k.RIGHT.isDown) ? 1 : 0) - ((k.A.isDown || k.LEFT.isDown) ? 1 : 0), + moveY: ((k.S.isDown || k.DOWN.isDown) ? 1 : 0) - ((k.W.isDown || k.UP.isDown) ? 1 : 0), + aimX: wp.x, + aimY: wp.y, + fire: ptr.leftButtonDown() || k.SPACE.isDown, + bomb: ptr.rightButtonDown() || k.SHIFT.isDown || k.E.isDown || k.Q.isDown, + }; + } + + onBlur() { + if (this.mode === 'playing') this.pauseGame(); + } + + sfx(key, { gap = 0, volume = 1, detune = 0 } = {}) { + const now = this.time.now; + if (gap > 0 && now - (this.sfxLast[key] ?? -1e9) < gap) return; + this.sfxLast[key] = now; + playSoundEx(this, key, { volume, detune: detune ? (Math.random() * 2 - 1) * detune : 0 }); + } + + // ── Frame loop ──────────────────────────────────────────────────────────── + + update(_time, delta) { + const dt = Math.min(delta, 50); + if (this.mode === 'title') this.updateTitle(dt); + else if (this.mode === 'playing' || this.mode === 'gameover') this.updatePlay(dt); + this.drawCrosshair(); + this.routeCameras(); + } + + viewRect() { + const cam = this.cameras.main; + return { x: cam.scrollX, y: cam.scrollY, w: GAME_WIDTH, h: GAME_HEIGHT }; + } + + stepGrid(dt, continuous) { + this.gridAcc = Math.min(this.gridAcc + dt, STEP_MS * 4); + while (this.gridAcc >= STEP_MS) { + this.gridAcc -= STEP_MS; + continuous(); + this.grid.update(); + } + } + + // ── Title screen ────────────────────────────────────────────────────────── + + showTitle() { + this.mode = 'title'; + this.input.setDefaultCursor('default'); + this.cameras.main.setScroll(TITLE_SCROLL_X, TITLE_SCROLL_Y); + + const view = this.viewRect(); + const decoTypes = ['wanderer', 'grunt', 'weaver', 'splitter', 'rocket', 'proton', 'mini', 'wanderer', 'grunt', 'weaver', 'splitter', 'rocket', 'wanderer', 'grunt']; + this.titleDeco = decoTypes.map((type) => this.makeDeco(type, view)); + this.titleHole = { a: Math.random() * Math.PI * 2, x: 0, y: 0, spin: 0 }; + + const ui = this.add.container(0, 0).setDepth(40); + this.titleUi = ui; + this.cardG = this.add.graphics(); + ui.add(this.cardG); + + const tag = this.add.text(GAME_WIDTH / 2, 420, 'A N E O N A R E N A S H O O T E R', { + fontFamily: UI_FONT, fontSize: '22px', color: '#8fa4ff', + }).setOrigin(0.5); + ui.add(tag); + + this.cards = []; + ['evolved', 'pacifism'].forEach((mode, idx) => { + const cx = GAME_WIDTH / 2 + (idx === 0 ? -300 : 300); + const cy = 590; + const desc = this.add.text(cx, cy + 4, MODE_INFO[mode].desc, { + fontFamily: UI_FONT, fontSize: '20px', color: '#c3cdf5', align: 'center', + wordWrap: { width: 470 }, lineSpacing: 4, + }).setOrigin(0.5); + const zone = this.add.zone(cx, cy, 540, 220).setInteractive({ useHandCursor: true }); + zone.on('pointerover', () => this.selectCard(idx)); + zone.on('pointerdown', () => this.startRun(mode)); + ui.add([desc, zone]); + this.cards.push({ mode, cx, cy, best: Number(readStore(bestKey(mode), 0)) || 0 }); + }); + + const guideY = 840; + GUIDE.forEach((type, i) => { + const x = GAME_WIDTH / 2 + (i - (GUIDE.length - 1) / 2) * 190; + const color = '#' + lighten(ENEMY_COLOR[type], 0.25).toString(16).padStart(6, '0'); + ui.add(this.add.text(x, guideY + 48, ENEMY_TYPES[type].name, { + fontFamily: UI_FONT, fontSize: '17px', color, + }).setOrigin(0.5)); + const note = type === 'snake' ? 'HEAD ONLY' : (type === 'blackhole' ? 'SHOOT TO WAKE' : `${ENEMY_TYPES[type].score} PTS`); + ui.add(this.add.text(x, guideY + 72, note, { + fontFamily: UI_FONT, fontSize: '14px', color: '#7f8fc8', + }).setOrigin(0.5)); + }); + + ui.add(this.add.text(GAME_WIDTH / 2, 972, + 'WASD / ARROWS move MOUSE aim LEFT CLICK / SPACE fire RIGHT CLICK / SHIFT bomb ESC pause', { + fontFamily: UI_FONT, fontSize: '19px', color: '#9aa6d8', + }).setOrigin(0.5)); + + const menu = new Button(this, 150, 1032, 'Menu', () => this.scene.start('GameMenu'), + { width: 200, height: 50, fontSize: 22, variant: 'ghost' }); + this.glowBtn = new Button(this, GAME_WIDTH - 170, 1032, this.glowLabel(), () => this.cycleGlow(), + { width: 240, height: 50, fontSize: 22, variant: 'ghost' }); + ui.add([menu, this.glowBtn]); + this.routeCameras(); + } + + glowLabel() { return `Glow: ${this.glowLevel[0].toUpperCase()}${this.glowLevel.slice(1)}`; } + + cycleGlow() { + const i = GLOW_ORDER.indexOf(this.glowLevel); + this.glowLevel = GLOW_ORDER[(i + 1) % GLOW_ORDER.length]; + writeStore(GLOW_KEY, this.glowLevel); + this.bloom.setLevel(this.glowLevel); + this.glowBtn?.text?.setText(this.glowLabel()); + this.pauseGlowBtn?.text?.setText(this.glowLabel()); + this.sfx(SFX.UI_PICK, { volume: 0.5 }); + } + + selectCard(idx) { + if (this.selectedCard === idx) return; + this.selectedCard = idx; + this.sfx(SFX.UI_PICK, { volume: 0.4, gap: 60 }); + } + + makeDeco(type, view) { + const a = Math.random() * Math.PI * 2; + const sp = 40 + Math.random() * 70; + return { + type, radius: ENEMY_TYPES[type].radius, + x: view.x + 80 + Math.random() * (view.w - 160), + y: view.y + 80 + Math.random() * (view.h - 160), + vx: Math.cos(a) * sp, vy: Math.sin(a) * sp, + spin: Math.random() * 6, heading: a, age: Math.random() * 5000, + spawnMs: 600, spawnTotal: 600, + }; + } + + destroyTitle() { + this.titleUi?.destroy(true); + this.titleUi = null; + this.glowBtn = null; + this.cards = []; + this.titleDeco = []; + this.titleHole = null; + this.titleG.clear(); + } + + updateTitle(dt) { + const view = this.viewRect(); + const t = this.time.now; + + // Drifting demo enemies; one pops every so often and warps back in. + for (const d of this.titleDeco) { + d.age += dt; + d.spin += dt * 0.003; + if (d.spawnMs > 0) d.spawnMs -= dt; + d.x += d.vx * dt / 1000; + d.y += d.vy * dt / 1000; + if (d.x < view.x + 40 || d.x > view.x + view.w - 40) d.vx = -d.vx; + if (d.y < view.y + 40 || d.y > view.y + view.h - 40) d.vy = -d.vy; + d.heading = Math.atan2(d.vy, d.vx); + } + this.decoPopMs -= dt; + if (this.decoPopMs <= 0 && this.titleDeco.length) { + this.decoPopMs = 1100 + Math.random() * 1300; + const i = Math.floor(Math.random() * this.titleDeco.length); + const d = this.titleDeco[i]; + this.burst(d.x, d.y, ENEMY_COLOR[d.type], 50); + this.grid.applyExplosiveForce(14, d.x, d.y, 160); + this.titleDeco[i] = this.makeDeco(d.type, view); + } + this.fireworkMs -= dt; + if (this.fireworkMs <= 0) { + this.fireworkMs = 700 + Math.random() * 900; + const colors = Object.values(ENEMY_COLOR); + const c = colors[Math.floor(Math.random() * colors.length)]; + const x = view.x + 100 + Math.random() * (view.w - 200); + const y = view.y + 100 + Math.random() * (view.h - 200); + this.fx.explosion(x, y, c, { count: 90, speed: 1100, life: 1300, color2: lighten(c, 0.6) }); + this.fx.ring(x, y, 160, 600, c, 2); + this.grid.applyExplosiveForce(22, x, y, 220); + } + + // A lone black hole circling the arena, warping the grid as it goes. + const hole = this.titleHole; + hole.a += dt * 0.00012; + hole.spin += dt * 0.0025; + hole.x = ARENA_W / 2 + Math.cos(hole.a) * 760; + hole.y = ARENA_H / 2 + Math.sin(hole.a * 1.7) * 330; + + this.stepGrid(dt, () => { + this.grid.applyImplosiveForce(3.6 + Math.sin(t * 0.004) * 1.2, hole.x, hole.y, 250); + }); + this.fx.update(dt, [{ x: hole.x, y: hole.y, r: 26 }]); + + this.gridG.clear(); + this.grid.draw(this.gridG, view, GRID_STYLE); + this.drawBorder(dt); + this.fxG.clear(); + this.fx.draw(this.fxG, view); + for (const g of [this.geomG, this.gateG, this.bulletG, this.playerG, this.popupG, this.hudG, this.bannerG]) g.clear(); + + // Demo enemies + the field guide icons share one batched draw per type. + const eg = this.enemyG; const hg = this.holeG; + eg.clear(); hg.clear(); + const batches = new Map(); + const push = (type, alpha, fn) => { + const key = `${type}|${alpha}`; + let b = batches.get(key); + if (!b) { b = { type, alpha, polys: [] }; batches.set(key, b); } + fn(b.polys); + }; + for (const d of this.titleDeco) { + const f = d.spawnMs > 0 ? 1 - d.spawnMs / d.spawnTotal : 1; + const s = 1 + (1 - f) * 1.4; + push(d.type, f < 1 ? 0.35 : 0.55, (out) => enemyShape(out, d, d.x, d.y, s, t)); + } + const guideY = 840 + TITLE_SCROLL_Y; + GUIDE.forEach((type, i) => { + const x = GAME_WIDTH / 2 + (i - (GUIDE.length - 1) / 2) * 190 + TITLE_SCROLL_X; + const fake = { type, radius: ENEMY_TYPES[type].radius, spin: t * 0.002 + i, age: t, heading: -Math.PI / 2 + Math.sin(t * 0.002 + i) * 0.4, vx: 0, vy: -1 }; + if (type === 'blackhole') { + this.drawBlackHole(hg, eg, { ...fake, active: true, absorbed: 0, radius: 20 }, x, guideY, t); + return; + } + if (type === 'snake') { + const segs = []; + for (let k = 1; k <= 5; k += 1) ngon(segs, x + Math.sin(t * 0.004 + k * 0.8) * 6, guideY + k * 9, 6 - k * 0.6, 4, Math.PI / 4); + this.strokeBatch(eg, segs, SNAKE_TAIL, 0.9, 6, 1.8); + } + push(type, 1, (out) => enemyShape(out, fake, x, guideY - (type === 'snake' ? 8 : 0), 1, t)); + }); + for (const b of batches.values()) this.strokeBatch(eg, b.polys, ENEMY_COLOR[b.type], b.alpha, 8, 2.2); + this.drawBlackHole(hg, eg, { type: 'blackhole', radius: 26, spin: hole.spin, active: true, absorbed: 0, age: t }, hole.x, hole.y, t); + + this.drawTitleText(t); + this.drawCards(t); + } + + drawTitleText(t) { + const g = this.titleG; + g.clear(); + const cx = GAME_WIDTH / 2 + TITLE_SCROLL_X; + const y1 = 190 + TITLE_SCROLL_Y; const y2 = 318 + TITLE_SCROLL_Y; + const skew = 0.16; + // Chromatic split that occasionally glitches wider. + const glitch = Math.sin(t * 0.0021) > 0.985 ? 10 : 0; + const off = 4 + Math.sin(t * 0.003) * 1.5 + glitch; + drawVectorText(g, 'GEOMETRY', cx - off, y1, 17, 0xff2fd0, { lineWidth: 3, glow: false, alpha: 0.6, skew, spacing: 0.5 }); + drawVectorText(g, 'GEOMETRY', cx + off, y1, 17, 0x29e7ff, { lineWidth: 3, glow: false, alpha: 0.6, skew, spacing: 0.5 }); + drawVectorText(g, 'GEOMETRY', cx, y1, 17, 0xffffff, { + lineWidth: 4.5, glowWidth: 18, glowColor: 0x6ff0ff, glowAlpha: 0.28, skew, spacing: 0.5, + }); + const pulse = 0.85 + 0.15 * Math.sin(t * 0.004); + drawVectorText(g, 'WARS', cx - off * 1.3, y2, 25, 0xff3b3b, { lineWidth: 4, glow: false, alpha: 0.55, skew, spacing: 0.7 }); + drawVectorText(g, 'WARS', cx + off * 1.3, y2, 25, 0xffd23a, { lineWidth: 4, glow: false, alpha: 0.55, skew, spacing: 0.7 }); + drawVectorText(g, 'WARS', cx, y2, 25, 0xfff2d0, { + lineWidth: 6, glowWidth: 26, glowColor: 0xff8a1a, glowAlpha: 0.3 * pulse, skew, spacing: 0.7, + }); + } + + drawCards(t) { + const g = this.cardG; + g.clear(); + if (!this.cards) return; + for (let i = 0; i < this.cards.length; i += 1) { + const card = this.cards[i]; + const info = MODE_INFO[card.mode]; + const sel = i === this.selectedCard; + const w = 540; const h = 220; + const x = card.cx - w / 2; const y = card.cy - h / 2; + const pulse = 0.5 + 0.5 * Math.sin(t * 0.006); + g.fillStyle(0x060a1e, sel ? 0.86 : 0.7); + g.fillRoundedRect(x, y, w, h, 16); + g.lineStyle(sel ? 12 : 8, info.color, sel ? 0.16 + 0.1 * pulse : 0.07); + g.strokeRoundedRect(x, y, w, h, 16); + g.lineStyle(sel ? 3 : 2, info.color, sel ? 1 : 0.45); + g.strokeRoundedRect(x, y, w, h, 16); + drawVectorText(g, info.title, card.cx, y + 42, 5, sel ? 0xffffff : lighten(info.color, 0.3), { + lineWidth: 3, glowWidth: 12, glowColor: info.color, glowAlpha: sel ? 0.35 : 0.15, skew: 0.12, spacing: 0.5, + }); + const best = card.best > 0 ? `BEST ${formatScore(card.best)}` : 'NO SCORE YET'; + drawVectorText(g, best, card.cx, y + h - 32, 2.2, card.best > 0 ? C.gold : 0x6c78b0, { + lineWidth: 1.8, glowWidth: 5, glowAlpha: 0.2, spacing: 0.4, + }); + if (sel) { + const ax = x - 24 - pulse * 6; + g.fillStyle(info.color, 0.9); + g.fillTriangle(ax, card.cy - 12, ax, card.cy + 12, ax + 14, card.cy); + const bx = x + w + 24 + pulse * 6; + g.fillTriangle(bx, card.cy - 12, bx, card.cy + 12, bx - 14, card.cy); + } + } + } + + // ── Running a game ──────────────────────────────────────────────────────── + + startRun(mode) { + if (this.mode === 'playing') return; + this.destroyTitle(); + this.playMode = mode; + this.sim = createGame({ seed: (Date.now() ^ Math.floor(Math.random() * 0xffffffff)) >>> 0, mode }); + this.best = Number(readStore(bestKey(mode), 0)) || 0; + this.mode = 'playing'; + this.popups = []; + this.borderHits = []; + this.fx.clear(); + this.input.setDefaultCursor('none'); + const p = this.sim.player; + this.cameras.main.setScroll( + clamp(p.x - GAME_WIDTH / 2, -CAM_MARGIN, ARENA_W - GAME_WIDTH + CAM_MARGIN), + clamp(p.y - GAME_HEIGHT / 2, -CAM_MARGIN, ARENA_H - GAME_HEIGHT + CAM_MARGIN), + ); + const info = MODE_INFO[mode]; + this.showBanner(info.title, info.color, 1900, mode === 'pacifism' ? 'FLY THROUGH THE GATES' : 'SURVIVE'); + this.fx.ring(p.x, p.y, 8, 700, C.playerGlow, 3, 260); + this.grid.applyImplosiveForce(8, p.x, p.y, 260); + this.sfx(SFX.COUNTDOWN_GO, { volume: 0.7 }); + this.routeCameras(); + } + + restartRun() { + this.scene.restart({ game: this.gameDef, startMode: this.playMode }); + } + + updatePlay(dt) { + const sim = this.sim; + if (!sim) return; + if (this.slowMoMs > 0) { + this.slowMoMs -= dt; + const f = 1 - Math.max(0, this.slowMoMs) / SLOWMO_MS; + this.timeScale = 0.25 + 0.75 * f * f; + } else { + this.timeScale = 1; + } + const sdt = dt * this.timeScale; + + if (this.mode === 'playing') { + setInput(sim, this.readInput()); + const events = step(sim, sdt); + for (const e of events) this.handleEvent(e); + } + + const t = this.time.now; + this.stepGrid(sdt, () => { + for (const b of sim.bullets) this.grid.applyExplosiveForce(2, b.x, b.y, 90); + for (const e of sim.enemies) { + if (e.type !== 'blackhole' || e.spawnMs > 0) continue; + if (e.active) this.grid.applyImplosiveForce(3.4 + Math.sin(t * 0.004 + e.id) * 1.1 + e.absorbed * 0.15, e.x, e.y, 240 + e.absorbed * 8); + else this.grid.applyImplosiveForce(0.5, e.x, e.y, 120); + } + const p = sim.player; + if (p.alive) this.grid.applyDirectedForce(p.vx * 0.0007, p.vy * 0.0007, 0, p.x, p.y, 70); + }); + this.emitExhaust(sdt); + const holes = []; + for (const e of sim.enemies) if (e.type === 'blackhole' && e.active && e.spawnMs <= 0) holes.push({ x: e.x, y: e.y, r: e.radius }); + this.fx.update(sdt, holes); + + if (sim.player.alive) this.updateCamera(dt); + this.drawWorld(dt); + this.drawHud(); + this.drawBanner(dt); + if (this.endPanel) this.drawEndPanel(dt); + } + + updateCamera(dt) { + const cam = this.cameras.main; + const p = this.sim.player; + const tx = clamp(p.x - GAME_WIDTH / 2, -CAM_MARGIN, ARENA_W - GAME_WIDTH + CAM_MARGIN); + const ty = clamp(p.y - GAME_HEIGHT / 2, -CAM_MARGIN, ARENA_H - GAME_HEIGHT + CAM_MARGIN); + const k = 1 - Math.pow(0.002, dt / 1000); + cam.setScroll(cam.scrollX + (tx - cam.scrollX) * k, cam.scrollY + (ty - cam.scrollY) * k); + } + + emitExhaust(dt) { + const p = this.sim.player; + if (!p.alive || dt <= 0) return; + const sp = Math.hypot(p.vx, p.vy); + if (sp < 60) { this.exhaustAcc = 0; return; } + this.exhaustAcc += dt; + const back = p.angle + Math.PI; + const ux = Math.cos(back); const uy = Math.sin(back); + const tx = p.x + ux * 15; const ty = p.y + uy * 15; + const thrust = Math.min(1, sp / TUNE.PLAYER_SPEED); + while (this.exhaustAcc >= 16) { + this.exhaustAcc -= 16; + const wob = Math.sin(this.time.now * 0.02) * 0.6; + const base = 240 * thrust; + const nx = -uy; const ny = ux; + for (const side of [-1, 1]) { + const vx = ux * base + nx * side * wob * 180 + (Math.random() - 0.5) * 30; + const vy = uy * base + ny * side * wob * 180 + (Math.random() - 0.5) * 30; + this.fx.emit(tx, ty, vx, vy, 380 + Math.random() * 120, 0xff4a10, { damp: 0.93 }); + } + this.fx.emit(tx, ty, ux * base * 1.2 + (Math.random() - 0.5) * 40, uy * base * 1.2 + (Math.random() - 0.5) * 40, + 420 + Math.random() * 120, 0xffbb1e, { damp: 0.93 }); + if (Math.random() < 0.5) { + this.fx.emit(tx, ty, ux * base, uy * base, 260, 0xfff2c0, { damp: 0.92, len: 0.6 }); + } + } + } + + burst(x, y, color, count, opts = {}) { + this.fx.explosion(x, y, color, { count, color2: lighten(color, 0.55), ...opts }); + } + + doFlash(alpha, ms) { + this.tweens.killTweensOf(this.flash); + this.flash.setAlpha(alpha); + this.tweens.add({ targets: this.flash, alpha: 0, duration: ms, ease: 'Quad.easeOut' }); + } + + addPopup(x, y, text, color) { + if (this.popups.length >= 45) this.popups.shift(); + this.popups.push({ x, y, text, color, life: 900, max: 900 }); + } + + handleEvent(e) { + const sim = this.sim; + switch (e.type) { + case 'shotFired': { + this.sfx(SFX.LASER_ZAP, { gap: 85, volume: 0.13, detune: 150 }); + const a = e.angle; + for (let i = 0; i < 2; i += 1) { + const da = (Math.random() - 0.5) * 0.6; + this.fx.emit(e.x + Math.cos(a) * 20, e.y + Math.sin(a) * 20, + Math.cos(a + da) * 500, Math.sin(a + da) * 500, 120, C.bullet, { len: 0.6 }); + } + break; + } + case 'enemySpawned': { + const color = ENEMY_COLOR[e.enemyType]; + this.fx.ring(e.x, e.y, 4, 650, color, 2, 48); + this.grid.applyImplosiveForce(1.5, e.x, e.y, 80); + this.sfx(SFX.SCIFI_PLONK, { gap: 180, volume: 0.1 }); + break; + } + case 'enemyKilled': { + const color = ENEMY_COLOR[e.enemyType]; + const mass = e.cause === 'bomb' || e.cause === 'death'; + if (e.cause === 'absorbed') { + this.burst(e.x, e.y, color, 14, { speed: 400, life: 500 }); + break; + } + if (e.enemyType === 'blackhole') { + this.burst(e.x, e.y, color, 180, { speed: 1400, life: 1500, wide: true }); + this.burst(e.x, e.y, 0xffffff, 60, { speed: 900, life: 900 }); + this.fx.ring(e.x, e.y, 320, 600, color, 4); + this.grid.applyExplosiveForce(40, e.x, e.y, 360); + this.cameras.main.shake(220, 0.006); + this.sfx(SFX.SCIFI_EXPLODE, { gap: 60, volume: 0.6 }); + } else { + this.burst(e.x, e.y, color, mass ? 16 : 55, mass ? { speed: 700, life: 800 } : {}); + this.grid.applyExplosiveForce(mass ? 6 : 12, e.x, e.y, mass ? 110 : 150); + if (!mass) this.sfx(SFX.EIGHTBIT_EXPLODE, { gap: 40, volume: 0.3, detune: 300 }); + } + if (e.points > 0) this.addPopup(e.x, e.y, formatScore(e.points), lighten(color, 0.35)); + break; + } + case 'enemyHit': + this.burst(e.x, e.y, 0xff7070, 10, { speed: 500, life: 380 }); + this.grid.applyExplosiveForce(4, e.x, e.y, 100); + this.sfx(SFX.SCIFI_PLINK, { gap: 60, volume: 0.14, detune: 200 }); + break; + case 'bulletWall': + this.fx.wallSparks(e.x, e.y, e.angle, C.bullet, 9); + if (this.borderHits.length > 40) this.borderHits.shift(); + this.borderHits.push({ x: e.x, y: e.y, life: 280 }); + this.grid.applyExplosiveForce(3, e.x, e.y, 80); + break; + case 'bulletDeflect': + this.burst(e.x, e.y, SNAKE_TAIL, 8, { speed: 500, life: 300 }); + this.sfx(SFX.SCIFI_PLINK, { gap: 90, volume: 0.1 }); + break; + case 'geomCollected': + this.multPulse = 1; + this.burst(e.x, e.y, C.geom, 5, { speed: 300, life: 300 }); + playSoundEx(this, SFX.SCIFI_PLINK, { volume: 0.12, detune: Math.min(1200, (e.multiplier % 40) * 30) }); + break; + case 'multiplierMilestone': + this.sfx(SFX.EIGHTBIT_COUNT, { volume: 0.5 }); + if (e.multiplier >= 25) this.showBanner(`×${e.multiplier}`, C.geom, 1100, 'MULTIPLIER'); + break; + case 'weaponUpgrade': + this.showBanner('WEAPON UPGRADE', C.bullet, 1700, e.name); + this.fx.ring(sim.player.x, sim.player.y, 180, 500, C.bullet, 3); + this.sfx(SFX.SCIFI_REVEAL, { volume: 0.55 }); + break; + case 'extraLife': + this.showBanner('EXTRA LIFE', 0x7dffb0, 1700); + this.sfx(SFX.VICTORY_SHORT, { volume: 0.6 }); + break; + case 'extraBomb': + this.showBanner('+1 BOMB', 0x9fd0ff, 1500); + this.sfx(SFX.UI_CHIME, { volume: 0.6 }); + break; + case 'bomb': + this.fx.ring(e.x, e.y, 1500, 900, 0xffffff, 6); + this.fx.ring(e.x, e.y, 1100, 700, 0x5fe8ff, 4); + this.fx.explosion(e.x, e.y, 0xffffff, { count: 260, speed: 1800, life: 1200, color2: 0x8ff0ff, wide: true }); + this.grid.applyExplosiveForce(40, e.x, e.y, 900); + this.grid.applyDirectedForce(0, 0, 150, e.x, e.y, 300); + this.doFlash(0.5, 380); + this.cameras.main.shake(380, 0.012); + this.sfx(SFX.SCIFI_EXPLODE, { volume: 0.8 }); + this.sfx(SFX.SCIFI_WOOSH, { volume: 0.6 }); + break; + case 'playerDied': + this.fx.explosion(e.x, e.y, 0xffffff, { count: 420, speed: 1700, life: 1700, color2: C.playerGlow, wide: true }); + this.fx.explosion(e.x, e.y, 0xffbb1e, { count: 160, speed: 1100, life: 1300, color2: 0xff4a10 }); + this.fx.ring(e.x, e.y, 700, 900, 0xffffff, 5); + this.fx.ring(e.x, e.y, 420, 650, C.playerGlow, 3); + this.grid.applyExplosiveForce(50, e.x, e.y, 700); + this.grid.applyDirectedForce(0, 0, 150, e.x, e.y, 260); + this.doFlash(0.6, 500); + this.cameras.main.shake(520, 0.018); + this.slowMoMs = SLOWMO_MS; + this.sfx(SFX.SCIFI_EXPLODE, { volume: 0.9 }); + this.sfx(SFX.EIGHTBIT_EXPLODE_2, { volume: 0.7 }); + if (e.lives > 0) this.showBanner(e.lives === 1 ? 'LAST SHIP' : `${e.lives} SHIPS LEFT`, C.danger, 1600); + break; + case 'playerRespawned': + this.fx.ring(e.x, e.y, 8, 700, C.playerGlow, 3, 240); + this.grid.applyImplosiveForce(8, e.x, e.y, 240); + this.sfx(SFX.SCIFI_WOOSH, { volume: 0.5 }); + break; + case 'blackHoleActivated': + this.fx.ring(e.x, e.y, 160, 500, ENEMY_COLOR.blackhole, 3); + this.sfx(SFX.ENERGY_HUM, { gap: 300, volume: 0.35 }); + break; + case 'blackHoleAbsorb': + this.grid.applyImplosiveForce(6, e.x, e.y, 200); + break; + case 'blackHoleOverload': + this.burst(e.x, e.y, ENEMY_COLOR.blackhole, 220, { speed: 1500, life: 1400, wide: true }); + this.burst(e.x, e.y, ENEMY_COLOR.proton, 80, { speed: 1000, life: 900 }); + this.fx.ring(e.x, e.y, 520, 700, 0xffffff, 4); + this.grid.applyExplosiveForce(45, e.x, e.y, 520); + this.cameras.main.shake(300, 0.009); + this.addPopup(e.x, e.y - 30, 'OVERLOAD!', ENEMY_COLOR.blackhole); + this.sfx(SFX.SCIFI_EXPLODE, { volume: 0.7 }); + break; + case 'rocketWall': + this.sfx(SFX.SCIFI_WOOSH, { gap: 400, volume: 0.3 }); + break; + case 'gateSpawned': + this.fx.ring(e.x, e.y, 6, 600, C.gate, 2, 110); + break; + case 'gateTriggered': + this.fx.ring(e.x, e.y, e.radius, 520, C.gold, 5); + this.fx.ring(e.x, e.y, e.radius * 0.6, 420, 0xffffff, 3); + this.burst(e.x, e.y, C.gold, 150, { speed: 1300, life: 1100, wide: true }); + this.burst(e.x, e.y, C.gateEnd, 60, { speed: 800, life: 800 }); + this.grid.applyExplosiveForce(45, e.x, e.y, 420); + this.cameras.main.shake(170, 0.005); + this.sfx(SFX.EIGHTBIT_EXPLODE_2, { volume: 0.55 }); + break; + case 'gameOver': + this.onGameOver(e); + break; + default: break; + } + } + + // ── World drawing ───────────────────────────────────────────────────────── + + strokeBatch(g, polys, color, alpha, glowW = 8, coreW = 2.2) { + if (!polys.length || alpha <= 0) return; + for (const [w, a] of [[glowW, 0.22 * alpha], [coreW, alpha]]) { + g.lineStyle(w, color, a); + g.beginPath(); + for (const p of polys) { + g.moveTo(p[0], p[1]); + for (let i = 2; i < p.length; i += 2) g.lineTo(p[i], p[i + 1]); + } + g.strokePath(); + } + } + + drawWorld(dt) { + const view = this.viewRect(); + const sim = this.sim; + const t = this.time.now; + + this.gridG.clear(); + this.grid.draw(this.gridG, view, GRID_STYLE); + this.drawBorder(dt); + this.fxG.clear(); + this.fx.draw(this.fxG, view); + + const vx0 = view.x - 80; const vx1 = view.x + view.w + 80; + const vy0 = view.y - 80; const vy1 = view.y + view.h + 80; + const inView = (x, y) => x > vx0 && x < vx1 && y > vy0 && y < vy1; + + // Geoms — blink out over their last 1.5s. + const gg = this.geomG; + gg.clear(); + const steady = []; const fading = []; + for (const g of sim.geoms) { + if (!inView(g.x, g.y)) continue; + if (g.lifeMs < 1500 && Math.floor(g.lifeMs / 120) % 2 === 0) continue; + ngon(g.lifeMs < 1500 ? fading : steady, g.x, g.y, 7, 4, g.spin); + } + this.strokeBatch(gg, steady, C.geom, 1, 6, 2); + this.strokeBatch(gg, fading, C.geom, 0.55, 6, 2); + + // Gates (pacifism). + const ga = this.gateG; + ga.clear(); + for (const g of sim.gates) { + const [a, b] = gateEndpoints(g); + const fade = g.spawnMs > 0 ? 0.3 + 0.4 * (Math.floor(t / 80) % 2) : 1; + const pulse = 0.75 + 0.25 * Math.sin(t * 0.008 + g.id); + ga.lineStyle(12, C.gold, 0.14 * fade * pulse); + ga.lineBetween(a.x, a.y, b.x, b.y); + ga.lineStyle(3, C.gate, 0.95 * fade * pulse); + ga.lineBetween(a.x, a.y, b.x, b.y); + for (const end of [a, b]) { + ga.lineStyle(7, C.gateEnd, 0.25 * fade); + ga.strokeCircle(end.x, end.y, TUNE.GATE_END_R); + ga.lineStyle(2.4, C.gateEnd, fade); + ga.strokeCircle(end.x, end.y, TUNE.GATE_END_R); + } + } + + // Enemies, batched per (type, alpha). + const eg = this.enemyG; const hg = this.holeG; + eg.clear(); hg.clear(); + const batches = new Map(); + const tail = []; + for (const e of sim.enemies) { + if (!inView(e.x, e.y) && e.type !== 'snake') continue; + if (e.type === 'blackhole') { this.drawBlackHole(hg, eg, e, e.x, e.y, t); continue; } + let s = 1; let alpha = 1; + if (e.spawnMs > 0 && e.spawnTotal > 0) { + const f = 1 - e.spawnMs / e.spawnTotal; + s = 1 + (1 - f) * 1.3; + alpha = Math.floor(t / 70) % 2 === 0 ? 0.65 : 0.3; + } + if (e.type === 'snake') { + const segs = snakeSegments(e); + let px = e.x; let py = e.y; + for (let i = 0; i < segs.length; i += 1) { + const sg = segs[i]; + if (inView(sg.x, sg.y)) ngon(tail, sg.x, sg.y, (11 - i * 0.45) * s, 4, Math.atan2(py - sg.y, px - sg.x) + Math.PI / 4); + px = sg.x; py = sg.y; + } + if (!inView(e.x, e.y)) continue; + } + const key = `${e.type}|${alpha}`; + let b = batches.get(key); + if (!b) { b = { type: e.type, alpha, polys: [] }; batches.set(key, b); } + enemyShape(b.polys, e, e.x, e.y, s, t); + } + this.strokeBatch(eg, tail, SNAKE_TAIL, 0.85, 6, 1.8); + for (const b of batches.values()) this.strokeBatch(eg, b.polys, ENEMY_COLOR[b.type], b.alpha, 8, 2.2); + + // Bullets: hot streaks. + const bg = this.bulletG; + bg.clear(); + if (sim.bullets.length) { + for (const [w, color, a] of [[7, C.bulletGlow, 0.3], [2.6, C.bullet, 1]]) { + bg.lineStyle(w, color, a); + bg.beginPath(); + for (const b of sim.bullets) { + if (!inView(b.x, b.y)) continue; + bg.moveTo(b.x - b.vx * 0.014, b.y - b.vy * 0.014); + bg.lineTo(b.x, b.y); + } + bg.strokePath(); + } + } + + // Player. + const pg = this.playerG; + pg.clear(); + const p = sim.player; + if (p.alive) { + const blink = p.invulnMs > 0 && Math.floor(p.invulnMs / 90) % 2 === 0; + this.drawShip(pg, p.x, p.y, p.angle, p.radius * 1.3, blink ? 0.35 : 1); + if (p.invulnMs > 0) { + pg.lineStyle(2, C.playerGlow, 0.25 + 0.2 * Math.sin(t * 0.02)); + pg.strokeCircle(p.x, p.y, 34); + } + } + + // Score popups. + const pp = this.popupG; + pp.clear(); + for (const pop of this.popups) pop.life -= dt * this.timeScale; + this.popups = this.popups.filter((pop) => pop.life > 0); + for (const pop of this.popups) { + const f = pop.life / pop.max; + drawVectorText(pp, pop.text, pop.x, pop.y - (1 - f) * 34, 1.7, pop.color, { + lineWidth: 1.6, glowWidth: 4, glowAlpha: 0.25, alpha: Math.min(1, f * 2), + }); + } + } + + drawShip(g, x, y, angle, scale, alpha) { + const polys = []; + tpoly(polys, SHIP, x, y, angle, scale); + this.strokeBatch(g, polys, C.player, alpha, 7, 2.4); + g.lineStyle(7, C.playerGlow, 0.3 * alpha); + g.beginPath(); + g.moveTo(polys[0][0], polys[0][1]); + for (let i = 2; i < polys[0].length; i += 2) g.lineTo(polys[0][i], polys[0][i + 1]); + g.strokePath(); + const core = []; + tpoly(core, SHIP_CORE, x, y, angle, scale); + this.strokeBatch(g, core, C.playerGlow, alpha, 5, 2); + } + + drawBlackHole(hg, eg, e, x, y, t) { + const r = e.radius; + const spawning = e.spawnMs > 0; + const color = ENEMY_COLOR.blackhole; + const alpha = spawning ? (Math.floor(t / 70) % 2 === 0 ? 0.6 : 0.3) : 1; + hg.fillStyle(0x000000, 0.9 * alpha); + hg.fillCircle(x, y, r * 1.05); + if (e.active && !spawning) { + const throb = 1 + 0.12 * Math.sin(t * 0.012 + (e.id ?? 0)); + eg.lineStyle(10, color, 0.22); + eg.strokeCircle(x, y, r * throb); + eg.lineStyle(3, color, 1); + eg.strokeCircle(x, y, r * throb); + eg.lineStyle(2, lighten(color, 0.4), 0.5); + eg.strokeCircle(x, y, r * 0.55); + for (let k = 0; k < 3; k += 1) { + const a0 = e.spin * 1.6 + (k / 3) * Math.PI * 2; + eg.lineStyle(6, color, 0.18); + eg.beginPath(); eg.arc(x, y, r * 1.5, a0, a0 + 1.2); eg.strokePath(); + eg.lineStyle(2, color, 0.85); + eg.beginPath(); eg.arc(x, y, r * 1.5, a0, a0 + 1.2); eg.strokePath(); + } + // A slow outward ripple so an active hole visibly "breathes". + const ph = ((t * 0.0012 + (e.id ?? 0) * 0.37) % 1); + eg.lineStyle(2, color, 0.5 * (1 - ph)); + eg.strokeCircle(x, y, r * (1.2 + ph * 1.8)); + } else { + for (let k = 0; k < 8; k += 1) { + const a0 = e.spin + (k / 8) * Math.PI * 2; + eg.lineStyle(5, color, 0.14 * alpha); + eg.beginPath(); eg.arc(x, y, r, a0, a0 + 0.45); eg.strokePath(); + eg.lineStyle(2, color, 0.75 * alpha); + eg.beginPath(); eg.arc(x, y, r, a0, a0 + 0.45); eg.strokePath(); + } + } + } + + drawBorder(dt) { + const g = this.borderG; + g.clear(); + g.lineStyle(12, C.border, 0.14); + g.strokeRect(0, 0, ARENA_W, ARENA_H); + g.lineStyle(3, C.border, 0.85); + g.strokeRect(0, 0, ARENA_W, ARENA_H); + for (const h of this.borderHits) h.life -= dt; + this.borderHits = this.borderHits.filter((h) => h.life > 0); + for (const h of this.borderHits) { + const a = h.life / 280; + const L = 70; + let x0; let y0; let x1; let y1; + if (h.x <= 1 || h.x >= ARENA_W - 1) { x0 = x1 = h.x; y0 = h.y - L; y1 = h.y + L; } + else { y0 = y1 = h.y; x0 = h.x - L; x1 = h.x + L; } + g.lineStyle(10, 0xffffff, 0.2 * a); + g.lineBetween(x0, y0, x1, y1); + g.lineStyle(3, 0xffffff, 0.9 * a); + g.lineBetween(x0, y0, x1, y1); + } + } + + // ── HUD (uiCam, screen space) ───────────────────────────────────────────── + + drawHud() { + const g = this.hudG; + g.clear(); + const s = this.sim; + if (!s || this.mode === 'gameover') return; + this.multPulse *= 0.88; + + drawVectorText(g, 'SCORE', 42, 30, 1.7, C.hudLabel, { align: 'left', lineWidth: 1.6, glowWidth: 4, spacing: 0.6 }); + drawVectorText(g, formatScore(s.score), 42, 66, 5, 0xffffff, { + align: 'left', lineWidth: 3, glowWidth: 11, glowColor: 0x5fe8ff, glowAlpha: 0.3, skew: 0.12, + }); + drawVectorText(g, `×${formatScore(s.multiplier)}`, 42, 114, 3.4 * (1 + 0.22 * this.multPulse), C.geom, { + align: 'left', lineWidth: 2.4, glowWidth: 9, glowAlpha: 0.3, skew: 0.12, + }); + + // Reserve ships (and bombs, in Evolved), centred along the top. `lives` + // counts the ship in play (or the one about to respawn), so reserve = -1. + const lives = Math.max(0, s.lives - 1); + const lifeW = 36; + const lx = GAME_WIDTH / 2 - ((lives - 1) * lifeW) / 2; + for (let i = 0; i < lives; i += 1) this.drawShip(g, lx + i * lifeW, 36, -Math.PI / 2, 12, 0.95); + if (s.mode === 'evolved') { + const bw = 30; + const bx = GAME_WIDTH / 2 - ((s.bombs - 1) * bw) / 2; + for (let i = 0; i < s.bombs; i += 1) { + g.lineStyle(6, 0x5fe8ff, 0.2); g.strokeCircle(bx + i * bw, 76, 9); + g.lineStyle(2, 0x9ff6ff, 1); g.strokeCircle(bx + i * bw, 76, 9); + g.fillStyle(0x9ff6ff, 0.8); g.fillCircle(bx + i * bw, 76, 3); + } + drawVectorText(g, WEAPONS[s.weaponTier].name, 42, 1046, 1.8, C.bullet, { + align: 'left', lineWidth: 1.6, glowWidth: 5, glowAlpha: 0.25, spacing: 0.5, alpha: 0.85, + }); + } else { + drawVectorText(g, `GATES ${s.stats.gates}`, 42, 1046, 1.8, C.gold, { + align: 'left', lineWidth: 1.6, glowWidth: 5, glowAlpha: 0.25, spacing: 0.5, alpha: 0.85, + }); + } + + const best = Math.max(this.best, s.score); + drawVectorText(g, 'HIGH SCORE', GAME_WIDTH - 42, 104, 1.7, C.hudLabel, { align: 'right', lineWidth: 1.6, glowWidth: 4, spacing: 0.6 }); + drawVectorText(g, formatScore(best), GAME_WIDTH - 42, 134, 3, s.score > this.best && this.best > 0 ? C.gold : 0xdfe6ff, { + align: 'right', lineWidth: 2.2, glowWidth: 7, glowAlpha: 0.25, skew: 0.12, + }); + const secs = Math.floor(s.timeMs / 1000); + drawVectorText(g, `${Math.floor(secs / 60)}:${String(secs % 60).padStart(2, '0')}`, GAME_WIDTH - 42, 1046, 1.8, C.hudLabel, { + align: 'right', lineWidth: 1.6, glowWidth: 4, spacing: 0.5, + }); + } + + showBanner(text, color, ms, sub = null) { + this.banner = { text, sub, color, ms, age: 0 }; + } + + drawBanner(dt) { + const g = this.bannerG; + g.clear(); + const b = this.banner; + if (!b || this.mode !== 'playing') return; + b.age += dt; + if (b.age >= b.ms) { this.banner = null; return; } + const f = b.age / b.ms; + const alpha = f < 0.1 ? f / 0.1 : (f > 0.72 ? (1 - f) / 0.28 : 1); + const pop = 1 + 0.3 * Math.max(0, 1 - b.age / 160); + drawVectorText(g, b.text, GAME_WIDTH / 2, 290, 7 * pop, b.color, { + lineWidth: 3.5, glowWidth: 16, glowAlpha: 0.3, alpha, skew: 0.12, spacing: 0.4, + }); + if (b.sub) { + drawVectorText(g, b.sub, GAME_WIDTH / 2, 360, 2.6, 0xdfe6ff, { + lineWidth: 2, glowWidth: 6, glowAlpha: 0.2, alpha: alpha * 0.9, spacing: 0.6, + }); + } + } + + drawCrosshair() { + const g = this.crossG; + g.clear(); + if (this.mode !== 'playing') return; + const ptr = this.input.activePointer; + const t = this.time.now * 0.003; + const x = ptr.x; const y = ptr.y; + for (const [w, a] of [[6, 0.2], [2, 0.95]]) { + g.lineStyle(w, 0x9ff6ff, a); + g.strokeCircle(x, y, 12); + g.beginPath(); + for (let k = 0; k < 4; k += 1) { + const ang = t + k * Math.PI / 2; + g.moveTo(x + Math.cos(ang) * 17, y + Math.sin(ang) * 17); + g.lineTo(x + Math.cos(ang) * 25, y + Math.sin(ang) * 25); + } + g.strokePath(); + } + g.fillStyle(0xffffff, 1); + g.fillCircle(x, y, 2); + } + + // ── Pause ───────────────────────────────────────────────────────────────── + + pauseGame() { + if (this.mode !== 'playing') return; + this.mode = 'paused'; + this.input.setDefaultCursor('default'); + this.crossG.clear(); + this.bannerG.clear(); + this.sfx(SFX.UI_FLIP, { volume: 0.5 }); + const cx = GAME_WIDTH / 2; const cy = GAME_HEIGHT / 2; + const root = this.add.container(0, 0).setDepth(70); + root.add(this.add.rectangle(cx, cy, GAME_WIDTH, GAME_HEIGHT, 0x000000, 0.55).setInteractive()); + const g = this.add.graphics(); + g.fillStyle(0x050816, 0.94); + g.fillRoundedRect(cx - 320, cy - 270, 640, 540, 20); + g.lineStyle(10, 0x5fe8ff, 0.14); + g.strokeRoundedRect(cx - 320, cy - 270, 640, 540, 20); + g.lineStyle(2.5, 0x5fe8ff, 0.9); + g.strokeRoundedRect(cx - 320, cy - 270, 640, 540, 20); + drawVectorText(g, 'PAUSED', cx, cy - 200, 7, 0xffffff, { + lineWidth: 3.5, glowWidth: 14, glowColor: 0x5fe8ff, glowAlpha: 0.3, skew: 0.12, spacing: 0.5, + }); + root.add(g); + const opts = { width: 340, height: 58, fontSize: 24 }; + const resume = new Button(this, cx, cy - 92, 'Resume', () => this.resumeGame(), opts); + const restart = new Button(this, cx, cy - 22, 'Restart', () => this.restartRun(), { ...opts, variant: 'ghost' }); + const title = new Button(this, cx, cy + 48, 'Title Screen', () => this.scene.restart({ game: this.gameDef }), { ...opts, variant: 'ghost' }); + this.pauseGlowBtn = new Button(this, cx, cy + 118, this.glowLabel(), () => this.cycleGlow(), { ...opts, variant: 'ghost' }); + const menu = new Button(this, cx, cy + 188, 'Quit to Menu', () => this.scene.start('GameMenu'), { ...opts, variant: 'ghost' }); + root.add([resume, restart, title, this.pauseGlowBtn, menu]); + this.pauseUi = root; + this.routeCameras(); + } + + resumeGame() { + if (this.mode !== 'paused') return; + this.pauseUi?.destroy(true); + this.pauseUi = null; + this.pauseGlowBtn = null; + this.mode = 'playing'; + this.input.setDefaultCursor('none'); + // Don't let a button click that resumed the game also fire a bomb. + if (this.sim) this.sim.bombLatch = true; + } + + // ── Game over ───────────────────────────────────────────────────────────── + + recordScore(score) { + const key = bestKey(this.playMode); + const prevBest = Number(readStore(key, 0)) || 0; + const newBest = score > prevBest; + if (newBest) writeStore(key, score); + api.post('/history/single-player', { + slug: 'geometrywars', score, opponentScores: [], result: 'loss', + }).catch(() => { /* best effort */ }); + return { prevBest, newBest }; + } + + onGameOver(e) { + this.mode = 'gameover'; + this.input.setDefaultCursor('default'); + this.banner = null; + const { prevBest, newBest } = this.recordScore(e.score); + this.time.delayedCall(700, () => this.showEndPanel(e, prevBest, newBest)); + } + + showEndPanel(e, prevBest, newBest) { + const cx = GAME_WIDTH / 2; const cy = GAME_HEIGHT / 2; + const root = this.add.container(0, 0).setDepth(70); + root.add(this.add.rectangle(cx, cy, GAME_WIDTH, GAME_HEIGHT, 0x000000, 0.5).setInteractive()); + const frame = this.add.graphics(); + const accent = MODE_INFO[this.playMode].color; + frame.fillStyle(0x050816, 0.94); + frame.fillRoundedRect(cx - 430, cy - 320, 860, 640, 22); + frame.lineStyle(12, accent, 0.14); + frame.strokeRoundedRect(cx - 430, cy - 320, 860, 640, 22); + frame.lineStyle(2.5, accent, 0.95); + frame.strokeRoundedRect(cx - 430, cy - 320, 860, 640, 22); + root.add(frame); + const live = this.add.graphics(); + root.add(live); + + const st = e.stats; + const secs = Math.floor(st.timeMs / 1000); + const rows = [ + ['TIME', `${Math.floor(secs / 60)}:${String(secs % 60).padStart(2, '0')}`], + ['KILLS', formatScore(st.kills)], + ['GEOMS', formatScore(st.geoms)], + ['PEAK MULTIPLIER', `×${formatScore(st.peakMult)}`], + this.playMode === 'evolved' ? ['BOMBS USED', String(st.bombsUsed)] : ['GATES', formatScore(st.gates)], + ['MODE', MODE_INFO[this.playMode].title], + ]; + rows.forEach(([label, value], i) => { + const col = i % 2; const row = Math.floor(i / 2); + const x0 = cx - 360 + col * 380; const y = cy + 40 + row * 46; + root.add(this.add.text(x0, y, label, { fontFamily: UI_FONT, fontSize: '20px', color: '#7f8fd8' }).setOrigin(0, 0.5)); + root.add(this.add.text(x0 + 340, y, value, { fontFamily: UI_FONT, fontSize: '24px', color: '#eef2ff' }).setOrigin(1, 0.5)); + }); + + const opts = { width: 240, height: 58, fontSize: 24 }; + const again = new Button(this, cx - 260, cy + 250, 'Play Again', () => this.restartRun(), opts); + const title = new Button(this, cx, cy + 250, 'Title Screen', () => this.scene.restart({ game: this.gameDef }), { ...opts, variant: 'ghost' }); + const menu = new Button(this, cx + 260, cy + 250, 'Menu', () => this.scene.start('GameMenu'), { ...opts, variant: 'ghost' }); + root.add([again, title, menu]); + + this.endPanel = { root, live, score: e.score, shown: 0, age: 0, newBest, prevBest }; + this.sfx(newBest ? SFX.EIGHTBIT_WIN : SFX.CASINO_LOSE, { volume: 0.55 }); + this.routeCameras(); + } + + drawEndPanel(dt) { + const p = this.endPanel; + const g = p.live; + g.clear(); + p.age += dt; + const f = Math.min(1, p.age / 1100); + p.shown = Math.round(p.score * (1 - Math.pow(1 - f, 3))); + const cx = GAME_WIDTH / 2; const cy = GAME_HEIGHT / 2; + drawVectorText(g, 'GAME OVER', cx, cy - 250, 8, C.danger, { + lineWidth: 3.5, glowWidth: 16, glowAlpha: 0.3, skew: 0.12, spacing: 0.5, + }); + drawVectorText(g, formatScore(p.shown), cx, cy - 130, 8, 0xffffff, { + lineWidth: 4, glowWidth: 18, glowColor: 0x5fe8ff, glowAlpha: 0.3, skew: 0.12, + }); + if (p.newBest) { + const blink = 0.6 + 0.4 * Math.sin(p.age * 0.01); + drawVectorText(g, 'NEW HIGH SCORE!', cx, cy - 50, 3, C.gold, { + lineWidth: 2.4, glowWidth: 9, glowAlpha: 0.35, alpha: blink, spacing: 0.5, + }); + } else if (p.prevBest > 0) { + drawVectorText(g, `BEST ${formatScore(p.prevBest)}`, cx, cy - 50, 2.4, 0x9aa6d8, { + lineWidth: 2, glowWidth: 6, glowAlpha: 0.2, spacing: 0.5, + }); + } + } +} diff --git a/src/games/geometrywars/GeometryWarsGrid.js b/src/games/geometrywars/GeometryWarsGrid.js new file mode 100644 index 0000000..9c36c0d --- /dev/null +++ b/src/games/geometrywars/GeometryWarsGrid.js @@ -0,0 +1,249 @@ +// The signature warping background grid: a lattice of point masses joined by +// springs, pushed around by bullets, explosions and black holes, and drawn +// with a slight perspective projection so pushes along z read as bulges. +// +// Phaser-free (it only calls Graphics methods on the object passed to +// draw()), so tools/verifyGeometryWars.js can exercise the physics in Node. +// +// Model after Michael Hoffman's well-known Geometry Wars grid write-up: +// springs only pull (target length is 95% of the rest length, which keeps +// the lattice taut), each mass's damping resets to 0.98 every update, and +// force calls temporarily raise damping so a blast punches in and then +// settles instead of ringing forever. Border masses are pinned outright +// (Hoffman anchors them with springs) so the lattice starts at equilibrium. +// Forces are in px/step units at the fixed 60Hz update rate. + +const SPRING_STIFF = 0.28; +const SPRING_DAMP = 0.06; +const ANCHOR_STIFF = 0.002; +const ANCHOR_DAMP = 0.02; +const ANCHOR_EVERY = 3; +const BASE_DAMPING = 0.98; +const PERSPECTIVE = 2000; + +export default class WarpGrid { + constructor(width, height, spacing = 40) { + this.width = width; + this.height = height; + this.spacing = spacing; + this.cols = Math.floor(width / spacing) + 1; + this.rows = Math.floor(height / spacing) + 1; + const n = this.cols * this.rows; + this.n = n; + + this.px = new Float32Array(n); this.py = new Float32Array(n); this.pz = new Float32Array(n); + this.vx = new Float32Array(n); this.vy = new Float32Array(n); this.vz = new Float32Array(n); + this.ax = new Float32Array(n); this.ay = new Float32Array(n); this.az = new Float32Array(n); + this.ox = new Float32Array(n); this.oy = new Float32Array(n); + this.inv = new Float32Array(n); + this.damp = new Float32Array(n).fill(BASE_DAMPING); + // Projected screen positions, refreshed by draw(). + this.sx = new Float32Array(n); this.sy = new Float32Array(n); + + for (let r = 0; r < this.rows; r += 1) { + for (let c = 0; c < this.cols; c += 1) { + const i = r * this.cols + c; + const x = c * spacing; const y = r * spacing; + this.px[i] = x; this.py[i] = y; + this.ox[i] = x; this.oy[i] = y; + const border = r === 0 || c === 0 || r === this.rows - 1 || c === this.cols - 1; + this.inv[i] = border ? 0 : 1; + } + } + + // Springs: neighbour links (b >= 0) and interior anchors (b === -1, + // pulling toward the mass's own rest position). + const sa = []; const sb = []; const rest = []; const k = []; const damp = []; + for (let r = 0; r < this.rows; r += 1) { + for (let c = 0; c < this.cols; c += 1) { + const i = r * this.cols + c; + if (c < this.cols - 1) { sa.push(i); sb.push(i + 1); rest.push(spacing * 0.95); k.push(SPRING_STIFF); damp.push(SPRING_DAMP); } + if (r < this.rows - 1) { sa.push(i); sb.push(i + this.cols); rest.push(spacing * 0.95); k.push(SPRING_STIFF); damp.push(SPRING_DAMP); } + if (this.inv[i] > 0 && r % ANCHOR_EVERY === 0 && c % ANCHOR_EVERY === 0) { + sa.push(i); sb.push(-1); rest.push(0); k.push(ANCHOR_STIFF); damp.push(ANCHOR_DAMP); + } + } + } + this.sa = Int32Array.from(sa); + this.sb = Int32Array.from(sb); + this.rest = Float32Array.from(rest); + this.k = Float32Array.from(k); + this.sdamp = Float32Array.from(damp); + } + + // Index range of lattice cells that could lie within `radius` of (x, y). + // Masses drift from their rest spots, so pad by a couple of cells. + range(x, y, radius) { + const pad = this.spacing * 2; + const s = this.spacing; + return [ + Math.max(1, Math.floor((x - radius - pad) / s)), + Math.min(this.cols - 2, Math.ceil((x + radius + pad) / s)), + Math.max(1, Math.floor((y - radius - pad) / s)), + Math.min(this.rows - 2, Math.ceil((y + radius + pad) / s)), + ]; + } + + applyDirectedForce(fx, fy, fz, x, y, radius) { + const [c0, c1, r0, r1] = this.range(x, y, radius); + const r2 = radius * radius; + for (let r = r0; r <= r1; r += 1) { + for (let c = c0; c <= c1; c += 1) { + const i = r * this.cols + c; + const dx = x - this.px[i]; const dy = y - this.py[i]; const dz = this.pz[i]; + const d2 = dx * dx + dy * dy + dz * dz; + if (d2 >= r2) continue; + const f = (10 / (10 + Math.sqrt(d2))) * this.inv[i]; + this.ax[i] += fx * f; this.ay[i] += fy * f; this.az[i] += fz * f; + } + } + } + + applyImplosiveForce(force, x, y, radius) { + const [c0, c1, r0, r1] = this.range(x, y, radius); + const r2 = radius * radius; + for (let r = r0; r <= r1; r += 1) { + for (let c = c0; c <= c1; c += 1) { + const i = r * this.cols + c; + const dx = x - this.px[i]; const dy = y - this.py[i]; const dz = -this.pz[i]; + const d2 = dx * dx + dy * dy + dz * dz; + if (d2 >= r2) continue; + const f = ((10 * force) / (100 + d2)) * this.inv[i]; + this.ax[i] += dx * f; this.ay[i] += dy * f; this.az[i] += dz * f; + this.damp[i] *= 0.6; + } + } + } + + applyExplosiveForce(force, x, y, radius) { + const [c0, c1, r0, r1] = this.range(x, y, radius); + const r2 = radius * radius; + for (let r = r0; r <= r1; r += 1) { + for (let c = c0; c <= c1; c += 1) { + const i = r * this.cols + c; + const dx = this.px[i] - x; const dy = this.py[i] - y; const dz = this.pz[i]; + const d2 = dx * dx + dy * dy + dz * dz; + if (d2 >= r2) continue; + const f = ((100 * force) / (10000 + d2)) * this.inv[i]; + this.ax[i] += dx * f; this.ay[i] += dy * f; this.az[i] += dz * f; + this.damp[i] *= 0.6; + } + } + } + + // One fixed 60Hz physics step. + update() { + const { px, py, pz, vx, vy, vz, ax, ay, az, inv, sa, sb, rest, k, sdamp } = this; + const m = sa.length; + for (let s = 0; s < m; s += 1) { + const a = sa[s]; const b = sb[s]; + let bxp; let byp; let bzp; let bvx; let bvy; let bvz; + if (b >= 0) { bxp = px[b]; byp = py[b]; bzp = pz[b]; bvx = vx[b]; bvy = vy[b]; bvz = vz[b]; } + else { bxp = this.ox[a]; byp = this.oy[a]; bzp = 0; bvx = 0; bvy = 0; bvz = 0; } + let dx = px[a] - bxp; let dy = py[a] - byp; let dz = pz[a] - bzp; + const len = Math.sqrt(dx * dx + dy * dy + dz * dz); + if (len <= rest[s]) continue; + const stretch = (len - rest[s]) / len; + dx *= stretch; dy *= stretch; dz *= stretch; + const kk = k[s]; const cc = sdamp[s]; + const fx = kk * dx - (bvx - vx[a]) * cc; + const fy = kk * dy - (bvy - vy[a]) * cc; + const fz = kk * dz - (bvz - vz[a]) * cc; + ax[a] -= fx * inv[a]; ay[a] -= fy * inv[a]; az[a] -= fz * inv[a]; + if (b >= 0) { ax[b] += fx * inv[b]; ay[b] += fy * inv[b]; az[b] += fz * inv[b]; } + } + const damp = this.damp; + for (let i = 0; i < this.n; i += 1) { + if (inv[i] === 0) { ax[i] = 0; ay[i] = 0; az[i] = 0; continue; } + vx[i] += ax[i]; vy[i] += ay[i]; vz[i] += az[i]; + px[i] += vx[i]; py[i] += vy[i]; pz[i] += vz[i]; + ax[i] = 0; ay[i] = 0; az[i] = 0; + if (vx[i] * vx[i] + vy[i] * vy[i] + vz[i] * vz[i] < 1e-6) { vx[i] = 0; vy[i] = 0; vz[i] = 0; } + const d = damp[i]; + vx[i] *= d; vy[i] *= d; vz[i] *= d; + damp[i] = BASE_DAMPING; + } + } + + // Largest displacement from rest anywhere on the lattice (verification aid). + maxDisplacement() { + let m = 0; + for (let i = 0; i < this.n; i += 1) { + const dx = this.px[i] - this.ox[i]; const dy = this.py[i] - this.oy[i]; const dz = this.pz[i]; + const d = Math.sqrt(dx * dx + dy * dy + dz * dz); + if (d > m) m = d; + } + return m; + } + + // Draw into a Graphics-like object. `view` = {x, y, w, h} world rect. + // `style` = { minor, minorAlpha, minorWidth, major, majorAlpha, majorWidth, + // hot, hotAlpha, hotter, hotterAlpha, majorEvery }. + draw(g, view, style) { + const { cols, rows, spacing, px, py, pz, sx, sy } = this; + const cx = view.x + view.w / 2; const cy = view.y + view.h / 2; + const c0 = Math.max(0, Math.floor(view.x / spacing) - 1); + const c1 = Math.min(cols - 1, Math.ceil((view.x + view.w) / spacing) + 1); + const r0 = Math.max(0, Math.floor(view.y / spacing) - 1); + const r1 = Math.min(rows - 1, Math.ceil((view.y + view.h) / spacing) + 1); + + for (let r = r0; r <= r1; r += 1) { + for (let c = c0; c <= c1; c += 1) { + const i = r * cols + c; + const f = (pz[i] + PERSPECTIVE) / PERSPECTIVE; + sx[i] = (px[i] - cx) * f + cx; + sy[i] = (py[i] - cy) * f + cy; + } + } + + const every = style.majorEvery ?? 4; + const pass = (major) => { + g.beginPath(); + for (let r = r0; r <= r1; r += 1) { + if ((r % every === 0) !== major) continue; + let i = r * cols + c0; + g.moveTo(sx[i], sy[i]); + for (let c = c0 + 1; c <= c1; c += 1) { i += 1; g.lineTo(sx[i], sy[i]); } + } + for (let c = c0; c <= c1; c += 1) { + if ((c % every === 0) !== major) continue; + let i = r0 * cols + c; + g.moveTo(sx[i], sy[i]); + for (let r = r0 + 1; r <= r1; r += 1) { i += cols; g.lineTo(sx[i], sy[i]); } + } + g.strokePath(); + }; + g.lineStyle(style.minorWidth, style.minor, style.minorAlpha); + pass(false); + g.lineStyle(style.majorWidth, style.major, style.majorAlpha); + pass(true); + + // Hot overlay: segments around strongly displaced masses glow brighter, + // so ripples and black-hole funnels light up instead of just bending. + const hot = []; const hotter = []; + for (let r = r0; r <= r1; r += 1) { + for (let c = c0; c <= c1; c += 1) { + const i = r * cols + c; + const dx = px[i] - this.ox[i]; const dy = py[i] - this.oy[i]; + const d2 = dx * dx + dy * dy + pz[i] * pz[i] * 0.0025; + if (d2 > 144) hotter.push(i); + else if (d2 > 25) hot.push(i); + } + } + // Only link to neighbours inside the projected window — anything outside + // it holds a stale (or never-written) screen position. + const segs = (list) => { + g.beginPath(); + for (const i of list) { + const c = i % cols; const r = (i - c) / cols; + if (c < c1) { g.moveTo(sx[i], sy[i]); g.lineTo(sx[i + 1], sy[i + 1]); } + if (r < r1) { g.moveTo(sx[i], sy[i]); g.lineTo(sx[i + cols], sy[i + cols]); } + if (c > c0) { g.moveTo(sx[i], sy[i]); g.lineTo(sx[i - 1], sy[i - 1]); } + if (r > r0) { g.moveTo(sx[i], sy[i]); g.lineTo(sx[i - cols], sy[i - cols]); } + } + g.strokePath(); + }; + if (hot.length) { g.lineStyle(style.majorWidth, style.hot, style.hotAlpha); segs(hot); } + if (hotter.length) { g.lineStyle(style.majorWidth + 0.5, style.hotter, style.hotterAlpha); segs(hotter); } + } +} diff --git a/src/games/geometrywars/GeometryWarsLogic.js b/src/games/geometrywars/GeometryWarsLogic.js new file mode 100644 index 0000000..0c2c257 --- /dev/null +++ b/src/games/geometrywars/GeometryWarsLogic.js @@ -0,0 +1,1071 @@ +// Geometry Wars — headless simulation. Zero Phaser imports so +// tools/verifyGeometryWars.js can drive it in Node. +// +// Fixed-tick accumulator loop (60Hz, MAX_STEPS spiral-of-death guard), same +// pattern as Defender/Total Annihilation. All randomness goes through the +// per-state mulberry32 stream and entity ids come from a per-state counter, +// so a seed + input sequence always replays identically. +// +// Two modes: +// evolved — twin-stick shooting, bombs, 3 lives. Geoms dropped by kills +// raise the multiplier, and the multiplier powers up the gun. +// pacifism — no gun, no bombs, one life. Fly through gates to detonate +// them; the blast takes out every enemy nearby. +// +// The sim only produces state + events. Every visual (particles, the warping +// grid, glow, screen shake) is the scene's job, driven by those events. + +export const STEP_MS = 1000 / 60; +export const MAX_STEPS = 5; + +export const ARENA_W = 2240; +export const ARENA_H = 1280; + +export const MODES = ['evolved', 'pacifism']; + +export const TUNE = { + PLAYER_RADIUS: 16, + PLAYER_SPEED: 560, + PLAYER_ACCEL: 16, // velocity smoothing rate, 1/s + BULLET_SPEED: 1500, + BULLET_RADIUS: 6, + BULLET_LIFE_MS: 1400, + MAX_BULLETS: 260, + START_LIVES: 3, + START_BOMBS: 3, + MAX_LIVES: 9, + MAX_BOMBS: 9, + RESPAWN_MS: 1900, + INVULN_MS: 2000, + BOMB_LULL_MS: 1600, // spawn pause after a bomb + SPAWN_WARMUP_MS: 700, // enemies fade in harmlessly for this long + SPAWN_SAFE_DIST: 340, // nothing spawns closer than this to the player + HIT_SLACK: 0.8, // player-vs-enemy hitboxes shrunk to feel fair + GEOM_LIFE_MS: 7000, + GEOM_MAGNET_R: 170, + GEOM_PICKUP_R: 30, + GEOM_MAX_SPEED: 900, + MAX_ENEMIES: 170, + MAX_GEOMS: 240, + SNAKE_SEGMENTS: 12, + SNAKE_SPACING: 4, // ticks of trail between tail segments + SNAKE_SEG_RADIUS: 10, + WEAVER_DODGE_R: 170, + BLACKHOLE_HP: 14, + BLACKHOLE_PULL_R: 520, + BLACKHOLE_PULL_ENEMY: 300, // px/s at the centre, falling off to 0 at PULL_R + BLACKHOLE_PULL_PLAYER: 230, + BLACKHOLE_BEND: 2600, // px/s² applied to bullets at the centre + BLACKHOLE_OVERLOAD: 12, // absorbed enemies before it bursts into protons + BLACKHOLE_MAX: 3, + GATE_LEN: 170, + GATE_END_R: 9, + GATE_BLAST_R: 300, + DEATH_DIFFICULTY_REWIND: 0.75, + FIRST_LIFE_AT: 100000, + FIRST_BOMB_AT: 200000, + THRESHOLD_GROWTH: 3, +}; + +// Per-type base stats. Points are multiplied by the current multiplier. +export const ENEMY_TYPES = { + wanderer: { radius: 20, speed: 140, score: 25, geoms: 1, name: 'WANDERER' }, + grunt: { radius: 18, speed: 215, score: 50, geoms: 1, name: 'GRUNT' }, + weaver: { radius: 18, speed: 250, score: 100, geoms: 2, name: 'WEAVER' }, + splitter: { radius: 22, speed: 150, score: 100, geoms: 1, name: 'SPLITTER' }, + mini: { radius: 11, speed: 270, score: 50, geoms: 1, name: 'MINI' }, + snake: { radius: 17, speed: 235, score: 150, geoms: 3, name: 'SNAKE' }, + blackhole: { radius: 26, speed: 25, score: 150, geoms: 4, name: 'BLACK HOLE' }, + proton: { radius: 9, speed: 340, score: 10, geoms: 0, name: 'PROTON' }, + rocket: { radius: 15, speed: 380, score: 25, geoms: 1, name: 'ROCKET' }, +}; + +// Gun tiers, unlocked by multiplier (which resets on death, so does the gun). +// `shots` entries: da = angle offset from the aim, off = sideways offset. +export const WEAPONS = [ + { name: 'TWIN STREAM', minMult: 1, intervalMs: 95, + shots: [{ da: 0, off: -8 }, { da: 0, off: 8 }] }, + { name: 'TRIPLE SPREAD', minMult: 10, intervalMs: 100, + shots: [{ da: -0.09, off: 0 }, { da: 0, off: 0 }, { da: 0.09, off: 0 }] }, + { name: 'QUAD CANNON', minMult: 40, intervalMs: 95, + shots: [{ da: -0.16, off: 0 }, { da: -0.03, off: -7 }, { da: 0.03, off: 7 }, { da: 0.16, off: 0 }] }, + { name: 'STARBURST', minMult: 100, intervalMs: 85, + shots: [ + { da: -0.24, off: 0 }, { da: -0.12, off: 0 }, { da: 0, off: 0 }, { da: 0.12, off: 0 }, { da: 0.24, off: 0 }, + { da: Math.PI - 0.18, off: 0 }, { da: Math.PI + 0.18, off: 0 }, + ] }, +]; + +const MULT_MILESTONES = [10, 25, 50, 100, 250, 500, 1000, 2000, 5000]; + +export function weaponTierFor(mult) { + let tier = 0; + for (let i = 0; i < WEAPONS.length; i += 1) if (mult >= WEAPONS[i].minMult) tier = i; + return tier; +} + +// ── Small math helpers ─────────────────────────────────────────────────────── + +function clamp(v, lo, hi) { return v < lo ? lo : (v > hi ? hi : v); } + +function wrapAngle(a) { + while (a > Math.PI) a -= Math.PI * 2; + while (a < -Math.PI) a += Math.PI * 2; + return a; +} + +function turnToward(current, target, maxStep) { + const d = wrapAngle(target - current); + if (Math.abs(d) <= maxStep) return target; + return current + Math.sign(d) * maxStep; +} + +// mulberry32 on the state's own stream. +export function rand(s) { + s.rng = (s.rng + 0x6D2B79F5) | 0; + let t = s.rng; + t = Math.imul(t ^ (t >>> 15), t | 1); + t ^= t + Math.imul(t ^ (t >>> 7), t | 61); + return ((t ^ (t >>> 14)) >>> 0) / 4294967296; +} + +function randRange(s, lo, hi) { return lo + (hi - lo) * rand(s); } + +function pickWeighted(s, entries) { + let total = 0; + for (const [, w] of entries) total += w; + let r = rand(s) * total; + for (const [k, w] of entries) { + r -= w; + if (r <= 0) return k; + } + return entries[entries.length - 1][0]; +} + +function emit(s, ev) { s.events.push(ev); } + +// ── State ──────────────────────────────────────────────────────────────────── + +export function createGame({ seed = 1, mode = 'evolved' } = {}) { + const pac = mode === 'pacifism'; + return { + mode: pac ? 'pacifism' : 'evolved', + rng: (seed >>> 0) || 1, + acc: 0, + tick: 0, + timeMs: 0, + phase: 'playing', // 'playing' | 'over' + player: { + x: ARENA_W / 2, y: ARENA_H / 2, vx: 0, vy: 0, angle: -Math.PI / 2, + radius: TUNE.PLAYER_RADIUS, alive: true, invulnMs: 0, respawnMs: 0, fireCd: 0, + }, + input: { moveX: 0, moveY: 0, aimX: ARENA_W / 2, aimY: ARENA_H / 2 - 200, fire: false, bomb: false }, + bombLatch: false, + bullets: [], + enemies: [], + geoms: [], + gates: [], + score: 0, + multiplier: 1, + lives: pac ? 1 : TUNE.START_LIVES, + bombs: pac ? 0 : TUNE.START_BOMBS, + weaponTier: 0, + nextLifeAt: TUNE.FIRST_LIFE_AT, + nextBombAt: TUNE.FIRST_BOMB_AT, + nextMilestone: 0, + director: { clockMs: 0, spawnCd: 1200, gateCd: 400 }, + stats: { kills: 0, geoms: 0, peakMult: 1, bombsUsed: 0, deaths: 0, gates: 0, timeMs: 0, shots: 0 }, + events: [], + nextId: 1, + }; +} + +export function setInput(s, input) { + Object.assign(s.input, input); +} + +export function weaponOf(s) { return WEAPONS[s.weaponTier]; } + +export function difficultySeconds(s) { return s.director.clockMs / 1000; } + +// Speed multiplier ramps gently with the difficulty clock. +function speedMul(s, type) { + const t = difficultySeconds(s); + let m = 1 + Math.min(0.45, t / 450); + if (s.mode === 'pacifism' && (type === 'grunt' || type === 'wanderer')) m *= 0.72 + Math.min(0.28, t / 400); + return m; +} + +// ── Spawning ───────────────────────────────────────────────────────────────── + +export function spawnEnemy(s, type, x, y, opts = {}) { + if (!opts.force && s.enemies.length >= TUNE.MAX_ENEMIES) return null; + const def = ENEMY_TYPES[type]; + const r = def.radius; + const e = { + id: s.nextId++, + type, + x: clamp(x, r, ARENA_W - r), + y: clamp(y, r, ARENA_H - r), + vx: opts.vx ?? 0, + vy: opts.vy ?? 0, + radius: r, + spawnMs: opts.spawnMs ?? TUNE.SPAWN_WARMUP_MS, + spawnTotal: opts.spawnMs ?? TUNE.SPAWN_WARMUP_MS, + spin: rand(s) * Math.PI * 2, + heading: opts.heading ?? rand(s) * Math.PI * 2, + age: 0, + dead: false, + }; + if (type === 'wanderer') e.turn = 0; + if (type === 'mini') e.orbit = rand(s) * Math.PI * 2; + if (type === 'blackhole') { e.hp = TUNE.BLACKHOLE_HP; e.active = false; e.absorbed = 0; } + if (type === 'snake') { + e.phase = rand(s) * Math.PI * 2; + const len = TUNE.SNAKE_SEGMENTS * TUNE.SNAKE_SPACING + 1; + e.trail = []; + for (let i = 0; i < len; i += 1) e.trail.push({ x: e.x, y: e.y }); + } + if (type === 'rocket') { + const sp = def.speed * speedMul(s, type); + e.vx = Math.cos(e.heading) * sp; + e.vy = Math.sin(e.heading) * sp; + } + s.enemies.push(e); + emit(s, { type: 'enemySpawned', enemyType: type, id: e.id, x: e.x, y: e.y }); + return e; +} + +// A random point at least `minDist` from the player. If random tries keep +// landing close, fall back to the arena corner farthest from the player — +// at least half the arena diagonal away, so always clear of the safe radius. +function farPoint(s, minDist = TUNE.SPAWN_SAFE_DIST, margin = 60) { + const p = s.player; + for (let i = 0; i < 24; i += 1) { + const x = randRange(s, margin, ARENA_W - margin); + const y = randRange(s, margin, ARENA_H - margin); + if (Math.hypot(x - p.x, y - p.y) >= minDist) return { x, y }; + } + return { + x: p.x < ARENA_W / 2 ? ARENA_W - margin : margin, + y: p.y < ARENA_H / 2 ? ARENA_H - margin : margin, + }; +} + +function tooClose(s, x, y) { + return Math.hypot(x - s.player.x, y - s.player.y) < TUNE.SPAWN_SAFE_DIST; +} + +function scatterPool(t) { + const pool = [['wanderer', 3], ['grunt', 4]]; + if (t >= 15) pool.push(['weaver', 2.5]); + if (t >= 30) pool.push(['splitter', 2]); + if (t >= 45) pool.push(['rocket', 1]); + if (t >= 55) pool.push(['snake', 0.8]); + return pool; +} + +function countType(s, type) { + let n = 0; + for (const e of s.enemies) if (!e.dead && e.type === type) n += 1; + return n; +} + +const PATTERN_DELAY = { scatter: 1, corners: 1.4, swarm: 1.3, ring: 1.8, rocketwall: 1.3, blackhole: 0.6, snakes: 1.2 }; + +function pickPattern(s, t) { + const opts = [['scatter', 5]]; + if (t >= 18) opts.push(['corners', 2]); + if (t >= 28) opts.push(['swarm', 2]); + if (t >= 35 && countType(s, 'blackhole') < TUNE.BLACKHOLE_MAX) opts.push(['blackhole', 0.9]); + if (t >= 45) opts.push(['rocketwall', 1.3]); + if (t >= 55) opts.push(['snakes', 1]); + if (t >= 75) opts.push(['ring', 1]); + return pickWeighted(s, opts); +} + +export function spawnIntervalMs(t) { + return clamp(1700 - t * 10, 420, 1700); +} + +export function runPattern(s, pattern) { + const t = difficultySeconds(s); + const p = s.player; + switch (pattern) { + case 'scatter': { + const n = 1 + Math.min(5, Math.floor(t / 30)); + const pool = scatterPool(t); + for (let i = 0; i < n; i += 1) { + const type = pickWeighted(s, pool); + const pt = farPoint(s); + spawnEnemy(s, type, pt.x, pt.y, type === 'rocket' ? { heading: Math.atan2(p.y - pt.y, p.x - pt.x) } : {}); + } + break; + } + case 'corners': { + const types = [['grunt', 3], ['wanderer', 2]]; + if (t >= 15) types.push(['weaver', 1.5]); + const type = pickWeighted(s, types); + const per = 3 + Math.min(8, Math.floor(t / 30)); + const m = 70; + let corners = [[m, m], [ARENA_W - m, m], [m, ARENA_H - m], [ARENA_W - m, ARENA_H - m]]; + if (t < 45) { + const a = Math.floor(rand(s) * 4); + corners = [corners[a], corners[3 - a]]; + } + for (const [cx, cy] of corners) { + for (let i = 0; i < per; i += 1) { + const x = cx + randRange(s, -45, 45); const y = cy + randRange(s, -45, 45); + if (!tooClose(s, x, y)) spawnEnemy(s, type, x, y); + } + } + break; + } + case 'swarm': { + const type = pickWeighted(s, [['grunt', 3], ['weaver', 2], ['splitter', 1.5], ['wanderer', 1.5]]); + const n = 6 + Math.min(18, Math.floor(t / 12)); + const c = farPoint(s, 600, 140); + for (let i = 0; i < n; i += 1) { + const a = rand(s) * Math.PI * 2; const r = Math.sqrt(rand(s)) * 95; + const x = c.x + Math.cos(a) * r; const y = c.y + Math.sin(a) * r; + if (!tooClose(s, x, y)) spawnEnemy(s, type, x, y); + } + break; + } + case 'ring': { + const n = 10 + Math.min(14, Math.floor(t / 25)); + const R = 520; + for (let i = 0; i < n; i += 1) { + const a = (i / n) * Math.PI * 2; + const x = p.x + Math.cos(a) * R; const y = p.y + Math.sin(a) * R; + if (x < 30 || y < 30 || x > ARENA_W - 30 || y > ARENA_H - 30) continue; + const type = (t >= 120 && i % 3 === 0) ? 'weaver' : 'grunt'; + spawnEnemy(s, type, x, y, { spawnMs: 1100 }); + } + break; + } + case 'rocketwall': { + const n = 8 + Math.min(10, Math.floor(t / 30)); + // The wall farthest from the player, so the line has room to read. + const walls = [ + { d: p.x, wall: 'left' }, { d: ARENA_W - p.x, wall: 'right' }, + { d: p.y, wall: 'top' }, { d: ARENA_H - p.y, wall: 'bottom' }, + ].sort((a, b) => b.d - a.d); + const wall = walls[Math.floor(rand(s) * 2)].wall; + for (let i = 0; i < n; i += 1) { + const f = (i + 0.5) / n; + let x; let y; let heading; + if (wall === 'left') { x = 40; y = f * ARENA_H; heading = 0; } + else if (wall === 'right') { x = ARENA_W - 40; y = f * ARENA_H; heading = Math.PI; } + else if (wall === 'top') { x = f * ARENA_W; y = 40; heading = Math.PI / 2; } + else { x = f * ARENA_W; y = ARENA_H - 40; heading = -Math.PI / 2; } + if (tooClose(s, x, y)) continue; + spawnEnemy(s, 'rocket', x, y, { heading, spawnMs: 850 }); + } + emit(s, { type: 'rocketWall', wall }); + break; + } + case 'blackhole': { + const n = Math.min(t >= 120 ? 2 : 1, TUNE.BLACKHOLE_MAX - countType(s, 'blackhole')); + for (let i = 0; i < n; i += 1) { + let pt = null; + for (let k = 0; k < 10; k += 1) { + const c = farPoint(s, 500, 160); + const clear = s.enemies.every((e) => e.type !== 'blackhole' || Math.hypot(e.x - c.x, e.y - c.y) > 350); + if (clear) { pt = c; break; } + } + if (pt) spawnEnemy(s, 'blackhole', pt.x, pt.y, { spawnMs: 1000 }); + } + break; + } + case 'snakes': { + const n = 2 + Math.min(3, Math.floor(t / 90)); + for (let i = 0; i < n; i += 1) { + const pt = farPoint(s, 500, 80); + spawnEnemy(s, 'snake', pt.x, pt.y, { heading: Math.atan2(p.y - pt.y, p.x - pt.x) }); + } + break; + } + default: break; + } +} + +function spawnGate(s) { + const len = TUNE.GATE_LEN; + const m = len / 2 + 30; + let pt = null; + for (let i = 0; i < 12; i += 1) { + const c = farPoint(s, 380, m); + const clear = s.gates.every((g) => Math.hypot(g.x - c.x, g.y - c.y) > 260); + if (clear) { pt = c; break; } + } + if (!pt) return null; + const driftA = rand(s) * Math.PI * 2; + const g = { + id: s.nextId++, + x: pt.x, y: pt.y, + vx: Math.cos(driftA) * 45, vy: Math.sin(driftA) * 45, + angle: rand(s) * Math.PI, + spinRate: randRange(s, -0.7, 0.7), + len, + spawnMs: 600, + dead: false, + }; + s.gates.push(g); + emit(s, { type: 'gateSpawned', id: g.id, x: g.x, y: g.y }); + return g; +} + +function runDirector(s) { + const d = s.director; + d.spawnCd -= STEP_MS; + const t = difficultySeconds(s); + + if (s.mode === 'pacifism') { + d.gateCd -= STEP_MS; + const gateTarget = 4 + Math.min(5, Math.floor(t / 20)); + if (d.gateCd <= 0 && s.gates.length < gateTarget) { + spawnGate(s); + d.gateCd = 1400; + } + if (d.spawnCd <= 0) { + const n = 4 + Math.min(18, Math.floor(t / 10)); + const type = rand(s) < Math.max(0.08, 0.35 - t / 90) ? 'wanderer' : 'grunt'; + const m = 70; + const spots = [ + [m, m], [ARENA_W - m, m], [m, ARENA_H - m], [ARENA_W - m, ARENA_H - m], + [ARENA_W / 2, m], [ARENA_W / 2, ARENA_H - m], [m, ARENA_H / 2], [ARENA_W - m, ARENA_H / 2], + ].filter(([x, y]) => !tooClose(s, x, y)); + const [cx, cy] = spots[Math.floor(rand(s) * spots.length)] ?? [m, m]; + for (let i = 0; i < n; i += 1) { + const x = cx + randRange(s, -60, 60); const y = cy + randRange(s, -60, 60); + if (!tooClose(s, x, y)) spawnEnemy(s, type, x, y); + } + d.spawnCd = clamp(2800 - t * 12, 800, 2800); + } + return; + } + + if (d.spawnCd > 0) return; + const pattern = t < 4 ? 'scatter' : pickPattern(s, t); + runPattern(s, pattern); + d.spawnCd = spawnIntervalMs(t) * (PATTERN_DELAY[pattern] ?? 1); +} + +// ── Kills, geoms, scoring ──────────────────────────────────────────────────── + +function dropGeoms(s, x, y, n) { + for (let i = 0; i < n; i += 1) { + if (s.geoms.length >= TUNE.MAX_GEOMS) s.geoms.shift(); + const a = rand(s) * Math.PI * 2; const sp = randRange(s, 40, 160); + s.geoms.push({ + id: s.nextId++, x, y, vx: Math.cos(a) * sp, vy: Math.sin(a) * sp, + lifeMs: TUNE.GEOM_LIFE_MS, spin: rand(s) * Math.PI * 2, dead: false, + }); + } +} + +// cause: 'bullet' | 'gate' (rewarded), 'bomb' | 'death' | 'absorbed' (not). +export function killEnemy(s, e, cause) { + if (e.dead) return 0; + e.dead = true; + const def = ENEMY_TYPES[e.type]; + let points = 0; + if (cause === 'bullet' || cause === 'gate') { + let base = def.score; + let geoms = def.geoms; + if (e.type === 'blackhole') { base += 50 * e.absorbed; geoms += Math.floor(e.absorbed / 2); } + points = base * s.multiplier; + s.score += points; + s.stats.kills += 1; + dropGeoms(s, e.x, e.y, geoms); + if (e.type === 'splitter') { + for (let i = 0; i < 3; i += 1) { + const a = e.spin + (i / 3) * Math.PI * 2; + spawnEnemy(s, 'mini', e.x + Math.cos(a) * 14, e.y + Math.sin(a) * 14, { + spawnMs: 0, vx: Math.cos(a) * 260, vy: Math.sin(a) * 260, force: true, + }); + } + } + } + emit(s, { + type: 'enemyKilled', enemyType: e.type, id: e.id, x: e.x, y: e.y, + points, cause, radius: e.radius, absorbed: e.absorbed ?? 0, + }); + return points; +} + +function collectGeom(s, g) { + g.dead = true; + s.multiplier += 1; + s.stats.geoms += 1; + if (s.multiplier > s.stats.peakMult) s.stats.peakMult = s.multiplier; + emit(s, { type: 'geomCollected', x: g.x, y: g.y, multiplier: s.multiplier }); + while (s.nextMilestone < MULT_MILESTONES.length && s.multiplier >= MULT_MILESTONES[s.nextMilestone]) { + emit(s, { type: 'multiplierMilestone', multiplier: MULT_MILESTONES[s.nextMilestone] }); + s.nextMilestone += 1; + } + if (s.mode === 'evolved') { + const tier = weaponTierFor(s.multiplier); + if (tier > s.weaponTier) { + s.weaponTier = tier; + emit(s, { type: 'weaponUpgrade', tier, name: WEAPONS[tier].name }); + } + } +} + +function checkThresholds(s) { + if (s.mode !== 'evolved') return; + while (s.score >= s.nextLifeAt) { + s.nextLifeAt = Math.round(s.nextLifeAt * TUNE.THRESHOLD_GROWTH); + if (s.lives < TUNE.MAX_LIVES) { + s.lives += 1; + emit(s, { type: 'extraLife', lives: s.lives }); + } + } + while (s.score >= s.nextBombAt) { + s.nextBombAt = Math.round(s.nextBombAt * TUNE.THRESHOLD_GROWTH); + if (s.bombs < TUNE.MAX_BOMBS) { + s.bombs += 1; + emit(s, { type: 'extraBomb', bombs: s.bombs }); + } + } +} + +// ── Player ─────────────────────────────────────────────────────────────────── + +function fireVolley(s) { + const p = s.player; + const ax = s.input.aimX - p.x; const ay = s.input.aimY - p.y; + const base = (ax * ax + ay * ay > 16) ? Math.atan2(ay, ax) : p.angle; + const weapon = WEAPONS[s.weaponTier]; + const cos = Math.cos(base); const sin = Math.sin(base); + for (const shot of weapon.shots) { + const a = base + shot.da + (rand(s) - 0.5) * 0.03; + if (s.bullets.length >= TUNE.MAX_BULLETS) s.bullets.shift(); + s.bullets.push({ + id: s.nextId++, + x: p.x + cos * 18 - sin * shot.off, + y: p.y + sin * 18 + cos * shot.off, + vx: Math.cos(a) * TUNE.BULLET_SPEED, + vy: Math.sin(a) * TUNE.BULLET_SPEED, + lifeMs: TUNE.BULLET_LIFE_MS, + dead: false, + }); + } + s.stats.shots += 1; + emit(s, { type: 'shotFired', x: p.x, y: p.y, angle: base, tier: s.weaponTier }); +} + +function detonateBomb(s) { + if (s.mode !== 'evolved' || s.bombs <= 0 || !s.player.alive) return false; + s.bombs -= 1; + s.stats.bombsUsed += 1; + emit(s, { type: 'bomb', x: s.player.x, y: s.player.y, bombs: s.bombs }); + for (const e of s.enemies) killEnemy(s, e, 'bomb'); + s.director.spawnCd = Math.max(s.director.spawnCd, TUNE.BOMB_LULL_MS); + s.player.invulnMs = Math.max(s.player.invulnMs, 400); + return true; +} + +function killPlayer(s, cause) { + const p = s.player; + if (!p.alive) return; + p.alive = false; + p.vx = 0; p.vy = 0; + p.respawnMs = TUNE.RESPAWN_MS; + s.lives -= 1; + s.stats.deaths += 1; + emit(s, { type: 'playerDied', x: p.x, y: p.y, cause, lives: s.lives }); + // Classic rule: losing a ship wipes the board (no points), and the + // multiplier and gun reset. + for (const e of s.enemies) killEnemy(s, e, 'death'); + s.geoms = []; + s.bullets = []; + for (const g of s.gates) g.dead = true; + s.multiplier = 1; + s.weaponTier = 0; + s.nextMilestone = 0; + s.director.clockMs *= TUNE.DEATH_DIFFICULTY_REWIND; +} + +function updatePlayer(s, dt) { + const p = s.player; + if (!p.alive) { + p.respawnMs -= STEP_MS; + if (p.respawnMs <= 0) { + if (s.lives <= 0) { + s.phase = 'over'; + emit(s, { type: 'gameOver', score: s.score, mode: s.mode, stats: { ...s.stats } }); + } else { + p.alive = true; + p.x = ARENA_W / 2; p.y = ARENA_H / 2; + p.vx = 0; p.vy = 0; + p.invulnMs = TUNE.INVULN_MS; + p.fireCd = 0; + s.director.spawnCd = 1200; + s.director.gateCd = 300; + emit(s, { type: 'playerRespawned', x: p.x, y: p.y, lives: s.lives }); + } + } + return; + } + + let mx = s.input.moveX || 0; let my = s.input.moveY || 0; + const m = Math.hypot(mx, my); + if (m > 1) { mx /= m; my /= m; } + const k = Math.min(1, TUNE.PLAYER_ACCEL * dt); + p.vx += (mx * TUNE.PLAYER_SPEED - p.vx) * k; + p.vy += (my * TUNE.PLAYER_SPEED - p.vy) * k; + p.x += p.vx * dt; + p.y += p.vy * dt; + const r = p.radius; + if (p.x < r) { p.x = r; p.vx = 0; } + if (p.x > ARENA_W - r) { p.x = ARENA_W - r; p.vx = 0; } + if (p.y < r) { p.y = r; p.vy = 0; } + if (p.y > ARENA_H - r) { p.y = ARENA_H - r; p.vy = 0; } + if (Math.hypot(p.vx, p.vy) > 40) { + p.angle = turnToward(p.angle, Math.atan2(p.vy, p.vx), 14 * dt); + } + if (p.invulnMs > 0) p.invulnMs -= STEP_MS; + + if (p.fireCd > 0) p.fireCd -= STEP_MS; + if (s.mode === 'evolved' && s.input.fire) { + while (p.fireCd <= 0) { + fireVolley(s); + p.fireCd += WEAPONS[s.weaponTier].intervalMs; + } + } else if (p.fireCd < 0) { + p.fireCd = 0; + } + + const wantBomb = !!s.input.bomb; + if (wantBomb && !s.bombLatch) detonateBomb(s); + s.bombLatch = wantBomb; +} + +// ── Enemies ────────────────────────────────────────────────────────────────── + +export function snakeSegments(e) { + const out = []; + for (let i = 1; i <= TUNE.SNAKE_SEGMENTS; i += 1) { + const idx = Math.min(e.trail.length - 1, i * TUNE.SNAKE_SPACING); + out.push(e.trail[idx]); + } + return out; +} + +function updateEnemies(s, dt) { + const p = s.player; + for (const e of s.enemies) { + if (e.dead) continue; + e.age += STEP_MS; + if (e.spawnMs > 0) { e.spawnMs -= STEP_MS; continue; } + const spd = ENEMY_TYPES[e.type].speed * speedMul(s, e.type); + const dx = p.x - e.x; const dy = p.y - e.y; + const d = Math.hypot(dx, dy) || 1; + const chase = p.alive; + + switch (e.type) { + case 'wanderer': { + e.turn = clamp(e.turn + (rand(s) - 0.5) * 7 * dt, -1.6, 1.6); + e.heading += e.turn * dt; + e.vx = Math.cos(e.heading) * spd; + e.vy = Math.sin(e.heading) * spd; + e.spin += 4.5 * dt; + break; + } + case 'grunt': + case 'proton': { + const tx = chase ? (dx / d) * spd : 0; const ty = chase ? (dy / d) * spd : 0; + const k = Math.min(1, (e.type === 'proton' ? 4 : 2.6) * dt); + e.vx += (tx - e.vx) * k; + e.vy += (ty - e.vy) * k; + e.spin += (e.type === 'proton' ? 9 : 3) * dt; + break; + } + case 'weaver': { + let tx = chase ? (dx / d) * spd : 0; let ty = chase ? (dy / d) * spd : 0; + let best = null; let bestD = TUNE.WEAVER_DODGE_R * TUNE.WEAVER_DODGE_R; + for (const b of s.bullets) { + if (b.dead) continue; + const bx = e.x - b.x; const by = e.y - b.y; + const dd = bx * bx + by * by; + if (dd < bestD && (b.vx * bx + b.vy * by) > 0) { best = b; bestD = dd; } + } + if (best) { + const bl = Math.hypot(best.vx, best.vy) || 1; + const nx = -best.vy / bl; const ny = best.vx / bl; + const side = (nx * (e.x - best.x) + ny * (e.y - best.y)) >= 0 ? 1 : -1; + tx += nx * side * spd * 1.6; + ty += ny * side * spd * 1.6; + } + const k = Math.min(1, 7 * dt); + e.vx += (tx - e.vx) * k; + e.vy += (ty - e.vy) * k; + e.spin += 2 * dt; + break; + } + case 'splitter': { + const a = Math.atan2(dy, dx) + Math.sin(e.age * 0.002 + e.heading) * 0.8; + const tx = chase ? Math.cos(a) * spd : 0; const ty = chase ? Math.sin(a) * spd : 0; + const k = Math.min(1, 1.8 * dt); + e.vx += (tx - e.vx) * k; + e.vy += (ty - e.vy) * k; + e.spin += 2.2 * dt; + break; + } + case 'mini': { + e.orbit += 7 * dt; + const cx = chase ? (dx / d) * spd * 0.65 : 0; const cy = chase ? (dy / d) * spd * 0.65 : 0; + const tx = cx + Math.cos(e.orbit) * spd * 0.75; + const ty = cy + Math.sin(e.orbit) * spd * 0.75; + const k = Math.min(1, 5 * dt); + e.vx += (tx - e.vx) * k; + e.vy += (ty - e.vy) * k; + e.spin += 6 * dt; + break; + } + case 'snake': { + const want = (chase ? Math.atan2(dy, dx) : e.heading) + Math.sin(e.age * 0.003 + e.phase) * 0.9; + e.heading = turnToward(e.heading, want, 2.6 * dt); + e.vx = Math.cos(e.heading) * spd; + e.vy = Math.sin(e.heading) * spd; + break; + } + case 'blackhole': { + e.vx *= 0.98; e.vy *= 0.98; + e.spin += (e.active ? 2.4 : 0.6) * dt; + break; + } + case 'rocket': + default: + break; + } + + e.x += e.vx * dt; + e.y += e.vy * dt; + + const r = e.radius; + let bounced = false; + if (e.x < r) { e.x = r; e.vx = Math.abs(e.vx); bounced = true; } + if (e.x > ARENA_W - r) { e.x = ARENA_W - r; e.vx = -Math.abs(e.vx); bounced = true; } + if (e.y < r) { e.y = r; e.vy = Math.abs(e.vy); bounced = true; } + if (e.y > ARENA_H - r) { e.y = ARENA_H - r; e.vy = -Math.abs(e.vy); bounced = true; } + if (bounced && (e.type === 'wanderer' || e.type === 'snake' || e.type === 'rocket')) { + e.heading = Math.atan2(e.vy, e.vx); + } + + if (e.type === 'snake') { + e.trail.unshift({ x: e.x, y: e.y }); + e.trail.length = TUNE.SNAKE_SEGMENTS * TUNE.SNAKE_SPACING + 1; + } + } +} + +function activeHoles(s) { + const out = []; + for (const e of s.enemies) if (e.type === 'blackhole' && e.active && !e.dead && e.spawnMs <= 0) out.push(e); + return out; +} + +function updateBlackHoles(s, dt) { + const holes = activeHoles(s); + if (holes.length === 0) return; + const R = TUNE.BLACKHOLE_PULL_R; const R2 = R * R; + const p = s.player; + for (const h of holes) { + if (h.dead) continue; + for (const e of s.enemies) { + if (e === h || e.dead || e.type === 'blackhole' || e.spawnMs > 0) continue; + const dx = h.x - e.x; const dy = h.y - e.y; + const d2 = dx * dx + dy * dy; + if (d2 >= R2) continue; + const d = Math.sqrt(d2) || 1; + if (d < h.radius + e.radius * 0.4) { + killEnemy(s, e, 'absorbed'); + h.absorbed += 1; + h.radius = ENEMY_TYPES.blackhole.radius + h.absorbed * 1.6; + emit(s, { type: 'blackHoleAbsorb', id: h.id, x: h.x, y: h.y, absorbed: h.absorbed }); + continue; + } + const pull = (TUNE.BLACKHOLE_PULL_ENEMY * (1 - d / R) + 30) * dt; + e.x += (dx / d) * pull; + e.y += (dy / d) * pull; + } + if (p.alive) { + const dx = h.x - p.x; const dy = h.y - p.y; + const d2 = dx * dx + dy * dy; + if (d2 < R2) { + const d = Math.sqrt(d2) || 1; + const pull = TUNE.BLACKHOLE_PULL_PLAYER * (1 - d / R) * dt; + p.x += (dx / d) * pull; + p.y += (dy / d) * pull; + } + } + for (const g of s.geoms) { + if (g.dead) continue; + const dx = h.x - g.x; const dy = h.y - g.y; + const d2 = dx * dx + dy * dy; + if (d2 >= R2) continue; + const d = Math.sqrt(d2) || 1; + if (d < h.radius) { g.dead = true; continue; } + g.vx += (dx / d) * 900 * (1 - d / R) * dt; + g.vy += (dy / d) * 900 * (1 - d / R) * dt; + } + if (h.absorbed >= TUNE.BLACKHOLE_OVERLOAD) { + h.dead = true; + const n = 6 + Math.floor(h.absorbed / 2); + emit(s, { type: 'blackHoleOverload', id: h.id, x: h.x, y: h.y, protons: n }); + for (let i = 0; i < n; i += 1) { + const a = (i / n) * Math.PI * 2; + spawnEnemy(s, 'proton', h.x + Math.cos(a) * 20, h.y + Math.sin(a) * 20, { + spawnMs: 250, vx: Math.cos(a) * 420, vy: Math.sin(a) * 420, force: true, + }); + } + } + } +} + +function updateBullets(s, dt) { + const holes = activeHoles(s); + const BR = 300; const BR2 = BR * BR; + for (const b of s.bullets) { + if (b.dead) continue; + for (const h of holes) { + const dx = h.x - b.x; const dy = h.y - b.y; + const d2 = dx * dx + dy * dy; + if (d2 >= BR2) continue; + const d = Math.sqrt(d2) || 1; + const f = TUNE.BLACKHOLE_BEND * (1 - d / BR) * dt; + b.vx += (dx / d) * f; + b.vy += (dy / d) * f; + } + b.x += b.vx * dt; + b.y += b.vy * dt; + b.lifeMs -= STEP_MS; + if (b.x < 0 || b.x > ARENA_W || b.y < 0 || b.y > ARENA_H) { + b.dead = true; + emit(s, { + type: 'bulletWall', + x: clamp(b.x, 0, ARENA_W), y: clamp(b.y, 0, ARENA_H), + angle: Math.atan2(b.vy, b.vx), + }); + } else if (b.lifeMs <= 0) { + b.dead = true; + } + } +} + +function collideBullets(s) { + const rB = TUNE.BULLET_RADIUS; + for (const b of s.bullets) { + if (b.dead) continue; + for (const e of s.enemies) { + if (e.dead || e.spawnMs > 0) continue; + const dx = e.x - b.x; const dy = e.y - b.y; + const rr = e.radius + rB; + if (dx * dx + dy * dy < rr * rr) { + b.dead = true; + if (e.type === 'blackhole') { + if (!e.active) { + e.active = true; + emit(s, { type: 'blackHoleActivated', id: e.id, x: e.x, y: e.y }); + } + e.hp -= 1; + emit(s, { type: 'enemyHit', enemyType: e.type, id: e.id, x: b.x, y: b.y }); + if (e.hp <= 0) killEnemy(s, e, 'bullet'); + } else { + killEnemy(s, e, 'bullet'); + } + break; + } + if (e.type === 'snake') { + const sr = TUNE.SNAKE_SEG_RADIUS + rB; + let deflected = false; + for (const seg of snakeSegments(e)) { + const sx = seg.x - b.x; const sy = seg.y - b.y; + if (sx * sx + sy * sy < sr * sr) { deflected = true; break; } + } + if (deflected) { + b.dead = true; + emit(s, { type: 'bulletDeflect', x: b.x, y: b.y }); + break; + } + } + } + } +} + +const NO_SEPARATE = new Set(['blackhole', 'snake', 'rocket']); + +function separateEnemies(s) { + const list = []; + for (const e of s.enemies) if (!e.dead && e.spawnMs <= 0 && !NO_SEPARATE.has(e.type)) list.push(e); + for (let i = 0; i < list.length; i += 1) { + const a = list[i]; + for (let j = i + 1; j < list.length; j += 1) { + const b = list[j]; + const dx = b.x - a.x; const dy = b.y - a.y; + const rr = (a.radius + b.radius) * 0.85; + const d2 = dx * dx + dy * dy; + if (d2 >= rr * rr || d2 < 1e-6) continue; + const d = Math.sqrt(d2); + const push = (rr - d) * 0.25; + const nx = dx / d; const ny = dy / d; + a.x -= nx * push; a.y -= ny * push; + b.x += nx * push; b.y += ny * push; + } + } +} + +function updateGeoms(s, dt) { + const p = s.player; + const fr = Math.pow(0.94, dt * 60); + const magR2 = TUNE.GEOM_MAGNET_R * TUNE.GEOM_MAGNET_R; + const pick = TUNE.GEOM_PICKUP_R + p.radius; + for (const g of s.geoms) { + if (g.dead) continue; + g.lifeMs -= STEP_MS; + if (g.lifeMs <= 0) { g.dead = true; continue; } + g.spin += 3 * dt; + g.vx *= fr; g.vy *= fr; + if (p.alive) { + const dx = p.x - g.x; const dy = p.y - g.y; + const d2 = dx * dx + dy * dy; + if (d2 < pick * pick) { collectGeom(s, g); continue; } + if (d2 < magR2) { + const d = Math.sqrt(d2) || 1; + g.vx += (dx / d) * 4200 * dt; + g.vy += (dy / d) * 4200 * dt; + const sp = Math.hypot(g.vx, g.vy); + if (sp > TUNE.GEOM_MAX_SPEED) { g.vx *= TUNE.GEOM_MAX_SPEED / sp; g.vy *= TUNE.GEOM_MAX_SPEED / sp; } + } + } + g.x = clamp(g.x + g.vx * dt, 4, ARENA_W - 4); + g.y = clamp(g.y + g.vy * dt, 4, ARENA_H - 4); + } +} + +export function gateEndpoints(g) { + const hx = Math.cos(g.angle) * g.len / 2; const hy = Math.sin(g.angle) * g.len / 2; + return [{ x: g.x - hx, y: g.y - hy }, { x: g.x + hx, y: g.y + hy }]; +} + +function pointSegDist(px, py, ax, ay, bx, by) { + const vx = bx - ax; const vy = by - ay; + const L2 = vx * vx + vy * vy || 1; + const t = clamp(((px - ax) * vx + (py - ay) * vy) / L2, 0, 1); + return Math.hypot(px - (ax + vx * t), py - (ay + vy * t)); +} + +function triggerGate(s, g) { + g.dead = true; + s.stats.gates += 1; + emit(s, { type: 'gateTriggered', id: g.id, x: g.x, y: g.y, angle: g.angle, len: g.len, radius: TUNE.GATE_BLAST_R }); + const R2 = TUNE.GATE_BLAST_R * TUNE.GATE_BLAST_R; + for (const e of s.enemies) { + if (e.dead) continue; + const dx = e.x - g.x; const dy = e.y - g.y; + if (dx * dx + dy * dy < R2) killEnemy(s, e, 'gate'); + } +} + +function updateGates(s, dt) { + const p = s.player; + for (const g of s.gates) { + if (g.dead) continue; + g.angle += g.spinRate * dt; + g.x += g.vx * dt; + g.y += g.vy * dt; + const m = g.len / 2 + 12; + if (g.x < m) { g.x = m; g.vx = Math.abs(g.vx); } + if (g.x > ARENA_W - m) { g.x = ARENA_W - m; g.vx = -Math.abs(g.vx); } + if (g.y < m) { g.y = m; g.vy = Math.abs(g.vy); } + if (g.y > ARENA_H - m) { g.y = ARENA_H - m; g.vy = -Math.abs(g.vy); } + if (g.spawnMs > 0) { g.spawnMs -= STEP_MS; continue; } + if (!p.alive) continue; + // The live (middle) part of the gate is the segment minus its two posts. + const [a, b] = gateEndpoints(g); + const inset = (TUNE.GATE_END_R + 6) / g.len; + const ax = a.x + (b.x - a.x) * inset; const ay = a.y + (b.y - a.y) * inset; + const bx = b.x + (a.x - b.x) * inset; const by = b.y + (a.y - b.y) * inset; + if (pointSegDist(p.x, p.y, ax, ay, bx, by) < p.radius * 0.9) triggerGate(s, g); + } +} + +function collidePlayer(s) { + const p = s.player; + if (!p.alive || p.invulnMs > 0) return; + const slack = TUNE.HIT_SLACK; + for (const e of s.enemies) { + if (e.dead || e.spawnMs > 0) continue; + const rr = (e.radius + p.radius) * slack; + const dx = e.x - p.x; const dy = e.y - p.y; + if (dx * dx + dy * dy < rr * rr) { killPlayer(s, e.type); return; } + if (e.type === 'snake') { + const sr = (TUNE.SNAKE_SEG_RADIUS + p.radius) * slack; + for (const seg of snakeSegments(e)) { + const sx = seg.x - p.x; const sy = seg.y - p.y; + if (sx * sx + sy * sy < sr * sr) { killPlayer(s, 'snake'); return; } + } + } + } + for (const g of s.gates) { + if (g.dead || g.spawnMs > 0) continue; + const rr = TUNE.GATE_END_R + p.radius * 0.7; + for (const end of gateEndpoints(g)) { + const dx = end.x - p.x; const dy = end.y - p.y; + if (dx * dx + dy * dy < rr * rr) { killPlayer(s, 'gate'); return; } + } + } +} + +function compact(s) { + if (s.enemies.some((e) => e.dead)) s.enemies = s.enemies.filter((e) => !e.dead); + if (s.bullets.some((b) => b.dead)) s.bullets = s.bullets.filter((b) => !b.dead); + if (s.geoms.some((g) => g.dead)) s.geoms = s.geoms.filter((g) => !g.dead); + if (s.gates.some((g) => g.dead)) s.gates = s.gates.filter((g) => !g.dead); +} + +// ── Main loop ──────────────────────────────────────────────────────────────── + +function tick(s) { + if (s.phase === 'over') return; + const dt = STEP_MS / 1000; + s.tick += 1; + updatePlayer(s, dt); + if (s.phase === 'over') return; + if (s.player.alive) { + s.timeMs += STEP_MS; + s.stats.timeMs = s.timeMs; + s.director.clockMs += STEP_MS; + runDirector(s); + } + updateEnemies(s, dt); + updateBlackHoles(s, dt); + updateBullets(s, dt); + collideBullets(s); + separateEnemies(s); + updateGeoms(s, dt); + updateGates(s, dt); + collidePlayer(s); + checkThresholds(s); + compact(s); +} + +export function step(s, deltaMs) { + s.acc = Math.min(s.acc + Math.max(0, deltaMs || 0), STEP_MS * MAX_STEPS); + while (s.acc >= STEP_MS - 1e-9) { + s.acc -= STEP_MS; + tick(s); + if (s.phase === 'over') { s.acc = 0; break; } + } + if (s.acc < 0) s.acc = 0; + const events = s.events; + s.events = []; + return events; +} + +// Expose a few internals for tools/verifyGeometryWars.js. +export const _internal = { tick, killPlayer, detonateBomb, triggerGate, collectGeom, spawnGate, runDirector }; diff --git a/src/games/geometrywars/GeometryWarsVectorFont.js b/src/games/geometrywars/GeometryWarsVectorFont.js new file mode 100644 index 0000000..61e5de5 --- /dev/null +++ b/src/games/geometrywars/GeometryWarsVectorFont.js @@ -0,0 +1,117 @@ +// Stroked vector font for Geometry Wars — the glyph table is the generic +// 4x6-cell set from TempestVectorFont.js / DefenderVectorFont.js (copied, per +// this repo's convention), extended with a few symbols, plus italic skew, +// left/right alignment, letter spacing and a separate glow colour so titles +// and HUD numbers can read as neon tubes. + +const GLYPH_W = 4; +const GLYPH_H = 6; +const GLYPH_GAP = 1.2; +const SPACE_W = 2.6; + +const GLYPHS = { + A: [[[0, 6], [2, 0], [4, 6]], [[1, 3.4], [3, 3.4]]], + B: [[[0, 0], [0, 6]], [[0, 0], [3, 0], [4, 1], [4, 2], [3, 2.8], [0, 2.8]], [[3, 2.8], [4, 3.6], [4, 5], [3, 6], [0, 6]]], + C: [[[3.6, 1], [1.2, 0], [0, 1.6], [0, 4.4], [1.2, 6], [3.6, 5]]], + D: [[[0, 0], [2.2, 0], [4, 1.6], [4, 4.4], [2.2, 6], [0, 6], [0, 0]]], + E: [[[3.4, 0], [0, 0], [0, 6], [3.4, 6]], [[0, 3], [2.4, 3]]], + F: [[[3.4, 0], [0, 0], [0, 6]], [[0, 3], [2.4, 3]]], + G: [[[3.6, 1], [1.2, 0], [0, 1.6], [0, 4.4], [1.2, 6], [3, 6], [4, 4.6], [4, 3.2], [2.2, 3.2]]], + H: [[[0, 0], [0, 6]], [[4, 0], [4, 6]], [[0, 3], [4, 3]]], + I: [[[1, 0], [3, 0]], [[2, 0], [2, 6]], [[1, 6], [3, 6]]], + J: [[[4, 0], [4, 5], [3, 6], [1, 6], [0, 5]]], + K: [[[0, 0], [0, 6]], [[4, 0], [0, 3.2]], [[1.4, 2.2], [4, 6]]], + L: [[[0, 0], [0, 6], [3.4, 6]]], + M: [[[0, 6], [0, 0], [2, 2.6], [4, 0], [4, 6]]], + N: [[[0, 6], [0, 0], [4, 6], [4, 0]]], + O: [[[1.2, 0], [2.8, 0], [4, 1.6], [4, 4.4], [2.8, 6], [1.2, 6], [0, 4.4], [0, 1.6], [1.2, 0]]], + P: [[[0, 6], [0, 0], [3, 0], [4, 1.2], [3, 2.6], [0, 2.6]]], + Q: [[[1.2, 0], [2.8, 0], [4, 1.6], [4, 4.4], [2.8, 6], [1.2, 6], [0, 4.4], [0, 1.6], [1.2, 0]], [[2.5, 4.4], [4, 6]]], + R: [[[0, 6], [0, 0], [3, 0], [4, 1.2], [3, 2.4], [0, 2.4]], [[1.6, 2.4], [4, 6]]], + S: [[[4, 1], [3, 0], [1, 0], [0, 1], [0, 2], [1, 2.8], [3, 3.2], [4, 4], [4, 5], [3, 6], [1, 6], [0, 5]]], + T: [[[0, 0], [4, 0]], [[2, 0], [2, 6]]], + U: [[[0, 0], [0, 5], [1, 6], [3, 6], [4, 5], [4, 0]]], + V: [[[0, 0], [2, 6], [4, 0]]], + W: [[[0, 0], [0.8, 6], [2, 3.2], [3.2, 6], [4, 0]]], + X: [[[0, 0], [4, 6]], [[4, 0], [0, 6]]], + Y: [[[0, 0], [2, 2.8], [4, 0]], [[2, 2.8], [2, 6]]], + Z: [[[0, 0], [4, 0], [0, 6], [4, 6]]], + 0: [[[1.2, 0], [2.8, 0], [4, 1.6], [4, 4.4], [2.8, 6], [1.2, 6], [0, 4.4], [0, 1.6], [1.2, 0]]], + 1: [[[1, 1], [2, 0], [2, 6]], [[1, 6], [3, 6]]], + 2: [[[0, 1], [1, 0], [3, 0], [4, 1], [4, 2.4], [0, 6], [4, 6]]], + 3: [[[0, 0], [4, 0], [2.4, 2.4], [4, 3.4], [4, 5], [3, 6], [1, 6], [0, 5]]], + 4: [[[3, 6], [3, 0], [0, 4], [4, 4]]], + 5: [[[4, 0], [0, 0], [0, 2.6], [3, 2.6], [4, 3.6], [4, 5], [3, 6], [1, 6], [0, 5]]], + 6: [[[3.5, 0], [1, 0], [0, 1.5], [0, 5], [1, 6], [3, 6], [4, 5], [4, 3.6], [3, 2.6], [0, 2.6]]], + 7: [[[0, 0], [4, 0], [1.5, 6]]], + 8: [[[1, 0], [3, 0], [4, 1], [4, 2], [3, 2.8], [1, 2.8], [0, 2], [0, 1], [1, 0]], [[3, 2.8], [4, 3.6], [4, 5], [3, 6], [1, 6], [0, 5], [0, 3.6], [1, 2.8]]], + 9: [[[0.5, 6], [3, 6], [4, 4.5], [4, 1], [3, 0], [1, 0], [0, 1], [0, 2.4], [1, 3.4], [4, 3.4]]], + '-': [[[0.8, 3], [3.2, 3]]], + '+': [[[0.6, 3], [3.4, 3]], [[2, 1.6], [2, 4.4]]], + '.': [[[1.8, 5.4], [2.2, 5.4], [2.2, 6], [1.8, 6], [1.8, 5.4]]], + ',': [[[2.2, 5.2], [2.2, 6], [1.6, 7]]], + ':': [[[1.8, 1.4], [2.2, 1.4], [2.2, 2], [1.8, 2], [1.8, 1.4]], [[1.8, 5.4], [2.2, 5.4], [2.2, 6], [1.8, 6], [1.8, 5.4]]], + '!': [[[2, 0], [2, 4]], [[1.8, 5.4], [2.2, 5.4], [2.2, 6], [1.8, 6], [1.8, 5.4]]], + '?': [[[0, 1], [1, 0], [3, 0], [4, 1], [4, 2], [2, 3.4], [2, 4.2]], [[1.8, 5.4], [2.2, 5.4], [2.2, 6], [1.8, 6], [1.8, 5.4]]], + '/': [[[3.6, 0], [0.4, 6]]], + "'": [[[2, 0], [2, 1.6]]], + '%': [[[3.6, 0], [0.4, 6]], [[0.4, 0.4], [1.4, 0.4], [1.4, 1.6], [0.4, 1.6], [0.4, 0.4]], [[2.6, 4.4], [3.6, 4.4], [3.6, 5.6], [2.6, 5.6], [2.6, 4.4]]], + '×': [[[0.6, 1.6], [3.4, 4.6]], [[3.4, 1.6], [0.6, 4.6]]], +}; + +function advance(ch, spacing) { + return ch === ' ' ? SPACE_W + spacing : GLYPH_W + GLYPH_GAP + spacing; +} + +export function measureVectorText(text, scale, spacing = 0) { + let w = 0; + for (const ch of String(text).toUpperCase()) w += advance(ch, spacing) * scale; + return Math.max(0, w - (GLYPH_GAP + spacing) * scale); +} + +export function vectorTextHeight(scale) { return GLYPH_H * scale; } + +function strokeGlyphs(g, text, x0, y0, scale, skew, spacing) { + let x = x0; + for (const ch of text) { + const strokes = GLYPHS[ch]; + if (strokes) { + for (const poly of strokes) { + g.beginPath(); + for (let i = 0; i < poly.length; i += 1) { + const [ux, uy] = poly[i]; + const px = x + (ux + (GLYPH_H - uy) * skew) * scale; + const py = y0 + uy * scale; + if (i === 0) g.moveTo(px, py); else g.lineTo(px, py); + } + g.strokePath(); + } + } + x += advance(ch, spacing) * scale; + } +} + +// (x, y) is the anchor: centre/left/right edge per `align`, vertical centre. +export function drawVectorText(g, text, x, y, scale, color, options = {}) { + const { + lineWidth = 3, glowWidth = 9, glowAlpha = 0.2, alpha = 1, + align = 'center', skew = 0, spacing = 0, glowColor = color, glow = true, + } = options; + const str = String(text).toUpperCase(); + const w = measureVectorText(str, scale, spacing); + let x0 = x - w / 2; + if (align === 'left') x0 = x; + else if (align === 'right') x0 = x - w; + const y0 = y - (GLYPH_H * scale) / 2; + if (glow && glowWidth > 0 && glowAlpha > 0) { + g.lineStyle(glowWidth, glowColor, glowAlpha * alpha); + strokeGlyphs(g, str, x0, y0, scale, skew, spacing); + } + g.lineStyle(lineWidth, color, alpha); + strokeGlyphs(g, str, x0, y0, scale, skew, spacing); + return w; +} + +export function formatScore(n) { + return Math.floor(n).toString().replace(/\B(?=(\d{3})+(?!\d))/g, ','); +} diff --git a/src/main.js b/src/main.js index d7f98a5..e9fec4b 100644 --- a/src/main.js +++ b/src/main.js @@ -111,6 +111,7 @@ import WolfensteinEditor from './games/wolfenstein/WolfensteinEditor.js'; import PipePuzzleGame from './games/pipepuzzle/PipePuzzleGame.js'; import TentsGame from './games/tents/TentsGame.js'; import DefenderGame from './games/defender/DefenderGame.js'; +import GeometryWarsGame from './games/geometrywars/GeometryWarsGame.js'; const config = { type: Phaser.AUTO, @@ -235,6 +236,7 @@ const config = { PipePuzzleGame, TentsGame, DefenderGame, + GeometryWarsGame, ], }; diff --git a/src/scenes/GameRoomScene.js b/src/scenes/GameRoomScene.js index 0a95bc7..0d39715 100644 --- a/src/scenes/GameRoomScene.js +++ b/src/scenes/GameRoomScene.js @@ -23,7 +23,7 @@ export default class GameRoomScene extends Phaser.Scene { } create() { - const slugDispatch = { backgammon: 'Backgammon', holdem: 'HoldemGame', blackjack: 'BlackjackGame', parchisi: 'ParchisiGame', yatzi: 'YatziGame', skipbo: 'SkipBoGame', phase10: 'Phase10Game', chinesecheckers: 'ChineseCheckersGame', gofish: 'GoFishGame', uno: 'UnoGame', craps: 'CrapsGame', roulette: 'RouletteGame', mexicantrain: 'MexicanTrainGame', hearts: 'HeartsGame', catan: 'CatanGame', tickettoride: 'TicketToRideGame', nerts: 'NertsGame', bingo: 'BingoGame', baccarat: 'BaccaratGame', dominion: 'DominionGame', checkers: 'CheckersGame', chess: 'ChessGame', wordle: 'WordleGame', scrabble: 'ScrabbleGame', ghost: 'GhostGame', wordladder: 'WordLadderGame', wordsearch: 'WordSearchGame', hangman: 'HangmanGame', sudoku: 'SudokuGame', othello: 'OthelloGame', go: 'GoGame', battleship: 'BattleshipGame', mastermind: 'MastermindGame', connect4: 'Connect4Game', boggle: 'BoggleGame', oldmaid: 'OldMaidGame', blokus: 'BlokusGame', spellingbee: 'SpellingBeeGame', minicrossword: 'MiniCrosswordGame', forbiddenisland: 'ForbiddenIslandGame', solitairetour: 'SolitaireTourGame', splendor: 'SplendorGame', tectonic: 'TectonicGame', labyrinth: 'LabyrinthGame', videopoker: 'VideoPokerGame', farkel: 'FarkelGame', stratego: 'StrategoGame', kiitos: 'KiitosGame', monopoly: 'MonopolyGame', triominoes: 'TriominoesGame', freecell: 'FreecellGame', rushhour: 'RushHourGame', hexsweeper: 'HexsweeperGame', puddingmonsters: 'PuddingMonstersGame', shift: 'ShiftGame', blockfighter: 'BlockFighterGame', mahjongmatch: 'MahjongMatchGame', mahjong: 'MahjongGame', jewelquest: 'JewelQuestGame', zuma: 'ZumaGame', bejeweled: 'BejeweledGame', minimotorways: 'MiniMotorwaysGame', slots: 'SlotsGame', cribbage: 'CribbageGame', canasta: 'CanastaGame', dotlink: 'DotLinkGame', '2048': '2048Game', rummikub: 'RummikubGame', ginrummy: 'GinRummyGame', risk: 'RiskGame', geniussquare: 'GeniusSquareGame', katamino: 'KataminoGame', bookwork: 'BookworkGame', paigow: 'PaiGowPokerGame', spireclimb: 'SpireClimbGame', azul: 'AzulGame', jumble: 'JumbleGame', dungeonboss: 'DungeonBossGame', swdbg: 'SWDBGGame', balatro: 'BalatroGame', peggle: 'PeggleGame', coloradodefense: 'ColoradoDefenseGame', starcontrol: 'StarControlGame', civilization: 'CivilizationGame', tempest: 'TempestGame', superkart: 'SuperKartGame', advancewars: 'AdvanceWarsGame', tetrisattack: 'TetrisAttackGame', totalannihilation: 'TotalAnnihilationGame', bloxorz: 'BloxorzGame', gootower: 'GooTowerGame', excitebike: 'ExcitebikeGame', mastervega: 'MasterOfVegaGame', wolfenstein: 'WolfensteinGame', pipepuzzle: 'PipePuzzleGame', tents: 'TentsGame', jigsaw: 'jigsaw-game', defender: 'DefenderGame' }; + const slugDispatch = { backgammon: 'Backgammon', holdem: 'HoldemGame', blackjack: 'BlackjackGame', parchisi: 'ParchisiGame', yatzi: 'YatziGame', skipbo: 'SkipBoGame', phase10: 'Phase10Game', chinesecheckers: 'ChineseCheckersGame', gofish: 'GoFishGame', uno: 'UnoGame', craps: 'CrapsGame', roulette: 'RouletteGame', mexicantrain: 'MexicanTrainGame', hearts: 'HeartsGame', catan: 'CatanGame', tickettoride: 'TicketToRideGame', nerts: 'NertsGame', bingo: 'BingoGame', baccarat: 'BaccaratGame', dominion: 'DominionGame', checkers: 'CheckersGame', chess: 'ChessGame', wordle: 'WordleGame', scrabble: 'ScrabbleGame', ghost: 'GhostGame', wordladder: 'WordLadderGame', wordsearch: 'WordSearchGame', hangman: 'HangmanGame', sudoku: 'SudokuGame', othello: 'OthelloGame', go: 'GoGame', battleship: 'BattleshipGame', mastermind: 'MastermindGame', connect4: 'Connect4Game', boggle: 'BoggleGame', oldmaid: 'OldMaidGame', blokus: 'BlokusGame', spellingbee: 'SpellingBeeGame', minicrossword: 'MiniCrosswordGame', forbiddenisland: 'ForbiddenIslandGame', solitairetour: 'SolitaireTourGame', splendor: 'SplendorGame', tectonic: 'TectonicGame', labyrinth: 'LabyrinthGame', videopoker: 'VideoPokerGame', farkel: 'FarkelGame', stratego: 'StrategoGame', kiitos: 'KiitosGame', monopoly: 'MonopolyGame', triominoes: 'TriominoesGame', freecell: 'FreecellGame', rushhour: 'RushHourGame', hexsweeper: 'HexsweeperGame', puddingmonsters: 'PuddingMonstersGame', shift: 'ShiftGame', blockfighter: 'BlockFighterGame', mahjongmatch: 'MahjongMatchGame', mahjong: 'MahjongGame', jewelquest: 'JewelQuestGame', zuma: 'ZumaGame', bejeweled: 'BejeweledGame', minimotorways: 'MiniMotorwaysGame', slots: 'SlotsGame', cribbage: 'CribbageGame', canasta: 'CanastaGame', dotlink: 'DotLinkGame', '2048': '2048Game', rummikub: 'RummikubGame', ginrummy: 'GinRummyGame', risk: 'RiskGame', geniussquare: 'GeniusSquareGame', katamino: 'KataminoGame', bookwork: 'BookworkGame', paigow: 'PaiGowPokerGame', spireclimb: 'SpireClimbGame', azul: 'AzulGame', jumble: 'JumbleGame', dungeonboss: 'DungeonBossGame', swdbg: 'SWDBGGame', balatro: 'BalatroGame', peggle: 'PeggleGame', coloradodefense: 'ColoradoDefenseGame', starcontrol: 'StarControlGame', civilization: 'CivilizationGame', tempest: 'TempestGame', superkart: 'SuperKartGame', advancewars: 'AdvanceWarsGame', tetrisattack: 'TetrisAttackGame', totalannihilation: 'TotalAnnihilationGame', bloxorz: 'BloxorzGame', gootower: 'GooTowerGame', excitebike: 'ExcitebikeGame', mastervega: 'MasterOfVegaGame', wolfenstein: 'WolfensteinGame', pipepuzzle: 'PipePuzzleGame', tents: 'TentsGame', jigsaw: 'jigsaw-game', defender: 'DefenderGame', geometrywars: 'GeometryWarsGame' }; if (slugDispatch[this.game.slug]) { const sceneKey = slugDispatch[this.game.slug]; const startData = { diff --git a/src/services/soundtrack.js b/src/services/soundtrack.js index 17cc872..0baadba 100644 --- a/src/services/soundtrack.js +++ b/src/services/soundtrack.js @@ -25,6 +25,7 @@ export const GAME_SOUNDTRACK_OVERRIDES = { coloradodefense: 'arcadedark', tempest: 'arcadedark', defender: 'arcadedark', + geometrywars: 'arcadedark', mastermind: 'hacker', balatro: 'hacker', hexsweeper: 'hacker', diff --git a/tools/verifyGeometryWars.js b/tools/verifyGeometryWars.js new file mode 100644 index 0000000..9956170 --- /dev/null +++ b/tools/verifyGeometryWars.js @@ -0,0 +1,650 @@ +// Headless verification for Geometry Wars. +// node tools/verifyGeometryWars.js +// Exits non-zero on any failure. +// +// 1. Setup per mode (lives, bombs, gun). +// 2. Movement: speed cap, diagonal normalisation, arena clamp. +// 3. Firing: volley size/rate per weapon tier; pacifism can't shoot. +// 4. Kill → score × multiplier, geoms drop, pickup raises multiplier + gun. +// 5. Splitter splits into three minis; bombs/deaths never split. +// 6. Snake: tail eats bullets, head dies, tail kills the player. +// 7. Black hole: dormant until shot, pulls + absorbs, overloads into protons. +// 8. Bomb: kills everything for no points, edge-triggered, consumes a bomb. +// 9. Death: board wipe, multiplier/gun reset, respawn invulnerability, +// game over when the last ship goes. +// 10. Extra life / bomb thresholds. +// 11. Pacifism gates: crossing detonates, posts kill, blast radius. +// 12. Spawning: nothing within the safe radius; warming-up enemies are inert. +// 13. Spiral-of-death guard. +// 14. Determinism. +// 15. Warp grid: settles back to rest, never NaNs. +// 16. Particle field: pool cap, NaN-free, hole capture. +// 17. Bot soak: many seeds, both modes, invariants every tick. + +import { + STEP_MS, MAX_STEPS, ARENA_W, ARENA_H, TUNE, ENEMY_TYPES, WEAPONS, + createGame, setInput, step, spawnEnemy, snakeSegments, gateEndpoints, + weaponTierFor, runPattern, _internal, +} from '../src/games/geometrywars/GeometryWarsLogic.js'; +import WarpGrid from '../src/games/geometrywars/GeometryWarsGrid.js'; +import ParticleField, { FX_MAX } from '../src/games/geometrywars/GeometryWarsFx.js'; + +let failures = 0; +let passes = 0; +function check(name, cond, detail = '') { + if (cond) { passes += 1; console.log(` ok ${name}`); return; } + failures += 1; + console.error(` FAIL ${name}${detail ? ` — ${detail}` : ''}`); +} + +function runTicks(s, count, input = null) { + if (input) setInput(s, input); + const events = []; + for (let i = 0; i < count; i += 1) events.push(...step(s, STEP_MS)); + return events; +} + +// A quiet arena: director held off so tests control every spawn. +function quiet(mode = 'evolved', seed = 7) { + const s = createGame({ seed, mode }); + s.director.spawnCd = 1e12; + s.director.gateCd = 1e12; + return s; +} + +const ofType = (events, type) => events.filter((e) => e.type === type); + +// --------------------------------------------------------------------------- +console.log('1. Setup per mode'); +{ + const ev = createGame({ seed: 1, mode: 'evolved' }); + check('evolved starts with 3 ships', ev.lives === 3); + check('evolved starts with 3 bombs', ev.bombs === 3); + check('evolved starts on the twin stream', ev.weaponTier === 0 && WEAPONS[0].shots.length === 2); + check('multiplier starts at 1', ev.multiplier === 1); + const pc = createGame({ seed: 1, mode: 'pacifism' }); + check('pacifism is one ship', pc.lives === 1); + check('pacifism has no bombs', pc.bombs === 0); + check('unknown mode falls back to evolved', createGame({ mode: 'bogus' }).mode === 'evolved'); + check('player starts mid-arena', ev.player.x === ARENA_W / 2 && ev.player.y === ARENA_H / 2); +} + +// --------------------------------------------------------------------------- +console.log('2. Movement'); +{ + const s = quiet(); + runTicks(s, 90, { moveX: 1, moveY: 0 }); + const sp = Math.hypot(s.player.vx, s.player.vy); + check('reaches but never exceeds top speed', sp <= TUNE.PLAYER_SPEED + 1e-6 && sp > TUNE.PLAYER_SPEED * 0.95, `speed ${sp.toFixed(1)}`); + const d = quiet(); + runTicks(d, 90, { moveX: 1, moveY: 1 }); + const dsp = Math.hypot(d.player.vx, d.player.vy); + check('diagonal is normalised (no √2 boost)', dsp <= TUNE.PLAYER_SPEED + 1e-6, `speed ${dsp.toFixed(1)}`); + const c = quiet(); + runTicks(c, 400, { moveX: -1, moveY: -1 }); + check('clamped inside arena (top-left)', c.player.x >= TUNE.PLAYER_RADIUS - 1e-9 && c.player.y >= TUNE.PLAYER_RADIUS - 1e-9); + runTicks(c, 600, { moveX: 1, moveY: 1 }); + check('clamped inside arena (bottom-right)', c.player.x <= ARENA_W - TUNE.PLAYER_RADIUS + 1e-9 && c.player.y <= ARENA_H - TUNE.PLAYER_RADIUS + 1e-9); + const f = quiet(); + runTicks(f, 60, { moveX: 0, moveY: 1 }); + check('ship turns to face its travel direction', Math.abs(f.player.angle - Math.PI / 2) < 0.05, `angle ${f.player.angle.toFixed(3)}`); +} + +// --------------------------------------------------------------------------- +console.log('3. Firing'); +{ + for (let tier = 0; tier < WEAPONS.length; tier += 1) { + const s = quiet(); + s.weaponTier = tier; + const ev = runTicks(s, 1, { fire: true, aimX: s.player.x + 400, aimY: s.player.y }); + check(`tier ${tier} (${WEAPONS[tier].name}) fires ${WEAPONS[tier].shots.length} bullets`, s.bullets.length === WEAPONS[tier].shots.length && ofType(ev, 'shotFired').length === 1, + `got ${s.bullets.length}`); + } + const s = quiet(); + const ev = runTicks(s, 60, { fire: true, aimX: s.player.x + 400, aimY: s.player.y }); + const volleys = ofType(ev, 'shotFired').length; + const expected = Math.floor(1000 / WEAPONS[0].intervalMs) + 1; + check('fire rate matches the weapon interval', Math.abs(volleys - expected) <= 1, `${volleys} volleys/s, expected ~${expected}`); + const forward = s.bullets.every((b) => b.vx > 0 && Math.abs(b.vy) < Math.abs(b.vx) * 0.1); + check('bullets travel toward the aim point', forward); + const pc = quiet('pacifism'); + runTicks(pc, 60, { fire: true, aimX: 0, aimY: 0 }); + check('pacifism cannot shoot', pc.bullets.length === 0); + const wall = quiet(); + const wev = runTicks(wall, 120, { fire: true, aimX: wall.player.x + 5000, aimY: wall.player.y }); + check('bullets that reach a wall emit bulletWall', ofType(wev, 'bulletWall').length > 0); + check('no bullet survives outside the arena', wall.bullets.every((b) => b.x >= 0 && b.x <= ARENA_W && b.y >= 0 && b.y <= ARENA_H)); + check('weaponTierFor thresholds', weaponTierFor(1) === 0 && weaponTierFor(10) === 1 && weaponTierFor(40) === 2 && weaponTierFor(100) === 3 && weaponTierFor(99999) === 3); +} + +// --------------------------------------------------------------------------- +console.log('4. Kills, geoms, multiplier'); +{ + const s = quiet(); + s.multiplier = 3; + const g = spawnEnemy(s, 'grunt', s.player.x + 300, s.player.y, { spawnMs: 0 }); + g.vx = 0; + const ev = runTicks(s, 30, { fire: true, aimX: s.player.x + 300, aimY: s.player.y }); + const kill = ofType(ev, 'enemyKilled')[0]; + check('grunt killed by bullet', kill && kill.enemyType === 'grunt' && kill.cause === 'bullet'); + check('points = base × multiplier', kill && kill.points === ENEMY_TYPES.grunt.score * 3, `points ${kill?.points}`); + check('score credited', s.score === ENEMY_TYPES.grunt.score * 3); + check('kill dropped geoms', s.geoms.length + s.stats.geoms >= ENEMY_TYPES.grunt.geoms); + + const p = quiet(); + for (let i = 0; i < 9; i += 1) p.geoms.push({ id: 1000 + i, x: p.player.x + 5, y: p.player.y, vx: 0, vy: 0, lifeMs: 5000, spin: 0, dead: false }); + const pev = runTicks(p, 2); + check('touching geoms collects them', p.multiplier === 10 && p.stats.geoms === 9, `mult ${p.multiplier}`); + check('crossing ×10 upgrades the gun', p.weaponTier === 1 && ofType(pev, 'weaponUpgrade').length === 1); + check('milestone event at ×10', ofType(pev, 'multiplierMilestone').some((e) => e.multiplier === 10)); + + const m = quiet(); + m.geoms.push({ id: 5, x: m.player.x + 120, y: m.player.y, vx: 0, vy: 0, lifeMs: 5000, spin: 0, dead: false }); + runTicks(m, 40); + check('geoms within magnet range fly to the ship', m.stats.geoms === 1); + + const e = quiet(); + e.geoms.push({ id: 6, x: 100, y: 100, vx: 0, vy: 0, lifeMs: 500, spin: 0, dead: false }); + runTicks(e, 40); + check('uncollected geoms expire', e.geoms.length === 0 && e.stats.geoms === 0); + + const cap = quiet(); + for (let i = 0; i < 200; i += 1) _internal.collectGeom(cap, { dead: false, x: 0, y: 0 }); + check('peak multiplier tracked', cap.stats.peakMult === 201); +} + +// --------------------------------------------------------------------------- +console.log('5. Splitter'); +{ + const s = quiet(); + const sp = spawnEnemy(s, 'splitter', s.player.x + 400, s.player.y, { spawnMs: 0 }); + sp.vx = 0; + const ev = runTicks(s, 30, { fire: true, aimX: s.player.x + 400, aimY: s.player.y }); + check('splitter dies to a bullet', ofType(ev, 'enemyKilled').some((k) => k.enemyType === 'splitter')); + const minis = ofType(ev, 'enemySpawned').filter((k) => k.enemyType === 'mini'); + check('splits into exactly three minis', minis.length === 3, `got ${minis.length}`); + const b = quiet(); + spawnEnemy(b, 'splitter', 300, 300, { spawnMs: 0 }); + _internal.detonateBomb(b); + runTicks(b, 1); + check('bombed splitter does not split', b.enemies.length === 0); +} + +// --------------------------------------------------------------------------- +console.log('6. Snake'); +{ + const s = quiet(); + const sn = spawnEnemy(s, 'snake', s.player.x + 500, s.player.y, { spawnMs: 0, heading: 0 }); + // Lay its tail out toward the player so shots hit tail before head. + sn.trail = sn.trail.map((_, i) => ({ x: sn.x - i * 6, y: sn.y })); + sn.heading = 0; + const segs = snakeSegments(sn); + check('snake exposes its segment count', segs.length === TUNE.SNAKE_SEGMENTS); + const ev = runTicks(s, 30, { fire: true, aimX: s.player.x + 500, aimY: s.player.y }); + check('tail absorbs bullets', ofType(ev, 'bulletDeflect').length > 0); + check('tail hits do not kill the snake', !ofType(ev, 'enemyKilled').some((k) => k.enemyType === 'snake')); + + const h = quiet(); + // Coming straight at the player, so the head leads and the tail trails away. + const head = spawnEnemy(h, 'snake', h.player.x + 300, h.player.y, { spawnMs: 0, heading: Math.PI }); + head.trail = head.trail.map((_, i) => ({ x: head.x + i * 6, y: head.y })); + const hev = runTicks(h, 30, { fire: true, aimX: h.player.x + 300, aimY: h.player.y }); + check('head shot kills the snake', ofType(hev, 'enemyKilled').some((k) => k.enemyType === 'snake')); + + const t = quiet(); + const tk = spawnEnemy(t, 'snake', t.player.x + 600, t.player.y + 600, { spawnMs: 0 }); + tk.trail = tk.trail.map(() => ({ x: t.player.x, y: t.player.y })); + tk.trail[0] = { x: tk.x, y: tk.y }; + const save = TUNE.SNAKE_SEGMENTS; + _internal.tick(t); + check('touching the tail kills the player', !t.player.alive && t.stats.deaths === 1); + check('(segment count unchanged)', TUNE.SNAKE_SEGMENTS === save); +} + +// --------------------------------------------------------------------------- +console.log('7. Black hole'); +{ + const s = quiet(); + const hole = spawnEnemy(s, 'blackhole', s.player.x + 500, s.player.y, { spawnMs: 0 }); + const g = spawnEnemy(s, 'wanderer', hole.x, hole.y - 200, { spawnMs: 0 }); + const gy0 = g.y; + runTicks(s, 20, { fire: false }); + check('dormant hole does not pull', Math.abs(g.y - gy0) > 0 && !hole.active); // wanderer moves on its own + const ev = runTicks(s, 30, { fire: true, aimX: hole.x, aimY: hole.y }); + check('first hit activates the hole', hole.active && ofType(ev, 'blackHoleActivated').length === 1); + check('hits drain hp', hole.hp < TUNE.BLACKHOLE_HP); + + const p = quiet(); + const h2 = spawnEnemy(p, 'blackhole', 1000, 600, { spawnMs: 0 }); + h2.active = true; + const gr = spawnEnemy(p, 'grunt', 1000, 420, { spawnMs: 0 }); + p.player.x = 200; p.player.y = 1200; // far away, so the grunt chases elsewhere + const d0 = Math.hypot(gr.x - h2.x, gr.y - h2.y); + runTicks(p, 10); + const d1 = Math.hypot(gr.x - h2.x, gr.y - h2.y); + check('active hole pulls enemies in', d1 < d0 || gr.dead, `${d0.toFixed(1)} → ${d1.toFixed(1)}`); + runTicks(p, 240); + check('hole absorbs what it pulls in (no points)', h2.absorbed >= 1 && p.score === 0); + + const pp = quiet(); + const h3 = spawnEnemy(pp, 'blackhole', pp.player.x + 250, pp.player.y, { spawnMs: 0 }); + h3.active = true; + const px0 = pp.player.x; + runTicks(pp, 10, { moveX: 0, moveY: 0 }); + check('active hole drags the player', pp.player.x > px0); + + const o = quiet(); + const h4 = spawnEnemy(o, 'blackhole', 1000, 600, { spawnMs: 0 }); + h4.active = true; + o.player.x = 200; o.player.y = 200; + for (let i = 0; i < TUNE.BLACKHOLE_OVERLOAD; i += 1) spawnEnemy(o, 'wanderer', 1000 + 5, 600, { spawnMs: 0 }); + const oev = runTicks(o, 3); + check('overfed hole overloads', ofType(oev, 'blackHoleOverload').length === 1 && !o.enemies.some((e) => e.type === 'blackhole')); + check('overload releases protons', o.enemies.filter((e) => e.type === 'proton').length >= 6); + + const k = quiet(); + const h5 = spawnEnemy(k, 'blackhole', k.player.x + 300, k.player.y, { spawnMs: 0 }); + h5.active = true; h5.hp = 1; h5.absorbed = 4; + k.multiplier = 2; + const kev = runTicks(k, 30, { fire: true, aimX: h5.x, aimY: h5.y }); + const kk = ofType(kev, 'enemyKilled').find((e) => e.enemyType === 'blackhole'); + check('shot-down hole scores base + 50/absorbed × mult', kk && kk.points === (ENEMY_TYPES.blackhole.score + 200) * 2, `points ${kk?.points}`); +} + +// --------------------------------------------------------------------------- +console.log('8. Bomb'); +{ + const s = quiet(); + for (let i = 0; i < 20; i += 1) spawnEnemy(s, 'grunt', 200 + i * 40, 200, { spawnMs: i % 2 ? 0 : 500 }); + const ev = runTicks(s, 1, { bomb: true }); + check('bomb clears every enemy (incl. warming up)', s.enemies.length === 0); + check('bomb kills score nothing', s.score === 0 && ofType(ev, 'enemyKilled').every((e) => e.points === 0)); + check('bomb consumes one bomb', s.bombs === 2 && s.stats.bombsUsed === 1); + runTicks(s, 30, { bomb: true }); + check('holding the button does not chain bombs', s.bombs === 2); + runTicks(s, 1, { bomb: false }); + runTicks(s, 1, { bomb: true }); + check('release + press fires the next bomb', s.bombs === 1); + check('bomb grants a spawn lull', s.director.spawnCd > 0); + const z = quiet(); + z.bombs = 0; + spawnEnemy(z, 'grunt', 200, 200, { spawnMs: 0 }); + runTicks(z, 1, { bomb: true }); + check('no bombs → nothing happens', z.enemies.length === 1); + const pc = quiet('pacifism'); + pc.bombs = 3; + spawnEnemy(pc, 'grunt', 200, 200, { spawnMs: 0 }); + runTicks(pc, 1, { bomb: true }); + check('pacifism never bombs', pc.enemies.length === 1 && pc.bombs === 3); +} + +// --------------------------------------------------------------------------- +console.log('9. Death and respawn'); +{ + const s = quiet(); + s.multiplier = 55; s.weaponTier = 2; s.director.clockMs = 100000; + for (let i = 0; i < 10; i += 1) spawnEnemy(s, 'wanderer', 300 + i * 50, 300, { spawnMs: 0 }); + s.geoms.push({ id: 9, x: 50, y: 50, vx: 0, vy: 0, lifeMs: 5000, spin: 0, dead: false }); + spawnEnemy(s, 'grunt', s.player.x, s.player.y, { spawnMs: 0 }); + const ev = runTicks(s, 1); + check('touching an enemy kills the player', ofType(ev, 'playerDied').length === 1 && !s.player.alive); + check('a ship is lost', s.lives === 2); + check('board wiped for no points', s.enemies.length === 0 && s.score === 0); + check('geoms cleared', s.geoms.length === 0); + check('multiplier and gun reset', s.multiplier === 1 && s.weaponTier === 0); + check('difficulty rewinds', s.director.clockMs < 100000); + const rev = runTicks(s, Math.ceil(TUNE.RESPAWN_MS / STEP_MS) + 1); + check('respawns after the delay', s.player.alive && ofType(rev, 'playerRespawned').length === 1); + check('respawn is invulnerable', s.player.invulnMs > 0); + spawnEnemy(s, 'grunt', s.player.x, s.player.y, { spawnMs: 0 }); + runTicks(s, 2); + check('invulnerable ship survives contact', s.player.alive); + + const warm = quiet(); + spawnEnemy(warm, 'grunt', warm.player.x, warm.player.y, { spawnMs: 400 }); + runTicks(warm, 3); + check('warming-up enemies are harmless', warm.player.alive); + + const last = quiet(); + last.lives = 1; + spawnEnemy(last, 'grunt', last.player.x, last.player.y, { spawnMs: 0 }); + runTicks(last, 1); + check('last ship lost → no respawn', !last.player.alive && last.lives === 0); + const gev = runTicks(last, Math.ceil(TUNE.RESPAWN_MS / STEP_MS) + 2); + const go = ofType(gev, 'gameOver'); + check('game over emitted exactly once', go.length === 1 && last.phase === 'over'); + check('game over carries score + stats', go[0] && go[0].score === last.score && go[0].stats.deaths === 1); + check('a finished game stays finished', runTicks(last, 60).length === 0 && last.phase === 'over'); +} + +// --------------------------------------------------------------------------- +console.log('10. Extra lives and bombs'); +{ + const s = quiet(); + s.score = TUNE.FIRST_LIFE_AT; + const ev = runTicks(s, 1); + check('first life threshold awards a ship', s.lives === 4 && ofType(ev, 'extraLife').length === 1); + check('next life threshold grows', s.nextLifeAt === Math.round(TUNE.FIRST_LIFE_AT * TUNE.THRESHOLD_GROWTH)); + s.score = TUNE.FIRST_BOMB_AT; + const bev = runTicks(s, 1); + check('bomb threshold awards a bomb', s.bombs === 4 && ofType(bev, 'extraBomb').length === 1); + const big = quiet(); + big.score = 1e12; + runTicks(big, 1); + check('lives capped', big.lives === TUNE.MAX_LIVES); + check('bombs capped', big.bombs === TUNE.MAX_BOMBS); + check('thresholds advanced past the score', big.nextLifeAt > big.score && big.nextBombAt > big.score); + const pc = quiet('pacifism'); + pc.score = 1e9; + runTicks(pc, 1); + check('pacifism awards no extra ships', pc.lives === 1); +} + +// --------------------------------------------------------------------------- +console.log('11. Pacifism gates'); +{ + const s = quiet('pacifism'); + const g = _internal.spawnGate(s); + check('gate spawns', !!g && s.gates.length === 1); + g.spawnMs = 0; g.vx = 0; g.vy = 0; g.spinRate = 0; g.angle = Math.PI / 2; // vertical + g.x = 1000; g.y = 600; + s.player.x = 900; s.player.y = 600; + s.multiplier = 2; + const near = spawnEnemy(s, 'grunt', 1000, 600 + TUNE.GATE_BLAST_R - 40, { spawnMs: 0 }); + const far = spawnEnemy(s, 'grunt', 1000, 600 + TUNE.GATE_BLAST_R + 200, { spawnMs: 0 }); + near.vx = 0; far.vx = 0; + const ev = runTicks(s, 30, { moveX: 1, moveY: 0 }); + check('flying through the middle detonates the gate', ofType(ev, 'gateTriggered').length === 1 && s.gates.length === 0); + check('blast kills enemies inside its radius', near.dead && ofType(ev, 'enemyKilled').some((k) => k.id === near.id && k.cause === 'gate')); + check('blast spares enemies outside it', !far.dead); + check('gate kills score × multiplier', s.score === ENEMY_TYPES.grunt.score * 2, `score ${s.score}`); + check('player survives a clean pass', s.player.alive); + check('gates counted', s.stats.gates === 1); + + const p = quiet('pacifism'); + const g2 = _internal.spawnGate(p); + g2.spawnMs = 0; g2.vx = 0; g2.vy = 0; g2.spinRate = 0; g2.angle = 0; + g2.x = 1000; g2.y = 600; + const [a] = gateEndpoints(g2); + p.player.x = a.x; p.player.y = a.y - 4; + runTicks(p, 1); + check('touching a gate post kills', !p.player.alive); + + const d = createGame({ seed: 3, mode: 'pacifism' }); + d.player.invulnMs = 1e12; + runTicks(d, 60 * 20); + check('director keeps gates on the field', d.stats.gates > 0 || d.gates.length >= 3, `gates ${d.gates.length}`); + check('pacifism director spawns enemies', d.enemies.length > 0 || d.stats.deaths > 0); +} + +// --------------------------------------------------------------------------- +console.log('12. Spawning'); +{ + let tooClose = 0; let total = 0; + for (const seed of [1, 2, 3, 4, 5, 6]) { + const s = createGame({ seed }); + s.player.invulnMs = 1e12; + s.director.clockMs = 200000; // everything unlocked + for (let i = 0; i < 1500; i += 1) { + s.player.x = 300 + ((i * 37) % (ARENA_W - 600)); + s.player.y = 300 + ((i * 53) % (ARENA_H - 600)); + const ev = step(s, STEP_MS); + for (const e of ev) { + if (e.type !== 'enemySpawned' || e.enemyType === 'mini' || e.enemyType === 'proton') continue; + total += 1; + if (Math.hypot(e.x - s.player.x, e.y - s.player.y) < TUNE.SPAWN_SAFE_DIST - 1) tooClose += 1; + } + s.enemies.length = Math.min(s.enemies.length, 120); + } + } + check('director spawns plenty', total > 500, `spawned ${total}`); + check('nothing spawns inside the safe radius', tooClose === 0, `${tooClose}/${total} too close`); + for (const pattern of ['scatter', 'corners', 'swarm', 'ring', 'rocketwall', 'blackhole', 'snakes']) { + const s = quiet('evolved', 11); + s.director.clockMs = 200000; + runPattern(s, pattern); + const inside = s.enemies.every((e) => e.x >= e.radius - 1e-6 && e.x <= ARENA_W - e.radius + 1e-6 && e.y >= e.radius - 1e-6 && e.y <= ARENA_H - e.radius + 1e-6); + check(`pattern '${pattern}' spawns inside the arena`, s.enemies.length > 0 && inside, `count ${s.enemies.length}`); + } + const cap = quiet(); + for (let i = 0; i < 400; i += 1) spawnEnemy(cap, 'grunt', 100, 100); + check('enemy cap respected', cap.enemies.length === TUNE.MAX_ENEMIES); + const bh = quiet(); + bh.director.clockMs = 200000; + for (let i = 0; i < 10; i += 1) runPattern(bh, 'blackhole'); + check('black hole count capped', bh.enemies.filter((e) => e.type === 'blackhole').length <= TUNE.BLACKHOLE_MAX); +} + +// --------------------------------------------------------------------------- +console.log('13. Spiral-of-death guard'); +{ + const s = quiet(); + const t0 = s.tick; + step(s, 10_000); + check('huge delta runs at most MAX_STEPS ticks', s.tick - t0 === MAX_STEPS, `ran ${s.tick - t0}`); + const u = quiet(); + step(u, -50); step(u, NaN); + check('negative/NaN deltas are ignored', u.tick === 0 && u.acc === 0); +} + +// --------------------------------------------------------------------------- +console.log('14. Determinism'); +{ + const run = () => { + const s = createGame({ seed: 4242 }); + let sig = 0; + for (let i = 0; i < 60 * 90; i += 1) { + const a = i * 0.013; + setInput(s, { + moveX: Math.cos(a), moveY: Math.sin(a * 1.3), + aimX: s.player.x + Math.cos(a * 3) * 300, aimY: s.player.y + Math.sin(a * 3) * 300, + fire: true, bomb: i % 900 === 0, + }); + const ev = step(s, STEP_MS * (1 + (i % 3) * 0.37)); + sig = (sig * 31 + ev.length + s.enemies.length * 7 + s.score) % 1e15; + } + return { sig, score: s.score, id: s.nextId, x: s.player.x, mult: s.multiplier, deaths: s.stats.deaths }; + }; + const a = run(); const b = run(); + check('same seed + inputs → identical run', JSON.stringify(a) === JSON.stringify(b), `${JSON.stringify(a)} vs ${JSON.stringify(b)}`); + const c = createGame({ seed: 1 }); const d = createGame({ seed: 2 }); + for (let i = 0; i < 600; i += 1) { step(c, STEP_MS); step(d, STEP_MS); } + check('different seeds diverge', JSON.stringify(c.enemies.map((e) => [e.x, e.y])) !== JSON.stringify(d.enemies.map((e) => [e.x, e.y]))); +} + +// --------------------------------------------------------------------------- +console.log('15. Warp grid'); +{ + const g = new WarpGrid(ARENA_W, ARENA_H, 40); + for (let i = 0; i < 30; i += 1) g.update(); + check('grid starts at rest', g.maxDisplacement() < 1e-6); + g.applyExplosiveForce(40, 1000, 600, 900); + g.applyDirectedForce(0, 0, 150, 1000, 600, 300); + for (let i = 0; i < 300; i += 1) g.applyImplosiveForce(4, 1500, 800, 240), g.update(); + const disp = g.maxDisplacement(); + check('forces displace the grid', disp > 5, `disp ${disp.toFixed(2)}`); + let finite = true; + for (let i = 0; i < g.n; i += 1) if (!Number.isFinite(g.px[i]) || !Number.isFinite(g.py[i]) || !Number.isFinite(g.pz[i])) finite = false; + check('grid stays finite under load', finite); + check('grid stays bounded under a sustained hole', disp < 400, `disp ${disp.toFixed(1)}`); + for (let i = 0; i < 60 * 30; i += 1) g.update(); + check('grid settles back to rest', g.maxDisplacement() < 1.5, `disp ${g.maxDisplacement().toFixed(3)}`); + let pinned = true; + for (let r = 0; r < g.rows; r += 1) for (let c = 0; c < g.cols; c += 1) { + if (r !== 0 && c !== 0 && r !== g.rows - 1 && c !== g.cols - 1) continue; + const i = r * g.cols + c; + if (g.px[i] !== g.ox[i] || g.py[i] !== g.oy[i]) pinned = false; + } + check('border masses stay pinned', pinned); + // Draw against a recording stub: every coordinate must be finite. + const calls = { n: 0, bad: 0 }; + const stub = { + lineStyle() {}, beginPath() {}, strokePath() {}, + moveTo(x, y) { calls.n += 1; if (!Number.isFinite(x) || !Number.isFinite(y)) calls.bad += 1; }, + lineTo(x, y) { calls.n += 1; if (!Number.isFinite(x) || !Number.isFinite(y)) calls.bad += 1; }, + }; + g.applyExplosiveForce(30, 600, 400, 300); + g.update(); + g.draw(stub, { x: 160, y: 100, w: 1920, h: 1080 }, { minor: 1, minorAlpha: 1, minorWidth: 1, major: 1, majorAlpha: 1, majorWidth: 2, hot: 1, hotAlpha: 1, hotter: 1, hotterAlpha: 1 }); + check('grid draw emits finite coordinates', calls.n > 1000 && calls.bad === 0, `${calls.n} calls, ${calls.bad} bad`); +} + +// --------------------------------------------------------------------------- +console.log('16. Particle field'); +{ + const fx = new ParticleField({ w: ARENA_W, h: ARENA_H }); + for (let i = 0; i < 20; i += 1) fx.explosion(1000, 600, 0xff00ff, { count: 300 }); + check('pool never exceeds FX_MAX', fx.liveCount() <= FX_MAX); + for (let i = 0; i < 30; i += 1) fx.update(16.7, [{ x: 1100, y: 600, r: 26 }]); + let ok = true; + for (let i = 0; i < FX_MAX; i += 1) { + if (fx.life[i] <= 0) continue; + if (!Number.isFinite(fx.x[i]) || !Number.isFinite(fx.y[i])) ok = false; + if (fx.x[i] < -1 || fx.x[i] > ARENA_W + 1 || fx.y[i] < -1 || fx.y[i] > ARENA_H + 1) ok = false; + } + check('particles stay finite and inside the arena', ok); + const before = fx.liveCount(); + for (let i = 0; i < 200; i += 1) fx.update(16.7, [{ x: 1000, y: 600, r: 60 }]); + check('particles expire', fx.liveCount() < before); + fx.ring(0, 0, 100, 300, 0xffffff); + fx.update(400); + check('rings expire', fx.rings.length === 0); + fx.wallSparks(0, 500, Math.PI, 0xffff00, 10); + fx.update(16.7); + let rightward = 0; + for (let i = 0; i < FX_MAX; i += 1) if (fx.life[i] > 0 && fx.vx[i] > 0) rightward += 1; + check('wall sparks bounce back into the arena', rightward >= 8); + const calls = { bad: 0 }; + const stub = { + lineStyle() {}, beginPath() {}, strokePath() {}, strokeCircle() {}, + moveTo(x, y) { if (!Number.isFinite(x + y)) calls.bad += 1; }, + lineTo(x, y) { if (!Number.isFinite(x + y)) calls.bad += 1; }, + }; + fx.explosion(500, 500, 0x00ffff, { count: 100 }); + fx.draw(stub, { x: 0, y: 0, w: 1920, h: 1080 }); + check('particle draw emits finite coordinates', calls.bad === 0); +} + +// --------------------------------------------------------------------------- +console.log('17. Bot soak'); +{ + // A dodging bot with human-ish limits: it only re-decides every `latency` + // ticks, aims with deterministic jitter, and only reacts to enemies inside + // `react` px. It aims at the nearest enemy, strafes away from anything + // close and bombs when crowded. Two profiles bracket the difficulty curve; + // invariants are checked every tick of every run. + const PROFILES = { + sharp: { latency: 10, noise: 0.15, react: 260 }, + sloppy: { latency: 18, noise: 0.2, react: 220 }, + }; + function makeBot(profile, seed) { + let r = seed * 7919 + 13; + const jitter = () => { r = (r * 1103515245 + 12345) % 2147483648; return r / 2147483648 - 0.5; }; + let held = null; let tick = 0; + return (s) => { + tick += 1; + if (held && tick % profile.latency !== 0) return held; + const p = s.player; + let nearest = null; let nd = Infinity; + let ax = 0; let ay = 0; + for (const e of s.enemies) { + const dx = e.x - p.x; const dy = e.y - p.y; + const d = Math.hypot(dx, dy) || 1; + if (d < nd && e.spawnMs <= 0) { nd = d; nearest = e; } + if (d < profile.react) { ax -= dx / d * (profile.react - d); ay -= dy / d * (profile.react - d); } + } + for (const g of s.gates) { + const dx = g.x - p.x; const dy = g.y - p.y; + const d = Math.hypot(dx, dy) || 1; + ax += dx / d * 120; ay += dy / d * 120; + for (const end of gateEndpoints(g)) { + const ex = end.x - p.x; const ey = end.y - p.y; + const ed = Math.hypot(ex, ey) || 1; + if (ed < 90) { ax -= ex / ed * (90 - ed) * 12; ay -= ey / ed * (90 - ed) * 12; } + } + } + ax += (ARENA_W / 2 - p.x) * 0.08; ay += (ARENA_H / 2 - p.y) * 0.08; + const close = s.enemies.filter((e) => Math.hypot(e.x - p.x, e.y - p.y) < 140).length; + const aim = (nearest ? Math.atan2(nearest.y - p.y, nearest.x - p.x) : 0) + jitter() * 2 * profile.noise; + held = { + moveX: Math.sign(ax) * Math.min(1, Math.abs(ax) / 50), + moveY: Math.sign(ay) * Math.min(1, Math.abs(ay) / 50), + aimX: p.x + Math.cos(aim) * 300, aimY: p.y + Math.sin(aim) * 300, + fire: true, + bomb: close >= 4, + }; + return held; + }; + } + + const runs = [ + ...[11, 22, 33, 44].map((seed) => ({ mode: 'evolved', profile: 'sharp', seed })), + ...[11, 22, 33, 44].map((seed) => ({ mode: 'evolved', profile: 'sloppy', seed })), + ...[11, 22, 33, 44].map((seed) => ({ mode: 'pacifism', profile: 'sharp', seed })), + ]; + const results = []; + let violations = []; + const seenTypes = new Set(); + for (const { mode, profile, seed } of runs) { + const s = createGame({ seed, mode }); + const bot = makeBot(PROFILES[profile], seed); + let lastScore = 0; + let firstDeathMs = null; + const limit = 60 * 60 * 8; // 8 simulated minutes + let ticks = 0; + const tag = `${mode}/${profile}/${seed}`; + while (s.phase !== 'over' && ticks < limit) { + setInput(s, bot(s)); + const ev = step(s, STEP_MS); + ticks += 1; + if (firstDeathMs == null && ev.some((e) => e.type === 'playerDied')) firstDeathMs = s.timeMs; + if (s.score < lastScore) violations.push(`${tag}: score went down`); + lastScore = s.score; + if (s.enemies.length > TUNE.MAX_ENEMIES + 40) violations.push(`${tag}: enemy overflow ${s.enemies.length}`); + if (s.bullets.length > TUNE.MAX_BULLETS) violations.push(`${tag}: bullet overflow`); + if (s.geoms.length > TUNE.MAX_GEOMS) violations.push(`${tag}: geom overflow`); + if (s.multiplier < 1 || s.lives < 0 || s.bombs < 0) violations.push(`${tag}: negative counters`); + if (mode === 'pacifism' && s.bullets.length) violations.push(`${tag}: pacifism bullet`); + if (ticks % 30 === 0) { + for (const e of s.enemies) { + seenTypes.add(e.type); + if (!Number.isFinite(e.x) || !Number.isFinite(e.y)) violations.push(`${tag}: NaN ${e.type}`); + if (e.x < -1 || e.x > ARENA_W + 1 || e.y < -1 || e.y > ARENA_H + 1) violations.push(`${tag}: ${e.type} escaped arena`); + } + const p = s.player; + if (!Number.isFinite(p.x) || p.x < 0 || p.x > ARENA_W || p.y < 0 || p.y > ARENA_H) violations.push(`${tag}: player escaped`); + if (s.enemies.some((e) => e.dead) || s.bullets.some((b) => b.dead)) violations.push(`${tag}: dead entity kept`); + } + if (violations.length > 20) break; + } + results.push({ + mode, profile, seed, over: s.phase === 'over', mins: s.timeMs / 60000, + firstDeath: firstDeathMs == null ? null : firstDeathMs / 60000, + score: s.score, kills: s.stats.kills, peak: s.stats.peakMult, deaths: s.stats.deaths, gates: s.stats.gates, + }); + } + violations = [...new Set(violations)]; + for (const r of results) { + console.log(` ${r.mode.padEnd(8)} ${r.profile.padEnd(6)} seed ${String(r.seed).padStart(2)}: ${r.over ? 'over ' : 'alive'} at ${r.mins.toFixed(1)}m` + + `, first death ${r.firstDeath == null ? '—' : `${r.firstDeath.toFixed(1)}m`}, deaths ${r.deaths}, score ${formatInt(r.score)}, peak ×${r.peak}` + + `${r.mode === 'pacifism' ? `, gates ${r.gates}` : ''}`); + } + const median = (xs) => { const m = [...xs].sort((a, b) => a - b); return m[Math.floor(m.length / 2)]; }; + const sharp = results.filter((r) => r.mode === 'evolved' && r.profile === 'sharp'); + const sloppy = results.filter((r) => r.mode === 'evolved' && r.profile === 'sloppy'); + check('no invariant violations across the soak', violations.length === 0, violations.slice(0, 6).join('; ')); + check('every run terminates or hits the time limit', results.length === runs.length); + check('evolved bots score', [...sharp, ...sloppy].every((r) => r.score > 0)); + check('pacifism bot detonates gates', results.filter((r) => r.mode === 'pacifism').some((r) => r.gates > 0)); + const allTypes = ['wanderer', 'grunt', 'weaver', 'splitter', 'snake', 'blackhole', 'rocket']; + check('every evolved enemy type appears in play', allTypes.every((t) => seenTypes.has(t)), `missing: ${allTypes.filter((t) => !seenTypes.has(t)).join(',')}`); + check('not too hard: sharp bot median run ≥ 3 min', median(sharp.map((r) => r.mins)) >= 3, `median ${median(sharp.map((r) => r.mins)).toFixed(2)}m`); + check('not too easy: sloppy bot loses a ship in most runs', sloppy.filter((r) => r.deaths > 0).length >= 3); + check('not too easy: sloppy bot first death before 4 min (median)', + median(sloppy.map((r) => r.firstDeath ?? 99)) < 4, `median ${median(sloppy.map((r) => r.firstDeath ?? 99)).toFixed(2)}m`); +} + +function formatInt(n) { return Math.floor(n).toString().replace(/\B(?=(\d{3})+(?!\d))/g, ','); } + +console.log(`\n${passes} passed, ${failures} failed`); +process.exit(failures ? 1 : 0);