Follower NPCs no longer move if the player would be forced back onto the same tile. (#8260)

This commit is contained in:
Bivurnum 2025-11-16 14:15:06 -06:00 committed by GitHub
parent b9baf78ba5
commit 48f15b14a2
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 176 additions and 21 deletions

View File

@ -81,6 +81,13 @@ enum FollowerNPCHandleEscalatorFinishTaskStates
MOVEMENT_FINISH
};
enum FollowerNPCForcedMovementStates
{
FNPC_FORCED_NONE,
FNPC_FORCED_FOLLOW,
FNPC_FORCED_STAY
};
#define FOLLOWER_NPC_FLAG_HAS_RUNNING_FRAMES 0x1
#define FNPC_NONE 0

View File

@ -222,9 +222,9 @@ struct NPCFollower
{
u8 inProgress:1;
u8 warpEnd:1;
u8 createSurfBlob:3;
u8 createSurfBlob:2;
u8 comeOutDoorStairs:2;
u8 forcedMovement:1;
u8 forcedMovement:2;
u8 objId;
u8 currentSprite;
u8 delayedState;

View File

@ -349,7 +349,7 @@ void PlayerStep(u8 direction, u16 newKeys, u16 heldKeys)
DoPlayerAvatarTransition();
if (TryDoMetatileBehaviorForcedMovement() == 0)
{
if (GetFollowerNPCData(FNPC_DATA_FORCED_MOVEMENT) != FALSE)
if (GetFollowerNPCData(FNPC_DATA_FORCED_MOVEMENT) != FNPC_FORCED_NONE)
{
gPlayerAvatar.preventStep = TRUE;
CreateTask(Task_MoveNPCFollowerAfterForcedMovement, 1);
@ -501,7 +501,7 @@ static bool8 DoForcedMovement(u8 direction, void (*moveFunc)(u8))
{
if (collision == COLLISION_LEDGE_JUMP)
{
SetFollowerNPCData(FNPC_DATA_FORCED_MOVEMENT, FALSE);
SetFollowerNPCData(FNPC_DATA_FORCED_MOVEMENT, FNPC_FORCED_NONE);
PlayerJumpLedge(direction);
}
@ -512,8 +512,8 @@ static bool8 DoForcedMovement(u8 direction, void (*moveFunc)(u8))
}
else
{
if (PlayerHasFollowerNPC())
SetFollowerNPCData(FNPC_DATA_FORCED_MOVEMENT, TRUE);
if (PlayerHasFollowerNPC() && GetFollowerNPCData(FNPC_DATA_FORCED_MOVEMENT) != FNPC_FORCED_STAY)
SetFollowerNPCData(FNPC_DATA_FORCED_MOVEMENT, FNPC_FORCED_FOLLOW);
playerAvatar->runningState = MOVING;
moveFunc(direction);

View File

@ -4,6 +4,7 @@
#include "battle.h"
#include "battle_setup.h"
#include "battle_tower.h"
#include "bike.h"
#include "event_data.h"
#include "event_object_movement.h"
#include "event_scripts.h"
@ -52,6 +53,8 @@ static void TurnNPCIntoFollower(u32 localId, u32 followerFlags, u32 setScript, c
static u32 GetFollowerNPCSprite(void);
static bool32 FollowerNPCHasRunningFrames(void);
static bool32 IsStateMovement(u32 state);
static u32 GetNewPlayerMovementDirection(u32 state);
static bool32 IsPlayerForcedOntoSameTile(u8 metatileBehavior, u8 direction);
static u32 GetPlayerFaceToDoorDirection(struct ObjectEvent *player, struct ObjectEvent *follower);
static u32 ReturnFollowerNPCDelayedState(u32 direction);
static void TryUpdateFollowerNPCSpriteUnderwater(void);
@ -351,6 +354,115 @@ static bool32 IsStateMovement(u32 state)
return TRUE;
}
// Because we want the NPC follower's movements to happen simultaneously with the player's,
// we need to set the follower's movement before the player object's movementDirection parameter gets set.
// This function allows us to determine the player's new movement direction before it gets set.
static u32 GetNewPlayerMovementDirection(u32 state)
{
switch (state)
{
case MOVEMENT_ACTION_WALK_SLOW_DOWN:
case MOVEMENT_ACTION_WALK_NORMAL_DOWN:
case MOVEMENT_ACTION_WALK_FAST_DOWN:
case MOVEMENT_ACTION_WALK_FASTER_DOWN:
case MOVEMENT_ACTION_PLAYER_RUN_DOWN:
case MOVEMENT_ACTION_ACRO_WHEELIE_HOP_DOWN:
case MOVEMENT_ACTION_ACRO_WHEELIE_JUMP_DOWN:
case MOVEMENT_ACTION_ACRO_POP_WHEELIE_MOVE_DOWN:
case MOVEMENT_ACTION_ACRO_WHEELIE_MOVE_DOWN:
case MOVEMENT_ACTION_ACRO_END_WHEELIE_MOVE_DOWN:
case MOVEMENT_ACTION_RIDE_WATER_CURRENT_DOWN:
case MOVEMENT_ACTION_JUMP_DOWN:
return DIR_SOUTH;
case MOVEMENT_ACTION_WALK_SLOW_UP:
case MOVEMENT_ACTION_WALK_NORMAL_UP:
case MOVEMENT_ACTION_WALK_FAST_UP:
case MOVEMENT_ACTION_WALK_FASTER_UP:
case MOVEMENT_ACTION_PLAYER_RUN_UP:
case MOVEMENT_ACTION_ACRO_WHEELIE_HOP_UP:
case MOVEMENT_ACTION_ACRO_WHEELIE_JUMP_UP:
case MOVEMENT_ACTION_ACRO_POP_WHEELIE_MOVE_UP:
case MOVEMENT_ACTION_ACRO_WHEELIE_MOVE_UP:
case MOVEMENT_ACTION_ACRO_END_WHEELIE_MOVE_UP:
case MOVEMENT_ACTION_RIDE_WATER_CURRENT_UP:
case MOVEMENT_ACTION_JUMP_UP:
return DIR_NORTH;
case MOVEMENT_ACTION_WALK_SLOW_LEFT:
case MOVEMENT_ACTION_WALK_NORMAL_LEFT:
case MOVEMENT_ACTION_WALK_FAST_LEFT:
case MOVEMENT_ACTION_WALK_FASTER_LEFT:
case MOVEMENT_ACTION_PLAYER_RUN_LEFT:
case MOVEMENT_ACTION_ACRO_WHEELIE_HOP_LEFT:
case MOVEMENT_ACTION_ACRO_WHEELIE_JUMP_LEFT:
case MOVEMENT_ACTION_ACRO_POP_WHEELIE_MOVE_LEFT:
case MOVEMENT_ACTION_ACRO_WHEELIE_MOVE_LEFT:
case MOVEMENT_ACTION_ACRO_END_WHEELIE_MOVE_LEFT:
case MOVEMENT_ACTION_RIDE_WATER_CURRENT_LEFT:
case MOVEMENT_ACTION_JUMP_LEFT:
return DIR_WEST;
case MOVEMENT_ACTION_WALK_SLOW_RIGHT:
case MOVEMENT_ACTION_WALK_NORMAL_RIGHT:
case MOVEMENT_ACTION_WALK_FAST_RIGHT:
case MOVEMENT_ACTION_WALK_FASTER_RIGHT:
case MOVEMENT_ACTION_PLAYER_RUN_RIGHT:
case MOVEMENT_ACTION_ACRO_WHEELIE_HOP_RIGHT:
case MOVEMENT_ACTION_ACRO_WHEELIE_JUMP_RIGHT:
case MOVEMENT_ACTION_ACRO_POP_WHEELIE_MOVE_RIGHT:
case MOVEMENT_ACTION_ACRO_WHEELIE_MOVE_RIGHT:
case MOVEMENT_ACTION_ACRO_END_WHEELIE_MOVE_RIGHT:
case MOVEMENT_ACTION_RIDE_WATER_CURRENT_RIGHT:
case MOVEMENT_ACTION_JUMP_RIGHT:
return DIR_EAST;
default:
return DIR_NONE;
}
}
static bool32 IsPlayerForcedOntoSameTile(u8 metatileBehavior, u8 direction)
{
u8 oppositeDirection = DIR_NONE;
switch (metatileBehavior)
{
case MB_WALK_EAST:
case MB_SLIDE_EAST:
case MB_EASTWARD_CURRENT:
oppositeDirection = DIR_WEST;
break;
case MB_WALK_WEST:
case MB_SLIDE_WEST:
case MB_WESTWARD_CURRENT:
oppositeDirection = DIR_EAST;
break;
case MB_WALK_NORTH:
case MB_SLIDE_NORTH:
case MB_NORTHWARD_CURRENT:
oppositeDirection = DIR_SOUTH;
break;
case MB_WALK_SOUTH:
case MB_SLIDE_SOUTH:
case MB_SOUTHWARD_CURRENT:
case MB_MUDDY_SLOPE:
case MB_WATERFALL:
oppositeDirection = DIR_NORTH;
break;
default:
return FALSE;
}
if (oppositeDirection == direction)
return TRUE;
return FALSE;
}
void GetXYCoordsPlayerMovementDest(u32 direction, s16 *x, s16 *y)
{
*x = gObjectEvents[gPlayerAvatar.objectEventId].currentCoords.x;
*y = gObjectEvents[gPlayerAvatar.objectEventId].currentCoords.y;
MoveCoords(direction, x, y);
}
static u32 GetPlayerFaceToDoorDirection(struct ObjectEvent *player, struct ObjectEvent *follower)
{
s32 delta_x = player->currentCoords.x - follower->currentCoords.x;
@ -760,6 +872,9 @@ u32 DetermineFollowerNPCState(struct ObjectEvent *follower, u32 state, u32 direc
u32 nextBehavior;
u32 noSpecialAnimFrames = (GetFollowerNPCSprite() == GetFollowerNPCData(FNPC_DATA_GFX_ID));
u32 delayedState = GetFollowerNPCData(FNPC_DATA_DELAYED_STATE);
s16 playerDestX, playerDestY;
u32 playerMoveDirection = GetNewPlayerMovementDirection(state);
u32 newPlayerMB;
MoveCoords(direction, &followerX, &followerY);
nextBehavior = MapGridGetMetatileBehaviorAt(followerX, followerY);
@ -769,6 +884,24 @@ u32 DetermineFollowerNPCState(struct ObjectEvent *follower, u32 state, u32 direc
if (!IsStateMovement(state) && delayedState)
return MOVEMENT_ACTION_NONE;
// Follower won't move if player is forced back onto the same tile.
if (GetFollowerNPCData(FNPC_DATA_FORCED_MOVEMENT) == FNPC_FORCED_STAY)
return MOVEMENT_ACTION_NONE;
GetXYCoordsPlayerMovementDest(playerMoveDirection, &playerDestX, &playerDestY);
newPlayerMB = MapGridGetMetatileBehaviorAt(playerDestX, playerDestY);
if (IsPlayerForcedOntoSameTile(newPlayerMB, playerMoveDirection)
&& !(gPlayerAvatar.flags & PLAYER_AVATAR_FLAG_MACH_BIKE && playerMoveDirection == DIR_NORTH && newPlayerMB == MB_MUDDY_SLOPE && GetPlayerSpeed() >= PLAYER_SPEED_FAST))
{
SetFollowerNPCData(FNPC_DATA_FORCED_MOVEMENT, FNPC_FORCED_STAY);
SetFollowerNPCData(FNPC_DATA_DELAYED_STATE, 0);
if (gPlayerAvatar.flags & PLAYER_AVATAR_FLAG_ON_FOOT)
ObjectEventSetHeldMovement(follower, GetFaceDirectionAnimNum(follower->facingDirection));
return MOVEMENT_INVALID;
}
if (IsStateMovement(state) && delayedState)
{
// Lock face direction for Acro side jump.
@ -1588,6 +1721,9 @@ void Task_MoveNPCFollowerAfterForcedMovement(u8 taskId)
struct ObjectEvent *follower = &gObjectEvents[GetFollowerNPCObjectId()];
struct ObjectEvent *player = &gObjectEvents[gPlayerAvatar.objectEventId];
// If follower moved during player's forced momvements.
if (GetFollowerNPCData(FNPC_DATA_FORCED_MOVEMENT) == FNPC_FORCED_FOLLOW)
{
// The NPC will take an extra step and be on the same tile as the player.
if (gTasks[taskId].tState == NPC_INTO_PLAYER && ObjectEventClearHeldMovementIfFinished(player) != 0 && ObjectEventClearHeldMovementIfFinished(follower) != 0)
{
@ -1604,10 +1740,22 @@ void Task_MoveNPCFollowerAfterForcedMovement(u8 taskId)
follower->facingDirectionLocked = FALSE;
HideNPCFollower();
SetFollowerNPCData(FNPC_DATA_WARP_END, FNPC_WARP_REAPPEAR);
SetFollowerNPCData(FNPC_DATA_FORCED_MOVEMENT, FALSE);
SetFollowerNPCData(FNPC_DATA_FORCED_MOVEMENT, FNPC_FORCED_NONE);
gPlayerAvatar.preventStep = FALSE;
DestroyTask(taskId);
}
}
// If player was forced back onto the same tile.
else if (GetFollowerNPCData(FNPC_DATA_FORCED_MOVEMENT) == FNPC_FORCED_STAY)
{
if (ObjectEventClearHeldMovementIfFinished(player) != 0)
{
SetFollowerNPCData(FNPC_DATA_FORCED_MOVEMENT, FNPC_FORCED_NONE);
SetFollowerNPCData(FNPC_DATA_DELAYED_STATE, 0);
gPlayerAvatar.preventStep = FALSE;
DestroyTask(taskId);
}
}
}
#undef tState