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:

  • 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.

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 (e.g. 0x801ECC00: weapon id → item subtype DAT_80074369 → equip record DAT_80074F68) to fold it into the damage / effect calculation - a read distinct from the gauge-build cost above.

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 three player files (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