At a glance

Ground
Procedural grid emitter func_0x801d02c0 (battle overlay), the sole draw call of the battle-mode render; no geometry file
Backdrop
The scene's own backdrop mesh stream, selected by scene data into _DAT_8007B864; registered once, drawn by two actors
Stage colour
Depth-cue pass FUN_80050120 (ambient 0x8007B7B0, far colour 0x8007BB48)
Camera pose
Rotation trio 0x8007B790 (pitch / yaw / roll, 4096 = one turn), translation trio 0x800840B8, projection distance H = 256
Framing
Dispatcher FUN_801D5854(actor, case), ten-entry jump table at 0x801CEA00 (PROT 0898)
Tween
Builder FUN_801D829C, walker FUN_8002149C
Intro
Transition overlay PROT 0979, tick FUN_801CF5BC, five visual styles
Port
scene-formats::battle_backdrop, engine-battle-vm::battle_cam_script / battle_camera / battle_attack_camera; both hosts only read the pose

The stage

authored halfbackdrop mesh, copy A second copysame mesh, mirrored or rotated, copy B ground grid: 28 × 28 cells, pitch 0x200 party seats monster seats Camerafar framing: TR (0, 1280, 7680) idle orbit: yaw is a clock Field scenerystays resident behind
Not to scale. The grid is a full plane centred on the actors, so it fills the ground at every orbit angle; the shell is half a bowl whose second draw closes the circle.

Ground grid

The floor is not geometry from a file. A GTE rasteriser emits a flat tiled grid each frame in two passes: the first transforms each grid point and writes a per-cell visibility byte, the second emits four gouraud-textured quads per visible cell. One 64×64 texel window is stretched over each cell. The routine contains no RNG and mirrors no corner - the tiling is deterministic. The grid has one extra cell of bias on the depth axis, so it is not quite centred.

Every grid quad is linked into one ordering-table entry at the far end of the table, so the floor is painted before anything else and hides nothing drawn after it - not even geometry reaching below the floor plane. The port depth-tests the grid like any other mesh, which is why Zeto's Call Wave rays only fan upward in the port where retail's fan all the way round him over the floor.

Which backdrop a fight uses

A scene bundle carries one backdrop mesh stream per sub-area, and the battle uses whichever one the chunk walker last recorded. The choice is scene data, not a code table, and it is not uniformly the block's first stream: Rim Elm carries four sub-area backdrops and the sparring match is fought in the second.

Two copies of one mesh

The shell is authored as half a bowl - across every stage entry the mesh barely crosses its own centre plane, and that is the real shape, not a truncated parse. The mesh is registered once and drawn by two actors; a per-stage mirror table and an outdoor table decide how the second copy is placed. One orbit-angle capture separates the models: with one copy, 90 screen columns are empty where the retail framebuffer has a mountain band in all 90.

Which side of the ribbon draws. The backdrop arm hands the primitive dispatcher the node’s colour word with a flag that, for the mirrored copy only, lowers the winding mask so the quad leaf’s back-face test (NCLIP) can no longer fail. The mirrored copy therefore draws both sides of every face and the unmirrored copy only its front faces. The ribbon faces the arena’s centre and the camera stands outside its ring, so the near quads of the unmirrored copy - the ones between the camera and the fighters - face away and drop out. Both play hosts pass the same per-draw NCLIP word.

How the floor is lit

The grid has no light source. Each lattice vertex is depth-cued by the GTE from a colour register the emitter never writes itself: a once-a-frame pass rewrites it from the stage’s ambient word, which is the stage base colour plus 0x404040. Settled, the base is 0x808080, so the floor’s near colour is 0xC0 per channel and the texel is lifted by half before the cue blends it toward the far colour.

  • Every fight’s floor fades in. The stage loader FUN_800513F0 seeds the base low, and the backdrop pass FUN_80050120 ramps it up every frame. The loader runs a few units into the entry sweep, and the ramp’s first step lands in that same pass; the port holds the floor until its own sweep counter reaches the same point.
  • A cast dims the stage. A latch in the battle context reverses the ramp toward a dark floor. The summon close-up raises it every pass, and the capture-class cast route and the Spirit / Item pre-arm raise it once on the way into their bands.

The camera

The view is a plain Euler pose: screen = H × (R·v + TR) / depth with R = Rx(pitch) × Ry(yaw). The projection distance and base matrix hold through every phase; only the pose moves. The reference pose is the far framing of the round prompt - pitch 32 (about 2.8° of down-tilt), no roll, TR = (0, 1280, 7680), looking at the world origin, with yaw the free orbit azimuth.

Who owns the camera this frame

Three owners hand it on in order, each calling the same dispatcher:

  1. Entry sweepthe SCUS frame driver: the camera rises and pulls in from battle init's pose, then closes with case 2
  2. Menu driverthe command flow: far framing on the prompt, a member close-up in the ring, a target shot on the cursor
  3. Action SMthe action state machine: per-action framing, a post-strike two-shot, then the target

The ten framings

CaseShotPitchTranslationLooks at
0member close-up (ring, arts input, pickers)32(-512, height[char], 2457)the member
1target cursor on one enemy0x100(0, 0x600, …)the commanding member
2the entry sweep's close-in0(0, 0x600, 0x700)acting actor, or the origin
3target cursor on one ally32(0, height, 2457)the ally
4 / 5none - whole-side cursors go straight to the shared tail---
6per-action framing0(0, 0x500, ctx[+0x6D0])acting actor, live position
7post-strike two-shot0as case 6attacker-target midpoint
8end of actionby target stateby target statethe target
9far framing (round prompt)32(0, 1280, max(span × 3, 0x800))formation centre

