Claim set · data/claims/sf64.yaml

Star Fox 64 (the sonicdcer/sf64 decompilation)

Game: Star Fox 64.

In the US 1.1 version of Star Fox 64, the Arwing rolls when Z or R is pressed twice. The first press sets that button's counter to 10, which Player_UpdateArwingRoll counts down once per player update, and a second press while it is above 0 starts the roll, turning the ship 30 degrees an update for 10 updates and then slowing by 5 degrees an update until it stops. barrelRollAlpha falls by 30 every update while it is above 0, and in each update the roll's count is still above 0 after counting down (9 of its 10) it then gains 100, capped at 180, a net rise of 70 an update. While that value is above 0, the enemy-shot update (Effect_EnemyShot_Update) widens the shot's box around the player by 100 units on each axis, sends a shot inside it off in a new direction, and applies no damage. sf64/barrel-roll

About: Star Fox 64 Barrel roll

Value: 10 player updates (the double-press counter and the roll's full-speed count)

Codesf64: a decompilation of Star Fox 64 (sonicdcer/sf64) · src/engine/fox_play.c#L3397-L3419if (gInputPress->button & Z_TRIG) { player->sfx.bank = 1; if (player->rollInputTimerL != 0) { player->rollState = 1; player->rollTimer = 10; player->rollRate = player->baseRollRate = 30; … player->rollInputTimerL = 10; … if (gInputPress->button & R_TRIG) { … if (player->rollInputTimerR != 0) { …sf64: a decompilation of Star Fox 64 (sonicdcer/sf64) · src/engine/fox_play.c#L4823-L4839if (player->barrelRollAlpha > 0) { player->barrelRollAlpha -= 30; if (player->barrelRollAlpha <= 0) { player->barrelRollAlpha = 0; … if (player->rollInputTimerL != 0) { player->rollInputTimerL--; } if (player->rollInputTimerR != 0) { player->rollInputTimerR--;sf64: a decompilation of Star Fox 64 (sonicdcer/sf64) · src/engine/fox_play.c#L4846-L4870if (player->rollTimer != 0) { player->rollTimer--; } … if (player->rollState != 0) { player->rollInputTimerR = 0; player->rollInputTimerL = 0; … player->zRotBarrelRoll += player->rollRate; if (player->rollTimer == 0) { if (player->rollRate > 0) { player->rollRate -= 5; …sf64: a decompilation of Star Fox 64 (sonicdcer/sf64) · src/engine/fox_play.c#L5534-L5549void Player_UpdateOnRails(Player* player) { … Player_ArwingBank(player); Player_ArwingBoost(player); Player_ArwingBrake(player); Play_dummy_800B41E0(player); Player_UpdateArwingRoll(player);sf64: a decompilation of Star Fox 64 (sonicdcer/sf64) · src/engine/fox_effect.c#L2902-L2926if (gPlayer[0].barrelRollAlpha != 0) { var_fa0 = 100.0f; } … if (fabsf(gPlayer[0].trueZpos - this->obj.pos.z) < (50.0f + var_fa0)) { if ((fabsf(gPlayer[0].pos.x - this->obj.pos.x) < (30.0f + var_fa0)) && (fabsf(gPlayer[0].pos.y - this->obj.pos.y) < (30.0f + var_fa0))) { if ((gPlayer[0].barrelRollAlpha != 0) || (gPlayer[0].meteoWarpTimer != 0)) { this->obj.rot.y = 90.0f; this->obj.rot.x = RAND_FLOAT(360.0f); … gPlayer[0].deflectCount++; …Checked by claude, 2026-10-22 — read both button branches of Player_ArwingBank (the function holding L3397-L3419), Player_UpdateArwingRoll whole, the call order in Player_UpdateOnRails (bank before roll update; Player_Update360 at L5604-L5608 calls them in the same order), and the deflect and damage tests in Effect_EnemyShot_Update; no VERSION_ conditional in either file

Computed, not measured: since the press is read before the counters count down in the same update, a second press up to 9 updates after the first starts a roll, and barrelRollAlpha runs 100, 170, 180 for seven updates, then 150, 120, 90, 60, 30, so it is above 0 for the first 14 updates of a roll. The same value draws the roll's glow (src/engine/fox_display.c#L916). The decompilers' comment on deflectCount reads "counts shots deflected by barrel rolls. never used" (include/sf64player.h#L316). A meteo warp (meteoWarpTimer) also deflects. The Landmaster's roll (Player_UseTankJets) has its own rules and isn't covered here. Player updates haven't been tied to displayed frames.

Cited by: Barrel roll, Star Fox 64

In the US 1.1 version of Star Fox 64, the Arwing's boost and brake share one meter, boostMeter, which counts up from 0 as it is used and stops at 90. Holding the boost button adds 3 a player update on rails and 1.5 in all-range mode (Player_ArwingBoost), and holding brake adds the same amounts (Player_ArwingBrake). Each of the two functions takes back 0.5 a update on rails (0.35 in all-range mode) whenever its own action isn't running, so the meter falls by 1 an update on rails when neither button is held. Reaching 90, holding both buttons with the meter above 0, or letting go of both with the meter above 0 sets boostCooldown, which blocks both actions until the meter is back to 0. sf64/boost-meter

About: Star Fox 64 Boost meter

Value: 90 meter units (full use)

Codesf64: a decompilation of Star Fox 64 (sonicdcer/sf64) · src/engine/fox_play.c#L4951-L4968void Player_ArwingBoost(Player* player) { … if ((player->boostMeter != 0.0f) && (gInputHold->button & gBrakeButton[player->num]) && (gInputHold->button & gBoostButton[player->num])) { player->boostCooldown = true; } … if (gLevelMode == LEVELMODE_ON_RAILS) { boostDepleteRate = 3.0f; boostRecoverRate = 0.5f; … boostDepleteRate = 1.5f; …sf64: a decompilation of Star Fox 64 (sonicdcer/sf64) · src/engine/fox_play.c#L5024-L5031if (!(gInputHold->button & gBrakeButton[player->num]) && !(gInputHold->button & gBoostButton[player->num])) { player->boostCooldown = true; if (player->boostMeter == 0.0f) { player->boostCooldown = false; } } if ((gInputHold->button & gBoostButton[player->num]) && !(gInputHold->button & gBrakeButton[player->num]) && (player->state != PLAYERSTATE_U_TURN) && !player->boostCooldown) {sf64: a decompilation of Star Fox 64 (sonicdcer/sf64) · src/engine/fox_play.c#L5047-L5051player->boostMeter += boostDepleteRate; if (player->boostMeter > 90.0f) { player->boostMeter = 90.0f; player->boostCooldown = true;sf64: a decompilation of Star Fox 64 (sonicdcer/sf64) · src/engine/fox_play.c#L5064-L5069} else { if (player->boostMeter > 0.0f) { player->boostMeter -= boostRecoverRate; if (player->boostMeter <= 0.0f) { player->boostMeter = 0.0f; player->boostCooldown = false;sf64: a decompilation of Star Fox 64 (sonicdcer/sf64) · src/engine/fox_play.c#L5092-L5098if (gLevelMode == LEVELMODE_ON_RAILS) { brakeDepleteRate = 3.0f; brakeRecoverRate = 0.5f; } else { brakeDepleteRate = 1.5f; brakeRecoverRate = 0.35f;sf64: a decompilation of Star Fox 64 (sonicdcer/sf64) · src/engine/fox_play.c#L5130-L5165if ((gInputHold->button & gBrakeButton[player->num]) && !(gInputHold->button & gBoostButton[player->num]) && (player->state != PLAYERSTATE_U_TURN) && !player->boostCooldown) { … player->boostMeter += brakeDepleteRate; if (player->boostMeter > 90.0f) { player->boostCooldown = true; player->boostMeter = 90.0f; … } else if (player->boostMeter > 0.0f) { player->boostMeter -= brakeRecoverRate; if (player->boostMeter <= 0.0f) { player->boostMeter = 0.0f; …sf64: a decompilation of Star Fox 64 (sonicdcer/sf64) · src/engine/fox_play.c#L5601-L5606void Player_Update360(Player* player) { switch (player->form) { case FORM_ARWING: Player_ArwingBank(player); Player_ArwingBoost(player); Player_ArwingBrake(player);Checked by claude, 2026-10-22 — read Player_ArwingBoost and Player_ArwingBrake whole, and the two callers (Player_UpdateOnRails at L5545-L5547 and Player_Update360), which run boost then brake each update; the recovery branches are the else of each function's own action test; no VERSION_ conditional in the file

Computed from these rates, not measured: boosting on rails the meter gains 3 and the brake function's else branch takes 0.5, a net 2.5 an update, so a full meter lasts 36 updates and refills in about 90 (1 an update); in all-range mode the net is 1.15, about 78 updates, and the refill 0.7, about 129. The HUD's gauge reading of the field isn't read. Loops, U-turns, and a meteo warp also set boostCooldown (other lines of src/engine/fox_play.c). The Landmaster has its own function, Player_TankBoostBrake. Player updates haven't been tied to displayed frames.

Cited by: Boost meter, Star Fox 64

In the US 1.1 version of Star Fox 64, holding A counts a charge counter up by 1 a player update to a cap of 21, and an update without A held counts it down by 1 (Player_UpdateLockOn). A fresh press of A, whose own update first adds 1 to the counter, fires the charged shot (PLAYERSHOT_LOCK_ON) if an enemy is locked on or the counter is then above 10 (so 10 or more before the press). Either path fires only if the player's charged-shot slot (gPlayerShots[14 - player->num]) is free, holds a shot of another type, or holds a charged shot that has lost its target (the locked path) or grown past scale 1 (the counter path); a press that fires nothing sets the counter to 0. Once the counter is above 20, while A is held, nothing is locked, and Z and R aren't both held, it launches a search shot every fourth game frame, and the first targetable enemy within 300 units of that shot on each axis (200 in all-range mode) is locked on for a count of 20, which counts down only while A is up. Each update while its target stays locked, the charged shot turns toward it (PlayerShot_UpdateLockOnShot). sf64/lock-on

About: Star Fox 64 Lock-on shot

Value: 10 charge counts before the press (a press from this many fires)

Codesf64: a decompilation of Star Fox 64 (sonicdcer/sf64) · src/engine/fox_play.c#L3186-L3220bool Player_UpdateLockOn(Player* player) { … if (gInputHold->button & A_BUTTON) { gChargeTimers[player->num]++; if (gChargeTimers[player->num] > 21) { gChargeTimers[player->num] = 21; } … ((gGameFrameCount % 4) == 0) && Player_CanLockOn(player->num)) { if (gChargeTimers[player->num] > 20) { … Player_SetupArwingShot(player, &gPlayerShots[i], 0.0f, 0.0f, PLAYERSHOT_LOCK_SEARCH, …sf64: a decompilation of Star Fox 64 (sonicdcer/sf64) · src/engine/fox_play.c#L3164-L3172bool Player_CanLockOn(s32 playerNum) { … if ((actor->obj.status == OBJ_ACTIVE) && (actor->lockOnTimers[playerNum] != 0)) { return false;sf64: a decompilation of Star Fox 64 (sonicdcer/sf64) · src/engine/fox_play.c#L3222-L3260if (gInputPress->button & A_BUTTON) { for (i = 0; i < ARRAY_COUNT(gActors); i++) { if ((gActors[i].obj.status == OBJ_ACTIVE) && (gActors[i].lockOnTimers[player->num] != 0)) { if ((gPlayerShots[14 - player->num].obj.status == SHOT_FREE) || (gPlayerShots[14 - player->num].obj.id != PLAYERSHOT_LOCK_ON) || ((gPlayerShots[14 - player->num].obj.id == PLAYERSHOT_LOCK_ON) && (gPlayerShots[14 - player->num].unk_60 != 0))) { … Player_SetupArwingShot(player, &gPlayerShots[14 - player->num], 0.0f, 0.0f, PLAYERSHOT_LOCK_ON, … break; … …sf64: a decompilation of Star Fox 64 (sonicdcer/sf64) · src/sys/sys_joybus.c#L69-L70gControllerPress[i].button = (gControllerHold[i].button ^ sPrevController[i].button) & gControllerHold[i].button;sf64: a decompilation of Star Fox 64 (sonicdcer/sf64) · src/engine/fox_beam.c#L1758-L1769if (gLevelMode == LEVELMODE_ON_RAILS) { lockRange = 300.0f; } else { lockRange = 200.0f; … if ((actor->obj.status == OBJ_ACTIVE) && (actor->info.targetOffset != 0.0f) && ((actor->lockOnTimers[shot->sourceId] == 0) && (fabsf(shot->obj.pos.x - actor->obj.pos.x) <= lockRange) && (fabsf(shot->obj.pos.y - (actor->obj.pos.y + actor->info.targetOffset)) <= lockRange) && (fabsf(shot->obj.pos.z - actor->obj.pos.z) <= lockRange))) { actor->lockOnTimers[shot->sourceId] = 20;sf64: a decompilation of Star Fox 64 (sonicdcer/sf64) · src/engine/fox_enmy.c#L2848-L2852} else if (this->lockOnTimers[TEAM_ID_FOX] != 0) { if (!(gControllerHold[gMainController].button & A_BUTTON)) { this->lockOnTimers[TEAM_ID_FOX]--; } gChargeTimers[0] = 0;sf64: a decompilation of Star Fox 64 (sonicdcer/sf64) · src/engine/fox_beam.c#L2174-L2222if (shot->unk_60 == 0) { … (actor->lockOnTimers[shot->sourceId] != 0)) { var_a3 = 1; actor->lockOnTimers[shot->sourceId] = 2; sLockOnPos = actor->obj.pos; … if (var_a3 != 0) { … Math_SmoothStepToAngle(&shot->vec_2C.y, sp58, 1.0f, shot->unk_50, 0.0f); Math_SmoothStepToAngle(&shot->vec_2C.x, sp5C, 1.0f, shot->unk_50, 0.0f); … …Checked by claude, 2026-10-22 — read Player_UpdateLockOn whole and Player_CanLockOn, the press mask in Controller_UpdateInput (a press is a button down this read and up the last), PlayerShot_FindLockTarget and PlayerShot_SearchLockOnTarget, the lock countdown in Actor_Update, and PlayerShot_UpdateLockOnShot's steering and its call from the shot update (L2332, outside Aquas); no VERSION_ conditional in the cited files

The shot fires on a new press of A, not on its release: after a full charge the counter falls from 21 by 1 an update with A up and the press's own update adds 1, so by the atlas's arithmetic a press after as many as 11 updates with A up still fires it. A lock zeroes the charge counter each update (src/engine/fox_enmy.c#L2852), and a search shot dies when A is let go (src/engine/fox_beam.c#L1814-L1816). The turn step unk_50 grows by up to 3 an update toward 360 (src/engine/fox_beam.c#L2226). The 300 and 200 limits are in the game's world units. B fires a smart bomb through the same shot type at a locked enemy (src/engine/fox_play.c#L3263-L3290). The search-shot loop carries the decompilers' comment "bug? should be 11". Player updates haven't been tied to displayed frames.

Cited by: Lock-on shot, Star Fox 64

In the US 1.1 version of Star Fox 64, Player_Setup gives the player 255 shield units (less with a Versus handicap), and healing stops at Play_GetMaxShields: 383 with three or more gold rings, otherwise 255. A hit's damage, doubled in Expert mode, drains from the shield at 2 units an update (Player_UpdateShields), and an enemy laser's damage is 20; a hit while gHasShield is set does none. Each Arwing wing starts at 60 health (10 in Expert mode). A hit the code gives direction 1 or 2 takes 20 from the right or left wing, directions 3 and 4 take 15 from one wing at random, and direction 0, which enemy shots pass, takes none; a wing at 0 breaks. While either wing is broken or absent (WINGSTATE_NONE; the test is <= WINGSTATE_BROKEN), Player_Shoot sets the lasers back to single every update. sf64/shields-and-wings

About: Star Fox 64 Locational damage

Value: 255 shield units at the start

Codesf64: a decompilation of Star Fox 64 (sonicdcer/sf64) · src/engine/fox_play.c#L4405-L4436void Player_Setup(Player* playerx) { … player->shields = 255; … if (gVersusMode) { switch (gHandicap[gPlayerNum]) { case 1: player->shields = 191; … player->shields = 127; … if (gExpertMode) { …sf64: a decompilation of Star Fox 64 (sonicdcer/sf64) · src/engine/fox_play.c#L64-L69s32 Play_GetMaxShields(void) { if (gGoldRingCount[0] >= 3) { return 383; } return 255;sf64: a decompilation of Star Fox 64 (sonicdcer/sf64) · src/engine/fox_play.c#L5463-L5484void Player_UpdateShields(Player* player) { if (player->damage > 0) { player->damage -= 2; … player->shields -= 2; … if (player->heal > 0) { … player->shields += 2; if (player->shields >= Play_GetMaxShields()) { player->shields = Play_GetMaxShields();sf64: a decompilation of Star Fox 64 (sonicdcer/sf64) · src/engine/fox_play.c#L867-L895if (gExpertMode) { player->damage = damage * 2; } else { player->damage = damage; } … if (gHasShield[player->num]) { … player->damage = 0;sf64: a decompilation of Star Fox 64 (sonicdcer/sf64) · src/engine/fox_play.c#L916-L978switch (player->hitDirection) { case 0: … case 1: … Player_DamageWings(player, 1, 20); … case 2: … Player_DamageWings(player, 2, 20); … case 3: …sf64: a decompilation of Star Fox 64 (sonicdcer/sf64) · src/engine/fox_play.c#L819-L827void Player_DamageWings(Player* player, s32 side, s32 damage) { if ((player->form == FORM_ARWING) && (gShieldAlpha[player->num] < 1.0f)) { if (side == 1) { … if (player->arwing.rightWingState == WINGSTATE_INTACT) { gRightWingHealth[player->num] -= damage; if (gRightWingHealth[player->num] <= 0) { … player->arwing.rightWingState = WINGSTATE_BROKEN;sf64: a decompilation of Star Fox 64 (sonicdcer/sf64) · src/engine/fox_play.c#L3299-L3305void Player_Shoot(Player* player) { switch (player->form) { case FORM_ARWING: if ((player->arwing.rightWingState <= WINGSTATE_BROKEN) || (player->arwing.leftWingState <= WINGSTATE_BROKEN)) { gLaserStrength[player->num] = LASERS_SINGLE;sf64: a decompilation of Star Fox 64 (sonicdcer/sf64) · include/sf64player.h#L27-L29/* 0 */ WINGSTATE_NONE, /* 1 */ WINGSTATE_BROKEN, /* 2 */ WINGSTATE_INTACTsf64: a decompilation of Star Fox 64 (sonicdcer/sf64) · src/engine/fox_effect.c#L2925-L2926if ((gPlayer[0].barrelRollAlpha == 0) && (gPlayer[0].mercyTimer == 0)) { Player_ApplyDamage(&gPlayer[0], 0, this->info.damage);sf64: a decompilation of Star Fox 64 (sonicdcer/sf64) · src/engine/fox_edata_info.c#L472/* OBJ_EFFECT_ENEMY_LASER */ {(void*) Effect_EnemyLaser_Draw, 1, (ObjectFunc) Effect_EnemyShot_Update, NULL, 100.0f, 0, 0, 20, 0, 0.0f, 0 },sf64: a decompilation of Star Fox 64 (sonicdcer/sf64) · include/sf64object.h#L153-L168typedef struct ObjectInfo { … /* 0x10 */ f32 cullDistance; // z coordinate of something /* 0x14 */ s16 unk_14; // can be -1, 0, 1. governs camera-related behavior in effects (billboarding?) /* 0x16 */ s16 unk_16; // can be 0, 1, 2. affects death behavior? /* 0x18 */ u8 damage;Checked by claude, 2026-10-22 — read Player_Setup's shield and wing lines, Play_GetMaxShields, Player_ApplyDamage whole (the Expert doubling, the gHasShield branch, and all five hit directions), Player_DamageWings for both sides, Player_UpdateShields, the top of Player_Shoot, the enemy-shot damage call, and the enemy laser's ObjectInfo row against the struct's field order (damage is the eighth field); no VERSION_ conditional in the cited files

By the atlas's arithmetic, not measured, 255 units take 13 enemy-laser hits and three side hits break a wing. The shield drains over updates, so a hit's damage shows over several updates. The gHasShield branch also sets gShieldAlpha to 2 (src/engine/fox_play.c#L894), and Player_DamageWings skips the wings while that value is 1 or more. What sets gHasShield and how a broken wing is repaired (on rails, and the all-range timer gAllRangeWingRepairTimer) aren't read. Expert mode and the gold-ring cap are named by the decompilers' variables (gExpertMode, gGoldRingCount).

Cited by: Locational damage, Star Fox 64