# 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// manifest.json identity, stats, move bindings, wardrobe states portrait.png select-screen / HUD portrait moves// 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 ```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) ```json "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@/` 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 ```json "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@/` 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 ```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.