The end-of-action gate arms nothing; the far framing takes over there.

The tween is an ease-out

Every framing is re-armed each pass: the target vectors are rebuilt from the live actor record every display frame and the step table re-emitted. The target is never frozen at the state change. Each rebuild divides the remaining gap by the duration again, so a pass covers about a sixth of what remains, and the camera is still closing in long after its nominal twelve frames. Captured step tables match that law exactly.

Idle orbit

Battle init zeroes the yaw; from there it is a clock. Two writers decrement it by twice the frame step - the battle tick while the flow byte sits on a prompt, and the action SM in its setup states. This sweep never consults the state machines, which is why a parked battle shows as an endless orbit.

The per-art attack camera

FUN_801D71B8 is an override, called from the dispatcher's shared tail after whichever case has armed its tween. It therefore overrides cases 7 and 8 as well as 6 - which matters, because the strike loop where most art swings are filmed is case 7. It seeds a fresh, tighter pose from the actor, runs a per-character, per-art arm, and re-arms a much shorter tween. An art with no arm returns without arming anything and the case's framing stands. Its first test is the target's live HP, so a death re-frame is never overridden. The arm offsets come from the attack-camera table.

The Battle Camera option is not a distance

The options row (Close / Normal / Far) is read in four places, each making the action shots calmer as it rises: Far skips the yaw drift, the setup-band orbit and the per-art override; Normal and Far skip case 8's dead-target and live-target arms.

The prompt and the target cursor

The menu driver re-arms the dispatcher on every cursor step, against the cursor’s scope. The Attack command’s cursor (the prompt’s Auto, and the ring’s Attack arm under the Automatic option) takes case 1 on the commanding member. The Auto | Command prompt itself is case 0: opening it leaves the ring’s close-up standing, and cancelling the cursor back to it re-arms case 0 on the member. Item and magic cursors switch on scope - one enemy is case 1, one ally case 3, a whole side cases 4 / 5, which leave the framing as it stands.

The 4× world scale

Battle meshes are small - a party member stands a couple of hundred units tall - yet they read at full size on screen. The reason is the battle mode’s base matrix, DAT_8007BF10, which holds a uniform 4.0 scale in every catalogued battle state. Every actor, the backdrop included (it is registered as an ordinary background actor), draws under that matrix, so the camera’s translations are authored in the scaled space: the far framing’s eye distance of 7680 is measured to a formation whose seats sit ±800 before the scale.

The scale belongs to the mode, not to the stage, so a fight whose stage stream does not resolve still draws its actors and effects at 4×. Both play hosts take it from one decision (legaia_engine_session::battle_fx::battle_world_scale). A draw class left at 1× would orbit at four times the intended radius - a fault invisible on a centred formation at the far framing, which is why a guard projects every retail seat through both the ground and the actors at every framing and requires the same pixel. The floor’s depth-cue window is not scaled: it is keyed on view depth, under a camera already in view units.

The frame step

Retail’s battle runs once per battle frame, not once per vsync. The frame driver measures what each frame cost and turns it into a step of one to four vsyncs (usually two, three or four in heavy summon close-ups); the action state machine, the camera walker, effect waits and root motion all scale by it.

The port runs retail’s own cadence by default: the whole battle pass once a frame of the default step two, with every counter tested once per pass and drained by the step. The Retail battle cadence toggle (WorldToggles::battle_sm_frame_cadence, see engine) turns it off, and the battle then steps every vsync with its timers draining by one. Measured against retail’s battle captures, the cadence matches retail’s frame on as many states or more than the per-vsync pass does, the summon close-ups most of all.

The field-to-battle transition

The transition between leaving the field and the battle coming up is its own overlay. It sequences the handoff and drives one of five visual styles over a one-shot capture of the field frame:

StyleRetail tick
Scatter particlesFUN_801CFDA0
Scatter with spin-up (boss entry)FUN_801D0370
Tile shatterFUN_801D0D24
Swirl fanFUN_801D1888 / FUN_801D1A20
Screen-strip curtainFUN_801D11D0

The transition runs 132 display frames, 252 for the swirl. Every battle entry rides it: the field step roll, a scripted fight, and a world-map contact. The scripted-fight bit of the per-battle flags selects the spin-up style. The curtain is a render-to-texture whose row pass is the only part that reaches the screen.

The capture is the frame the player was looking at, as retail’s copy of the displayed framebuffer is. Both hosts read back their finished frame on the tick the transition arms: the 3D pass with every enhancement on it (the lighting and its time of day, the ground fog, the see-through walls, the glow) plus the screen layers over it (retail’s fog-pool particles, the drop shadows, the light pools, the fades). Only the transition’s own layer is left out, since it is drawn from the capture. With the enhancements off, the capture is the faithful frame.

The frame lands where the console puts it: retail draws a 224-row area from VRAM row 4 and copies the display from row 0, so every style’s texture coordinates expect drawn row d at v = d + 4, with the rows around the frame cleared to transparent. Landing the port’s frame at v = 0 started the shatter four rows high and left a band of garbage across the bottom rows, where the tile grid’s wrapped last row showed through below retail’s 224 rows.

See also

The full write-up - grid emitter constants, the two culls, the depth-cue colours, the mirror and outdoor tables, each framing case arm by arm, the frame step, screen shake and the transition's per-style packets - is in docs/subsystems/battle-stage-camera.md.