foitin/characters/_spec/CHARACTER_SPEC.md
type-two c1eca9e1a0 hp_bands wardrobe mode: states as mini health bars + segmented HUD
Each wardrobe state owns an equal hp band; emptying a band breaks the
fighter into the next look (white flash). HUD draws one colored segment
per band. --demo=bands + --shot-ticks for multi-shot verification.
Vesper (geared/base/torn) switched to hp_bands as the pilot.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-16 15:11:36 +10:00

5.0 KiB
Raw Blame History

FOITIN character folder spec (v1)

A character is a self-describing folder under characters/. The engine scans characters/* at boot (folders starting with _ or . are skipped) — dropping a valid folder in IS shipping a character. This is the BKB per-character-folder idea (Data/Venus/{bik,zon,geo,me2}) modernized: PNG/WebP frames instead of Bink video, one JSON per move instead of the binary BKB!ZONE/GEOM/DAT2 sidecars.

characters/<id>/
  manifest.json            identity, stats, move bindings, wardrobe states
  portrait.png             select-screen / HUD portrait
  moves/<move>/
    data.json              frame data (timing, boxes, damage, root motion)
    frames/0000.png ...    sprite sequence, alphabetical order (base state)
    frames@torn/ ...       optional wardrobe-state variants (same frame count;
    frames@geared/ ...     missing variants fall back to base per-move)

Conventions

  • Timing is in 60Hz ticks. Sprite frames are spread evenly across a move's total ticks, so art frame-rate is decoupled from game logic (render at 15, 20, 30 or 60fps — the engine doesn't care).
  • Boxes are fighter-local, right-facing: origin at the ground pivot (feet centre), +x forward, y up. Rects are [x, y, w, h] (y usually negative). The engine mirrors everything when facing left.
  • Frames must share one canvas size with the ground pivot at bottom-centre (the BKB .geo lesson: bake a consistent pivot or sprites jitter).

manifest.json

{
  "id": "alpha",
  "name": "ALPHA",
  "health": 1000,
  "walk_speed": 3.2,
  "back_speed": 2.6,
  "jump_velocity": -13.5,
  "color": "#3ec6a8",
  "normals": { "P": "jab", "6P": "straight", "K": "kick" },
  "command_moves": [
    { "motion": [2, 3, 6], "button": "P", "move": "rush_palm" }
  ]
}
  • normals: button → move. Optional numpad-direction prefix (6P = forward+P, 2K = crouching K). Plain button is the fallback.
  • command_moves: checked before normals; motion is numpad notation ([2,3,6] = quarter-circle-forward), completed within 14 ticks.

Wardrobe (battle damage)

"wardrobe": { "states": ["geared", "base", "torn"], "tear_below_hp": 0.35 }

Ordered degradation states. Getting hit by a move with "tears": true advances one state (gear knocked off → clothes shredded); dropping below tear_below_hp × max health forces the final state. States map to frames@<name>/ variant dirs per move ("base" = the default frames/). Render variants with pipeline/render_moves.py --torn ... (alpha-hole shredding) or --attach ... (bone-parented gear, see pipeline/render_geared.sh for calibrated specs). Per-character: a fighter with ["base","torn"] simply has no gear stage.

hp_bands mode — punch through the layers

"wardrobe": { "states": ["geared", "base", "torn"], "hp_bands": true }

With hp_bands, the ordered states split max health into equal hp bands and the active state follows the band hp is in. Each state is a mini health bar: the HUD draws one colored segment per band, and emptying a segment breaks the fighter through into the next look (white sprite flash on the break). Forward-only — a passed band is never re-entered. tears and tear_below_hp are ignored in this mode. States don't need full variant coverage: missing frames@<state>/ dirs fall back to base per-move, so a style tier that only covers idle/walk/hit/jab/straight/kick still reads. The states can be any ordered looks, not just wardrobe — e.g. render styles (["base", "sketch", "wireframe"]) made with the restyle pipeline.

moves//data.json

{
  "name": "jab",
  "total": 22,
  "loop": false,
  "startup": 5,
  "active": [6, 8],
  "damage": 30,
  "hitstun": 14,
  "blockstun": 8,
  "pushback": 7,
  "knockdown": false,
  "cancels": [],
  "root_motion": [0, 0, 1.5, 3, 3, 1.5],
  "hurtboxes": {
    "default": [[-22, -305, 44, 50], [-30, -255, 60, 105], [-26, -150, 52, 150]],
    "frames": { "6-8": [[-22, -305, 44, 50], [-30, -255, 90, 105], [-26, -150, 52, 150]] }
  },
  "hitboxes": { "6-8": [[30, -272, 78, 34]] }
}
  • active: [first, last] tick (inclusive) the move can hit. One hit per move execution (no multi-hit yet).
  • hitboxes / hurtboxes.frames: keys are a tick or tick-range string ("6", "6-8") → list of rects. hurtboxes.default covers unlisted ticks.
  • root_motion: per-tick forward displacement in px, optional, truncates to 0.
  • cancels: reserved for the cancel system (not enforced yet).

Required move names

The FSM binds these animation names — every character must provide them:

idle walk_f walk_b crouch jump block hit knockdown getup

plus whatever attack moves the manifest binds. Recommended full set and budget: see STUDY.md §2 (~30 clips).

Where assets come from

Placeholder: tools/gen_placeholder_char.py. Production: the MODELBEAST pipeline (pipeline/README.md) renders a rigged 3D character to frames, then pipeline/autohitbox.py generates hurtboxes from pose estimation and pipeline/pack_character.py assembles this folder.