Not a flat double

The mechanic is usually described as "an off-class weapon doubles the arm command." Byte-for-byte it is a base cost plus an escalating class penalty, not a flat ×2. Reading the arm command's cost for the same character with different weapons isolates it exactly:

EquipClass vs characterArm (0x0C) cost
Gala + Ra-Seru Clubfavored (club user, club weapon)0x1E (30)
Gala + Nail Gloveoff-class (club user, claw weapon)0x2A (42)
Vahn + Astral Swordalways-double exception0x36 (54)

The other commands (0x0D/0x0E/0x0F) hold a constant 0x1E. So: base 0x1E, +0x0C for an off-class weapon, +0x18 for the Astral Sword. The Astral penalty is twice the off-class penalty - the source of the "double" shorthand - but the off-class case itself is +0x0C over base (×1.4), not ×2.

Where the cost lives

The per-command cost is a runtime field, not a static table row. (Names like DAT_801C9360 / FUN_801D388C are Ghidra's labels for traced data and functions at those RAM addresses; the battle code lives in overlay 0898, a chunk of code the game loads into RAM for battles.)

  • DAT_801C9360 - per-character command-data pointer table (one pointer per active party member), in battle bss. Each entry points into the loaded player battle data (the battle_data block, extraction 863..866).
  • DAT_801C9360[char] - that character's array of per-command struct pointers, indexed by command code (cmd * 4).
  • …[cmd] + 0x74 - the arm width / AP cost byte. Full access: *(u8*)( *(u32*)( *(u32*)(DAT_801C9360 + char*4) + cmd*4 ) + 0x74 ).

The default 4-command display uses command codes DAT_801F4B8C = [0x0C 0x0F 0x0E 0x0D] (overlay 0898 rodata) with an icon-base sibling DAT_801F4B94 = [0x0D 0x10 0x11 0x0C]. Command 0x0C is the arm command whose +0x74 tracks the weapon; the others stay constant.

How the gauge consumes it

The gauge is assembled by FUN_801D388C (the battle action/animation event handler, driven by the battle main dispatcher FUN_801D0748). Its case 9/0x2C reads the cost; case 0xB spends it against the remaining AP at ctx + 0x6DC:

// case 9 / 0x2C  (gauge build)
bVar3 = *(u8*)( *(u32*)(DAT_801C9360[char][cmd]) + 0x74 );  // arm width / AP cost
ctx[slot + 0x14] = bVar3;                                    // per-slot AP cost
gauge_slot.icon_pos = bVar3 - 6;                             // visual width on the bar

// case 0xB  (spend)
if (ctx[0x6DC] < ctx[slot + 0x14]) return;                  // not enough AP
ctx[0x6DC] -= ctx[slot + 0x14];                             // consume the command

A higher +0x74 widens the gauge slot (bVar3 - 6) and drains more of the AP pool, so an off-class arm both looks wider and lets fewer total commands fit.

The same case-9/0xB machinery also deals and spends the Muscle Dome hand: a dome card is one of the four direction commands (0xC..=0xF, the deck table DAT_801f4b8c), its cost is this same +0x74 byte, and the commit debits the same ctx+0x6DC pool - see the minigames page and the Muscle Dome write-up.

The enemy analogue is the AGL action-budget in FUN_801E9FD4: a monster fills its per-turn action queue by rolling candidate moves and paying each move's +0x74 cost out of the per-round AGL gauge (actor[+0x154]) - the same "wider cost = fewer commands" mechanic on the AI side (see battle action).

Status limb gating

A Rot (or similar limb-disable) status grays individual command arrows and refuses their input. The gauge-input arm FUN_801D0748 state 0x50 (overlay_battle_action_801d0748.txt:3311-3360) reads the active actor's +0x16E status halfword; the gray-draw pass and the input gate agree bit-for-bit:

+0x16E bitArrow grayed (draw pos)Blocks command
0x08 (limb 0)LEFT (0xb3 - w/2, 0x42)Square 0x8000 / dir 0
0x10 (limb 1)RIGHT (0xe5 + w/2, 0x42)Circle 0x2000 / dir 3
0x20 (limb 2)UP (0xcc, 0x22) and DOWN (0xcc, 0x62)Triangle 0x1000 / dir 1 and Cross 0x4000 / dir 2
0x1000 (Curse)the whole MAGIC command (FUN_801dbec4(0xf8, 0x42))Magic

With all three limb bits set (0x38) the whole Arm command is skipped and Attack is unusable. Note the retail assignment: Left = 0x08, Right = 0x10, and Up + Down together = 0x20 (a single bit grays two arrows) - not one bit per arrow. Rot rolls exactly one of these three bits (1 << (rand%3 + 3)); see battle formulas § status application.

Weapon classes and favored mapping

"Off-class" is the equipped weapon's class versus the character's favored class. The class is legible from the static item-property records (DAT_80074368 + id*12): the description pointer (+8) is shared per class, and the description carries a Best:<character> token. Universal weapons (equip-mask 0b111) partition cleanly:

Class"Best" characterExample universal weapons
knife / swordVahnSurvival Knife, Battle Knife, Short Sword
clawNoaNail Glove, Crimson Nails, Fighter Claw, Bloody Claw
club / axeGalaSurvival Club, Red Club, Survival Axe, Battle Axe

Character-specific weapons (Ra-Seru Blade / Fangs / Club, etc.) are locked to one owner by the equip-character mask (equip-record +6) and are always favored for that owner. The Astral Sword (0xBA) has its own description pointer, matches no character, and takes the maximum penalty. Favored mapping: knife/sword → Vahn, claw → Noa, club/axe → Gala.

Execution path

Once a combo is committed it is replayed by the Arms execution resolver FUN_801EC3E4 (overlay 0898), called from SCUS_942.54 at 0x800478A0 (jal 0x801EC3E4) - the execution driver is the static side, which is why the resolver has no caller inside the overlay. It advances the input cursor (actor + 0x1F4) one step per recorded command and dispatches per-command sub-handlers through PTR_801CF4B4[(actor + 0x1D9) - 0xC]. Those sub-handlers read the equipped weapon again to fold it into the damage calculation - a read distinct from the gauge-build cost above.

The execution-time weapon fold

The dispatch is bounded at six arms ((command - 0x0C) < 6, commands 0x0C..=0x11), and the head admission gate is a different band read from a different place: it tests the caller's command-record byte with (cmd - 0x0C) < 0x14 (0x0C..=0x1F). A command in 0x12..=0x1F is admitted and then folds nothing.

Each arm resolves one or more of the character record's five equipment slots (+0x196..+0x19B) through the two-hop lookup item property record DAT_80074368 + id*0xC byte +1 → equipment stat row DAT_80074F68 + row*8 byte +1 (the attack bonus), and adds it into the actor's ATK working halfword +0x158:

commandequipment slotsfold into +0x158
0x0C2atk[2] >> 1
0x0D3atk[3] >> 1
0x0E / 0x0F4atk[4] >> 1
0x10nonenothing
0x110,1,2,3,4(sum of all five) >> 1

0x0E and 0x0F share a jump-table arm, and 0x10's slot is the same address the bounds check bails to - a live table entry that folds nothing. Retail applies no empty-slot test and no kind == 1 item-class guard here.

This is the counterpart to the battle-load asymmetry: the seeder FUN_80053CB8 folds the equipment table's UDF / LDF / SPD bytes and folds neither INT nor ATK, so a weapon's attack bonus never reaches the actor's ATK base (+0x15A). It reaches ATK working here instead, per committed command - the seeder's omission is correct, not a gap.

Who writes the cost

The cost is not computed by a runtime favored-class comparison. It is written once at battle load (the game_mode 0x14 → 0x15 transition) as a verbatim copy out of the assembled battle-character buffer:

  • The writer is FUN_800557B8 (the per-command-struct copy routine in SCUS_942.54): a fixed 43-word block copy from source a1 to the runtime struct a0 (lw v0,(a1)sw v0,(a0); the cost word at struct +0x74 lands inside that block). There is no arithmetic on the cost value between load and store - confirmed by a live write-watch on the field through a field→battle transition, where the only write fires here at pc = 0x80055810.
  • It is called from the battle character-assembly chain (FUN_80052770 … the call site at 0x80053330), which splices the equipped item's section into the per-character battle buffer.

So the arm cost originates in the equipped weapon's section of the per-character player battle file (extraction 863..866) and is carried verbatim into the runtime struct. The "off-class penalty" is therefore per-(character, weapon) data baked into those files - favored-class weapons carry a low arm cost in that character's file and off-class weapons a higher one - not a class comparison the engine performs. The same weapon yields different costs in different characters' files (a claw is cheap in Noa's file, expensive in Gala's).

Disc location

Inside the player battle file, the cost sits in the weapon's section, reached through the section's swing-action record:

section (decoded)
  +0x04  u32 swing_rec_a   ; offset (within the section) to the swing/arm command record
  ...
  swing_rec_a + 0x74       ; u8 arm cost  ← the weapon-specialty byte

The descriptor table keys sections by equippable item id, so each weapon has its own section and swing record. Decoding the four player files (863..866; asset battle-data-pack <file> --out) and reading section[+0x04] + 0x74 per weapon gives a byte-exact picture - favored-class weapons carry 0x1E (30), off-class weapons higher costs that scale with class distance:

character (file)favored → 0x1Eoff-class → 0x2Afar off-class → 0x36
Vahn (863)blade / knife / sword / fistclaw, axe-
Noa (864)claw / feral / fang (+ knife)sword / bladeclub / axe
Gala (865)club / axe / maceclaw, knife-

Cross-checked against live RAM: Gala + Nail Glove reads 0x2A, Gala + Ra-Seru Club reads 0x1E - matching that file's 0x28 and 0x21 sections. The cost lives inside the section's LZS-compressed stream, so an editor decompresses the section, rewrites the byte at swing_rec_a + 0x74, recompresses, and writes back within the slot footprint.

Confidence

  • Confirmed (live-pinned + byte-validated against the disc) the cost field DAT_801C9360[char][0x0C] + 0x74, its values, the case-9 read / case-0xB spend in FUN_801D388C, the resolver call site, the writer (FUN_800557B8, verbatim copy from the LZS-decoded equipment section at battle load), and the disc location (section[+0x04] swing record +0x74 in the player battle files, tabulated above).
  • Inferred that command 0x0C is "the arm" (it is the only command whose cost tracks the weapon).

The mechanic is therefore a fully editable data table: rewrite a character's favored-class arm costs up / another class's down to reassign their specialty - which is exactly what the randomizer's --weapon-specialty does, permuting the three favored families among the characters by rewriting these bytes in place.

See also