emberline.fighters.index — the roster
local Roster = require("emberline.fighters.index")
Requiring this module is the registration. It builds MAG, KITE, BELL and
THE WARDEN, checks every one of them against the invariants in
emberline.fighters.framedata, and publishes them into
Bus.registry.fighters keyed by id. Nothing else needs to happen — require
it once, anywhere, and the registry is populated.
What a fighter def carries
Everything in Rules.FighterDef and Rules.MoveDef, plus the numbers those
two types do not have room for. All fourteen slots in Rules.MOVE_SLOTS are
filled for all four fighters.
Per fighter: walkSpeed walkBackSpeed dashSpeed dashFrames
dashDistance backDashFrames backDashDistance backDashInvuln
jumpArc jumpVelX weight reach health stunResist meterScale
pushWidth crouchHeight throwRange crossStageFrames colors
specials aiProfile ai select moves.
Per move: startup active recovery total whiffPunishWindow
damage chip hitLevel hitstun blockstun onBlock onHit
hitbox reach pushback blockPushback hitstop meterGain
cancels desc, and where the move has them launch knockdown armor
armorWindow invuln invulnKind advance projectile superFreeze
meterCost fx sfx voice.
Speeds are world units per frame at 60 Hz. Distances are world units.
Frame numbers are frames. meterGain is a fraction of Rules.METER_MAX.
The three things worth knowing before you use it
recovery and onBlock are the same number. A move is authored with
its startup, its active frames, the stun it inflicts and the advantage it is
meant to have on block; the recovery that pays for that advantage is derived.
So onBlock is never a claim — it is arithmetic, and framedata.validate
fails the build of any move where the two disagree.
reach scales a hitbox's WIDTH, not its position. A move's real reach is
hitbox.x + hitbox.w * fighter.reach, already computed into move.reach.
For a projectile, framedata.effectiveReach(move) adds everything the
projectile covers before it expires — the spawn box is where the fireball
starts, not how far the move reaches.
A move connects against a body d away when move.reach >= d - 0.40.
Two fighters standing against each other have their origins
framedata.POINT_BLANK (1.24) apart. framedata.connectsAt(move, d) is the
call.
API
| call | gives you |
|---|---|
Roster.get(id) | one fighter def, or nil |
Roster.byId | all four, keyed by id |
Roster.selectable() | the three on the select grid, in order |
Roster.ladder(playerId) | four rungs: opponent, stage, difficulty, isBoss, isMirror |
Roster.audit() | { ok, failures, notes } — every tuning claim, re-checked |
Roster.report() | the whole roster's frame tables plus the audit, as text |
Roster.punishMatrix(id, slot) | who can punish that move on block, and with what |
Roster.comboDamage(id, slots) | combo damage with Rules.COMBO_SCALING applied |
Roster.bestTouch(id) | the biggest single non-super hit, and its slot |
Roster.framedata | the module above, for connectsAt / punishOptions / links |
Roster.audit() is the one to run after any tuning change. It is not a smoke
test — it re-derives the design claims from the numbers, so if somebody makes
a light safe-r than -3, or lets a heavy stop being punishable, or shortens
Kite's fireball past being a poke, it says so by name.
The ladder
Roster.ladder("mag") returns four rungs: the two fighters you did not pick,
then your own mirror, then the Warden — on platform, lantern, coaldeck
and endofline, at difficulty 1, 2, 3 and 4.
The select screen
Every fighter carries a select table with order, name, fullName,
subtitle, archetype, tagline, blurb, difficulty (1–5),
bars (power/speed/range/technique/defence, each 1–5), quoteSelect,
quoteWin, moveList (input notation, name, one line each), portraitClip,
homeStage and playable. The Warden is in the table with
playable = false.
The AI's half
Every fighter carries an ai table: profile, idealRange {min,max},
approach, antiAir (the move id to reversal a jump-in with), punish (move
ids, cheapest first), poke, meterMove, throwAt, aggression and
zoning. These are what let one brain play four characters correctly instead
of walking all of them forward.
Seeing it
emberline.fighters.reachchart draws the roster's ranges on the fight plane
at true scale — the same 0.02 units per pixel the fighters are drawn at.
local Chart = require("emberline.fighters.reachchart")
Chart.show(Roster.byId) -- puts the card up
Chart.hide() -- takes it down
It spawns nothing until you call it and touches only its own entity.
Scoped to this part · feeds back into the world's score.