Kajar notes of ChronoRET, rebuilding Chrono Trigger one matched routine at a time

Last updated: October 9, 2026

Battle actions

How the battle plays one action, an attack, a tech or the like: service 4 of bank $C1's service dispatcher loads the action's script, graphics and palette steps, then runs the script frame by frame until it ends. The whole of this machinery, $C1:4058–$C1:7C3C, is matched: the service, its loaders, the reach check and path search, the thread runner and every opcode handler. "Action", "attack" and "tech" are the headers' readings of the records the loaders take; they are not checked against what the player sees.

These routines belong to the "Battle engine" group, so their table is on the Battle engine page.

See also: Battle engine, Battle targeting.

Service 4: BattleSys_RunAction

BattleSys_RunAction ($C1:4058, 358 bytes) is reached only through the dispatcher (entry 4 of the table at $C1:0051). The one request found is BattleSys_UnkBFA4, called only by BattleSys_UnkAC57, which then applies the hits (Battle_ApplyHits); BattleSys_UnkAC57 is called by the enemy turn (BattleAi_EnemyTurn), the PC command runner (BattleSys_UnkB967, BattleSys_RunPcAttack), the KO check and a turn-list helper. It plays the action described by the block at Battle_ActCaster (caster, kind, id, target masks), filled from the chosen command (for an enemy by BattleAi_FillActBlock):

  1. reset the action state and wait a frame;
  2. build the battler lists (BattleAct_BuildBattlerList) and load the script (BattleAct_LoadScript);
  3. each frame: the battlers' action animations (BattleAct_StepBattlerAnims, at most one frame decoded per frame), the menu, palette steps, the per-entry calculations (BattleAct_TickCalcs), the occupied-cell map, the probes, and the script threads (BattleAct_RunThreads), until the script says it has ended;
  4. send APU command $18, wait until Battle_Unk5D9B is 0, and pick every battler's status animation again (Battle_PickNextStatusAnim);
  5. if a PC was among the targets, flag each PC whose pending hit reaches its HP (BattleAct_FlagLethalPcHits) and draw that PC's KO frame.

Loading an action

BattleAct_LoadScript ($C1:4310) clears the 16-byte parameter block and calls one loader per Battle_ActKind through BattleAct_LoaderTable ($C1:7A63):

Kind Loader Reads
0 (and 4 up) BattleAct_LoadNone nothing; no thread starts
1 BattleAct_LoadAttack ($C1:432C) three record sets per PC, two records per enemy type (the "attack" reading is a guess)
2 BattleAct_LoadTech ($C1:45A0) a 7-byte record per action id, for PCs and enemies (the "tech" reading is a guess)
3 BattleAct_LoadKind3 ($C1:475B) like the PC tech branch, for any caster, with id − $BC

Each record gives a script number, two graphics sets, and indexes of the compressed sprite frames, the palette steps and the object lists. All loaders end in BattleAct_LoadCommon, which loads the graphics sets, unpacks the sprite frames into $7E:2D00 (BattleAct_UnpackFrames, through the $C3:0002 vector) and splits the object records (BattleAct_IndexObjLists). The PC command runner uses kind 3 for items (id = item id); whether BattleAct_LoadKind3 is "the item loader" is still open in NEXT.md.

Before a PC's attack is loaded, BattleAct_CheckReach decides whether the target is near enough, and BattleAct_FindPath ($C1:4A71) is a breadth-first search on the path map from the attacker's cell to the target's.

Script threads

A script starts with two 16-bit thread masks (bit 15 down to bit 0 for threads 0–15), then one offset per set bit. Every frame BattleAct_RunThreads ($C1:4BBE) runs one step of each live thread, in the order 0–4, then 8–15. A thread stands for an actor:

Threads Actor
0–3 The battler in that entry of Battle_ActBattlers: the caster, the two partner slots, the main target
4 Every slot of the target set, while the target mask is non-zero
5–7 Never run
8–15 Object threads 0–7, which drive the action's own objects

BattleAct_RunThread ($C1:4C9E) runs opcodes through BattleAct_OpcodeTable ($C1:7A6B, 224 words) until a handler stops it for the frame. Only opcodes below $DB run, so the last five entries are never reached; 75 entries point at BattleAct_OpEndScript (opcode $01 and the unused opcodes), and there are 115 distinct handlers. A handler moves the script on with BattleAct_AdvanceScript; one that does not (a wait) runs again next frame.

The opcode handlers ($C1:4CF0–$C1:75BA) fall into groups, each with its own banner:

  • Control: end the thread or script, call and return, pause, the waits on script variables, follow / unfollow.
  • Animation: play, loop or show an animation frame of the actor, set its speed and facing.
  • Movement: straight moves (BattleAct_OpMoveTo), curving moves, path walks (BattleAct_OpPathTo, using BattleAct_FindPath), arcs (BattleAct_OpArcToCalc), circles and ellipses, moves along a heading, the midpoint move, the shake (BattleAct_OpShake), and the position history.
  • Script variables: set, count, add, copy and swap them.
  • Hit numbers: BattleAct_OpShowHitNumbers ($C1:5ADB).
  • Palettes: load an entry, start and stop palette sequences, a special palette and its restore, colour rotation and blinking.
  • Sound: APU commands $18/$19 with an id (BattleAct_OpSound), and the PC attack sounds.

Many opcodes take a point from one of two dispatchers: BattleAct_CalcFacing (25 facing modes: like an entry, toward an entry, fixed, toward the centre) and BattleAct_RunCalc (72 point calculations: a battler's position, a midpoint, a centroid, a fixed point, a saved position, an object's offset, a point along a line). Their tables are BattleAct_FacingModeTable and BattleAct_CalcTable ($C1:79A1–$C1:7A62).

Enemy movers

Outside actions, enemies move by themselves in 8-pixel steps ($C1:3714–$C1:4057): Battle_TickEnemyMovers runs, each frame, the mover picked by the enemy's animation through Battle_EnemyMoverTable (9 entries): stay put, approach the target, charge, line up on one axis, keep to a ring round the target, orbit it, loop round an ellipse, fixed directions, head for the screen centre. Each picks the heading of the next step and tests the box there against the cell map and the other battlers; the stepper that then moves the enemy ($CF:F978) is not matched.

Not matched yet

From NEXT.md: the movers in bank $CF that the movement opcodes hand over to (dispatch at $CF:EFC4), the position-history recorder $CF:ED1D, the hit-number bounce $CF:E781, the colour rotation $CF:E600, the palette stepper BattleAct_StepPalettes and the action helpers in bank $CC. NEXT.md also still lists as open "the code at $C1:ACF0 that fills the action block"; those bytes now lie inside matched routines (BattleAi_ArgToDp0E ends at $C1:ACF1, BattleAi_FillActBlock starts at $C1:ACF2).

The banner before BattleSys_RunAction still says the script interpreter "is not matched yet", and the thread banner calls BattleAct_OpcodeTable unmatched; both are now matched (the second is listed in NEXT.md as stale).