On this page
  1. Related
  2. Games
  3. References
  4. Sources

A lock-on shot finds its own target. In Star Fox 64 holding A charges the shot and sends a search shot ahead; the first enemy it passes near is locked, and the next press of A fires a shot that steers toward it1.

  • Search and homeStar Fox 64 (1997). Holding A charges to 21 and sends search shots; a press at 10 or more, or with an enemy locked, fires a shot that steers toward the lock.1

Games

GameYearHowNote
Star Fox 641997Search and homeholding A charges to 21 and sends search shots; a press at 10 or more, or with an enemy locked, fires a shot that steers toward the lock
Every claim about this mechanic, by game (1)

References

  1. ^ abIn 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).Codesonicdcer/sf64revised
    How we know
    3186bool Player_UpdateLockOn(Player* player) {
    ⋮
    3190    if (gInputHold->button & A_BUTTON) {
    3191        gChargeTimers[player->num]++;
    3192        if (gChargeTimers[player->num] > 21) {
    3193            gChargeTimers[player->num] = 21;
    ⋮
    3201            ((gGameFrameCount % 4) == 0) && Player_CanLockOn(player->num)) {
    3202            if (gChargeTimers[player->num] > 20) {
    ⋮
    3206                            Player_SetupArwingShot(player, &gPlayerShots[i], 0.0f, 0.0f, PLAYERSHOT_LOCK_SEARCH,
    ⋮
    3217        if (gChargeTimers[player->num] != 0) {
    3218            gChargeTimers[player->num]--;
    
    src/engine/fox_play.c lines 3186–3220 at 4235c59 · upstream ↗
    3164bool Player_CanLockOn(s32 playerNum) {
    ⋮
    3169        if ((actor->obj.status == OBJ_ACTIVE) && (actor->lockOnTimers[playerNum] != 0)) {
    3170            return false;
    
    src/engine/fox_play.c lines 3164–3172 at 4235c59 · upstream ↗
    3222    if (gInputPress->button & A_BUTTON) {
    3223        for (i = 0; i < ARRAY_COUNT(gActors); i++) {
    3224            if ((gActors[i].obj.status == OBJ_ACTIVE) && (gActors[i].lockOnTimers[player->num] != 0)) {
    3225                if ((gPlayerShots[14 - player->num].obj.status == SHOT_FREE) ||
    3226                    (gPlayerShots[14 - player->num].obj.id != PLAYERSHOT_LOCK_ON) ||
    3227                    ((gPlayerShots[14 - player->num].obj.id == PLAYERSHOT_LOCK_ON) &&
    3228                     (gPlayerShots[14 - player->num].unk_60 != 0))) {
    ⋮
    3230                        Player_SetupArwingShot(player, &gPlayerShots[14 - player->num], 0.0f, 0.0f, PLAYERSHOT_LOCK_ON,
    ⋮
    3239                break;
    ⋮
    3243        if (gChargeTimers[player->num] > 10) {
    3244            if ((gPlayerShots[14 - player->num].obj.status == SHOT_FREE) ||
    3245                (gPlayerShots[14 - player->num].obj.id != PLAYERSHOT_LOCK_ON) ||
    ⋮
    
    src/engine/fox_play.c lines 3222–3260 at 4235c59 · upstream ↗
    69            gControllerPress[i].button =
    70                (gControllerHold[i].button ^ sPrevController[i].button) & gControllerHold[i].button;
    
    src/sys/sys_joybus.c lines 69–70 at 4235c59 · upstream ↗
    1758    if (gLevelMode == LEVELMODE_ON_RAILS) {
    1759        lockRange = 300.0f;
    1760    } else {
    1761        lockRange = 200.0f;
    ⋮
    1765        if ((actor->obj.status == OBJ_ACTIVE) && (actor->info.targetOffset != 0.0f) &&
    1766            ((actor->lockOnTimers[shot->sourceId] == 0) && (fabsf(shot->obj.pos.x - actor->obj.pos.x) <= lockRange) &&
    1767             (fabsf(shot->obj.pos.y - (actor->obj.pos.y + actor->info.targetOffset)) <= lockRange) &&
    1768             (fabsf(shot->obj.pos.z - actor->obj.pos.z) <= lockRange))) {
    1769            actor->lockOnTimers[shot->sourceId] = 20;
    
    src/engine/fox_beam.c lines 1758–1769 at 4235c59 · upstream ↗
    2848    } else if (this->lockOnTimers[TEAM_ID_FOX] != 0) {
    2849        if (!(gControllerHold[gMainController].button & A_BUTTON)) {
    2850            this->lockOnTimers[TEAM_ID_FOX]--;
    ⋮
    2852        gChargeTimers[0] = 0;
    
    src/engine/fox_enmy.c lines 2848–2852 at 4235c59 · upstream ↗
    2174        if (shot->unk_60 == 0) {
    ⋮
    2177                    (actor->lockOnTimers[shot->sourceId] != 0)) {
    2178                    var_a3 = 1;
    2179                    actor->lockOnTimers[shot->sourceId] = 2;
    2180                    sLockOnPos = actor->obj.pos;
    ⋮
    2188                    var_a3 = 1;
    ⋮
    2193                    } else {
    ⋮
    2200            if (var_a3 != 0) {
    ⋮
    2218                Math_SmoothStepToAngle(&shot->vec_2C.y, sp58, 1.0f, shot->unk_50, 0.0f);
    2219                Math_SmoothStepToAngle(&shot->vec_2C.x, sp5C, 1.0f, shot->unk_50, 0.0f);
    ⋮
    2221            } else {
    2222                shot->unk_60 = 1;
    
    src/engine/fox_beam.c lines 2174–2222 at 4235c59 · upstream ↗

    Checked against the evidence by its author, an AI model, 2026-10-22.

    Revised after a check by a second AI model, 2026-10-22.

    In the claim ledger

Sources

Code