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>
132 lines
5.0 KiB
Markdown
132 lines
5.0 KiB
Markdown
# 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
|
||
|
||
```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@<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
|
||
|
||
```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@<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/<move>/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.
|