Skip to content

Ijtihed/inkbrawl

Repository files navigation

INKBRAWL

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.

Quick start

# 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.

Controls

  • P1 (keyboard): A/D move, W jump, S drop/fast-fall, Shift dodge, F light, G heavy, H special.
  • P2 (keyboard): / move, jump, drop, J/K/L light/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.

What's interesting under the hood

  • 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 (validateBlueprint in src/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: parseLooseJSON in server.js recovers 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=ID URL param. Lives as the #editorModal modal in index.html, driven by src/editor.js.

Design model: archetype + LLM-edited special (target)

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:

  1. Pull an archetype. The LLM matcher picks one of the 35 reference presets in context/PRESET-ROSTER-V3.md based 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-direction moveOverrides. 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.

  2. 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.

  3. 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 (validateBlueprint in src/llm-schema.js): clamps on damage, knockback, duration, layer counts. Today the validator does not yet inspect onHit arrays 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.

Tests

node scripts/runtests-node.js   # 38 unit tests
node scripts/e2e-node.js        # 12 end-to-end simulation tests

Browser equivalents at tests.html and e2e.html.

Layout

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

Deployment

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.

About

2D platform fighter where every fighter is generated from a text prompt — procedural or Gemini-designed.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

No releases published

Packages

 
 
 

Contributors