Battle HUD and screen chrome
Everything the battle screen draws around the fight: the actor-name plaque, the party status readout, the command chips, banners, status and element badges, the item and magic windows. Retail does not draw this per frame from game state. It keeps a list of retained text actors, rebuilds that list from disc data at every state transition, and frames each actor with sprites looked up in two static tables. Knowing those two tables is enough to derive every seat, plate and palette on the screen.
At a glance
- Placement table
SCUS_942.540x80076C10, 24-byte records, 103 initialised; parserlegaia_ui_formats::screen_elements- Widget-class table
SCUS_942.540x800732A4, 12-byte records,0x9Dof them; parserlegaia_ui_formats::ui_widgets- Sub-draw script
- Fifty steps in the battle overlay's rodata (
PTR_DAT_801F4D34): which records are up in which phase - Element raiser
FUN_801D8DE8(record, mode)(PROT 0898)- Text actors
- Spawner
FUN_8003541C; retained handle listctx[+0x1074], forty handles - Art
- The resident system-UI sheet at VRAM page
(896, 256), palette row 511; text from the menu-glyph atlas at(896, 0), row 510 - Port
- Model
engine-battle-hud, draw builderengine-ui::battle_hud; both hosts draw one shared surface
Screen layout
How an element reaches the screen
Retail’s battle HUD is a list of retained text actors - ctx[+0x1074], forty handles - that the two battle state machines rebuild at every transition. Three pieces of disc data decide everything the list holds and how it looks.
- Mode bits. Bit 0 picks the spawn seat (A for 0, B for 1) and the glide runs to the other; bit 1 suppresses the glide. Which seat is on screen is per record, so mode 0 means appear for the bar and unfold for a chip.
- The glide counts vsyncs. Its elapsed counter grows by the frame step each battle pass, so the sixteen-frame raise lasts sixteen vsyncs at any cadence. The port steps every tracked glide by one a tick.
Who holds the screen in each phase
The menu state machine runs one sub-draw step per state edge. The steps decide which of the four HUD surfaces is up - and because they are disc data, the table below is read straight off them and agrees, element for element, with the handle lists of the catalogued save states.
| Transition | Roster panels | Active-actor bar | Breadcrumb tab + plaque | AP plate |
|---|---|---|---|---|
| round prompt opens | up | - | - | - |
| command ring opens | park | up | both slide in | slides in |
| item / magic window | back up | park | snap | leaves |
| item / magic target step | park | up, pointed member | snap | - |
| attack-mode prompt (Auto / Command) | - | snap | snap | snap |
| target cursor cancelled back to the prompt | - | up | snap | slides in |
| attack target cursor | - | park | snap | leaves |
| arts entry | - | park (the AP bar takes the seat) | snap | snap |
| all members committed | - | - | tab only | leaves |
The roster panels belong to the round prompt and the browsed windows; the full-width bar to the ring, the attack-mode prompt and the target steps; the AP plate to the ring, the attack-mode prompt and the arts entry. The attack-mode prompt is the ring with its arms swapped: its step opens the Auto and Command chips on the left and right arm seats, sends the other three arms off and keeps everything else where it stands.
Parked is below the screen, not gone. When the bar takes over, retail moves the whole panel cluster to y = 230, just under the 228-line display window. The port’s stage is 240 lines tall, so it simply emits no panel draws while retail parks them. Inside an action the panels come back for a party-wide target, and a cast module can raise them itself - Orb’s heal glides them up so its HP readout counts up in them.
The placement record
Each record is one element's pair of seats plus a shared content box. The two seats are the element's from and to: which one is the parked seat is per record, so some elements slide in from the right and others from the left or the top.
Mode bit 0 picks the spawn seat (A for 0, B for 1) and the glide runs to the other; bit 1 suppresses the glide.
- The pen is
(x, y - 2). Both seat arms subtract two from the seat's y before spawning the text actor. +0x14is the string being drawn. Name banners measure their string, write the width into+0x06and centre both seats on it - which is how the move name stays centred on the screen whatever its length.- The plate is not stored anywhere. It is derived from the content box; one record derives every plate.
Three movers
Three small routines copy geometry and content between records - never the id, the box height or the frame - which is how one row can borrow another's content:
| Routine | Role |
|---|---|
FUN_801D5718 | lands an element: both seats become the source's seat B, so nothing is left to glide |
FUN_801D57E8 | adopts a source's whole geometry into a row that keeps its identity |
FUN_801D5778 | launches it: seat B becomes the source's seat B minus 0x140 - one display width, so it glides a screen to the left |
The commit log uses all three: each committed member's name, command and target are landed into a row of three records, and earlier rows are launched off as the party commits.
The widget-class table
Every sprite the chrome draws comes out of one array. A placement record's frame-style bytes are indices into it: style 0x01 is the blue plate body, style 0x02 the carved-gold one. The join holds for all 103 placement records.
- A widget is a run of records. The chain delta steps to the next record of the same widget; zero ends the run. A roster panel is such a chain: plate, labels and separators.
- The palette byte is a packed CLUT address, with bit 7 marking semi-transparency.
- Two readers, one difference.
FUN_8002C488draws exactly one sprite at the caller's position and never applies the bias;FUN_8002C69Cdraws a sized widget, applies the bias, then follows the chain. The bias converts the glyph pen to the frame origin - the plate run's(-8, -4)takes the plaque's pen(16, 12)to a plate at(8, 8).
The table's end is structural: it runs exactly up to the frame tile-set pool the class arms read next.
Chrome facts worth knowing
- One sheet, one three-slice, two palettes. The name plaque, the party bar and every command chip are the same three tiles (left cap, body, right cap) at two sheet rows: blue for the bar and chips, carved gold for the plaque. The gold row is the same art as the pause menu's title tab.
- Numbers are cells, names are glyphs. Every number is a run of fixed 8×12 cells; only names use the proportional dialog font. Both halves of a current / maximum pair are right-aligned.
- Status and element badges are widget records too: 48×16 word cells for the status element, 20×12 cells for the element badge, which is selected by markup in the name string rather than a placement record.
- Four badge ids are not on the sheet at all - they resolve to the save-slot portraits.
- No gauge anywhere. Neither the roster panels nor the active-actor bar draws an HP or MP meter, for the party or for monsters: a label sprite, numerals and a separator, nothing else. Retail draws no monster readout at all, so the acting monster’s name on the plaque is everything an enemy contributes to the HUD.
- The plaques slide. An action’s name plaque and its target plaque are raised by the action seed in mode 0, so they glide in over sixteen frames - the plaque from above the screen, the target plaque from the right. A frame saved on the step the seed ran shows neither.
- The breadcrumb trail. From the command ring on, the round prompt’s Begin chip glides to the top-left as a gold tab, the plaque rests beside it, and choosing a ring arm glides that arm’s chip on as a third tab - Attack, the member’s Ra-Seru name for magic, or Item.
- The ring’s magic arm is a name. Its chip reads the member’s Ra-Seru when the party battle-actor init found Ra-Seru equipment on the record, and a lone
-otherwise. An unavailable command still gets its chip; a forbidden one wears a red cross-out mark instead. - The selection cue is the plate, not a tint. Every chip draws at neutral grey. Only under the Battle Command option’s cross-button setting does the highlighted arm switch frame style, to the carved-gold run; on the default directional setting all chips stay blue.
Message and intro banners
- The message banner is a class-0 window at
(8, 4), 296 wide and 28 tall, with a fixed 280-pixel content box - it is not measured from the message. The frame is filled: under the border sprites the frame emitterFUN_8002BDC4tiles a blue-marbled 32×32 patch in columns, with a top-to-bottom grey ramp. It shares its seat with the actor-name plaque; the two are alternatives, not layers. - The bottom message bar (record
0x5B) carries the steal captions and the petrify result line on the active-actor bar’s seat. It has the bar’s content box but the banner’s frame style, so it is a framed window rather than a plate run. - The intro enemy-name labels are not placement records. The battle-open composer
FUN_801D9D3Clays them out itself with immediate geometry: one label per run of identical monsters (the second member turns the name into name * 2, each further one bumps the digit), centred over the group’s average battle X, then relaxed apart pair by pair and clamped to the screen until a pass moves nothing. They spawn at their final seat - no slide - and retail sweeps them on the pass the intro timer expires, so no action ever runs under the names. The port does not hold the round prompt back for that span, and sweeps the labels when it hands a round to the action SM early.
The item and magic windows
State 0x3C opens a list window on the right and a description window below-left, both drawn from the system-UI window skin, with a pointing-hand cursor on a 14-pixel row pitch, eight rows a page and a PAGE n/m header. The breadcrumb trail replaces the actor plaque while it is up.
- A battle row is enabled on one bit. The shared list builder branches on the menu context: in the field, a usable item is enabled only if some member would benefit; in battle, the descriptor’s battle-usable bit alone decides. A Healing Leaf with the party at full HP is pickable in a fight and greyed in the pause menu.
- Target confirm is the bar. Picking an item closes both windows and the target strip at the foot of the screen appears. It is not a widget of its own: it is the active-actor bar, re-opened by the target step and re-pointed per cursor move.
- The magic window (
0x46) is the same frame with spell content: a leading element icon per row, a two-line description (name, then scope), and two gold tabs top right for the highlighted spell’s level and MP cost.
Both are composed by engine-ui::battle_item_ui through the shared menu-window chrome and drawn by both hosts. The port does not yet draw the element and Ra-Seru icon cells, the world-anchored target name tag or the magic target step’s All plate.
The Arts announcement banner
NEW ARTS!!, HYPER ARTS!!, MIRACLE ARTS!! and SUPER ARTS!! are one banner: two textured quads from the value-readout glyph sheet - a position-selected first word and the shared ARTS!! - whose halves march toward a seam as a slide clock runs. Each live frame draws it four times at four points of its own travel, at 5 / 10 / 20 / 50 percent brightness, so the word arrives trailing ghosts that switch off as it lands.
Stage byte ctx[+0x28B] | Banner | Raised when | |
|---|---|---|---|
1 | NEW | the queue builder | the queue builder marks a newly learned art | rsquo;s build loop leaves its mark in the side array
2 | HYPER | the default: the byte is still zero after the other two writes | |
3 | MIRACLE | the queue builder raised the seat’s Miracle flag | |
4 | SUPER | the Super tail-replace leaves its mark in the side array | |
5..=8 | none | a commit on the running actor retires a live banner |
The raises sit in the executable’s anim commit FUN_8004AD80 and fire only for a party actor committing the SpecialStarter clip. The three writes run in sequence and the later one wins - they are not an if / else chain. Each raise restarts the slide and queues the character’s Arts fanfare. Port: engine-battle-vm::battle_action::flash_ramp, stepped once per battle frame and drawn by both hosts.
In the port
engine-battle-hud is the renderer-agnostic model: per-slot HP / MP / AP and status, damage popups and log lines, fed by the world’s strike, status and round events. engine-ui::battle_hud::battle_hud_draws_for turns one frame of it into two draw lists - glyphs off the dialog-font atlas and sprites off the resident system-UI sheet - which both hosts composite in the slot they already use for menu chrome.
- The surface samples the disc’s own cells: the marbled panel plate, the blue plate three-slice, the separator and the numerals are baked into the shared atlas and drawn 1:1. Without the atlas the plates degrade to solid rects and the numerals to font glyphs on the same cells - letterforms change, layout does not.
- Numerals follow the displayed HP, the value retail ramps down over frames, through the ports of retail’s readout-tint laws; a dead member’s row dims.
- Slot indices are absolute actor-table indices, so a host passes inactive slots through as empty rows rather than compacting the list.
- A diagnostic surface (monster HP rows and gauge bars, K.O. tags, an encounter banner) exists for development behind
LEGAIA_DIAG_HUD; it is off on both hosts, and retail draws none of it.
See also
The full write-up - each plate's packet-pinned derivation, the opening glides, the badge sheet and per-badge palettes, the commit log, the record-41/42 shift and every capture behind them - is in docs/subsystems/battle-hud.md.