1768 lines
61 KiB
C
1768 lines
61 KiB
C
#include "global.h"
|
|
#include "follower_npc.h"
|
|
#include "follower_npc_alternate_sprites.h"
|
|
#include "battle.h"
|
|
#include "battle_setup.h"
|
|
#include "battle_tower.h"
|
|
#include "event_data.h"
|
|
#include "event_object_movement.h"
|
|
#include "event_scripts.h"
|
|
#include "field_door.h"
|
|
#include "field_effect.h"
|
|
#include "field_effect_helpers.h"
|
|
#include "field_player_avatar.h"
|
|
#include "field_control_avatar.h"
|
|
#include "field_screen_effect.h"
|
|
#include "field_weather.h"
|
|
#include "fieldmap.h"
|
|
#include "fldeff_misc.h"
|
|
#include "frontier_util.h"
|
|
#include "item.h"
|
|
#include "load_save.h"
|
|
#include "metatile_behavior.h"
|
|
#include "overworld.h"
|
|
#include "party_menu.h"
|
|
#include "script.h"
|
|
#include "script_movement.h"
|
|
#include "script_pokemon_util.h"
|
|
#include "sound.h"
|
|
#include "task.h"
|
|
#include "trig.h"
|
|
#include "constants/event_object_movement.h"
|
|
#include "constants/field_effects.h"
|
|
#include "constants/frontier_util.h"
|
|
#include "constants/map_types.h"
|
|
#include "constants/metatile_behaviors.h"
|
|
#include "constants/songs.h"
|
|
|
|
/*
|
|
* Known Issues:
|
|
* -follower gets messed up if you go into a map with a maximum number of event objects
|
|
* -inherits incorrect palette, may get directionally confused
|
|
*/
|
|
|
|
// Task data
|
|
#define tState data[0]
|
|
#define tDoorX data[2]
|
|
#define tDoorY data[3]
|
|
|
|
static void SetFollowerNPCScriptPointer(const u8 *script);
|
|
static void PlayerLogCoordinates(struct ObjectEvent *player);
|
|
static void TurnNPCIntoFollower(u32 localId, u32 followerFlags, u32 setScript, const u8 *script);
|
|
static u32 GetFollowerNPCSprite(void);
|
|
static bool32 FollowerNPCHasRunningFrames(void);
|
|
static bool32 IsStateMovement(u32 state);
|
|
static u32 GetPlayerFaceToDoorDirection(struct ObjectEvent *player, struct ObjectEvent *follower);
|
|
static u32 ReturnFollowerNPCDelayedState(u32 direction);
|
|
static void TryUpdateFollowerNPCSpriteUnderwater(void);
|
|
static void SetSurfJump(void);
|
|
static void SetUpSurfBlobFieldEffect(struct ObjectEvent *npc);
|
|
static void SetSurfDismount(void);
|
|
static void Task_BindSurfBlobToFollowerNPC(u8 taskId);
|
|
static void Task_FinishSurfDismount(u8 taskId);
|
|
static void Task_ReallowPlayerMovement(u8 taskId);
|
|
static void Task_FollowerNPCOutOfDoor(u8 taskId);
|
|
static void Task_FollowerNPCHandleEscalator(u8 taskId);
|
|
static void Task_FollowerNPCHandleEscalatorFinish(u8 taskId);
|
|
static void CalculateFollowerNPCEscalatorTrajectoryUp(struct Task *task);
|
|
static void CalculateFollowerNPCEscalatorTrajectoryDown(struct Task *task);
|
|
|
|
void SetFollowerNPCData(enum FollowerNPCDataTypes type, u32 value)
|
|
{
|
|
#if FNPC_ENABLE_NPC_FOLLOWERS
|
|
switch (type)
|
|
{
|
|
case FNPC_DATA_IN_PROGRESS:
|
|
gSaveBlock3Ptr->NPCfollower.inProgress = value;
|
|
break;
|
|
case FNPC_DATA_WARP_END:
|
|
gSaveBlock3Ptr->NPCfollower.warpEnd = value;
|
|
break;
|
|
case FNPC_DATA_SURF_BLOB:
|
|
gSaveBlock3Ptr->NPCfollower.createSurfBlob = value;
|
|
break;
|
|
case FNPC_DATA_COME_OUT_DOOR:
|
|
gSaveBlock3Ptr->NPCfollower.comeOutDoorStairs = value;
|
|
break;
|
|
case FNPC_DATA_OBJ_ID:
|
|
gSaveBlock3Ptr->NPCfollower.objId = value;
|
|
break;
|
|
case FNPC_DATA_CURRENT_SPRITE:
|
|
gSaveBlock3Ptr->NPCfollower.currentSprite = value;
|
|
break;
|
|
case FNPC_DATA_DELAYED_STATE:
|
|
gSaveBlock3Ptr->NPCfollower.delayedState = value;
|
|
break;
|
|
case FNPC_DATA_EVENT_FLAG:
|
|
gSaveBlock3Ptr->NPCfollower.flag = value;
|
|
break;
|
|
case FNPC_DATA_GFX_ID:
|
|
gSaveBlock3Ptr->NPCfollower.graphicsId = value;
|
|
break;
|
|
case FNPC_DATA_FOLLOWER_FLAGS:
|
|
gSaveBlock3Ptr->NPCfollower.flags = value;
|
|
break;
|
|
case FNPC_DATA_BATTLE_PARTNER:
|
|
gSaveBlock3Ptr->NPCfollower.battlePartner = value;
|
|
break;
|
|
}
|
|
#endif
|
|
}
|
|
|
|
static void SetFollowerNPCScriptPointer(const u8 *script)
|
|
{
|
|
#if FNPC_ENABLE_NPC_FOLLOWERS
|
|
gSaveBlock3Ptr->NPCfollower.script = script;
|
|
#endif
|
|
}
|
|
|
|
static void PlayerLogCoordinates(struct ObjectEvent *player)
|
|
{
|
|
#if FNPC_ENABLE_NPC_FOLLOWERS
|
|
gSaveBlock3Ptr->NPCfollower.log.x = player->currentCoords.x;
|
|
gSaveBlock3Ptr->NPCfollower.log.y = player->currentCoords.y;
|
|
#endif
|
|
}
|
|
|
|
const u8 *GetFollowerNPCScriptPointer(void)
|
|
{
|
|
#if FNPC_ENABLE_NPC_FOLLOWERS
|
|
if (PlayerHasFollowerNPC())
|
|
return gSaveBlock3Ptr->NPCfollower.script;
|
|
|
|
#endif
|
|
return NULL;
|
|
}
|
|
|
|
u32 GetFollowerNPCData(enum FollowerNPCDataTypes type)
|
|
{
|
|
#if FNPC_ENABLE_NPC_FOLLOWERS
|
|
switch (type)
|
|
{
|
|
case FNPC_DATA_IN_PROGRESS:
|
|
return gSaveBlock3Ptr->NPCfollower.inProgress;
|
|
case FNPC_DATA_WARP_END:
|
|
return gSaveBlock3Ptr->NPCfollower.warpEnd;
|
|
case FNPC_DATA_SURF_BLOB:
|
|
return gSaveBlock3Ptr->NPCfollower.createSurfBlob;
|
|
case FNPC_DATA_COME_OUT_DOOR:
|
|
return gSaveBlock3Ptr->NPCfollower.comeOutDoorStairs;
|
|
case FNPC_DATA_OBJ_ID:
|
|
return gSaveBlock3Ptr->NPCfollower.objId;
|
|
case FNPC_DATA_CURRENT_SPRITE:
|
|
return gSaveBlock3Ptr->NPCfollower.currentSprite;
|
|
case FNPC_DATA_DELAYED_STATE:
|
|
return gSaveBlock3Ptr->NPCfollower.delayedState;
|
|
case FNPC_DATA_EVENT_FLAG:
|
|
return gSaveBlock3Ptr->NPCfollower.flag;
|
|
case FNPC_DATA_GFX_ID:
|
|
return gSaveBlock3Ptr->NPCfollower.graphicsId;
|
|
case FNPC_DATA_FOLLOWER_FLAGS:
|
|
return gSaveBlock3Ptr->NPCfollower.flags;
|
|
case FNPC_DATA_BATTLE_PARTNER:
|
|
return gSaveBlock3Ptr->NPCfollower.battlePartner;
|
|
}
|
|
#endif
|
|
return 0;
|
|
}
|
|
|
|
void ClearFollowerNPCData(void)
|
|
{
|
|
#if FNPC_ENABLE_NPC_FOLLOWERS
|
|
memset(&gSaveBlock3Ptr->NPCfollower, 0, sizeof(gSaveBlock3Ptr->NPCfollower));
|
|
#endif
|
|
}
|
|
|
|
static void TurnNPCIntoFollower(u32 localId, u32 followerFlags, u32 setScript, const u8 *scriptPtr)
|
|
{
|
|
struct ObjectEventTemplate npc;
|
|
struct ObjectEvent *follower;
|
|
u32 eventObjId = GetObjectEventIdByLocalId(localId);
|
|
u32 npcX = gObjectEvents[eventObjId].currentCoords.x;
|
|
u32 npcY = gObjectEvents[eventObjId].currentCoords.y;
|
|
const u8 *script;
|
|
u32 flag;
|
|
|
|
flag = GetObjectEventFlagIdByLocalIdAndMap(localId, gSaveBlock1Ptr->location.mapNum, gSaveBlock1Ptr->location.mapGroup);
|
|
// If the object does not have an event flag, don't create follower.
|
|
if (flag == 0)
|
|
return;
|
|
|
|
if (setScript == TRUE)
|
|
// Set the custom script.
|
|
script = scriptPtr;
|
|
else
|
|
// Use the object's original script.
|
|
script = GetObjectEventScriptPointerByObjectEventId(eventObjId);
|
|
|
|
RemoveObjectEvent(&gObjectEvents[eventObjId]);
|
|
FlagSet(flag);
|
|
|
|
npc = *GetObjectEventTemplateByLocalIdAndMap(localId, gSaveBlock1Ptr->location.mapNum, gSaveBlock1Ptr->location.mapGroup);
|
|
npc.movementType = 0;
|
|
npc.script = script;
|
|
npc.localId = OBJ_EVENT_ID_NPC_FOLLOWER;
|
|
SetFollowerNPCData(FNPC_DATA_OBJ_ID, TrySpawnObjectEventTemplate(&npc, gSaveBlock1Ptr->location.mapNum, gSaveBlock1Ptr->location.mapGroup, npcX, npcY));
|
|
follower = &gObjectEvents[GetFollowerNPCData(FNPC_DATA_OBJ_ID)];
|
|
MoveObjectEventToMapCoords(follower, npcX, npcY);
|
|
ObjectEventTurn(follower, gObjectEvents[eventObjId].facingDirection);
|
|
follower->movementType = MOVEMENT_TYPE_NONE;
|
|
gSprites[follower->spriteId].callback = MovementType_None;
|
|
|
|
SetFollowerNPCData(FNPC_DATA_IN_PROGRESS, TRUE);
|
|
SetFollowerNPCData(FNPC_DATA_GFX_ID, follower->graphicsId);
|
|
SetFollowerNPCScriptPointer(script);
|
|
SetFollowerNPCData(FNPC_DATA_EVENT_FLAG, flag);
|
|
SetFollowerNPCData(FNPC_DATA_SURF_BLOB, FNPC_SURF_BLOB_NONE);
|
|
SetFollowerNPCData(FNPC_DATA_COME_OUT_DOOR, FNPC_DOOR_NONE);
|
|
if (FollowerNPCHasRunningFrames())
|
|
followerFlags |= FOLLOWER_NPC_FLAG_HAS_RUNNING_FRAMES;
|
|
|
|
SetFollowerNPCData(FNPC_DATA_FOLLOWER_FLAGS, followerFlags);
|
|
|
|
// If the player is biking and the follower flags prohibit biking, force the player to dismount the bike.
|
|
if (!CheckFollowerNPCFlag(FOLLOWER_NPC_FLAG_CAN_BIKE)
|
|
&& TestPlayerAvatarFlags(PLAYER_AVATAR_FLAG_BIKE))
|
|
SetPlayerAvatarTransitionFlags(PLAYER_AVATAR_FLAG_ON_FOOT);
|
|
|
|
// Set the follower sprite to match the player state.
|
|
if (!TestPlayerAvatarFlags(PLAYER_AVATAR_FLAG_ON_FOOT))
|
|
FollowerNPC_HandleSprite();
|
|
}
|
|
|
|
static u32 GetFollowerNPCSprite(void)
|
|
{
|
|
u32 i;
|
|
|
|
switch (GetFollowerNPCData(FNPC_DATA_CURRENT_SPRITE))
|
|
{
|
|
case FOLLOWER_NPC_SPRITE_INDEX_MACH_BIKE:
|
|
for (i = 0; i < NELEMS(gFollowerNPCAlternateSprites); i++)
|
|
if (gFollowerNPCAlternateSprites[i].normalId == GetFollowerNPCData(FNPC_DATA_GFX_ID))
|
|
return gFollowerNPCAlternateSprites[i].machBikeId;
|
|
break;
|
|
case FOLLOWER_NPC_SPRITE_INDEX_ACRO_BIKE:
|
|
for (i = 0; i < NELEMS(gFollowerNPCAlternateSprites); i++)
|
|
if (gFollowerNPCAlternateSprites[i].normalId == GetFollowerNPCData(FNPC_DATA_GFX_ID))
|
|
return gFollowerNPCAlternateSprites[i].acroBikeId;
|
|
break;
|
|
case FOLLOWER_NPC_SPRITE_INDEX_SURF:
|
|
for (i = 0; i < NELEMS(gFollowerNPCAlternateSprites); i++)
|
|
if (gFollowerNPCAlternateSprites[i].normalId == GetFollowerNPCData(FNPC_DATA_GFX_ID))
|
|
return gFollowerNPCAlternateSprites[i].surfId;
|
|
break;
|
|
case FOLLOWER_NPC_SPRITE_INDEX_UNDERWATER:
|
|
for (i = 0; i < NELEMS(gFollowerNPCAlternateSprites); i++)
|
|
if (gFollowerNPCAlternateSprites[i].normalId == GetFollowerNPCData(FNPC_DATA_GFX_ID))
|
|
return gFollowerNPCAlternateSprites[i].underwaterId;
|
|
break;
|
|
}
|
|
|
|
return GetFollowerNPCData(FNPC_DATA_GFX_ID);
|
|
}
|
|
|
|
static bool32 FollowerNPCHasRunningFrames(void)
|
|
{
|
|
for (u32 i = 0; i < NELEMS(gFollowerNPCAlternateSprites); i++)
|
|
{
|
|
if (gFollowerNPCAlternateSprites[i].normalId == GetFollowerNPCData(FNPC_DATA_GFX_ID)
|
|
&& gFollowerNPCAlternateSprites[i].hasRunningFrames == TRUE)
|
|
return TRUE;
|
|
}
|
|
|
|
return FALSE;
|
|
}
|
|
|
|
static bool32 IsStateMovement(u32 state)
|
|
{
|
|
switch (state)
|
|
{
|
|
case MOVEMENT_ACTION_FACE_DOWN:
|
|
case MOVEMENT_ACTION_FACE_UP:
|
|
case MOVEMENT_ACTION_FACE_LEFT:
|
|
case MOVEMENT_ACTION_FACE_RIGHT:
|
|
case MOVEMENT_ACTION_DELAY_1:
|
|
case MOVEMENT_ACTION_DELAY_2:
|
|
case MOVEMENT_ACTION_DELAY_4:
|
|
case MOVEMENT_ACTION_DELAY_8:
|
|
case MOVEMENT_ACTION_DELAY_16:
|
|
case MOVEMENT_ACTION_FACE_PLAYER:
|
|
case MOVEMENT_ACTION_FACE_AWAY_PLAYER:
|
|
case MOVEMENT_ACTION_LOCK_FACING_DIRECTION:
|
|
case MOVEMENT_ACTION_UNLOCK_FACING_DIRECTION:
|
|
case MOVEMENT_ACTION_SET_INVISIBLE:
|
|
case MOVEMENT_ACTION_SET_VISIBLE:
|
|
case MOVEMENT_ACTION_EMOTE_EXCLAMATION_MARK:
|
|
case MOVEMENT_ACTION_EMOTE_QUESTION_MARK:
|
|
case MOVEMENT_ACTION_EMOTE_HEART:
|
|
case MOVEMENT_ACTION_WALK_IN_PLACE_SLOW_DOWN:
|
|
case MOVEMENT_ACTION_WALK_IN_PLACE_SLOW_UP:
|
|
case MOVEMENT_ACTION_WALK_IN_PLACE_SLOW_LEFT:
|
|
case MOVEMENT_ACTION_WALK_IN_PLACE_SLOW_RIGHT:
|
|
case MOVEMENT_ACTION_WALK_IN_PLACE_NORMAL_DOWN:
|
|
case MOVEMENT_ACTION_WALK_IN_PLACE_NORMAL_UP:
|
|
case MOVEMENT_ACTION_WALK_IN_PLACE_NORMAL_LEFT:
|
|
case MOVEMENT_ACTION_WALK_IN_PLACE_NORMAL_RIGHT:
|
|
case MOVEMENT_ACTION_WALK_IN_PLACE_FAST_DOWN:
|
|
case MOVEMENT_ACTION_WALK_IN_PLACE_FAST_UP:
|
|
case MOVEMENT_ACTION_WALK_IN_PLACE_FAST_LEFT:
|
|
case MOVEMENT_ACTION_WALK_IN_PLACE_FAST_RIGHT:
|
|
case MOVEMENT_ACTION_WALK_IN_PLACE_FASTER_DOWN:
|
|
case MOVEMENT_ACTION_WALK_IN_PLACE_FASTER_UP:
|
|
case MOVEMENT_ACTION_WALK_IN_PLACE_FASTER_LEFT:
|
|
case MOVEMENT_ACTION_WALK_IN_PLACE_FASTER_RIGHT:
|
|
case MOVEMENT_ACTION_JUMP_IN_PLACE_DOWN:
|
|
case MOVEMENT_ACTION_JUMP_IN_PLACE_UP:
|
|
case MOVEMENT_ACTION_JUMP_IN_PLACE_LEFT:
|
|
case MOVEMENT_ACTION_JUMP_IN_PLACE_RIGHT:
|
|
case MOVEMENT_ACTION_JUMP_IN_PLACE_DOWN_UP:
|
|
case MOVEMENT_ACTION_JUMP_IN_PLACE_UP_DOWN:
|
|
case MOVEMENT_ACTION_JUMP_IN_PLACE_LEFT_RIGHT:
|
|
case MOVEMENT_ACTION_JUMP_IN_PLACE_RIGHT_LEFT:
|
|
case MOVEMENT_ACTION_ACRO_WHEELIE_FACE_DOWN:
|
|
case MOVEMENT_ACTION_ACRO_WHEELIE_FACE_UP:
|
|
case MOVEMENT_ACTION_ACRO_WHEELIE_FACE_RIGHT:
|
|
case MOVEMENT_ACTION_ACRO_WHEELIE_FACE_LEFT:
|
|
case MOVEMENT_ACTION_ACRO_POP_WHEELIE_DOWN:
|
|
case MOVEMENT_ACTION_ACRO_POP_WHEELIE_UP:
|
|
case MOVEMENT_ACTION_ACRO_POP_WHEELIE_RIGHT:
|
|
case MOVEMENT_ACTION_ACRO_POP_WHEELIE_LEFT:
|
|
case MOVEMENT_ACTION_ACRO_END_WHEELIE_FACE_DOWN:
|
|
case MOVEMENT_ACTION_ACRO_END_WHEELIE_FACE_UP:
|
|
case MOVEMENT_ACTION_ACRO_END_WHEELIE_FACE_RIGHT:
|
|
case MOVEMENT_ACTION_ACRO_END_WHEELIE_FACE_LEFT:
|
|
case MOVEMENT_ACTION_ACRO_WHEELIE_HOP_FACE_DOWN:
|
|
case MOVEMENT_ACTION_ACRO_WHEELIE_HOP_FACE_UP:
|
|
case MOVEMENT_ACTION_ACRO_WHEELIE_HOP_FACE_RIGHT:
|
|
case MOVEMENT_ACTION_ACRO_WHEELIE_HOP_FACE_LEFT:
|
|
case MOVEMENT_ACTION_ACRO_WHEELIE_IN_PLACE_DOWN:
|
|
case MOVEMENT_ACTION_ACRO_WHEELIE_IN_PLACE_UP:
|
|
case MOVEMENT_ACTION_ACRO_WHEELIE_IN_PLACE_RIGHT:
|
|
case MOVEMENT_ACTION_ACRO_WHEELIE_IN_PLACE_LEFT:
|
|
return FALSE;
|
|
}
|
|
|
|
return TRUE;
|
|
}
|
|
|
|
static u32 GetPlayerFaceToDoorDirection(struct ObjectEvent *player, struct ObjectEvent *follower)
|
|
{
|
|
s32 delta_x = player->currentCoords.x - follower->currentCoords.x;
|
|
|
|
if (delta_x < 0)
|
|
return DIR_EAST;
|
|
else if (delta_x > 0)
|
|
return DIR_WEST;
|
|
|
|
return DIR_NORTH;
|
|
}
|
|
|
|
static u32 ReturnFollowerNPCDelayedState(u32 direction)
|
|
{
|
|
u32 newState = GetFollowerNPCData(FNPC_DATA_DELAYED_STATE);
|
|
SetFollowerNPCData(FNPC_DATA_DELAYED_STATE, 0);
|
|
|
|
return newState + direction;
|
|
}
|
|
|
|
static void TryUpdateFollowerNPCSpriteUnderwater(void)
|
|
{
|
|
if (gMapHeader.mapType == MAP_TYPE_UNDERWATER)
|
|
{
|
|
struct ObjectEvent *follower = &gObjectEvents[GetFollowerNPCObjectId()];
|
|
SetFollowerNPCSprite(FOLLOWER_NPC_SPRITE_INDEX_UNDERWATER);
|
|
|
|
follower = &gObjectEvents[GetFollowerNPCObjectId()];
|
|
follower->fieldEffectSpriteId = StartUnderwaterSurfBlobBobbing(follower->spriteId);
|
|
}
|
|
}
|
|
|
|
static void SetSurfJump(void)
|
|
{
|
|
struct ObjectEvent *follower = &gObjectEvents[GetFollowerNPCObjectId()];
|
|
u32 direction;
|
|
u32 jumpState;
|
|
|
|
ObjectEventClearHeldMovement(follower);
|
|
|
|
// Jump animation according to direction.
|
|
direction = DetermineFollowerNPCDirection(&gObjectEvents[gPlayerAvatar.objectEventId], follower);
|
|
jumpState = GetJumpMovementAction(direction);
|
|
SetUpSurfBlobFieldEffect(follower);
|
|
|
|
// Adjust surf head spawn location infront of follower.
|
|
switch (direction)
|
|
{
|
|
case DIR_SOUTH:
|
|
gFieldEffectArguments[1]++; // effect_y
|
|
break;
|
|
case DIR_NORTH:
|
|
gFieldEffectArguments[1]--;
|
|
break;
|
|
case DIR_WEST:
|
|
gFieldEffectArguments[0]--; // effect_x
|
|
break;
|
|
default: // DIR_EAST
|
|
gFieldEffectArguments[0]++;
|
|
};
|
|
|
|
// Execute, store sprite ID in fieldEffectSpriteId and bind surf blob.
|
|
follower->fieldEffectSpriteId = FieldEffectStart(FLDEFF_SURF_BLOB);
|
|
CreateTask(Task_BindSurfBlobToFollowerNPC, 0x1);
|
|
SetFollowerNPCSprite(FOLLOWER_NPC_SPRITE_INDEX_SURF);
|
|
|
|
follower = &gObjectEvents[GetFollowerNPCObjectId()];
|
|
ObjectEventSetHeldMovement(follower, jumpState);
|
|
}
|
|
|
|
static void SetUpSurfBlobFieldEffect(struct ObjectEvent *npc)
|
|
{
|
|
// Set up gFieldEffectArguments for execution.
|
|
gFieldEffectArguments[0] = npc->currentCoords.x; // effect_x
|
|
gFieldEffectArguments[1] = npc->currentCoords.y; // effect_y
|
|
gFieldEffectArguments[2] = GetFollowerNPCData(FNPC_DATA_OBJ_ID); // objId
|
|
}
|
|
|
|
#define tSpriteId data[0]
|
|
|
|
static void SetSurfDismount(void)
|
|
{
|
|
struct ObjectEvent *follower = &gObjectEvents[GetFollowerNPCObjectId()];
|
|
u32 direction;
|
|
u32 jumpState;
|
|
u32 task;
|
|
|
|
ObjectEventClearHeldMovement(follower);
|
|
|
|
// Jump animation according to direction
|
|
direction = DetermineFollowerNPCDirection(&gObjectEvents[gPlayerAvatar.objectEventId], follower);
|
|
jumpState = GetJumpMovementAction(direction);
|
|
|
|
// Unbind and destroy Surf Blob
|
|
task = CreateTask(Task_FinishSurfDismount, 1);
|
|
gTasks[task].tSpriteId = follower->fieldEffectSpriteId;
|
|
SetSurfBlob_BobState(follower->fieldEffectSpriteId, 2);
|
|
follower->fieldEffectSpriteId = 0;
|
|
FollowerNPC_HandleSprite();
|
|
|
|
follower = &gObjectEvents[GetFollowerNPCObjectId()];
|
|
ObjectEventSetHeldMovement(follower, jumpState);
|
|
}
|
|
|
|
static void Task_BindSurfBlobToFollowerNPC(u8 taskId)
|
|
{
|
|
struct ObjectEvent *npc = &gObjectEvents[GetFollowerNPCObjectId()];
|
|
// Wait for the jump animation.
|
|
bool32 animStatus = ObjectEventClearHeldMovementIfFinished(npc);
|
|
if (animStatus == 0)
|
|
return;
|
|
|
|
// Bind the blob to the follower.
|
|
SetSurfBlob_BobState(npc->fieldEffectSpriteId, 0x1);
|
|
UnfreezeObjectEvents();
|
|
DestroyTask(taskId);
|
|
gPlayerAvatar.preventStep = FALSE;
|
|
return;
|
|
}
|
|
|
|
static void Task_FinishSurfDismount(u8 taskId)
|
|
{
|
|
struct ObjectEvent *npc = &gObjectEvents[GetFollowerNPCObjectId()];
|
|
// Wait for the animation to finish.
|
|
bool32 animStatus = ObjectEventClearHeldMovementIfFinished(npc);
|
|
|
|
if (animStatus == 0)
|
|
{
|
|
// Temporarily stop running.
|
|
if (TestPlayerAvatarFlags(PLAYER_AVATAR_FLAG_DASH) && ObjectEventClearHeldMovementIfFinished(&gObjectEvents[gPlayerAvatar.objectEventId]))
|
|
SetPlayerAvatarTransitionFlags(PLAYER_AVATAR_FLAG_ON_FOOT);
|
|
|
|
return;
|
|
}
|
|
|
|
SetFollowerNPCSprite(FOLLOWER_NPC_SPRITE_INDEX_NORMAL);
|
|
DestroySprite(&gSprites[gTasks[taskId].tSpriteId]);
|
|
UnfreezeObjectEvents();
|
|
DestroyTask(taskId);
|
|
gPlayerAvatar.preventStep = FALSE;
|
|
}
|
|
|
|
#undef tSpriteId
|
|
|
|
static void Task_ReallowPlayerMovement(u8 taskId)
|
|
{
|
|
bool32 animStatus = ObjectEventClearHeldMovementIfFinished(&gObjectEvents[GetFollowerNPCObjectId()]);
|
|
if (animStatus == 0)
|
|
{
|
|
// Temporarily stop running.
|
|
if (TestPlayerAvatarFlags(PLAYER_AVATAR_FLAG_DASH)
|
|
&& ObjectEventClearHeldMovementIfFinished(&gObjectEvents[gPlayerAvatar.objectEventId]))
|
|
SetPlayerAvatarTransitionFlags(PLAYER_AVATAR_FLAG_ON_FOOT);
|
|
|
|
return;
|
|
}
|
|
|
|
gPlayerAvatar.preventStep = FALSE;
|
|
DestroyTask(taskId);
|
|
}
|
|
|
|
// Task data.
|
|
#define tDoorTask data[1]
|
|
|
|
static void Task_FollowerNPCOutOfDoor(u8 taskId)
|
|
{
|
|
struct ObjectEvent *follower = &gObjectEvents[GetFollowerNPCObjectId()];
|
|
struct ObjectEvent *player = &gObjectEvents[gPlayerAvatar.objectEventId];
|
|
struct Task *task = &gTasks[taskId];
|
|
s16 *x = &task->tDoorX;
|
|
s16 *y = &task->tDoorY;
|
|
|
|
// The player faces follower as they exit the door.
|
|
if (FNPC_FACE_NPC_FOLLOWER_ON_DOOR_EXIT == TRUE && ObjectEventClearHeldMovementIfFinished(player))
|
|
ObjectEventTurn(player, GetPlayerFaceToDoorDirection(player, follower));
|
|
|
|
switch (task->tState)
|
|
{
|
|
case OPEN_DOOR:
|
|
FreezeObjectEvents();
|
|
task->tDoorTask = FieldAnimateDoorOpen(follower->currentCoords.x, follower->currentCoords.y);
|
|
// Only play SE for animated doors.
|
|
if (task->tDoorTask != -1)
|
|
PlaySE(GetDoorSoundEffect(*x, *y));
|
|
task->tState = NPC_WALK_OUT;
|
|
break;
|
|
case NPC_WALK_OUT:
|
|
// If the door isn't still opening.
|
|
if (task->tDoorTask < 0 || gTasks[task->tDoorTask].isActive != TRUE)
|
|
{
|
|
follower->invisible = FALSE;
|
|
// If the follower should be surfing.
|
|
if (TestPlayerAvatarFlags(PLAYER_AVATAR_FLAG_SURFING))
|
|
{
|
|
SetUpSurfBlobFieldEffect(follower);
|
|
follower->fieldEffectSpriteId = FieldEffectStart(FLDEFF_SURF_BLOB);
|
|
SetSurfBlob_BobState(follower->fieldEffectSpriteId, 1);
|
|
}
|
|
ObjectEventTurn(follower, DIR_SOUTH);
|
|
follower->singleMovementActive = FALSE;
|
|
follower->heldMovementActive = FALSE;
|
|
ObjectEventSetHeldMovement(follower, MOVEMENT_ACTION_WALK_NORMAL_DOWN);
|
|
task->tState = CLOSE_DOOR;
|
|
}
|
|
break;
|
|
case CLOSE_DOOR:
|
|
if (ObjectEventClearHeldMovementIfFinished(follower))
|
|
{
|
|
task->tDoorTask = FieldAnimateDoorClose(*x, *y);
|
|
task->tState = UNFREEZE_OBJECTS;
|
|
}
|
|
break;
|
|
case UNFREEZE_OBJECTS:
|
|
// Wait for door to close.
|
|
if (task->tDoorTask < 0 || gTasks[task->tDoorTask].isActive != TRUE)
|
|
{
|
|
UnfreezeObjectEvents();
|
|
task->tState = REALLOW_MOVEMENT;
|
|
}
|
|
break;
|
|
case REALLOW_MOVEMENT:
|
|
FollowerNPC_HandleSprite();
|
|
SetFollowerNPCData(FNPC_DATA_COME_OUT_DOOR, FNPC_DOOR_NONE);
|
|
gPlayerAvatar.preventStep = FALSE;
|
|
DestroyTask(taskId);
|
|
break;
|
|
}
|
|
}
|
|
|
|
#undef tDoorTask
|
|
|
|
static void Task_FollowerNPCHandleEscalator(u8 taskId)
|
|
{
|
|
struct ObjectEvent *follower = &gObjectEvents[GetFollowerNPCObjectId()];
|
|
struct ObjectEvent *player = &gObjectEvents[gPlayerAvatar.objectEventId];
|
|
|
|
ObjectEventClearHeldMovementIfActive(follower);
|
|
ObjectEventSetHeldMovement(follower, DetermineFollowerNPCState(follower, MOVEMENT_ACTION_WALK_NORMAL_DOWN, DetermineFollowerNPCDirection(player, follower)));
|
|
DestroyTask(taskId);
|
|
}
|
|
|
|
#define tCounter data[1]
|
|
#define tMetatileBehavior data[2]
|
|
#define tTimer data[7]
|
|
|
|
static void Task_FollowerNPCHandleEscalatorFinish(u8 taskId)
|
|
{
|
|
s16 x, y;
|
|
struct ObjectEvent *follower = &gObjectEvents[GetFollowerNPCObjectId()];
|
|
struct ObjectEvent *player = &gObjectEvents[gPlayerAvatar.objectEventId];
|
|
struct Sprite *sprite = &gSprites[follower->spriteId];
|
|
struct Task *task = &gTasks[taskId];
|
|
|
|
switch (task->tState)
|
|
{
|
|
case MOVE_TO_PLAYER_POS:
|
|
MoveObjectEventToMapCoords(follower, player->currentCoords.x, player->currentCoords.y);
|
|
PlayerGetDestCoords(&x, &y);
|
|
task->tMetatileBehavior = MapGridGetMetatileBehaviorAt(x, y);
|
|
task->tTimer = 0;
|
|
task->tState = WAIT_FOR_PLAYER_MOVE;
|
|
break;
|
|
case WAIT_FOR_PLAYER_MOVE:
|
|
// Wait half a second before revealing the follower.
|
|
if (task->tTimer++ < 32)
|
|
break;
|
|
|
|
task->tState = SHOW_FOLLOWER_DOWN;
|
|
task->tCounter = 16;
|
|
SetFollowerNPCData(FNPC_DATA_WARP_END, FNPC_WARP_NONE);
|
|
gPlayerAvatar.preventStep = TRUE;
|
|
ObjectEventClearHeldMovementIfActive(follower);
|
|
ObjectEventSetHeldMovement(follower, GetFaceDirectionMovementAction(DIR_EAST));
|
|
if (task->tMetatileBehavior == 0x6b)
|
|
task->tState = SHOW_FOLLOWER_UP;
|
|
|
|
break;
|
|
case SHOW_FOLLOWER_DOWN:
|
|
follower->invisible = FALSE;
|
|
CalculateFollowerNPCEscalatorTrajectoryDown(task);
|
|
task->tState = MOVE_FOLLOWER_DOWN;
|
|
break;
|
|
case MOVE_FOLLOWER_DOWN:
|
|
CalculateFollowerNPCEscalatorTrajectoryDown(task);
|
|
task->tMetatileBehavior++;
|
|
if (task->tMetatileBehavior & 1)
|
|
task->tCounter--;
|
|
|
|
if (task->tCounter == 0)
|
|
{
|
|
sprite->x2 = 0;
|
|
sprite->y2 = 0;
|
|
task->tState = MOVEMENT_FINISH;
|
|
}
|
|
break;
|
|
case SHOW_FOLLOWER_UP:
|
|
follower->invisible = FALSE;
|
|
CalculateFollowerNPCEscalatorTrajectoryUp(task);
|
|
task->tState = MOVE_FOLLOWER_UP;
|
|
break;
|
|
case MOVE_FOLLOWER_UP:
|
|
CalculateFollowerNPCEscalatorTrajectoryUp(task);
|
|
task->tMetatileBehavior++;
|
|
if (task->tMetatileBehavior & 1)
|
|
task->tCounter--;
|
|
|
|
if (task->tCounter == 0)
|
|
{
|
|
sprite->x2 = 0;
|
|
sprite->y2 = 0;
|
|
task->tState = MOVEMENT_FINISH;
|
|
}
|
|
break;
|
|
case MOVEMENT_FINISH:
|
|
if (ObjectEventClearHeldMovementIfFinished(follower))
|
|
{
|
|
gPlayerAvatar.preventStep = FALSE;
|
|
DestroyTask(taskId);
|
|
}
|
|
}
|
|
}
|
|
|
|
#undef tMetatileBehavior
|
|
#undef tTimer
|
|
|
|
static void CalculateFollowerNPCEscalatorTrajectoryUp(struct Task *task)
|
|
{
|
|
struct Sprite *sprite = &gSprites[gObjectEvents[GetFollowerNPCObjectId()].spriteId];
|
|
|
|
sprite->x2 = Cos(0x7c, task->tCounter);
|
|
sprite->y2 = Sin(0x76, task->tCounter);
|
|
}
|
|
|
|
static void CalculateFollowerNPCEscalatorTrajectoryDown(struct Task *task)
|
|
{
|
|
struct Sprite *sprite = &gSprites[gObjectEvents[GetFollowerNPCObjectId()].spriteId];
|
|
|
|
sprite->x2 = Cos(0x84, task->tCounter);
|
|
sprite->y2 = Sin(0x94, task->tCounter);
|
|
}
|
|
|
|
#undef tCounter
|
|
|
|
void CreateFollowerNPC(u32 gfx, u32 followerFlags, const u8 *scriptPtr)
|
|
{
|
|
if (PlayerHasFollowerNPC())
|
|
return;
|
|
|
|
struct ObjectEvent *player = &gObjectEvents[gPlayerAvatar.objectEventId];
|
|
struct ObjectEvent *follower;
|
|
struct ObjectEventTemplate npc =
|
|
{
|
|
.localId = OBJ_EVENT_ID_NPC_FOLLOWER,
|
|
.graphicsId = gfx,
|
|
.x = player->currentCoords.x,
|
|
.y = player->currentCoords.y,
|
|
.elevation = PlayerGetElevation(),
|
|
.script = scriptPtr
|
|
};
|
|
|
|
SetFollowerNPCData(FNPC_DATA_OBJ_ID, TrySpawnObjectEventTemplate(&npc, gSaveBlock1Ptr->location.mapNum, gSaveBlock1Ptr->location.mapGroup, player->currentCoords.x, player->currentCoords.y));
|
|
follower = &gObjectEvents[GetFollowerNPCData(FNPC_DATA_OBJ_ID)];
|
|
follower->movementType = MOVEMENT_TYPE_NONE;
|
|
gSprites[follower->spriteId].callback = MovementType_None;
|
|
|
|
SetFollowerNPCData(FNPC_DATA_IN_PROGRESS, TRUE);
|
|
SetFollowerNPCData(FNPC_DATA_GFX_ID, follower->graphicsId);
|
|
SetFollowerNPCData(FNPC_DATA_SURF_BLOB, FNPC_SURF_BLOB_NONE);
|
|
SetFollowerNPCData(FNPC_DATA_COME_OUT_DOOR, FNPC_DOOR_NONE);
|
|
SetFollowerNPCScriptPointer(scriptPtr);
|
|
if (FollowerNPCHasRunningFrames())
|
|
followerFlags |= FOLLOWER_NPC_FLAG_HAS_RUNNING_FRAMES;
|
|
|
|
SetFollowerNPCData(FNPC_DATA_FOLLOWER_FLAGS, followerFlags);
|
|
|
|
// If the player is biking and the follower flags prohibit biking, force the player to dismount the bike.
|
|
if (!CheckFollowerNPCFlag(FOLLOWER_NPC_FLAG_CAN_BIKE)
|
|
&& TestPlayerAvatarFlags(PLAYER_AVATAR_FLAG_BIKE))
|
|
SetPlayerAvatarTransitionFlags(PLAYER_AVATAR_FLAG_ON_FOOT);
|
|
|
|
// Set the follower sprite to match the player state.
|
|
if (!TestPlayerAvatarFlags(PLAYER_AVATAR_FLAG_ON_FOOT))
|
|
FollowerNPC_HandleSprite();
|
|
|
|
HideNPCFollower();
|
|
SetFollowerNPCData(FNPC_DATA_WARP_END, FNPC_WARP_REAPPEAR);
|
|
}
|
|
|
|
void DestroyFollowerNPC(void)
|
|
{
|
|
if (!PlayerHasFollowerNPC())
|
|
return;
|
|
|
|
RemoveObjectEvent(&gObjectEvents[GetFollowerNPCData(FNPC_DATA_OBJ_ID)]);
|
|
ClearFollowerNPCData();
|
|
UpdateFollowingPokemon();
|
|
}
|
|
|
|
#define RETURN_STATE(state, dir) return newState == MOVEMENT_INVALID ? state + (dir - 1) : ReturnFollowerNPCDelayedState(dir - 1);
|
|
u32 DetermineFollowerNPCState(struct ObjectEvent *follower, u32 state, u32 direction)
|
|
{
|
|
u32 newState = MOVEMENT_INVALID;
|
|
u32 collision = COLLISION_NONE;
|
|
s16 followerX = follower->currentCoords.x;
|
|
s16 followerY = follower->currentCoords.y;
|
|
u32 currentBehavior = MapGridGetMetatileBehaviorAt(followerX, followerY);
|
|
u32 nextBehavior;
|
|
u32 noSpecialAnimFrames = (GetFollowerNPCSprite() == GetFollowerNPCData(FNPC_DATA_GFX_ID));
|
|
u32 delayedState = GetFollowerNPCData(FNPC_DATA_DELAYED_STATE);
|
|
|
|
MoveCoords(direction, &followerX, &followerY);
|
|
nextBehavior = MapGridGetMetatileBehaviorAt(followerX, followerY);
|
|
|
|
if (FindTaskIdByFunc(Task_MoveNPCFollowerAfterForcedMovement) == TASK_NONE)
|
|
follower->facingDirectionLocked = FALSE;
|
|
|
|
// Follower won't do delayed movement until player does a movement.
|
|
if (!IsStateMovement(state) && delayedState)
|
|
return MOVEMENT_ACTION_NONE;
|
|
|
|
if (IsStateMovement(state) && delayedState)
|
|
{
|
|
// Lock face direction for Acro side jump.
|
|
if (delayedState == MOVEMENT_ACTION_JUMP_DOWN && TestPlayerAvatarFlags(PLAYER_AVATAR_FLAG_ACRO_BIKE))
|
|
follower->facingDirectionLocked = TRUE;
|
|
|
|
newState = delayedState + (direction -1);
|
|
}
|
|
|
|
// Clear ice tile stuff.
|
|
follower->disableAnim = FALSE;
|
|
|
|
// Clear overwrite movement.
|
|
follower->directionOverwrite = DIR_NONE;
|
|
|
|
// Sideways stairs checks.
|
|
collision = GetSidewaysStairsCollision(follower, direction, currentBehavior, nextBehavior, collision);
|
|
switch (collision)
|
|
{
|
|
case COLLISION_SIDEWAYS_STAIRS_TO_LEFT:
|
|
follower->directionOverwrite = GetLeftSideStairsDirection(direction);
|
|
break;
|
|
case COLLISION_SIDEWAYS_STAIRS_TO_RIGHT:
|
|
follower->directionOverwrite = GetRightSideStairsDirection(direction);
|
|
break;
|
|
}
|
|
|
|
switch (state)
|
|
{
|
|
case MOVEMENT_ACTION_WALK_SLOW_DOWN ... MOVEMENT_ACTION_WALK_SLOW_RIGHT:
|
|
// Slow walk.
|
|
RETURN_STATE(MOVEMENT_ACTION_WALK_SLOW_DOWN, direction);
|
|
|
|
case MOVEMENT_ACTION_WALK_NORMAL_DOWN ... MOVEMENT_ACTION_WALK_NORMAL_RIGHT:
|
|
// Normal walk.
|
|
RETURN_STATE(MOVEMENT_ACTION_WALK_NORMAL_DOWN, direction);
|
|
|
|
case MOVEMENT_ACTION_JUMP_2_DOWN ... MOVEMENT_ACTION_JUMP_2_RIGHT:
|
|
// Ledge jump.
|
|
if (delayedState == MOVEMENT_ACTION_JUMP_2_DOWN)
|
|
return (MOVEMENT_ACTION_JUMP_2_DOWN + (direction - 1));
|
|
|
|
if (delayedState == MOVEMENT_ACTION_ACRO_WHEELIE_JUMP_DOWN)
|
|
return (MOVEMENT_ACTION_ACRO_WHEELIE_JUMP_DOWN + (direction - 1));
|
|
|
|
SetFollowerNPCData(FNPC_DATA_DELAYED_STATE, MOVEMENT_ACTION_JUMP_2_DOWN);
|
|
RETURN_STATE(MOVEMENT_ACTION_WALK_NORMAL_DOWN, direction);
|
|
|
|
case MOVEMENT_ACTION_WALK_FAST_DOWN ... MOVEMENT_ACTION_WALK_FAST_RIGHT:
|
|
// Handle player on waterfall.
|
|
if (PlayerIsUnderWaterfall(&gObjectEvents[gPlayerAvatar.objectEventId]) && (state == MOVEMENT_ACTION_WALK_FAST_UP))
|
|
return MOVEMENT_INVALID;
|
|
|
|
// Handle ice tile (some walking animation).
|
|
if (MetatileBehavior_IsIce(follower->currentMetatileBehavior) || MetatileBehavior_IsTrickHouseSlipperyFloor(follower->currentMetatileBehavior))
|
|
follower->disableAnim = TRUE;
|
|
|
|
// Handle surfing.
|
|
if (GetFollowerNPCData(FNPC_DATA_CURRENT_SPRITE) == FOLLOWER_NPC_SPRITE_INDEX_SURF && GetFollowerNPCSprite() == GetFollowerNPCData(FNPC_DATA_GFX_ID))
|
|
RETURN_STATE(MOVEMENT_ACTION_SURF_STILL_DOWN, direction);
|
|
|
|
RETURN_STATE(MOVEMENT_ACTION_WALK_FAST_DOWN, direction);
|
|
|
|
case MOVEMENT_ACTION_WALK_FASTER_DOWN ... MOVEMENT_ACTION_WALK_FASTER_RIGHT:
|
|
if (MetatileBehavior_IsIce(follower->currentMetatileBehavior) || MetatileBehavior_IsTrickHouseSlipperyFloor(follower->currentMetatileBehavior))
|
|
follower->disableAnim = TRUE;
|
|
|
|
RETURN_STATE(MOVEMENT_ACTION_WALK_FASTER_DOWN, direction);
|
|
|
|
case MOVEMENT_ACTION_RIDE_WATER_CURRENT_DOWN ... MOVEMENT_ACTION_RIDE_WATER_CURRENT_RIGHT:
|
|
// Handle player on waterfall.
|
|
if (PlayerIsUnderWaterfall(&gObjectEvents[gPlayerAvatar.objectEventId]) && IsPlayerSurfingNorth())
|
|
return MOVEMENT_INVALID;
|
|
|
|
RETURN_STATE(MOVEMENT_ACTION_RIDE_WATER_CURRENT_DOWN, direction);
|
|
|
|
// Acro bike.
|
|
case MOVEMENT_ACTION_ACRO_WHEELIE_FACE_DOWN ... MOVEMENT_ACTION_ACRO_WHEELIE_FACE_RIGHT:
|
|
if (noSpecialAnimFrames)
|
|
return MOVEMENT_ACTION_NONE;
|
|
|
|
RETURN_STATE(MOVEMENT_ACTION_ACRO_WHEELIE_FACE_DOWN, direction);
|
|
|
|
case MOVEMENT_ACTION_ACRO_POP_WHEELIE_DOWN ... MOVEMENT_ACTION_ACRO_POP_WHEELIE_RIGHT:
|
|
if (noSpecialAnimFrames)
|
|
return MOVEMENT_ACTION_NONE;
|
|
|
|
RETURN_STATE(MOVEMENT_ACTION_ACRO_POP_WHEELIE_DOWN, direction);
|
|
|
|
case MOVEMENT_ACTION_ACRO_END_WHEELIE_FACE_DOWN ... MOVEMENT_ACTION_ACRO_END_WHEELIE_FACE_RIGHT:
|
|
if (noSpecialAnimFrames)
|
|
return MOVEMENT_ACTION_NONE;
|
|
|
|
RETURN_STATE(MOVEMENT_ACTION_ACRO_END_WHEELIE_FACE_DOWN, direction);
|
|
|
|
case MOVEMENT_ACTION_ACRO_WHEELIE_HOP_FACE_DOWN ... MOVEMENT_ACTION_ACRO_WHEELIE_HOP_FACE_RIGHT:
|
|
if (noSpecialAnimFrames)
|
|
return MOVEMENT_ACTION_NONE;
|
|
|
|
RETURN_STATE(MOVEMENT_ACTION_ACRO_WHEELIE_HOP_FACE_DOWN, direction);
|
|
|
|
case MOVEMENT_ACTION_ACRO_WHEELIE_HOP_DOWN ... MOVEMENT_ACTION_ACRO_WHEELIE_HOP_RIGHT:
|
|
if (noSpecialAnimFrames)
|
|
RETURN_STATE(MOVEMENT_ACTION_JUMP_DOWN, direction);
|
|
|
|
RETURN_STATE(MOVEMENT_ACTION_ACRO_WHEELIE_HOP_DOWN, direction);
|
|
|
|
case MOVEMENT_ACTION_ACRO_WHEELIE_JUMP_DOWN ... MOVEMENT_ACTION_ACRO_WHEELIE_JUMP_RIGHT:
|
|
// Ledge jump.
|
|
if (noSpecialAnimFrames)
|
|
{
|
|
if (delayedState == MOVEMENT_ACTION_JUMP_2_DOWN)
|
|
return (MOVEMENT_ACTION_JUMP_2_DOWN + (direction - 1));
|
|
|
|
SetFollowerNPCData(FNPC_DATA_DELAYED_STATE, MOVEMENT_ACTION_JUMP_2_DOWN);
|
|
}
|
|
else
|
|
{
|
|
if (delayedState == MOVEMENT_ACTION_ACRO_WHEELIE_JUMP_DOWN)
|
|
return (MOVEMENT_ACTION_ACRO_WHEELIE_JUMP_DOWN + (direction - 1));
|
|
|
|
if (delayedState == MOVEMENT_ACTION_JUMP_2_DOWN)
|
|
return (MOVEMENT_ACTION_JUMP_2_DOWN + (direction - 1));
|
|
|
|
SetFollowerNPCData(FNPC_DATA_DELAYED_STATE, MOVEMENT_ACTION_ACRO_WHEELIE_JUMP_DOWN);
|
|
}
|
|
|
|
RETURN_STATE(MOVEMENT_ACTION_WALK_FAST_DOWN, direction);
|
|
|
|
case MOVEMENT_ACTION_ACRO_WHEELIE_IN_PLACE_DOWN ... MOVEMENT_ACTION_ACRO_WHEELIE_IN_PLACE_RIGHT:
|
|
if (noSpecialAnimFrames)
|
|
return MOVEMENT_ACTION_NONE;
|
|
|
|
RETURN_STATE(MOVEMENT_ACTION_ACRO_WHEELIE_IN_PLACE_DOWN, direction);
|
|
|
|
case MOVEMENT_ACTION_ACRO_POP_WHEELIE_MOVE_DOWN ... MOVEMENT_ACTION_ACRO_POP_WHEELIE_MOVE_RIGHT:
|
|
if (noSpecialAnimFrames)
|
|
RETURN_STATE(MOVEMENT_ACTION_WALK_FAST_DOWN, direction);
|
|
|
|
RETURN_STATE(MOVEMENT_ACTION_ACRO_POP_WHEELIE_MOVE_DOWN, direction);
|
|
|
|
case MOVEMENT_ACTION_ACRO_WHEELIE_MOVE_DOWN ... MOVEMENT_ACTION_ACRO_WHEELIE_MOVE_RIGHT:
|
|
if (noSpecialAnimFrames)
|
|
RETURN_STATE(MOVEMENT_ACTION_WALK_FAST_DOWN, direction);
|
|
|
|
RETURN_STATE(MOVEMENT_ACTION_ACRO_WHEELIE_MOVE_DOWN, direction);
|
|
|
|
case MOVEMENT_ACTION_ACRO_END_WHEELIE_MOVE_DOWN ... MOVEMENT_ACTION_ACRO_END_WHEELIE_MOVE_RIGHT:
|
|
if (noSpecialAnimFrames)
|
|
RETURN_STATE(MOVEMENT_ACTION_WALK_FAST_DOWN, direction);
|
|
|
|
RETURN_STATE(MOVEMENT_ACTION_ACRO_END_WHEELIE_MOVE_DOWN, direction);
|
|
|
|
// Sliding.
|
|
case MOVEMENT_ACTION_SLIDE_DOWN ... MOVEMENT_ACTION_SLIDE_RIGHT:
|
|
RETURN_STATE(MOVEMENT_ACTION_SLIDE_DOWN, direction);
|
|
|
|
case MOVEMENT_ACTION_PLAYER_RUN_DOWN ... MOVEMENT_ACTION_PLAYER_RUN_RIGHT:
|
|
// Running frames.
|
|
if (CheckFollowerNPCFlag(FOLLOWER_NPC_FLAG_HAS_RUNNING_FRAMES))
|
|
RETURN_STATE(MOVEMENT_ACTION_PLAYER_RUN_DOWN, direction);
|
|
|
|
RETURN_STATE(MOVEMENT_ACTION_WALK_FAST_DOWN, direction);
|
|
|
|
case MOVEMENT_ACTION_JUMP_SPECIAL_DOWN ... MOVEMENT_ACTION_JUMP_SPECIAL_RIGHT:
|
|
SetFollowerNPCData(FNPC_DATA_DELAYED_STATE, MOVEMENT_ACTION_JUMP_SPECIAL_DOWN);
|
|
RETURN_STATE(MOVEMENT_ACTION_WALK_NORMAL_DOWN, direction);
|
|
|
|
case MOVEMENT_ACTION_JUMP_DOWN ... MOVEMENT_ACTION_JUMP_RIGHT:
|
|
// Acro side hop.
|
|
if (delayedState == MOVEMENT_ACTION_JUMP_DOWN)
|
|
{
|
|
if (TestPlayerAvatarFlags(PLAYER_AVATAR_FLAG_ACRO_BIKE))
|
|
follower->facingDirectionLocked = TRUE;
|
|
|
|
return newState;
|
|
}
|
|
else
|
|
{
|
|
SetFollowerNPCData(FNPC_DATA_DELAYED_STATE, MOVEMENT_ACTION_JUMP_DOWN);
|
|
RETURN_STATE(MOVEMENT_ACTION_WALK_NORMAL_DOWN, direction);
|
|
}
|
|
|
|
// Run slow.
|
|
case MOVEMENT_ACTION_RUN_DOWN_SLOW ... MOVEMENT_ACTION_RUN_RIGHT_SLOW:
|
|
if (CheckFollowerNPCFlag(FOLLOWER_NPC_FLAG_HAS_RUNNING_FRAMES))
|
|
RETURN_STATE(MOVEMENT_ACTION_RUN_DOWN_SLOW, direction);
|
|
|
|
RETURN_STATE(MOVEMENT_ACTION_WALK_NORMAL_DOWN, direction);
|
|
|
|
default:
|
|
return MOVEMENT_INVALID;
|
|
}
|
|
|
|
return newState;
|
|
}
|
|
|
|
/*
|
|
* Reload the entire event object.
|
|
* It would usually be enough just to change the sprite Id, but if the original
|
|
* sprite and the new sprite have different palettes, the palette would need to
|
|
* be reloaded.
|
|
*/
|
|
void SetFollowerNPCSprite(u32 spriteIndex)
|
|
{
|
|
u32 oldSpriteId;
|
|
u32 newSpriteId;
|
|
u32 newGraphicsId;
|
|
struct ObjectEventTemplate clone;
|
|
struct ObjectEvent backupFollower;
|
|
struct ObjectEvent *follower;
|
|
|
|
if (!PlayerHasFollowerNPC())
|
|
return;
|
|
|
|
if (GetFollowerNPCData(FNPC_DATA_CURRENT_SPRITE) == spriteIndex)
|
|
return;
|
|
|
|
// Save the sprite.
|
|
follower = &gObjectEvents[GetFollowerNPCObjectId()];
|
|
SetFollowerNPCData(FNPC_DATA_CURRENT_SPRITE, spriteIndex);
|
|
oldSpriteId = follower->spriteId;
|
|
newGraphicsId = GetFollowerNPCSprite();
|
|
clone = *GetObjectEventTemplateByLocalIdAndMap(OBJ_EVENT_ID_NPC_FOLLOWER, gSaveBlock1Ptr->location.mapNum, gSaveBlock1Ptr->location.mapGroup);
|
|
|
|
backupFollower = *follower;
|
|
backupFollower.graphicsId = newGraphicsId;
|
|
DestroySprite(&gSprites[oldSpriteId]);
|
|
RemoveObjectEvent(&gObjectEvents[GetFollowerNPCObjectId()]);
|
|
|
|
clone.graphicsId = newGraphicsId;
|
|
clone.movementType = 0;
|
|
clone.localId = OBJ_EVENT_ID_NPC_FOLLOWER;
|
|
SetFollowerNPCData(FNPC_DATA_OBJ_ID, TrySpawnObjectEventTemplate(&clone, gSaveBlock1Ptr->location.mapNum, gSaveBlock1Ptr->location.mapGroup, clone.x, clone.y));
|
|
if (GetFollowerNPCData(FNPC_DATA_OBJ_ID) != OBJECT_EVENTS_COUNT)
|
|
{
|
|
follower = &gObjectEvents[GetFollowerNPCObjectId()];
|
|
newSpriteId = follower->spriteId;
|
|
*follower = backupFollower;
|
|
follower->spriteId = newSpriteId;
|
|
MoveObjectEventToMapCoords(follower, follower->currentCoords.x, follower->currentCoords.y);
|
|
ObjectEventTurn(follower, follower->facingDirection);
|
|
}
|
|
else
|
|
{
|
|
ClearFollowerNPCData();
|
|
}
|
|
}
|
|
|
|
static void ChooseFirstThreeEligibleMons(void)
|
|
{
|
|
u32 i;
|
|
u32 count = 0;
|
|
|
|
ClearSelectedPartyOrder();
|
|
|
|
for (i = 0; i < PARTY_SIZE; i++)
|
|
{
|
|
if (GetMonData(&gPlayerParty[i], MON_DATA_HP) != 0
|
|
&& GetMonData(&gPlayerParty[i], MON_DATA_IS_EGG) == FALSE
|
|
&& GetMonData(&gPlayerParty[i], MON_DATA_SPECIES) != SPECIES_NONE)
|
|
{
|
|
gSelectedOrderFromParty[count] = (i + 1);
|
|
count++;
|
|
}
|
|
|
|
if (count == 3)
|
|
break;
|
|
}
|
|
}
|
|
|
|
bool32 PlayerHasFollowerNPC(void)
|
|
{
|
|
return GetFollowerNPCData(FNPC_DATA_IN_PROGRESS);
|
|
}
|
|
|
|
void NPCFollow(struct ObjectEvent *npc, u32 state, bool32 ignoreScriptActive)
|
|
{
|
|
struct ObjectEvent *player = &gObjectEvents[gPlayerAvatar.objectEventId];
|
|
struct ObjectEvent *follower = &gObjectEvents[GetFollowerNPCObjectId()];
|
|
u32 dir;
|
|
u32 newState;
|
|
u32 taskId;
|
|
|
|
// Only when the player moves.
|
|
if (player != npc)
|
|
return;
|
|
// Only if a follower exists.
|
|
else if (!PlayerHasFollowerNPC())
|
|
return;
|
|
// Don't follow during a script.
|
|
else if (ArePlayerFieldControlsLocked() && !ignoreScriptActive)
|
|
return;
|
|
|
|
// If the follower's object has been removed, create a new one and set it to reappear.
|
|
if (!follower->active)
|
|
{
|
|
CreateFollowerNPCAvatar();
|
|
SetFollowerNPCData(FNPC_DATA_WARP_END, FNPC_WARP_REAPPEAR);
|
|
}
|
|
|
|
// Restore post warp behavior after setobjectxy.
|
|
if (GetFollowerNPCData(FNPC_DATA_COME_OUT_DOOR) == FNPC_DOOR_NO_POS_SET)
|
|
SetFollowerNPCData(FNPC_DATA_COME_OUT_DOOR, FNPC_DOOR_NONE);
|
|
|
|
// Follower changes to normal sprite after getting off surf blob.
|
|
if (GetFollowerNPCData(FNPC_DATA_CURRENT_SPRITE) == FOLLOWER_NPC_SPRITE_INDEX_SURF && !CheckFollowerNPCFlag(PLAYER_AVATAR_FLAG_SURFING) && follower->fieldEffectSpriteId == 0)
|
|
{
|
|
SetFollowerNPCSprite(FOLLOWER_NPC_SPRITE_INDEX_NORMAL);
|
|
SetFollowerNPCData(FNPC_DATA_SURF_BLOB, FNPC_SURF_BLOB_NONE);
|
|
}
|
|
|
|
// Check if the state would cause hidden follower to reappear.
|
|
if (IsStateMovement(state) && GetFollowerNPCData(FNPC_DATA_WARP_END) == FNPC_WARP_REAPPEAR)
|
|
{
|
|
SetFollowerNPCData(FNPC_DATA_WARP_END, FNPC_WARP_NONE);
|
|
|
|
if (GetFollowerNPCData(FNPC_DATA_COME_OUT_DOOR) == FNPC_DOOR_NEEDS_TO_EXIT)
|
|
{
|
|
gPlayerAvatar.preventStep = TRUE;
|
|
taskId = CreateTask(Task_FollowerNPCOutOfDoor, 1);
|
|
gTasks[taskId].tState = 0;
|
|
gTasks[taskId].tDoorX = follower->currentCoords.x;
|
|
gTasks[taskId].tDoorY = follower->currentCoords.y;
|
|
TryUpdateFollowerNPCSpriteUnderwater();
|
|
ObjectEventClearHeldMovementIfFinished(follower);
|
|
return;
|
|
}
|
|
|
|
follower->invisible = FALSE;
|
|
MoveObjectEventToMapCoords(follower, player->currentCoords.x, player->currentCoords.y);
|
|
// The follower should be facing the same direction as the player when it comes out of hiding.
|
|
ObjectEventTurn(follower, player->facingDirection);
|
|
|
|
// Recreate the surf blob if needed.
|
|
if (GetFollowerNPCData(FNPC_DATA_SURF_BLOB) == FNPC_SURF_BLOB_RECREATE)
|
|
{
|
|
SetUpSurfBlobFieldEffect(follower);
|
|
follower->fieldEffectSpriteId = FieldEffectStart(FLDEFF_SURF_BLOB);
|
|
SetSurfBlob_BobState(follower->fieldEffectSpriteId, 1);
|
|
}
|
|
else
|
|
{
|
|
TryUpdateFollowerNPCSpriteUnderwater();
|
|
}
|
|
}
|
|
|
|
dir = DetermineFollowerNPCDirection(player, follower);
|
|
|
|
if (dir == DIR_NONE)
|
|
{
|
|
ObjectEventClearHeldMovementIfFinished(follower);
|
|
return;
|
|
}
|
|
|
|
newState = DetermineFollowerNPCState(follower, state, dir);
|
|
if (newState == MOVEMENT_INVALID)
|
|
{
|
|
ObjectEventClearHeldMovementIfFinished(follower);
|
|
return;
|
|
}
|
|
|
|
// Follower gets on surf blob.
|
|
if (GetFollowerNPCData(FNPC_DATA_SURF_BLOB) == FNPC_SURF_BLOB_NEW && IsStateMovement(state))
|
|
{
|
|
SetFollowerNPCData(FNPC_DATA_SURF_BLOB, FNPC_SURF_BLOB_RECREATE);
|
|
gPlayerAvatar.preventStep = TRUE;
|
|
SetSurfJump();
|
|
ObjectEventClearHeldMovementIfFinished(follower);
|
|
return;
|
|
}
|
|
// Follower gets off surf blob.
|
|
else if (GetFollowerNPCData(FNPC_DATA_SURF_BLOB) == FNPC_SURF_BLOB_DESTROY)
|
|
{
|
|
SetFollowerNPCData(FNPC_DATA_SURF_BLOB, FNPC_SURF_BLOB_NONE);
|
|
gPlayerAvatar.preventStep = TRUE;
|
|
SetSurfDismount();
|
|
ObjectEventClearHeldMovementIfFinished(follower);
|
|
return;
|
|
}
|
|
|
|
ObjectEventClearHeldMovementIfActive(follower);
|
|
ObjectEventSetHeldMovement(follower, newState);
|
|
PlayerLogCoordinates(player);
|
|
|
|
switch (newState)
|
|
{
|
|
case MOVEMENT_ACTION_JUMP_2_DOWN ... MOVEMENT_ACTION_JUMP_2_RIGHT:
|
|
case MOVEMENT_ACTION_JUMP_DOWN ... MOVEMENT_ACTION_JUMP_RIGHT:
|
|
case MOVEMENT_ACTION_ACRO_WHEELIE_JUMP_DOWN ... MOVEMENT_ACTION_ACRO_WHEELIE_JUMP_RIGHT:
|
|
// Synchronize movements on stairs and ledges.
|
|
CreateTask(Task_ReallowPlayerMovement, 1);
|
|
gPlayerAvatar.preventStep = TRUE;
|
|
}
|
|
|
|
ObjectEventClearHeldMovementIfFinished(follower);
|
|
}
|
|
|
|
void CreateFollowerNPCAvatar(void)
|
|
{
|
|
if (!PlayerHasFollowerNPC())
|
|
return;
|
|
|
|
struct ObjectEvent *player = &gObjectEvents[gPlayerAvatar.objectEventId];
|
|
struct ObjectEventTemplate clone =
|
|
{
|
|
.localId = OBJ_EVENT_ID_NPC_FOLLOWER,
|
|
.graphicsId = GetFollowerNPCSprite(),
|
|
.x = player->currentCoords.x - 7,
|
|
.y = player->currentCoords.y - 7,
|
|
.elevation = player->currentElevation,
|
|
.script = GetFollowerNPCScriptPointer(),
|
|
.movementType = MOVEMENT_TYPE_FACE_DOWN
|
|
};
|
|
|
|
switch (GetPlayerFacingDirection())
|
|
{
|
|
case DIR_NORTH:
|
|
clone.movementType = MOVEMENT_TYPE_FACE_UP;
|
|
break;
|
|
case DIR_WEST:
|
|
clone.movementType = MOVEMENT_TYPE_FACE_LEFT;
|
|
break;
|
|
case DIR_EAST:
|
|
clone.movementType = MOVEMENT_TYPE_FACE_RIGHT;
|
|
break;
|
|
}
|
|
|
|
// Create NPC and store ID.
|
|
SetFollowerNPCData(FNPC_DATA_OBJ_ID, TrySpawnObjectEventTemplate(&clone, gSaveBlock1Ptr->location.mapNum, gSaveBlock1Ptr->location.mapGroup, clone.x, clone.y));
|
|
if (GetFollowerNPCData(FNPC_DATA_OBJ_ID) == OBJECT_EVENTS_COUNT)
|
|
{
|
|
ClearFollowerNPCData();
|
|
return;
|
|
}
|
|
|
|
if (gMapHeader.mapType == MAP_TYPE_UNDERWATER)
|
|
SetFollowerNPCData(FNPC_DATA_SURF_BLOB, FNPC_SURF_BLOB_NONE);
|
|
|
|
gObjectEvents[GetFollowerNPCData(FNPC_DATA_OBJ_ID)].invisible = TRUE;
|
|
}
|
|
|
|
void FollowerNPC_HandleSprite(void)
|
|
{
|
|
if (TestPlayerAvatarFlags(PLAYER_AVATAR_FLAG_BIKE) && CheckFollowerNPCFlag(FOLLOWER_NPC_FLAG_CAN_BIKE))
|
|
{
|
|
if (gPlayerAvatar.flags & PLAYER_AVATAR_FLAG_MACH_BIKE)
|
|
SetFollowerNPCSprite(FOLLOWER_NPC_SPRITE_INDEX_MACH_BIKE);
|
|
else if (gPlayerAvatar.flags & PLAYER_AVATAR_FLAG_ACRO_BIKE)
|
|
SetFollowerNPCSprite(FOLLOWER_NPC_SPRITE_INDEX_ACRO_BIKE);
|
|
}
|
|
else if (gPlayerAvatar.flags & PLAYER_AVATAR_FLAG_ON_FOOT)
|
|
{
|
|
SetFollowerNPCSprite(FOLLOWER_NPC_SPRITE_INDEX_NORMAL);
|
|
}
|
|
}
|
|
|
|
u32 DetermineFollowerNPCDirection(struct ObjectEvent *player, struct ObjectEvent *follower)
|
|
{
|
|
s32 delta_x = follower->currentCoords.x - player->currentCoords.x;
|
|
s32 delta_y = follower->currentCoords.y - player->currentCoords.y;
|
|
|
|
if (delta_x < 0)
|
|
return DIR_EAST;
|
|
else if (delta_x > 0)
|
|
return DIR_WEST;
|
|
|
|
if (delta_y < 0)
|
|
return DIR_SOUTH;
|
|
else if (delta_y > 0)
|
|
return DIR_NORTH;
|
|
|
|
return DIR_NONE;
|
|
}
|
|
|
|
u32 GetFollowerNPCObjectId(void)
|
|
{
|
|
if (PlayerHasFollowerNPC())
|
|
return GetFollowerNPCData(FNPC_DATA_OBJ_ID);
|
|
|
|
return 0;
|
|
}
|
|
|
|
bool32 CheckFollowerNPCFlag(u32 flag)
|
|
{
|
|
if (!PlayerHasFollowerNPC())
|
|
return TRUE;
|
|
|
|
if (GetFollowerNPCData(FNPC_DATA_FOLLOWER_FLAGS) & flag)
|
|
return TRUE;
|
|
|
|
return FALSE;
|
|
}
|
|
|
|
bool32 FollowerNPC_IsCollisionExempt(struct ObjectEvent *obstacle, struct ObjectEvent *collider)
|
|
{
|
|
struct ObjectEvent *follower = &gObjectEvents[GetFollowerNPCObjectId()];
|
|
struct ObjectEvent *player = &gObjectEvents[gPlayerAvatar.objectEventId];
|
|
|
|
if (!PlayerHasFollowerNPC())
|
|
return FALSE;
|
|
|
|
if (obstacle == follower && collider == player)
|
|
return TRUE;
|
|
|
|
return FALSE;
|
|
}
|
|
|
|
void HideNPCFollower(void)
|
|
{
|
|
if (!PlayerHasFollowerNPC() || gObjectEvents[GetFollowerNPCObjectId()].invisible)
|
|
return;
|
|
|
|
if (GetFollowerNPCData(FNPC_DATA_SURF_BLOB) == FNPC_SURF_BLOB_RECREATE || GetFollowerNPCData(FNPC_DATA_SURF_BLOB) == FNPC_SURF_BLOB_DESTROY)
|
|
{
|
|
SetSurfBlob_BobState(gObjectEvents[GetFollowerNPCObjectId()].fieldEffectSpriteId, 2);
|
|
DestroySprite(&gSprites[gObjectEvents[GetFollowerNPCObjectId()].fieldEffectSpriteId]);
|
|
gObjectEvents[GetFollowerNPCObjectId()].fieldEffectSpriteId = 0;
|
|
}
|
|
|
|
SetFollowerNPCData(FNPC_DATA_COME_OUT_DOOR, FNPC_DOOR_NONE);
|
|
|
|
gObjectEvents[GetFollowerNPCObjectId()].invisible = TRUE;
|
|
}
|
|
|
|
void FollowerNPC_WarpSetEnd(void)
|
|
{
|
|
struct ObjectEvent *player;
|
|
struct ObjectEvent *follower;
|
|
|
|
if (!PlayerHasFollowerNPC())
|
|
return;
|
|
|
|
player = &gObjectEvents[gPlayerAvatar.objectEventId];
|
|
follower = &gObjectEvents[GetFollowerNPCObjectId()];
|
|
|
|
PlayerLogCoordinates(player);
|
|
|
|
// Skip setting position if setobjectxy was used during ON_WARP_INTO_MAP_TABLE.
|
|
if (GetFollowerNPCData(FNPC_DATA_COME_OUT_DOOR) == FNPC_DOOR_NO_POS_SET)
|
|
{
|
|
SetFollowerNPCData(FNPC_DATA_WARP_END, FNPC_WARP_NONE);
|
|
SetFollowerNPCData(FNPC_DATA_COME_OUT_DOOR, FNPC_DOOR_NONE);
|
|
}
|
|
else
|
|
{
|
|
u32 toY = GetFollowerNPCData(FNPC_DATA_COME_OUT_DOOR) == FNPC_DOOR_NEEDS_TO_EXIT ? (player->currentCoords.y - 1) : player->currentCoords.y;
|
|
MoveObjectEventToMapCoords(follower, player->currentCoords.x, toY);
|
|
SetFollowerNPCData(FNPC_DATA_WARP_END, FNPC_WARP_REAPPEAR);
|
|
}
|
|
|
|
if (gPlayerAvatar.flags & PLAYER_AVATAR_FLAG_ON_FOOT)
|
|
{
|
|
SetFollowerNPCSprite(FOLLOWER_NPC_SPRITE_INDEX_NORMAL);
|
|
SetFollowerNPCData(FNPC_DATA_SURF_BLOB, FNPC_SURF_BLOB_NONE);
|
|
}
|
|
else if (gPlayerAvatar.flags & PLAYER_AVATAR_FLAG_SURFING)
|
|
{
|
|
SetFollowerNPCSprite(FOLLOWER_NPC_SPRITE_INDEX_SURF);
|
|
SetFollowerNPCData(FNPC_DATA_SURF_BLOB, FNPC_SURF_BLOB_RECREATE);
|
|
}
|
|
|
|
follower->facingDirection = player->facingDirection;
|
|
follower->movementDirection = player->movementDirection;
|
|
}
|
|
|
|
bool32 FollowerNPCCanBike(void)
|
|
{
|
|
if (!PlayerHasFollowerNPC())
|
|
return TRUE;
|
|
else if (CheckFollowerNPCFlag(FOLLOWER_NPC_FLAG_CAN_BIKE))
|
|
return TRUE;
|
|
else
|
|
return FALSE;
|
|
}
|
|
|
|
void FollowerNPC_HandleBike(void)
|
|
{
|
|
// Wait until after get off surf blob to start biking.
|
|
if (GetFollowerNPCData(FNPC_DATA_CURRENT_SPRITE) == FOLLOWER_NPC_SPRITE_INDEX_SURF)
|
|
return;
|
|
|
|
if (gPlayerAvatar.flags & PLAYER_AVATAR_FLAG_MACH_BIKE && FollowerNPCCanBike() && GetFollowerNPCData(FNPC_DATA_COME_OUT_DOOR) != FNPC_DOOR_NEEDS_TO_EXIT) //Coming out door
|
|
{
|
|
SetFollowerNPCSprite(FOLLOWER_NPC_SPRITE_INDEX_MACH_BIKE);
|
|
}
|
|
else if (gPlayerAvatar.flags & PLAYER_AVATAR_FLAG_ACRO_BIKE && FollowerNPCCanBike() && GetFollowerNPCData(FNPC_DATA_COME_OUT_DOOR) != FNPC_DOOR_NEEDS_TO_EXIT) //Coming out door
|
|
{
|
|
SetFollowerNPCSprite(FOLLOWER_NPC_SPRITE_INDEX_ACRO_BIKE);
|
|
}
|
|
else
|
|
{
|
|
SetFollowerNPCSprite(FOLLOWER_NPC_SPRITE_INDEX_NORMAL);
|
|
// Disable saved Acro side jump.
|
|
if (GetFollowerNPCData(FNPC_DATA_DELAYED_STATE) == MOVEMENT_ACTION_JUMP_DOWN)
|
|
SetFollowerNPCData(FNPC_DATA_DELAYED_STATE, 0);
|
|
}
|
|
}
|
|
|
|
void FollowerNPC_FollowerToWater(void)
|
|
{
|
|
if (!PlayerHasFollowerNPC())
|
|
return;
|
|
|
|
// Prepare for making the follower do the jump and spawn the surf blob right in front of the follower's location.
|
|
NPCFollow(&gObjectEvents[gPlayerAvatar.objectEventId], MOVEMENT_ACTION_JUMP_DOWN, TRUE);
|
|
SetFollowerNPCData(FNPC_DATA_SURF_BLOB, FNPC_SURF_BLOB_NEW);
|
|
}
|
|
|
|
void FollowerNPC_SetIndicatorToRecreateSurfBlob(void)
|
|
{
|
|
if (PlayerHasFollowerNPC())
|
|
SetFollowerNPCData(FNPC_DATA_SURF_BLOB, FNPC_SURF_BLOB_RECREATE);
|
|
}
|
|
|
|
void FollowerNPC_BindToSurfBlobOnReloadScreen(void)
|
|
{
|
|
struct ObjectEvent *follower;
|
|
|
|
if (!PlayerHasFollowerNPC())
|
|
return;
|
|
|
|
follower = &gObjectEvents[GetFollowerNPCObjectId()];
|
|
TryUpdateFollowerNPCSpriteUnderwater();
|
|
|
|
if (follower->invisible || (GetFollowerNPCData(FNPC_DATA_SURF_BLOB) != FNPC_SURF_BLOB_RECREATE && GetFollowerNPCData(FNPC_DATA_SURF_BLOB) != FNPC_SURF_BLOB_DESTROY))
|
|
return;
|
|
|
|
// Spawn the surf blob under the follower.
|
|
SetUpSurfBlobFieldEffect(follower);
|
|
follower->fieldEffectSpriteId = FieldEffectStart(FLDEFF_SURF_BLOB);
|
|
SetSurfBlob_BobState(follower->fieldEffectSpriteId, 1);
|
|
}
|
|
|
|
void PrepareFollowerNPCDismountSurf(void)
|
|
{
|
|
if (!PlayerHasFollowerNPC())
|
|
return;
|
|
|
|
NPCFollow(&gObjectEvents[gPlayerAvatar.objectEventId], MOVEMENT_ACTION_WALK_NORMAL_DOWN, TRUE);
|
|
SetFollowerNPCData(FNPC_DATA_SURF_BLOB, FNPC_SURF_BLOB_DESTROY);
|
|
}
|
|
|
|
void SetFollowerNPCSurfSpriteAfterDive(void)
|
|
{
|
|
SetFollowerNPCSprite(FOLLOWER_NPC_SPRITE_INDEX_SURF);
|
|
SetFollowerNPCData(FNPC_DATA_SURF_BLOB, FNPC_SURF_BLOB_RECREATE);
|
|
}
|
|
|
|
bool32 FollowerNPCComingThroughDoor(void)
|
|
{
|
|
if (!PlayerHasFollowerNPC())
|
|
return FALSE;
|
|
|
|
if (GetFollowerNPCData(FNPC_DATA_COME_OUT_DOOR))
|
|
return TRUE;
|
|
|
|
return FALSE;
|
|
}
|
|
|
|
void FollowerNPC_SetIndicatorToComeOutDoor(void)
|
|
{
|
|
if (PlayerHasFollowerNPC())
|
|
SetFollowerNPCData(FNPC_DATA_COME_OUT_DOOR, FNPC_DOOR_NEEDS_TO_EXIT);
|
|
}
|
|
|
|
void EscalatorMoveFollowerNPC(u32 movementType)
|
|
{
|
|
u8 taskId;
|
|
|
|
if (!PlayerHasFollowerNPC())
|
|
return;
|
|
|
|
taskId = CreateTask(Task_FollowerNPCHandleEscalator, 1);
|
|
gTasks[taskId].data[1] = movementType;
|
|
}
|
|
|
|
void EscalatorMoveFollowerNPCFinish(void)
|
|
{
|
|
if (!PlayerHasFollowerNPC())
|
|
return;
|
|
|
|
CreateTask(Task_FollowerNPCHandleEscalatorFinish, 1);
|
|
}
|
|
|
|
void FollowerNPCWalkIntoPlayerForLeaveMap(void)
|
|
{
|
|
u32 followerObjId = GetFollowerNPCObjectId();
|
|
struct ObjectEvent *follower = &gObjectEvents[GetFollowerNPCObjectId()];
|
|
|
|
if (followerObjId == OBJECT_EVENTS_COUNT)
|
|
return;
|
|
|
|
follower->singleMovementActive = FALSE;
|
|
follower->heldMovementActive = FALSE;
|
|
switch (DetermineFollowerNPCDirection(&gObjectEvents[gPlayerAvatar.objectEventId], &gObjectEvents[followerObjId]))
|
|
{
|
|
case DIR_NORTH:
|
|
ObjectEventSetHeldMovement(follower, MOVEMENT_ACTION_WALK_NORMAL_UP);
|
|
break;
|
|
case DIR_SOUTH:
|
|
ObjectEventSetHeldMovement(follower, MOVEMENT_ACTION_WALK_NORMAL_DOWN);
|
|
break;
|
|
case DIR_EAST:
|
|
ObjectEventSetHeldMovement(follower, MOVEMENT_ACTION_WALK_NORMAL_RIGHT);
|
|
break;
|
|
case DIR_WEST:
|
|
ObjectEventSetHeldMovement(follower, MOVEMENT_ACTION_WALK_NORMAL_LEFT);
|
|
break;
|
|
}
|
|
}
|
|
|
|
void FollowerNPCHideForLeaveMap(struct ObjectEvent *follower)
|
|
{
|
|
SetFollowerNPCSprite(FOLLOWER_NPC_SPRITE_INDEX_NORMAL);
|
|
follower->invisible = TRUE;
|
|
SetFollowerNPCData(FNPC_DATA_WARP_END, FNPC_WARP_REAPPEAR);
|
|
SetFollowerNPCData(FNPC_DATA_COME_OUT_DOOR, FNPC_DOOR_NONE);
|
|
SetFollowerNPCData(FNPC_DATA_SURF_BLOB, FNPC_SURF_BLOB_NONE);
|
|
SetFollowerNPCData(FNPC_DATA_DELAYED_STATE, 0);
|
|
}
|
|
|
|
void FollowerNPCReappearAfterLeaveMap(struct ObjectEvent *follower, struct ObjectEvent *player)
|
|
{
|
|
if (PlayerHasFollowerNPC())
|
|
{
|
|
follower->invisible = FALSE;
|
|
MoveObjectEventToMapCoords(follower, player->currentCoords.x, player->currentCoords.y);
|
|
ObjectEventTurn(follower, DIR_SOUTH);
|
|
follower->singleMovementActive = FALSE;
|
|
follower->heldMovementActive = FALSE;
|
|
|
|
// Follower only steps onto a tile without collision.
|
|
if (GetCollisionAtCoords(player, player->currentCoords.x, player->currentCoords.y + 1, DIR_SOUTH) == COLLISION_NONE)
|
|
ObjectEventSetHeldMovement(follower, MOVEMENT_ACTION_WALK_NORMAL_DOWN);
|
|
else if (GetCollisionAtCoords(player, player->currentCoords.x + 1, player->currentCoords.y, DIR_EAST) == COLLISION_NONE)
|
|
ObjectEventSetHeldMovement(follower, MOVEMENT_ACTION_WALK_NORMAL_RIGHT);
|
|
else if (GetCollisionAtCoords(player, player->currentCoords.x - 1, player->currentCoords.y, DIR_WEST) == COLLISION_NONE)
|
|
ObjectEventSetHeldMovement(follower, MOVEMENT_ACTION_WALK_NORMAL_LEFT);
|
|
else
|
|
HideNPCFollower();
|
|
}
|
|
}
|
|
|
|
void FollowerNPCFaceAfterLeaveMap(void)
|
|
{
|
|
struct ObjectEvent *player = &gObjectEvents[gPlayerAvatar.objectEventId];
|
|
struct ObjectEvent *follower = &gObjectEvents[GetFollowerNPCData(FNPC_DATA_OBJ_ID)];
|
|
|
|
if (follower->invisible)
|
|
return;
|
|
|
|
ObjectEventTurn(follower, DetermineFollowerNPCDirection(player, follower));
|
|
SetFollowerNPCData(FNPC_DATA_WARP_END, FNPC_WARP_NONE);
|
|
}
|
|
|
|
bool32 FollowerNPCIsBattlePartner(void)
|
|
{
|
|
if (PlayerHasFollowerNPC() && GetFollowerNPCData(FNPC_DATA_BATTLE_PARTNER))
|
|
return TRUE;
|
|
|
|
return FALSE;
|
|
}
|
|
|
|
u32 GetFollowerNPCBattlePartner(void)
|
|
{
|
|
return GetFollowerNPCData(FNPC_DATA_BATTLE_PARTNER);
|
|
}
|
|
|
|
bool32 IsNPCFollowerWildBattle(void)
|
|
{
|
|
if (FollowerNPCIsBattlePartner() && FNPC_FLAG_PARTNER_WILD_BATTLES != 0
|
|
&& (FNPC_FLAG_PARTNER_WILD_BATTLES == FNPC_ALWAYS || FlagGet(FNPC_FLAG_PARTNER_WILD_BATTLES)))
|
|
return TRUE;
|
|
|
|
return FALSE;
|
|
}
|
|
|
|
void PrepareForFollowerNPCBattle(void)
|
|
{
|
|
// Load the partner party if the NPC follower should participate.
|
|
if (gBattleTypeFlags & BATTLE_TYPE_INGAME_PARTNER && FollowerNPCIsBattlePartner())
|
|
{
|
|
SavePlayerParty();
|
|
ChooseFirstThreeEligibleMons();
|
|
ReducePlayerPartyToSelectedMons();
|
|
VarSet(VAR_0x8004, FRONTIER_UTIL_FUNC_SET_DATA);
|
|
VarSet(VAR_0x8005, FRONTIER_DATA_SELECTED_MON_ORDER);
|
|
CallFrontierUtilFunc();
|
|
gPartnerTrainerId = TRAINER_PARTNER(GetFollowerNPCData(FNPC_DATA_BATTLE_PARTNER));
|
|
FillPartnerParty(gPartnerTrainerId);
|
|
}
|
|
}
|
|
|
|
void RestorePartyAfterFollowerNPCBattle(void)
|
|
{
|
|
VarSet(VAR_0x8004, FRONTIER_UTIL_FUNC_SAVE_PARTY);
|
|
CallFrontierUtilFunc();
|
|
LoadPlayerParty();
|
|
}
|
|
|
|
void FollowerNPC_TryRemoveFollowerOnWhiteOut(void)
|
|
{
|
|
if (PlayerHasFollowerNPC())
|
|
{
|
|
if (CheckFollowerNPCFlag(FOLLOWER_NPC_FLAG_CLEAR_ON_WHITE_OUT))
|
|
ClearFollowerNPCData();
|
|
else
|
|
FollowerNPC_WarpSetEnd();
|
|
}
|
|
}
|
|
|
|
#undef tDoorX
|
|
#undef tDoorY
|
|
|
|
// Task data
|
|
#define PREVENT_PLAYER_STEP 0
|
|
#define DO_ALL_FORCED_MOVEMENTS 1
|
|
#define NPC_INTO_PLAYER 2
|
|
#define ENABLE_PLAYER_STEP 3
|
|
|
|
void Task_MoveNPCFollowerAfterForcedMovement(u8 taskId)
|
|
{
|
|
struct ObjectEvent *follower = &gObjectEvents[GetFollowerNPCObjectId()];
|
|
struct ObjectEvent *player = &gObjectEvents[gPlayerAvatar.objectEventId];
|
|
|
|
// Prevent player input until all forced mmovements are done and the follower is hidden.
|
|
if (gTasks[taskId].tState == PREVENT_PLAYER_STEP)
|
|
{
|
|
gPlayerAvatar.preventStep = TRUE;
|
|
gTasks[taskId].tState = DO_ALL_FORCED_MOVEMENTS;
|
|
}
|
|
// The player will keep doing forced movments until they land on a non-forced-move metatile or hit collision.
|
|
else if (gTasks[taskId].tState == DO_ALL_FORCED_MOVEMENTS && ObjectEventClearHeldMovementIfFinished(player) != 0)
|
|
{
|
|
// Lock follower facing direction for muddy slope.
|
|
if (follower->currentMetatileBehavior == MB_MUDDY_SLOPE)
|
|
follower->facingDirectionLocked = TRUE;
|
|
|
|
if (TryDoMetatileBehaviorForcedMovement() == 0)
|
|
gTasks[taskId].tState = NPC_INTO_PLAYER;
|
|
|
|
return;
|
|
}
|
|
// The NPC will take an extra step and be on the same tile as the player.
|
|
else if (gTasks[taskId].tState == NPC_INTO_PLAYER && ObjectEventClearHeldMovementIfFinished(player) != 0 && ObjectEventClearHeldMovementIfFinished(follower) != 0)
|
|
{
|
|
ObjectEventSetHeldMovement(follower, GetWalkFastMovementAction(DetermineFollowerNPCDirection(player, follower)));
|
|
gTasks[taskId].tState = ENABLE_PLAYER_STEP;
|
|
return;
|
|
}
|
|
// Hide the NPC until the player takes a step. Reallow player input.
|
|
else if (gTasks[taskId].tState == ENABLE_PLAYER_STEP && ObjectEventClearHeldMovementIfFinished(follower) != 0)
|
|
{
|
|
follower->facingDirectionLocked = FALSE;
|
|
HideNPCFollower();
|
|
SetFollowerNPCData(FNPC_DATA_WARP_END, FNPC_WARP_REAPPEAR);
|
|
gPlayerAvatar.preventStep = FALSE;
|
|
DestroyTask(taskId);
|
|
}
|
|
}
|
|
|
|
#undef tState
|
|
#undef PREVENT_PLAYER_STEP
|
|
#undef DO_ALL_FORCED_MOVEMENTS
|
|
#undef NPC_INTO_PLAYER
|
|
#undef ENABLE_PLAYER_STEP
|
|
|
|
void Task_HideNPCFollowerAfterMovementFinish(u8 taskId)
|
|
{
|
|
struct ObjectEvent *npcFollower = &gObjectEvents[GetFollowerNPCObjectId()];
|
|
|
|
if (ObjectEventClearHeldMovementIfFinished(npcFollower) != 0)
|
|
{
|
|
HideNPCFollower();
|
|
SetFollowerNPCData(FNPC_DATA_WARP_END, FNPC_WARP_REAPPEAR);
|
|
gPlayerAvatar.preventStep = FALSE;
|
|
DestroyTask(taskId);
|
|
}
|
|
}
|
|
|
|
// Script commands.
|
|
void ScriptSetFollowerNPC(struct ScriptContext *ctx)
|
|
{
|
|
u32 localId = ScriptReadByte(ctx);
|
|
u32 flags = ScriptReadHalfword(ctx);
|
|
u32 setScript = ScriptReadByte(ctx);
|
|
u32 battlePartner = ScriptReadHalfword(ctx);
|
|
const u8 *script = (const u8 *)ScriptReadWord(ctx);
|
|
|
|
if (PlayerHasFollowerNPC())
|
|
return;
|
|
|
|
SetFollowerNPCData(FNPC_DATA_BATTLE_PARTNER, battlePartner);
|
|
TurnNPCIntoFollower(localId, flags, setScript, script);
|
|
}
|
|
|
|
void ScriptCreateFollowerNPC(struct ScriptContext *ctx)
|
|
{
|
|
u32 gfx = ScriptReadHalfword(ctx);
|
|
u32 flags = ScriptReadHalfword(ctx);
|
|
const u8 *script = (const u8 *)ScriptReadWord(ctx);
|
|
u32 battlePartner = ScriptReadHalfword(ctx);
|
|
|
|
if (!FNPC_ENABLE_NPC_FOLLOWERS || PlayerHasFollowerNPC())
|
|
return;
|
|
|
|
SetFollowerNPCData(FNPC_DATA_BATTLE_PARTNER, battlePartner);
|
|
CreateFollowerNPC(gfx, flags, script);
|
|
}
|
|
|
|
void ScriptDestroyFollowerNPC(struct ScriptContext *ctx)
|
|
{
|
|
DestroyFollowerNPC();
|
|
}
|
|
|
|
void ScriptFaceFollowerNPC(struct ScriptContext *ctx)
|
|
{
|
|
if (!FNPC_ENABLE_NPC_FOLLOWERS || !PlayerHasFollowerNPC())
|
|
return;
|
|
|
|
u32 playerDirection, followerDirection;
|
|
struct ObjectEvent *player, *follower;
|
|
player = &gObjectEvents[gPlayerAvatar.objectEventId];
|
|
follower = &gObjectEvents[GetFollowerNPCData(FNPC_DATA_OBJ_ID)];
|
|
|
|
if (follower->invisible == FALSE)
|
|
{
|
|
playerDirection = DetermineFollowerNPCDirection(player, follower);
|
|
followerDirection = playerDirection;
|
|
|
|
//Flip direction.
|
|
switch (playerDirection)
|
|
{
|
|
case DIR_NORTH:
|
|
playerDirection = DIR_SOUTH;
|
|
break;
|
|
case DIR_SOUTH:
|
|
playerDirection = DIR_NORTH;
|
|
break;
|
|
case DIR_WEST:
|
|
playerDirection = DIR_EAST;
|
|
break;
|
|
case DIR_EAST:
|
|
playerDirection = DIR_WEST;
|
|
break;
|
|
}
|
|
|
|
ObjectEventTurn(player, playerDirection);
|
|
ObjectEventTurn(follower, followerDirection);
|
|
}
|
|
}
|
|
|
|
static const u8 *const FollowerNPCHideMovementsSpeedTable[][4] =
|
|
{
|
|
[DIR_SOUTH] = {Common_Movement_WalkDownSlow, Common_Movement_WalkDown, Common_Movement_WalkDownFast, Common_Movement_WalkDownFaster},
|
|
[DIR_NORTH] = {Common_Movement_WalkUpSlow, Common_Movement_WalkUp, Common_Movement_WalkUpFast, Common_Movement_WalkUpFaster},
|
|
[DIR_WEST] = {Common_Movement_WalkLeftSlow, Common_Movement_WalkLeft, Common_Movement_WalkLeftFast, Common_Movement_WalkLeftFaster},
|
|
[DIR_EAST] = {Common_Movement_WalkRightSlow, Common_Movement_WalkRight, Common_Movement_WalkRightFast, Common_Movement_WalkRightFaster}
|
|
};
|
|
|
|
void ScriptHideNPCFollower(struct ScriptContext *ctx)
|
|
{
|
|
if (!FNPC_ENABLE_NPC_FOLLOWERS || !PlayerHasFollowerNPC())
|
|
return;
|
|
|
|
u32 walkSpeed = ScriptReadByte(ctx);
|
|
struct ObjectEvent *npc = &gObjectEvents[GetFollowerNPCObjectId()];
|
|
|
|
if (npc->invisible == FALSE)
|
|
{
|
|
u32 direction = DetermineFollowerNPCDirection(&gObjectEvents[gPlayerAvatar.objectEventId], npc);
|
|
|
|
if (walkSpeed > 3)
|
|
walkSpeed = 3;
|
|
|
|
ScriptMovement_StartObjectMovementScript(OBJ_EVENT_ID_NPC_FOLLOWER, npc->mapGroup, npc->mapNum, FollowerNPCHideMovementsSpeedTable[direction][walkSpeed]);
|
|
SetFollowerNPCData(FNPC_DATA_WARP_END, FNPC_WARP_REAPPEAR);
|
|
}
|
|
}
|
|
|
|
void ScriptCheckFollowerNPC(struct ScriptContext *ctx)
|
|
{
|
|
gSpecialVar_Result = PlayerHasFollowerNPC();
|
|
}
|
|
|
|
void ScriptUpdateFollowingMon(struct ScriptContext *ctx)
|
|
{
|
|
UpdateFollowingPokemon();
|
|
}
|
|
|
|
void ScriptChangeFollowerNPCBattlePartner(struct ScriptContext *ctx)
|
|
{
|
|
if (!FNPC_ENABLE_NPC_FOLLOWERS || !PlayerHasFollowerNPC())
|
|
return;
|
|
|
|
u32 newBattlePartner = ScriptReadHalfword(ctx);
|
|
|
|
SetFollowerNPCData(FNPC_DATA_BATTLE_PARTNER, newBattlePartner);
|
|
}
|