A 2D platform fighter where every fighter is generated from a text prompt. Two paths: a deterministic offline generator and an opt-in Gemini LLM that designs custom moves and visual effects on the fly.
"fire ninja", "stone golem", "thunder samurai", "shadow assassin", "rocket pirate", "celestial monk", type anything and fight.
# 1. Optional: enable AI-designed fighters
cp .env.example .env
# put your Gemini key in .env (https://aistudio.google.com/app/apikey)
# 2. Run the server (also serves the static game files)
node server.js
# 3. Open http://127.0.0.1:8088/No npm install, no bundler, no TypeScript. Just open index.html via the
local server and it runs.
- P1 (keyboard):
A/Dmove,Wjump,Sdrop/fast-fall,Shiftdodge,Flight,Gheavy,Hspecial. - P2 (keyboard):
←/→move,↑jump,↓drop,J/K/Llight/heavy/special,/dodge. - Gamepad: auto-detected. Left stick / D-pad to move, A jump, X light, Y/RT heavy, B/LT special, RB/LB dodge. Rumbles on hit.
Hold direction modifiers (A/D or S) while attacking for side / down attacks.
- Hand-rolled kinematic physics for fighters (
physics.js), Matter.js used only for projectile-vs-terrain. Action games can't use rigid bodies for player control, friction-based locomotion feels mushy. We tried. - 24-move grid per weapon: light & heavy × neutral / side / down × ground / air. Hammers reach far and land HARD; daggers chain combos; blasters keep distance.
- State-driven procedural rig: idle / run / rise / fall / land / dodge / hitstun / attack, pose angles + weapon swing computed each frame from state and move-phase position. No spritesheets.
- LLM trust boundary: when AI design is enabled, the validator
(
validateBlueprintinsrc/llm-schema.js) clamps every numeric field before any of it touches the engine. A hostile / broken LLM response can't produce overpowered or game-breaking fighters. Stats budgeted to ≤ 2.7, damage ≤ 22, knockback ≤ 540, visual layers ≤ 5 per move, etc. - Robust JSON repair:
parseLooseJSONinserver.jsrecovers from Gemini's occasional structured-output failures (truncation, runaway numeric tokens) so the LLM path is reliable in practice. - Stage editor: drag-place platforms (mouse, keyboard, or gamepad),
save to localStorage, share via the
?stage=IDURL param. Lives as the#editorModalmodal inindex.html, driven bysrc/editor.js.
Status: target architecture, in progress. This section describes where the prompt-to-fighter pipeline is headed. Today the LLM authors light/heavy/special from scratch; the inheritance flow below is the migration goal tracked in
context/FUTURE.md§ "V3 migration plan".
The target shape:
-
Pull an archetype. The LLM matcher picks one of the 35 reference presets in
context/PRESET-ROSTER-V3.mdbased on the prompt's family / mechanic shape (a "ninja with chain lightning" → seeking-projectile chain, a "samurai who counters" → counter parry, a "wrestler" → command-grab). The archetype hands over the regular move grid (light / heavy × N/S/D × ground/air) and per-directionmoveOverrides. Regular moves are not re-invented per prompt, they're inherited so the kit reads consistently across the family. A charged_finisher always charges; a counter always has its window; a command_grab always locks on contact. This is what makes the matcher tractable. -
Draw over it. The LLM authors the visual identity: palette, slot draws (head, body, weapon, accessory), voice profile, swing overrides, projectile renderer flavor. Prompt-specific weirdness (a samurai with octopus arms, a knight in a beekeeper veil) lives entirely in this skin layer. None of it touches move data.
-
Edit the special. The signature move (
sig_kind+onHit) is the one mechanical lever the LLM is encouraged to bend. It starts from the archetype's listed special, then the LLM tunes parameters (cooldown, charge curve, on-hit list, projectile speed/size, chain count) to fit the prompt. The special is allowed to go beyond what the doc lists, if the player prompts something the archetype's canonical special doesn't cover ("a samurai whose iaido draw spawns a phantom clone that mirrors the next attack", "a witch whose chain-lightning revives the last fallen ally"), the LLM composes a new on-hit kind from the dispatcher vocabulary (src/world.js#applyOnHitEffect). The fairness floor is the validator (validateBlueprintinsrc/llm-schema.js): clamps on damage, knockback, duration, layer counts. Today the validator does not yet inspectonHitarrays at all, extending it with an on-hit allow-list + per-kind param clamps is item #8 of the migration plan and the prerequisite for actually shipping beyond-roster specials safely. Once that lands, beyond-roster specials are fine as long as the clamp holds.
The roster doc therefore captures reference shapes for the matcher to interpolate from, not the universe of legal characters. Regular moves inherit; visuals draw over; the special is where creativity lives.
For the full archetype catalog, family taxonomy, new on-hit kinds, and
engine work implied by the V3 plan, see
context/PRESET-ROSTER-V3.md.
Authoring vocabulary (every onHit kind, moveOverrides shape, combo
guidance) lives in context/PRESET-DESIGN.md.
node scripts/runtests-node.js # 38 unit tests
node scripts/e2e-node.js # 12 end-to-end simulation testsBrowser equivalents at tests.html and e2e.html.
server.js ── Node proxy: serves static + POST /api/generate-fighter
index.html ── start menu, HUD, overlays + every modal (editor, vault, replays, ranks, settings)
src/
main.js ── boot, RAF loop with hitstop & substep, camera, KO/respawn
fighter.js ── kinematic controller, state machine, attacks, hits, knockback
physics.js ── gravity, run/decel, AABB platform collision, wall-slide
moves.js ── move data: phases, hitboxes, specials, directional resolver
weapons.js ── per-weapon 12-cell directional move grid
generator.js ── procedural prompt → fighter blueprint (deterministic, offline)
llm-schema.js ── LLM JSON schema, system prompt, validator, AI move builder
llm-fx.js ── data-driven visual effect engine (12 composable layer kinds)
ai.js ── CPU controller producing the same shape as the keyboard view
input.js / gamepad.js ── input layers (keyboard, gamepad with rumble)
fx.js ── particles, screen shake, hitstop, slash arcs, motion trails
render.js ── arena + procedural fighter art + projectile drawing
world.js ── arena + projectiles + nearestOpponent / safeXFor / ringOut
engine.js ── Matter.js setup (projectile world only)
stages.js ── built-in stages + localStorage custom stages
editor.js ── stage editor UI logic
scripts/ ── pure-Node test + e2e runners
Production deploy targets Fly.io (multi-region: US East + EU) with
Supabase as the auth + DB + storage backend. Step-by-step in
DEPLOY.md. The repo ships Dockerfile, .dockerignore,
and fly.toml; flyctl + the Supabase migrations are all you need to
go from clone to public URL.