diff --git a/asm/macros/event.inc b/asm/macros/event.inc index d93829f55b..e118d787d8 100644 --- a/asm/macros/event.inc +++ b/asm/macros/event.inc @@ -2453,7 +2453,7 @@ callnative Script_SetDifficulty, requests_effects=1 .byte \difficulty .endm - + .macro forcesave callnative Script_ForceSaveGame waitstate @@ -2519,3 +2519,67 @@ compare \a, \b cant_see_if 5 .endm + +@ Follower NPCs + + @ Sets an NPC up to follow the player. + @ Follower flags are defined in include/constants/follower_npc.h + @ If you want to specify a battle partner without specifying a custom script, you can set the script parameter to 0. + .macro setfollower localId:req, flags:req, script=0, battlePartner=0 + checkfollower + goto_if_eq VAR_RESULT, TRUE, 1f + hidefollower + delay 16 + callnative ScriptSetFollowerNPC + .if \script == 0 + .set setScript, FALSE + .else + .set setScript, TRUE + .endif + .byte \localId + .2byte \flags + .byte setScript + .2byte \battlePartner + .4byte \script + updatefollowingmon + 1: + .endm + + @ Remove the follower NPC (assumes there will only ever be one). + .macro destroyfollower + callnative ScriptDestroyFollowerNPC + .endm + + @ Makes the player and follower NPC face one another. + .macro facefollower + callnative ScriptFaceFollowerNPC + .endm + + @ Makes the follower NPC walk into the player and get hidden. + @ Optionally, you can set the walk speed for the movement: + @ 0 = Slow + @ 1 = Normal (default) + @ 2 = Fast + @ 3 = Faster + .macro hidefollowernpc speed=1 + callnative ScriptHideNPCFollower + .byte \speed + waitmovement OBJ_EVENT_ID_NPC_FOLLOWER + callnative HideNPCFollower + .endm + + @ Checks if you have a follower NPC. Returns the result to VAR_RESULT. + .macro checkfollower + callnative ScriptCheckFollowerNPC + .endm + + @ Updates Pokemon follower. + .macro updatefollowingmon + callnative ScriptUpdateFollowingMon + .endm + + @ Changes the battle partner of the existing follower NPC. + .macro changefollowerbattler battlePartner:req + callnative ScriptChangeFollowerNPCBattlePartner + .2byte \battlePartner + .endm diff --git a/data/event_scripts.s b/data/event_scripts.s index b887876f6e..fc6b62fc1c 100644 --- a/data/event_scripts.s +++ b/data/event_scripts.s @@ -30,6 +30,7 @@ #include "constants/field_tasks.h" #include "constants/field_weather.h" #include "constants/flags.h" +#include "constants/follower_npc.h" #include "constants/frontier_util.h" #include "constants/game_stat.h" #include "constants/item.h" diff --git a/data/scripts/field_move_scripts.inc b/data/scripts/field_move_scripts.inc index a8f8144f23..4cd069c7ee 100644 --- a/data/scripts/field_move_scripts.inc +++ b/data/scripts/field_move_scripts.inc @@ -293,7 +293,9 @@ EventScript_UseWaterfall:: msgbox Text_WantToWaterfall, MSGBOX_YESNO goto_if_eq VAR_RESULT, NO, EventScript_EndWaterfall msgbox Text_MonUsedWaterfall, MSGBOX_DEFAULT + hidefollowernpc dofieldeffect FLDEFF_USE_WATERFALL + callnative FollowerNPC_WarpSetEnd goto EventScript_EndWaterfall EventScript_CannotUseWaterfall:: @@ -321,12 +323,16 @@ EventScript_UseDive:: lockall checkpartymove MOVE_DIVE goto_if_eq VAR_RESULT, PARTY_SIZE, EventScript_CantDive + copyvar 0x8004 VAR_RESULT bufferpartymonnick STR_VAR_1, VAR_RESULT setfieldeffectargument 0, VAR_RESULT setfieldeffectargument 1, 1 msgbox Text_WantToDive, MSGBOX_YESNO goto_if_eq VAR_RESULT, NO, EventScript_EndDive msgbox Text_MonUsedDive, MSGBOX_DEFAULT + hidefollowernpc + setfieldeffectargument 0, VAR_0x8004 + setfieldeffectargument 1, 1 dofieldeffect FLDEFF_USE_DIVE goto EventScript_EndDive end @@ -350,6 +356,7 @@ EventScript_UseDiveUnderwater:: msgbox Text_WantToSurface, MSGBOX_YESNO goto_if_eq VAR_RESULT, NO, EventScript_EndSurface msgbox Text_MonUsedDive, MSGBOX_DEFAULT + hidefollowernpc dofieldeffect FLDEFF_USE_DIVE goto EventScript_EndSurface end @@ -361,6 +368,7 @@ EventScript_CantSurface:: end EventScript_EndSurface:: + callnative SetFollowerNPCSurfSpriteAfterDive releaseall end diff --git a/data/scripts/movement.inc b/data/scripts/movement.inc index a5ebc0ae7f..cf6ce3e9b2 100644 --- a/data/scripts/movement.inc +++ b/data/scripts/movement.inc @@ -43,19 +43,19 @@ Common_Movement_WalkInPlaceFasterDown: walk_in_place_faster_down step_end -Common_Movement_FaceRight: +Common_Movement_FaceRight:: face_right step_end -Common_Movement_FaceLeft: +Common_Movement_FaceLeft:: face_left step_end -Common_Movement_FaceDown: +Common_Movement_FaceDown:: face_down step_end -Common_Movement_FaceUp: +Common_Movement_FaceUp:: face_up step_end @@ -96,7 +96,7 @@ Common_Movement_Delay32: delay_16 step_end -Common_Movement_WalkUp: +Common_Movement_WalkUp:: walk_up step_end @@ -105,3 +105,64 @@ Common_Movement_WalkUp2:: walk_up walk_up step_end + +@ Follower NPC +Common_Movement_WalkUpSlow:: + walk_slow_up + step_end + +Common_Movement_WalkDownSlow:: + walk_slow_down + step_end + +Common_Movement_WalkRightSlow:: + walk_slow_right + step_end + +Common_Movement_WalkLeftSlow:: + walk_slow_left + step_end + +Common_Movement_WalkDown:: + walk_down + step_end + +Common_Movement_WalkRight:: + walk_right + step_end + +Common_Movement_WalkLeft:: + walk_left + step_end + +Common_Movement_WalkUpFast:: + walk_fast_up + step_end + +Common_Movement_WalkDownFast:: + walk_fast_down + step_end + +Common_Movement_WalkRightFast:: + walk_fast_right + step_end + +Common_Movement_WalkLeftFast:: + walk_fast_left + step_end + +Common_Movement_WalkUpFaster:: + walk_faster_up + step_end + +Common_Movement_WalkDownFaster:: + walk_faster_down + step_end + +Common_Movement_WalkRightFaster:: + walk_faster_right + step_end + +Common_Movement_WalkLeftFaster:: + walk_faster_left + step_end diff --git a/include/battle_tower.h b/include/battle_tower.h index 45c9d683ff..dc118d0f21 100644 --- a/include/battle_tower.h +++ b/include/battle_tower.h @@ -77,5 +77,6 @@ u16 FacilityClassToGraphicsId(u8 facilityClass); bool32 ValidateBattleTowerRecord(u8 recordId); // unused void TrySetLinkBattleTowerEnemyPartyLevel(void); void CreateFacilityMon(const struct TrainerMon *fmon, u16 level, u8 fixedIV, u32 otID, u32 flags, struct Pokemon *dst); +void FillPartnerParty(u16 trainerId); #endif //GUARD_BATTLE_TOWER_H diff --git a/include/config/follower_npc.h b/include/config/follower_npc.h new file mode 100644 index 0000000000..222248ae80 --- /dev/null +++ b/include/config/follower_npc.h @@ -0,0 +1,13 @@ +#ifndef GUARD_FOLLOWER_NPC_OVERWORLD_H +#define GUARD_FOLLOWER_NPC_OVERWORLD_H + +// NPC Followers +#define FNPC_ENABLE_NPC_FOLLOWERS FALSE // Enables the use of script macros to designate NPCs to follow behind the player, DPP style. Slightly increases the size of the saveblock (SaveBlock3). +#define FNPC_FLAG_HEAL_AFTER_FOLLOWER_BATTLE 0 // Replace the 0 with a flag in order to use that flag to toggle whether the Player's party will be automatically healed after every follower partner battle. If you want this to always be active without using a flag, replace 0 with FNPC_ALWAYS. +#define FNPC_FLAG_PARTNER_WILD_BATTLES 0 // Replace the 0 with a flag in order to use that flag to toggle whether the follower partner will join you for wild battles. If you want this to always be active without using a flag, replace 0 with FNPC_ALWAYS. +#define FNPC_NPC_FOLLOWER_WILD_BATTLE_VS_2 TRUE // If set to TRUE, two wild Pokemon will show up to the partner battle instead of just one. +#define FNPC_NPC_FOLLOWER_PARTY_PREVIEW TRUE // If set to TRUE, a preview of the player's and partner's teams will be shown before every trainer battle. +#define FNPC_FACE_NPC_FOLLOWER_ON_DOOR_EXIT TRUE // If set to TRUE, the player will turn to face the follower when they exit a doorway. +#define FNPC_NPC_FOLLOWER_SHOW_AFTER_LEAVE_ROUTE TRUE // If set to TRUE, the follower will reappear and walk out of the player after using Fly, Escape Rope, or Teleport. + +#endif // GUARD_FOLLOWER_NPC_OVERWORLD_H diff --git a/include/constants/event_object_movement.h b/include/constants/event_object_movement.h index bc2081a54b..4e1b3a34ea 100755 --- a/include/constants/event_object_movement.h +++ b/include/constants/event_object_movement.h @@ -255,6 +255,10 @@ #define MOVEMENT_ACTION_WALK_SLOW_STAIRS_UP 0xA7 #define MOVEMENT_ACTION_WALK_SLOW_STAIRS_LEFT 0xA8 #define MOVEMENT_ACTION_WALK_SLOW_STAIRS_RIGHT 0xA9 +#define MOVEMENT_ACTION_SURF_STILL_DOWN 0xAA +#define MOVEMENT_ACTION_SURF_STILL_UP 0xAB +#define MOVEMENT_ACTION_SURF_STILL_LEFT 0xAC +#define MOVEMENT_ACTION_SURF_STILL_RIGHT 0xAD #define MOVEMENT_ACTION_STEP_END 0xFE #define MOVEMENT_ACTION_NONE 0xFF diff --git a/include/constants/event_objects.h b/include/constants/event_objects.h index 7cd210275c..b064fa8d75 100644 --- a/include/constants/event_objects.h +++ b/include/constants/event_objects.h @@ -323,6 +323,7 @@ #define OBJ_EVENT_ID_PLAYER 0xFF #define OBJ_EVENT_ID_CAMERA 0x7F #define OBJ_EVENT_ID_FOLLOWER 0xFE +#define OBJ_EVENT_ID_NPC_FOLLOWER 0xFD // Object event local ids referenced in C files #define LOCALID_ROUTE111_PLAYER_FALLING 45 diff --git a/include/constants/follower_npc.h b/include/constants/follower_npc.h new file mode 100644 index 0000000000..9c41cb0895 --- /dev/null +++ b/include/constants/follower_npc.h @@ -0,0 +1,34 @@ +#ifndef GUARD_CONSTANTS_FOLLOWER_NPC_H +#define GUARD_CONSTANTS_FOLLOWER_NPC_H + +// NPC Follower Flags +#define FOLLOWER_NPC_FLAG_HAS_RUNNING_FRAMES 0x1 // Only use this if the NPC has running anim frames. Part of FOLLOWER_NPC_FLAG_ALL_LAND. +#define FOLLOWER_NPC_FLAG_CAN_BIKE 0x2 // Player is allowed to use the Bike. Part of FOLLOWER_NPC_FLAG_ALL_LAND. +#define FOLLOWER_NPC_FLAG_CAN_LEAVE_ROUTE 0x4 // Player is allowed to use Fly/Teleport/EscapeRope/etc. Part of FOLLOWER_NPC_FLAG_ALL_LAND. +#define FOLLOWER_NPC_FLAG_CAN_SURF 0x8 // Player is allowed to Surf. Part of FOLLOWER_NPC_FLAG_ALL_WATER. +#define FOLLOWER_NPC_FLAG_CAN_WATERFALL 0x10 // Player is allowed to use Waterfall. Part of FOLLOWER_NPC_FLAG_ALL_WATER. +#define FOLLOWER_NPC_FLAG_CAN_DIVE 0x20 // Player is allowed to use Dive. Part of FOLLOWER_NPC_FLAG_ALL_WATER. +#define FOLLOWER_NPC_FLAG_CLEAR_ON_WHITE_OUT 0x80 // The NPC follower will be destroyed if the player whites out. + +#define FOLLOWER_NPC_FLAG_ALL_LAND FOLLOWER_NPC_FLAG_HAS_RUNNING_FRAMES | FOLLOWER_NPC_FLAG_CAN_BIKE | FOLLOWER_NPC_FLAG_CAN_LEAVE_ROUTE +#define FOLLOWER_NPC_FLAG_ALL_WATER FOLLOWER_NPC_FLAG_CAN_SURF | FOLLOWER_NPC_FLAG_CAN_WATERFALL | FOLLOWER_NPC_FLAG_CAN_DIVE +#define FOLLOWER_NPC_FLAG_ALL FOLLOWER_NPC_FLAG_ALL_LAND | FOLLOWER_NPC_FLAG_ALL_WATER | FOLLOWER_NPC_FLAG_CLEAR_ON_WHITE_OUT + +// Shorter flag names for ease of use in setfollower script macro +#define FNPC_RUNNING FOLLOWER_NPC_FLAG_HAS_RUNNING_FRAMES +#define FNPC_BIKE FOLLOWER_NPC_FLAG_CAN_BIKE +#define FNPC_LEAVE_ROUTE FOLLOWER_NPC_FLAG_CAN_LEAVE_ROUTE +#define FNPC_SURF FOLLOWER_NPC_FLAG_CAN_SURF +#define FNPC_WATERFALL FOLLOWER_NPC_FLAG_CAN_WATERFALL +#define FNPC_DIVE FOLLOWER_NPC_FLAG_CAN_DIVE +#define FNPC_WHITE_OUT FOLLOWER_NPC_FLAG_CLEAR_ON_WHITE_OUT + +#define FNPC_ALL_LAND FOLLOWER_NPC_FLAG_ALL_LAND +#define FNPC_ALL_WATER FOLLOWER_NPC_FLAG_ALL_WATER +#define FNPC_ALL FOLLOWER_NPC_FLAG_ALL + + +#define FNPC_NONE 0 +#define FNPC_ALWAYS 2 + +#endif // GUARD_CONSTANTS_FOLLOWER_NPC_H diff --git a/include/constants/global.h b/include/constants/global.h index 8ca3861709..4698dbc3b4 100644 --- a/include/constants/global.h +++ b/include/constants/global.h @@ -11,6 +11,7 @@ #include "config/dexnav.h" #include "config/summary_screen.h" #include "config/ai.h" +#include "config/follower_npc.h" // Invalid Versions show as "----------" in Gen 4 and Gen 5's summary screen. // In Gens 6 and 7, invalid versions instead show "a distant land" in the summary screen. diff --git a/include/event_object_movement.h b/include/event_object_movement.h index 85e3c62413..7d8309257e 100644 --- a/include/event_object_movement.h +++ b/include/event_object_movement.h @@ -128,6 +128,7 @@ u8 GetObjectEventIdByXY(s16 x, s16 y); void SetObjectEventDirection(struct ObjectEvent *objectEvent, u8 direction); u8 GetFirstInactiveObjectEventId(void); u8 GetObjectEventIdByLocalId(u8); +void RemoveObjectEvent(struct ObjectEvent *objectEvent); void RemoveObjectEventByLocalIdAndMap(u8 localId, u8 mapNum, u8 mapGroup); void LoadSpecialObjectReflectionPalette(u16 tag, u8 slot); void TryMoveObjectEventToMapCoords(u8 localId, u8 mapNum, u8 mapGroup, s16 x, s16 y); @@ -253,6 +254,11 @@ void CameraObjectFreeze(void); u8 GetObjectEventBerryTreeId(u8 objectEventId); void SetBerryTreeJustPicked(u8 mapId, u8 mapNumber, u8 mapGroup); bool8 IsBerryTreeSparkling(u8 localId, u8 mapNum, u8 mapGroup); +const struct ObjectEventTemplate *GetObjectEventTemplateByLocalIdAndMap(u8 localId, u8 mapNum, u8 mapGroup); +u8 TrySpawnObjectEventTemplate(const struct ObjectEventTemplate *objectEventTemplate, u8 mapNum, u8 mapGroup, s16 cameraX, s16 cameraY); +bool8 GetFollowerInfo(u32 *species, bool32 *shiny, bool32 *female); +const struct ObjectEventGraphicsInfo *SpeciesToGraphicsInfo(u32 species, bool32 shiny, bool32 female); +u16 GetObjectEventFlagIdByLocalIdAndMap(u8 localId, u8 mapNum, u8 mapGroup); void MovementType_None(struct Sprite *); void MovementType_LookAround(struct Sprite *); @@ -311,7 +317,6 @@ void MovementType_Invisible(struct Sprite *); void MovementType_WalkSlowlyInPlace(struct Sprite *); void MovementType_FollowPlayer(struct Sprite *); u8 GetSlideMovementAction(u32); -u8 GetJumpMovementAction(u32); u8 GetJump2MovementAction(u32); u8 CopySprite(struct Sprite *sprite, s16 x, s16 y, u8 subpriority); u8 CreateCopySpriteAt(struct Sprite *sprite, s16 x, s16 y, u8 subpriority); @@ -505,5 +510,6 @@ u8 GetSidewaysStairsCollision(struct ObjectEvent *objectEvent, u8 dir, u8 curren bool8 MovementAction_EmoteX_Step0(struct ObjectEvent *, struct Sprite *); bool8 MovementAction_EmoteDoubleExclamationMark_Step0(struct ObjectEvent *, struct Sprite *); +bool8 PlayerIsUnderWaterfall(struct ObjectEvent *objectEvent); #endif //GUARD_EVENT_OBJECT_MOVEMENT_H diff --git a/include/event_scripts.h b/include/event_scripts.h index b10a4d602b..fa2df54740 100644 --- a/include/event_scripts.h +++ b/include/event_scripts.h @@ -653,6 +653,27 @@ extern const u8 EventScript_VsSeekerChargingDone[]; extern const u8 Common_Movement_FollowerSafeStart[]; extern const u8 Common_Movement_FollowerSafeEnd[]; +extern const u8 Common_Movement_WalkUpSlow[]; +extern const u8 Common_Movement_WalkDownSlow[]; +extern const u8 Common_Movement_WalkRightSlow[]; +extern const u8 Common_Movement_WalkLeftSlow[]; +extern const u8 Common_Movement_WalkUp[]; +extern const u8 Common_Movement_WalkDown[]; +extern const u8 Common_Movement_WalkRight[]; +extern const u8 Common_Movement_WalkLeft[]; +extern const u8 Common_Movement_WalkUpFast[]; +extern const u8 Common_Movement_WalkDownFast[]; +extern const u8 Common_Movement_WalkRightFast[]; +extern const u8 Common_Movement_WalkLeftFast[]; +extern const u8 Common_Movement_WalkUpFaster[]; +extern const u8 Common_Movement_WalkDownFaster[]; +extern const u8 Common_Movement_WalkRightFaster[]; +extern const u8 Common_Movement_WalkLeftFaster[]; +extern const u8 Common_Movement_FaceUp[]; +extern const u8 Common_Movement_FaceDown[]; +extern const u8 Common_Movement_FaceRight[]; +extern const u8 Common_Movement_FaceLeft[]; + extern const u8 EventScript_CancelMessageBox[]; extern const u8 Common_EventScript_ShowPokemonCenterSign[]; extern const u8 Common_EventScript_ShowPokemartSign[]; diff --git a/include/field_player_avatar.h b/include/field_player_avatar.h index d05ca5eedb..2db24300cc 100644 --- a/include/field_player_avatar.h +++ b/include/field_player_avatar.h @@ -2,6 +2,7 @@ #define GUARD_FIELD_PLAYER_AVATAR_H void PlayerStep(u8 direction, u16 newKeys, u16 heldKeys); +bool8 TryDoMetatileBehaviorForcedMovement(); void ClearPlayerAvatarInfo(void); void SetPlayerAvatarExtraStateTransition(u16, u8); u8 GetPlayerAvatarGenderByGraphicsId(u16); diff --git a/include/field_screen_effect.h b/include/field_screen_effect.h index 3f120ffdeb..1c77b2b375 100644 --- a/include/field_screen_effect.h +++ b/include/field_screen_effect.h @@ -44,5 +44,8 @@ void WriteFlashScanlineEffectBuffer(u8 flashLevel); bool8 IsPlayerStandingStill(void); void DoStairWarp(u16 metatileBehavior, u16 delay); bool32 IsDirectionalStairWarpMetatileBehavior(u16 metatileBehavior, u8 playerDirection); +void SetPlayerVisibility(bool8 visible); +void Task_WarpAndLoadMap(u8 taskId); +void Task_DoDoorWarp(u8 taskId); #endif // GUARD_FIELD_SCREEN_EFFECT_H diff --git a/include/follower_npc.h b/include/follower_npc.h new file mode 100644 index 0000000000..90941e56cf --- /dev/null +++ b/include/follower_npc.h @@ -0,0 +1,129 @@ +#ifndef GUARD_FOLLOWER_NPC_H +#define GUARD_FOLLOWER_NPC_H + +#include "constants/follower_npc.h" + +#define MOVEMENT_INVALID 0xFE + +struct FollowerNPCSpriteGraphics +{ + u16 normalId; + u16 machBikeId; + u16 acroBikeId; + u16 surfId; + u16 underwaterId; +}; + +enum FollowerNPCDataTypes +{ + FNPC_DATA_IN_PROGRESS, + FNPC_DATA_WARP_END, + FNPC_DATA_SURF_BLOB, + FNPC_DATA_COME_OUT_DOOR, + FNPC_DATA_OBJ_ID, + FNPC_DATA_CURRENT_SPRITE, + FNPC_DATA_DELAYED_STATE, + FNPC_DATA_MAP_ID, + FNPC_DATA_MAP_NUM, + FNPC_DATA_MAP_GROUP, + FNPC_DATA_EVENT_FLAG, + FNPC_DATA_GFX_ID, + FNPC_DATA_FOLLOWER_FLAGS, + FNPC_DATA_BATTLE_PARTNER +}; + +enum FollowerNPCSpriteTypes +{ + FOLLOWER_NPC_SPRITE_INDEX_NORMAL, + FOLLOWER_NPC_SPRITE_INDEX_MACH_BIKE, + FOLLOWER_NPC_SPRITE_INDEX_ACRO_BIKE, + FOLLOWER_NPC_SPRITE_INDEX_SURF, + FOLLOWER_NPC_SPRITE_INDEX_UNDERWATER, +}; + +enum FollowerNPCDoorStairsStates +{ + FNPC_DOOR_NONE, + FNPC_DOOR_NEEDS_TO_EXIT +}; + +enum FollowerNPCWarpEndStates +{ + FNPC_WARP_NONE, + FNPC_WARP_REAPPEAR +}; + +enum FollowerNPCSurfBlobStates +{ + FNPC_SURF_BLOB_NONE, + FNPC_SURF_BLOB_NEW, + FNPC_SURF_BLOB_RECREATE, + FNPC_SURF_BLOB_DESTROY +}; + +enum FollowerNPCOutOfDoorTaskStates{ + OPEN_DOOR, + NPC_WALK_OUT, + CLOSE_DOOR, + UNFREEZE_OBJECTS, + REALLOW_MOVEMENT +}; + +enum FollowerNPCHandleEscalatorFinishTaskStates{ + MOVE_TO_PLAYER_POS, + WAIT_FOR_PLAYER_MOVE, + SHOW_FOLLOWER_DOWN, + MOVE_FOLLOWER_DOWN, + SHOW_FOLLOWER_UP, + MOVE_FOLLOWER_UP, + MOVEMENT_FINISH +}; + +void SetFollowerNPCData(enum FollowerNPCDataTypes type, u32 value); +const u8 *GetFollowerNPCScriptPointer(void); +u32 GetFollowerNPCData(enum FollowerNPCDataTypes type); +void ClearFollowerNPCData(void); + +u32 DetermineFollowerNPCState(struct ObjectEvent *follower, u32 state, u32 direction); +void SetFollowerNPCSprite(u32 spriteIndex); + +bool32 PlayerHasFollowerNPC(void); +void NPCFollow(struct ObjectEvent *npc, u32 state, bool32 ignoreScriptActive); +void CreateFollowerNPCAvatar(void); +void FollowerNPC_HandleSprite(void); +u32 DetermineFollowerNPCDirection(struct ObjectEvent *player, struct ObjectEvent *follower); +u32 GetFollowerNPCObjectId(void); +bool32 CheckFollowerNPCFlag(u32 flag); +bool32 FollowerNPC_IsCollisionExempt(struct ObjectEvent *obstacle, struct ObjectEvent *collider); +void HideNPCFollower(void); +void FollowerNPC_WarpSetEnd(void); + +bool32 FollowerNPCCanBike(void); +void FollowerNPC_HandleBike(void); + +void FollowerNPC_FollowerToWater(void); +void FollowerNPC_SetIndicatorToRecreateSurfBlob(void); +void FollowerNPC_BindToSurfBlobOnReloadScreen(void); +void PrepareFollowerNPCDismountSurf(void); + +bool32 FollowerNPCComingThroughDoor(void); +void FollowerNPC_SetIndicatorToComeOutDoor(void); + +void EscalatorMoveFollowerNPC(u32 movementType); +void EscalatorMoveFollowerNPCFinish(void); + +void FollowerNPCWalkIntoPlayerForLeaveMap(void); +void FollowerNPCHideForLeaveMap(struct ObjectEvent *follower); +void FollowerNPCReappearAfterLeaveMap(struct ObjectEvent *follower, struct ObjectEvent *player); +void FollowerNPCFaceAfterLeaveMap(void); + +bool32 FollowerNPCIsBattlePartner(void); +bool32 IsNPCFollowerWildBattle(void); +void PrepareForFollowerNPCBattle(void); +void RestorePartyAfterFollowerNPCBattle(void); +void FollowerNPC_TryRemoveFollowerOnWhiteOut(void); + +void Task_MoveNPCFollowerAfterForcedMovement(u8 taskId); +void Task_HideNPCFollowerAfterMovementFinish(u8 taskId); + +#endif // GUARD_FOLLOWER_NPC_H diff --git a/include/follower_npc_alternate_sprites.h b/include/follower_npc_alternate_sprites.h new file mode 100644 index 0000000000..a42795ddf9 --- /dev/null +++ b/include/follower_npc_alternate_sprites.h @@ -0,0 +1,33 @@ +#ifndef GUARD_FOLLOWER_NPC_ALTERNATE_SPRITES_H +#define GUARD_FOLLOWER_NPC_ALTERNATE_SPRITES_H + +#include "constants/event_objects.h" + +// This is where alternate sprites for NPC followers should be listed. +// If the appropriate alternate sprites are not added here, the NPC follower will not use the correct sprites for biking, +// surfing, etc. +// The normalId tells the game what GFX to tie the associated alternate sprites to. This is usually the sprite you would +// give to the object in Porymap. +// For example, Rival May's normalId is OBJ_EVENT_GFX_RIVAL_MAY_NORMAL. All the rest of the sprites in that same block +// will be used with an NPC follower that has that normalId. + +static const struct FollowerNPCSpriteGraphics gFollowerNPCAlternateSprites[] = +{ + { + .normalId = OBJ_EVENT_GFX_RIVAL_MAY_NORMAL, + .machBikeId = OBJ_EVENT_GFX_RIVAL_MAY_MACH_BIKE, + .acroBikeId = OBJ_EVENT_GFX_RIVAL_MAY_ACRO_BIKE, + .surfId = OBJ_EVENT_GFX_RIVAL_MAY_SURFING, + .underwaterId = OBJ_EVENT_GFX_MAY_UNDERWATER, + }, + { + .normalId = OBJ_EVENT_GFX_RIVAL_BRENDAN_NORMAL, + .machBikeId = OBJ_EVENT_GFX_RIVAL_BRENDAN_MACH_BIKE, + .acroBikeId = OBJ_EVENT_GFX_RIVAL_BRENDAN_ACRO_BIKE, + .surfId = OBJ_EVENT_GFX_RIVAL_BRENDAN_SURFING, + .underwaterId = OBJ_EVENT_GFX_BRENDAN_UNDERWATER, + }, + +}; + +#endif // GUARD_FOLLOWER_NPC_ALTERNATE_SPRITES_H diff --git a/include/gba/defines.h b/include/gba/defines.h index 2a68042421..6d2b9f910a 100644 --- a/include/gba/defines.h +++ b/include/gba/defines.h @@ -3,8 +3,8 @@ #include -#define TRUE 1 -#define FALSE 0 +#define TRUE 1 +#define FALSE 0 #define IWRAM_DATA __attribute__((section(".bss"))) #define EWRAM_DATA __attribute__((section(".sbss"))) diff --git a/include/global.h b/include/global.h index ecf35b9777..74ed0c11d3 100644 --- a/include/global.h +++ b/include/global.h @@ -200,6 +200,31 @@ struct Time /*0x04*/ s8 seconds; }; +struct NPCFollowerMapData +{ + u8 id; + u8 number; + u8 group; +}; + +struct NPCFollower +{ + u8 inProgress:1; + u8 warpEnd:1; + u8 createSurfBlob:3; + u8 comeOutDoorStairs:3; + u8 objId; + u8 currentSprite; + u8 delayedState; + struct NPCFollowerMapData map; + struct Coords16 log; + const u8 *script; + u16 flag; + u16 graphicsId; + u16 flags; + u8 battlePartner; // If you have more than 255 total battle partners defined, change this to a u16 +}; + #include "constants/items.h" #define ITEM_FLAGS_COUNT ((ITEMS_COUNT / 8) + ((ITEMS_COUNT % 8) ? 1 : 0)) @@ -208,6 +233,9 @@ struct SaveBlock3 #if OW_USE_FAKE_RTC struct SiiRtcInfo fakeRTC; #endif +#if FNPC_ENABLE_NPC_FOLLOWERS + struct NPCFollower NPCfollower; +#endif #if OW_SHOW_ITEM_DESCRIPTIONS == OW_ITEM_DESCRIPTIONS_FIRST_TIME u8 itemFlags[ITEM_FLAGS_COUNT]; #endif diff --git a/src/battle_main.c b/src/battle_main.c index 19b49ce308..8b960191cb 100644 --- a/src/battle_main.c +++ b/src/battle_main.c @@ -26,6 +26,7 @@ #include "event_data.h" #include "evolution_scene.h" #include "field_weather.h" +#include "follower_npc.h" #include "graphics.h" #include "gpu_regs.h" #include "international_string_util.h" @@ -430,7 +431,11 @@ void CB2_InitBattle(void) gLoadFail = FALSE; #endif // T_SHOULD_RUN_MOVE_ANIM - if (gBattleTypeFlags & BATTLE_TYPE_MULTI) +#if T_SHOULD_RUN_MOVE_ANIM + gLoadFail = FALSE; +#endif // T_SHOULD_RUN_MOVE_ANIM + + if (gBattleTypeFlags & BATTLE_TYPE_MULTI && gBattleTypeFlags & BATTLE_TYPE_TRAINER) { if (gBattleTypeFlags & BATTLE_TYPE_RECORDED) { @@ -1411,7 +1416,9 @@ static void CB2_PreInitIngamePlayerPartnerBattle(void) *savedCallback = gMain.savedCallback; *savedBattleTypeFlags = gBattleTypeFlags; gMain.savedCallback = CB2_PreInitIngamePlayerPartnerBattle; - ShowPartyMenuToShowcaseMultiBattleParty(); + if (!PlayerHasFollowerNPC() || !FollowerNPCIsBattlePartner() || (FNPC_NPC_FOLLOWER_PARTY_PREVIEW && FollowerNPCIsBattlePartner())) + ShowPartyMenuToShowcaseMultiBattleParty(); + break; case 1: if (!gPaletteFade.active) diff --git a/src/battle_setup.c b/src/battle_setup.c index 5a96c0b9c3..f38135adcc 100644 --- a/src/battle_setup.c +++ b/src/battle_setup.c @@ -2,6 +2,7 @@ #include "battle.h" #include "load_save.h" #include "battle_setup.h" +#include "battle_tower.h" #include "battle_transition.h" #include "main.h" #include "task.h" @@ -11,6 +12,7 @@ #include "metatile_behavior.h" #include "field_player_avatar.h" #include "fieldmap.h" +#include "follower_npc.h" #include "random.h" #include "starter_choose.h" #include "script_pokemon_util.h" @@ -246,6 +248,7 @@ static void Task_BattleStart(u8 taskId) case 1: if (IsBattleTransitionDone() == TRUE) { + PrepareForFollowerNPCBattle(); CleanupOverworldWindowsAndTilemaps(); SetMainCallback2(CB2_InitBattle); RestartWildEncounterImmunitySteps(); @@ -327,7 +330,11 @@ static void DoStandardWildBattle(bool32 isDouble) StopPlayerAvatar(); gMain.savedCallback = CB2_EndWildBattle; gBattleTypeFlags = 0; - if (isDouble) + if (IsNPCFollowerWildBattle()) + { + gBattleTypeFlags |= BATTLE_TYPE_MULTI | BATTLE_TYPE_INGAME_PARTNER | BATTLE_TYPE_DOUBLE; + } + else if (isDouble) gBattleTypeFlags |= BATTLE_TYPE_DOUBLE; if (InBattlePyramid()) { @@ -573,6 +580,15 @@ static void CB2_EndWildBattle(void) { CpuFill16(0, (void *)(BG_PLTT), BG_PLTT_SIZE); ResetOamRange(0, 128); + + if (IsNPCFollowerWildBattle()) + { + RestorePartyAfterFollowerNPCBattle(); + if (FNPC_FLAG_HEAL_AFTER_FOLLOWER_BATTLE != 0 + && (FNPC_FLAG_HEAL_AFTER_FOLLOWER_BATTLE == FNPC_ALWAYS + || FlagGet(FNPC_FLAG_HEAL_AFTER_FOLLOWER_BATTLE))) + HealPlayerParty(); + } if (IsPlayerDefeated(gBattleOutcome) == TRUE && !InBattlePyramid() && !InBattlePike()) { @@ -1160,9 +1176,24 @@ void ClearTrainerFlag(u16 trainerId) void BattleSetup_StartTrainerBattle(void) { if (gNoOfApproachingTrainers == 2) - gBattleTypeFlags = (BATTLE_TYPE_DOUBLE | BATTLE_TYPE_TWO_OPPONENTS | BATTLE_TYPE_TRAINER); + { + if (FollowerNPCIsBattlePartner()) + gBattleTypeFlags = (BATTLE_TYPE_MULTI | BATTLE_TYPE_DOUBLE | BATTLE_TYPE_INGAME_PARTNER | BATTLE_TYPE_TWO_OPPONENTS | BATTLE_TYPE_TRAINER); + else + gBattleTypeFlags = (BATTLE_TYPE_DOUBLE | BATTLE_TYPE_TWO_OPPONENTS | BATTLE_TYPE_TRAINER); + } else - gBattleTypeFlags = (BATTLE_TYPE_TRAINER); + { + if (FollowerNPCIsBattlePartner()) + { + gBattleTypeFlags = (BATTLE_TYPE_MULTI | BATTLE_TYPE_INGAME_PARTNER | BATTLE_TYPE_DOUBLE | BATTLE_TYPE_TRAINER); + TRAINER_BATTLE_PARAM.opponentB = 0xFFFF; + } + else + { + gBattleTypeFlags = (BATTLE_TYPE_TRAINER); + } + } if (InBattlePyramid()) { @@ -1252,6 +1283,15 @@ static void CB2_EndTrainerBattle(void) { HandleBattleVariantEndParty(); + if (FollowerNPCIsBattlePartner()) + { + RestorePartyAfterFollowerNPCBattle(); + if (FNPC_FLAG_HEAL_AFTER_FOLLOWER_BATTLE != 0 + && (FNPC_FLAG_HEAL_AFTER_FOLLOWER_BATTLE == FNPC_ALWAYS + || FlagGet(FNPC_FLAG_HEAL_AFTER_FOLLOWER_BATTLE))) + HealPlayerParty(); + } + if (TRAINER_BATTLE_PARAM.opponentA == TRAINER_SECRET_BASE) { DowngradeBadPoison(); diff --git a/src/battle_tower.c b/src/battle_tower.c index 140d411843..74eeea380e 100644 --- a/src/battle_tower.c +++ b/src/battle_tower.c @@ -75,7 +75,6 @@ static void FillTentTrainerParty_(u16 trainerId, u8 firstMonId, u8 monCount); static void FillFactoryFrontierTrainerParty(u16 trainerId, u8 firstMonId); static void FillFactoryTentTrainerParty(u16 trainerId, u8 firstMonId); static u8 GetFrontierTrainerFixedIvs(u16 trainerId); -static void FillPartnerParty(u16 trainerId); #if FREE_BATTLE_TOWER_E_READER == FALSE static void SetEReaderTrainerChecksum(struct BattleTowerEReaderTrainer *ereaderTrainer); #endif //FREE_BATTLE_TOWER_E_READER @@ -2958,7 +2957,7 @@ void TryHideBattleTowerReporter(void) #define STEVEN_OTID 61226 -static void FillPartnerParty(u16 trainerId) +void FillPartnerParty(u16 trainerId) { s32 i, j, k; u32 firstIdPart = 0, secondIdPart = 0, thirdIdPart = 0; diff --git a/src/battle_util.c b/src/battle_util.c index e76c324234..5af83fb3ad 100644 --- a/src/battle_util.c +++ b/src/battle_util.c @@ -2571,7 +2571,7 @@ bool32 HasNoMonsToSwitch(u32 battler, u8 partyIdBattlerOn1, u8 partyIdBattlerOn2 party = gEnemyParty; // Edge case: If both opposing Pokemon were knocked out on the same turn, - // make sure opponent only sents out the final Pokemon once. + // make sure opponent only sends out the final Pokemon once. if (battler == playerId && (gHitMarker & HITMARKER_FAINTED(flankId)) && (gHitMarker & HITMARKER_FAINTED(playerId))) diff --git a/src/data/object_events/movement_action_func_tables.h b/src/data/object_events/movement_action_func_tables.h index 4f5fd57a82..44639b6c9b 100755 --- a/src/data/object_events/movement_action_func_tables.h +++ b/src/data/object_events/movement_action_func_tables.h @@ -279,6 +279,15 @@ u8 MovementAction_WalkSlowStairsLeft_Step0(struct ObjectEvent *objectEvent, stru u8 MovementAction_WalkSlowStairsLeft_Step1(struct ObjectEvent *objectEvent, struct Sprite *sprite); u8 MovementAction_WalkSlowStairsRight_Step0(struct ObjectEvent *objectEvent, struct Sprite *sprite); u8 MovementAction_WalkSlowStairsRight_Step1(struct ObjectEvent *objectEvent, struct Sprite *sprite); +u8 MovementAction_SurfStillDown_Step0(struct ObjectEvent *objectEvent, struct Sprite *sprite); +u8 MovementAction_SurfStillDown_Step1(struct ObjectEvent *objectEvent, struct Sprite *sprite); +u8 MovementAction_SurfStillUp_Step0(struct ObjectEvent *objectEvent, struct Sprite *sprite); +u8 MovementAction_SurfStillUp_Step1(struct ObjectEvent *objectEvent, struct Sprite *sprite); +u8 MovementAction_SurfStillLeft_Step0(struct ObjectEvent *objectEvent, struct Sprite *sprite); +u8 MovementAction_SurfStillLeft_Step1(struct ObjectEvent *objectEvent, struct Sprite *sprite); +u8 MovementAction_SurfStillRight_Step0(struct ObjectEvent *objectEvent, struct Sprite *sprite); +u8 MovementAction_SurfStillRight_Step1(struct ObjectEvent *objectEvent, struct Sprite *sprite); + u8 (*const gMovementActionFuncs_FaceDown[])(struct ObjectEvent *, struct Sprite *); u8 (*const gMovementActionFuncs_FaceUp[])(struct ObjectEvent *, struct Sprite *); @@ -450,6 +459,10 @@ u8 (*const gMovementActionFuncs_WalkSlowStairsDown[])(struct ObjectEvent *, stru u8 (*const gMovementActionFuncs_WalkSlowStairsUp[])(struct ObjectEvent *, struct Sprite *); u8 (*const gMovementActionFuncs_WalkSlowStairsLeft[])(struct ObjectEvent *, struct Sprite *); u8 (*const gMovementActionFuncs_WalkSlowStairsRight[])(struct ObjectEvent *, struct Sprite *); +u8 (*const gMovementActionFuncs_SurfStillDown[])(struct ObjectEvent *, struct Sprite *); +u8 (*const gMovementActionFuncs_SurfStillUp[])(struct ObjectEvent *, struct Sprite *); +u8 (*const gMovementActionFuncs_SurfStillLeft[])(struct ObjectEvent *, struct Sprite *); +u8 (*const gMovementActionFuncs_SurfStillRight[])(struct ObjectEvent *, struct Sprite *); u8 (*const *const gMovementActionFuncs[])(struct ObjectEvent *, struct Sprite *) = { [MOVEMENT_ACTION_FACE_DOWN] = gMovementActionFuncs_FaceDown, @@ -622,6 +635,10 @@ u8 (*const *const gMovementActionFuncs[])(struct ObjectEvent *, struct Sprite *) [MOVEMENT_ACTION_WALK_SLOW_STAIRS_UP] = gMovementActionFuncs_WalkSlowStairsUp, [MOVEMENT_ACTION_WALK_SLOW_STAIRS_LEFT] = gMovementActionFuncs_WalkSlowStairsLeft, [MOVEMENT_ACTION_WALK_SLOW_STAIRS_RIGHT] = gMovementActionFuncs_WalkSlowStairsRight, + [MOVEMENT_ACTION_SURF_STILL_DOWN] = gMovementActionFuncs_SurfStillDown, + [MOVEMENT_ACTION_SURF_STILL_UP] = gMovementActionFuncs_SurfStillUp, + [MOVEMENT_ACTION_SURF_STILL_LEFT] = gMovementActionFuncs_SurfStillLeft, + [MOVEMENT_ACTION_SURF_STILL_RIGHT] = gMovementActionFuncs_SurfStillRight, }; u8 (*const gMovementActionFuncs_FaceDown[])(struct ObjectEvent *, struct Sprite *) = { @@ -1631,3 +1648,31 @@ bool8 (*const gMovementActionFuncs_WalkSlowStairsRight[])(struct ObjectEvent *, MovementAction_WalkSlowStairsRight_Step1, MovementAction_PauseSpriteAnim, }; + +u8 (*const gMovementActionFuncs_SurfStillDown[])(struct ObjectEvent *, struct Sprite *) = +{ + MovementAction_SurfStillDown_Step0, + MovementAction_SurfStillDown_Step1, + MovementAction_PauseSpriteAnim, +}; + +u8 (*const gMovementActionFuncs_SurfStillUp[])(struct ObjectEvent *, struct Sprite *) = +{ + MovementAction_SurfStillUp_Step0, + MovementAction_SurfStillUp_Step1, + MovementAction_PauseSpriteAnim, +}; + +u8 (*const gMovementActionFuncs_SurfStillLeft[])(struct ObjectEvent *, struct Sprite *) = +{ + MovementAction_SurfStillLeft_Step0, + MovementAction_SurfStillLeft_Step1, + MovementAction_PauseSpriteAnim, +}; + +u8 (*const gMovementActionFuncs_SurfStillRight[])(struct ObjectEvent *, struct Sprite *) = +{ + MovementAction_SurfStillRight_Step0, + MovementAction_SurfStillRight_Step1, + MovementAction_PauseSpriteAnim, +}; diff --git a/src/event_object_movement.c b/src/event_object_movement.c index 483a52b007..aefa8e8ceb 100644 --- a/src/event_object_movement.c +++ b/src/event_object_movement.c @@ -18,6 +18,7 @@ #include "field_player_avatar.h" #include "field_weather.h" #include "fieldmap.h" +#include "follower_npc.h" #include "follower_helper.h" #include "gpu_regs.h" #include "graphics.h" @@ -47,6 +48,7 @@ #include "constants/items.h" #include "constants/map_types.h" #include "constants/mauville_old_man.h" +#include "constants/metatile_behaviors.h" #include "constants/rgb.h" #include "constants/region_map_sections.h" #include "constants/songs.h" @@ -169,7 +171,7 @@ static u16 GetObjectEventFlagIdByObjectEventId(u8); static void UpdateObjectEventVisibility(struct ObjectEvent *, struct Sprite *); static void MakeSpriteTemplateFromObjectEventTemplate(const struct ObjectEventTemplate *, struct SpriteTemplate *, const struct SubspriteTable **); static void GetObjectEventMovingCameraOffset(s16 *, s16 *); -static const struct ObjectEventTemplate *GetObjectEventTemplateByLocalIdAndMap(u8, u8, u8); +const struct ObjectEventTemplate *GetObjectEventTemplateByLocalIdAndMap(u8 localId, u8 mapNum, u8 mapGroup); static void RemoveObjectEventIfOutsideView(struct ObjectEvent *); static void SpawnObjectEventOnReturnToField(u8, s16, s16); static void SetPlayerAvatarObjectEventIdAndObjectId(u8, u8); @@ -199,9 +201,8 @@ static u8 DoJumpSpriteMovement(struct Sprite *); static u8 DoJumpSpecialSpriteMovement(struct Sprite *); static void CreateLevitateMovementTask(struct ObjectEvent *); static void DestroyLevitateMovementTask(u8); -static bool8 GetFollowerInfo(u32 *species, bool32 *shiny, bool32 *female); static u32 LoadDynamicFollowerPalette(u32 species, bool32 shiny, bool32 female); -static const struct ObjectEventGraphicsInfo *SpeciesToGraphicsInfo(u32 species, bool32 shiny, bool32 female); +const struct ObjectEventGraphicsInfo *SpeciesToGraphicsInfo(u32 species, bool32 shiny, bool32 female); static bool8 NpcTakeStep(struct Sprite *); static bool8 AreElevationsCompatible(u8, u8); static void CopyObjectGraphicsInfoToSpriteTemplate_WithMovementType(u16 graphicsId, u16 movementType, struct SpriteTemplate *spriteTemplate, const struct SubspriteTable **subspriteTables); @@ -1353,7 +1354,12 @@ u8 GetFirstInactiveObjectEventId(void) u8 GetObjectEventIdByLocalIdAndMap(u8 localId, u8 mapNum, u8 mapGroupId) { if (localId < OBJ_EVENT_ID_DYNAMIC_BASE) - return GetObjectEventIdByLocalIdAndMapInternal(localId, mapNum, mapGroupId); + { + if (PlayerHasFollowerNPC() && localId == OBJ_EVENT_ID_NPC_FOLLOWER) + return GetFollowerNPCObjectId(); + else + return GetObjectEventIdByLocalIdAndMapInternal(localId, mapNum, mapGroupId); + } return GetObjectEventIdByLocalId(localId); } @@ -1506,7 +1512,7 @@ static bool8 GetAvailableObjectEventId(u16 localId, u8 mapNum, u8 mapGroup, u8 * return FALSE; } -static void RemoveObjectEvent(struct ObjectEvent *objectEvent) +void RemoveObjectEvent(struct ObjectEvent *objectEvent) { objectEvent->active = FALSE; RemoveObjectEventInternal(objectEvent); @@ -1727,7 +1733,7 @@ static u8 TrySetupObjectEventSprite(const struct ObjectEventTemplate *objectEven return objectEventId; } -static u8 TrySpawnObjectEventTemplate(const struct ObjectEventTemplate *objectEventTemplate, u8 mapNum, u8 mapGroup, s16 cameraX, s16 cameraY) +u8 TrySpawnObjectEventTemplate(const struct ObjectEventTemplate *objectEventTemplate, u8 mapNum, u8 mapGroup, s16 cameraX, s16 cameraY) { u8 objectEventId; u16 graphicsId = objectEventTemplate->graphicsId; @@ -1968,7 +1974,7 @@ struct ObjectEvent *GetFollowerObject(void) } // Return graphicsInfo for a pokemon species & form -static const struct ObjectEventGraphicsInfo *SpeciesToGraphicsInfo(u32 species, bool32 shiny, bool32 female) +const struct ObjectEventGraphicsInfo *SpeciesToGraphicsInfo(u32 species, bool32 shiny, bool32 female) { const struct ObjectEventGraphicsInfo *graphicsInfo = NULL; #if OW_POKEMON_OBJECT_EVENTS @@ -2180,7 +2186,7 @@ static bool8 GetMonInfo(struct Pokemon *mon, u32 *species, bool32 *shiny, bool32 } // Retrieve graphic information about the following pokemon, if any -static bool8 GetFollowerInfo(u32 *species, bool32 *shiny, bool32 *female) +bool8 GetFollowerInfo(u32 *species, bool32 *shiny, bool32 *female) { return GetMonInfo(GetFirstLiveMon(), species, shiny, female); } @@ -2197,13 +2203,16 @@ void UpdateFollowingPokemon(void) // 1. GetFollowerInfo returns FALSE // 2. Map is indoors and gfx is larger than 32x32 // 3. flag is set + // 4. a follower NPC is present if (OW_POKEMON_OBJECT_EVENTS == FALSE || OW_FOLLOWERS_ENABLED == FALSE || FlagGet(B_FLAG_FOLLOWERS_DISABLED) || !GetFollowerInfo(&species, &shiny, &female) || SpeciesToGraphicsInfo(species, shiny, female) == NULL || (gMapHeader.mapType == MAP_TYPE_INDOOR && SpeciesToGraphicsInfo(species, shiny, female)->oam->size > ST_OAM_SIZE_2) - || FlagGet(FLAG_TEMP_HIDE_FOLLOWER)) + || FlagGet(FLAG_TEMP_HIDE_FOLLOWER) + || PlayerHasFollowerNPC() + ) { RemoveFollowingPokemon(); return; @@ -2776,7 +2785,9 @@ void RemoveObjectEventsOutsideView(void) // Followers should not go OOB, or their sprites may be freed early during a cross-map scripting event, // such as Wally's Ralts catch sequence - if (objectEvent->active && !objectEvent->isPlayer && objectEvent->localId != OBJ_EVENT_ID_FOLLOWER) + if (objectEvent->active && !objectEvent->isPlayer && objectEvent->localId != OBJ_EVENT_ID_FOLLOWER + && (PlayerHasFollowerNPC() && i != GetFollowerNPCObjectId()) + ) RemoveObjectEventIfOutsideView(objectEvent); } } @@ -3522,7 +3533,7 @@ const u8 *GetObjectEventScriptPointerByObjectEventId(u8 objectEventId) return GetObjectEventScriptPointerByLocalIdAndMap(gObjectEvents[objectEventId].localId, gObjectEvents[objectEventId].mapNum, gObjectEvents[objectEventId].mapGroup); } -static u16 GetObjectEventFlagIdByLocalIdAndMap(u8 localId, u8 mapNum, u8 mapGroup) +u16 GetObjectEventFlagIdByLocalIdAndMap(u8 localId, u8 mapNum, u8 mapGroup) { const struct ObjectEventTemplate *obj = GetObjectEventTemplateByLocalIdAndMap(localId, mapNum, mapGroup); #ifdef UBFIX @@ -3569,7 +3580,7 @@ u8 GetObjectEventBerryTreeId(u8 objectEventId) return gObjectEvents[objectEventId].trainerRange_berryTreeId; } -static const struct ObjectEventTemplate *GetObjectEventTemplateByLocalIdAndMap(u8 localId, u8 mapNum, u8 mapGroup) +const struct ObjectEventTemplate *GetObjectEventTemplateByLocalIdAndMap(u8 localId, u8 mapNum, u8 mapGroup) { const struct ObjectEventTemplate *templates; const struct MapHeader *mapHeader; @@ -6429,7 +6440,9 @@ u32 GetObjectObjectCollidesWith(struct ObjectEvent *objectEvent, s16 x, s16 y, b for (i = 0; i < OBJECT_EVENTS_COUNT; i++) { curObject = &gObjectEvents[i]; - if (curObject->active && (curObject->movementType != MOVEMENT_TYPE_FOLLOW_PLAYER || objectEvent != &gObjectEvents[gPlayerAvatar.objectEventId]) && curObject != objectEvent) + if (curObject->active && (curObject->movementType != MOVEMENT_TYPE_FOLLOW_PLAYER || objectEvent != &gObjectEvents[gPlayerAvatar.objectEventId]) && curObject != objectEvent + && !FollowerNPC_IsCollisionExempt(curObject, objectEvent) + ) { // check for collision if curObject is active, not the object in question, and not exempt from collisions if ((curObject->currentCoords.x == x && curObject->currentCoords.y == y) || (curObject->previousCoords.x == x && curObject->previousCoords.y == y)) @@ -6621,6 +6634,7 @@ bool8 ObjectEventSetHeldMovement(struct ObjectEvent *objectEvent, u8 movementAct objectEvent->heldMovementActive = TRUE; objectEvent->heldMovementFinished = FALSE; gSprites[objectEvent->spriteId].sActionFuncId = 0; + NPCFollow(objectEvent, movementActionId, FALSE); // When player is moved via script, set copyable movement // for any followers via a lookup table @@ -11206,6 +11220,20 @@ bool8 MovementAction_EmoteDoubleExclamationMark_Step0(struct ObjectEvent *object return TRUE; } +bool8 PlayerIsUnderWaterfall(struct ObjectEvent *objectEvent) +{ + s16 x; + s16 y; + + x = objectEvent->currentCoords.x; + y = objectEvent->currentCoords.y; + MoveCoordsInDirection(DIR_NORTH, &x, &y, 0, 1); + if (MetatileBehavior_IsWaterfall(MapGridGetMetatileBehaviorAt(x, y))) + return TRUE; + + return FALSE; +} + // Get gfx data from daycare pokemon and store it in vars void GetDaycareGraphics(struct ScriptContext *ctx) { @@ -11385,3 +11413,86 @@ static u16 GetUnownSpecies(struct Pokemon *mon) return SPECIES_UNOWN; return SPECIES_UNOWN_B + form - 1; } + +static void InitMovementSurfStill(struct ObjectEvent *objectEvent, struct Sprite *sprite, u8 direction, u8 speed) +{ + u8 (*functions[ARRAY_COUNT(sDirectionAnimFuncsBySpeed)])(u8); + + memcpy(functions, sDirectionAnimFuncsBySpeed, sizeof sDirectionAnimFuncsBySpeed); + InitNpcForMovement(objectEvent, sprite, direction, speed); + ObjectEventTurn(objectEvent, direction); +} + +bool8 MovementAction_SurfStillDown_Step0(struct ObjectEvent *objectEvent, struct Sprite *sprite) +{ + InitMovementSurfStill(objectEvent, sprite, DIR_SOUTH, MOVE_SPEED_FAST_1); + sprite->animPaused = TRUE; + return MovementAction_SurfStillDown_Step1(objectEvent, sprite); +} + +bool8 MovementAction_SurfStillDown_Step1(struct ObjectEvent *objectEvent, struct Sprite *sprite) +{ + if (UpdateMovementNormal(objectEvent, sprite)) + { + sprite->sActionFuncId = 2; + return TRUE; + } + return FALSE; +} + +bool8 MovementAction_SurfStillUp_Step0(struct ObjectEvent *objectEvent, struct Sprite *sprite) +{ + InitMovementSurfStill(objectEvent, sprite, DIR_NORTH, MOVE_SPEED_FAST_1); + sprite->animPaused = TRUE; + return MovementAction_SurfStillUp_Step1(objectEvent, sprite); +} + +bool8 MovementAction_SurfStillUp_Step1(struct ObjectEvent *objectEvent, struct Sprite *sprite) +{ + if (UpdateMovementNormal(objectEvent, sprite)) + { + sprite->sActionFuncId = 2; + return TRUE; + } + return FALSE; +} + +bool8 MovementAction_SurfStillLeft_Step0(struct ObjectEvent *objectEvent, struct Sprite *sprite) +{ + if (objectEvent->directionOverwrite) + InitMovementSurfStill(objectEvent, sprite, objectEvent->directionOverwrite, MOVE_SPEED_FAST_1); + else + InitMovementSurfStill(objectEvent, sprite, DIR_WEST, MOVE_SPEED_FAST_1); + sprite->animPaused = TRUE; + return MovementAction_SurfStillLeft_Step1(objectEvent, sprite); +} + +bool8 MovementAction_SurfStillLeft_Step1(struct ObjectEvent *objectEvent, struct Sprite *sprite) +{ + if (UpdateMovementNormal(objectEvent, sprite)) + { + sprite->sActionFuncId = 2; + return TRUE; + } + return FALSE; +} + +bool8 MovementAction_SurfStillRight_Step0(struct ObjectEvent *objectEvent, struct Sprite *sprite) +{ + if (objectEvent->directionOverwrite) + InitMovementSurfStill(objectEvent, sprite, objectEvent->directionOverwrite, MOVE_SPEED_FAST_1); + else + InitMovementSurfStill(objectEvent, sprite, DIR_EAST, MOVE_SPEED_FAST_1); + sprite->animPaused = TRUE; + return MovementAction_SurfStillRight_Step1(objectEvent, sprite); +} + +bool8 MovementAction_SurfStillRight_Step1(struct ObjectEvent *objectEvent, struct Sprite *sprite) +{ + if (UpdateMovementNormal(objectEvent, sprite)) + { + sprite->sActionFuncId = 2; + return TRUE; + } + return FALSE; +} diff --git a/src/field_control_avatar.c b/src/field_control_avatar.c index 2c4247fe21..e1b9cb3a83 100644 --- a/src/field_control_avatar.c +++ b/src/field_control_avatar.c @@ -17,6 +17,7 @@ #include "field_screen_effect.h" #include "field_specials.h" #include "fldeff_misc.h" +#include "follower_npc.h" #include "item_menu.h" #include "link.h" #include "match_call.h" @@ -399,6 +400,8 @@ static const u8 *GetInteractedObjectEventScript(struct MapPosition *position, u8 if (InTrainerHill() == TRUE) script = GetTrainerHillTrainerScript(); + else if (PlayerHasFollowerNPC() && objectEventId == GetFollowerNPCObjectId()) + script = GetFollowerNPCScriptPointer(); else script = GetObjectEventScriptPointerByObjectEventId(objectEventId); @@ -557,10 +560,14 @@ static const u8 *GetInteractedMetatileScript(struct MapPosition *position, u8 me static const u8 *GetInteractedWaterScript(struct MapPosition *unused1, u8 metatileBehavior, u8 direction) { - if (FlagGet(FLAG_BADGE05_GET) == TRUE && PartyHasMonWithSurf() == TRUE && IsPlayerFacingSurfableFishableWater() == TRUE) + if (FlagGet(FLAG_BADGE05_GET) == TRUE && PartyHasMonWithSurf() == TRUE && IsPlayerFacingSurfableFishableWater() == TRUE + && CheckFollowerNPCFlag(FOLLOWER_NPC_FLAG_CAN_SURF) + ) return EventScript_UseSurf; - if (MetatileBehavior_IsWaterfall(metatileBehavior) == TRUE) + if (MetatileBehavior_IsWaterfall(metatileBehavior) == TRUE + && CheckFollowerNPCFlag(FOLLOWER_NPC_FLAG_CAN_WATERFALL) + ) { if (FlagGet(FLAG_BADGE08_GET) == TRUE && IsPlayerSurfingNorth() == TRUE) return EventScript_UseWaterfall; @@ -572,6 +579,9 @@ static const u8 *GetInteractedWaterScript(struct MapPosition *unused1, u8 metati static bool32 TrySetupDiveDownScript(void) { + if (!CheckFollowerNPCFlag(FOLLOWER_NPC_FLAG_CAN_DIVE)) + return FALSE; + if (FlagGet(FLAG_BADGE07_GET) && TrySetDiveWarp() == 2) { ScriptContext_SetupScript(EventScript_UseDive); @@ -582,6 +592,9 @@ static bool32 TrySetupDiveDownScript(void) static bool32 TrySetupDiveEmergeScript(void) { + if (!CheckFollowerNPCFlag(FOLLOWER_NPC_FLAG_CAN_DIVE)) + return FALSE; + if (FlagGet(FLAG_BADGE07_GET) && gMapHeader.mapType == MAP_TYPE_UNDERWATER && TrySetDiveWarp() == 1) { ScriptContext_SetupScript(EventScript_UseDiveUnderwater); diff --git a/src/field_effect.c b/src/field_effect.c index 72b542ee03..1f2feaba8f 100644 --- a/src/field_effect.c +++ b/src/field_effect.c @@ -12,6 +12,7 @@ #include "field_weather.h" #include "fieldmap.h" #include "fldeff.h" +#include "follower_npc.h" #include "gpu_regs.h" #include "main.h" #include "malloc.h" @@ -145,6 +146,7 @@ static bool8 LavaridgeGym1FWarpEffect_Warp(struct Task *, struct ObjectEvent *, static void Task_EscapeRopeWarpOut(u8); static void EscapeRopeWarpOutEffect_Init(struct Task *); +static void EscapeRopeWarpOutEffect_HideFollowerNPC(struct Task *); static void EscapeRopeWarpOutEffect_Spin(struct Task *); static void FieldCallback_EscapeRopeWarpIn(void); @@ -696,6 +698,7 @@ static bool8 (*const sLavaridgeGym1FWarpEffectFuncs[])(struct Task *, struct Obj static void (*const sEscapeRopeWarpOutEffectFuncs[])(struct Task *) = { EscapeRopeWarpOutEffect_Init, + EscapeRopeWarpOutEffect_HideFollowerNPC, EscapeRopeWarpOutEffect_Spin, }; @@ -1377,32 +1380,61 @@ void FieldCallback_UseFly(void) gFieldCallback = NULL; } +#define taskState task->data[3] +#define fieldEffectStarted task->data[0] + static void Task_UseFly(u8 taskId) { + struct ObjectEvent *follower = &gObjectEvents[GetFollowerNPCObjectId()]; struct Task *task; task = &gTasks[taskId]; - if (!task->data[0]) + if (taskState == 0) { - if (!IsWeatherNotFadingIn()) - return; - - gFieldEffectArguments[0] = GetCursorSelectionMonId(); - if ((int)gFieldEffectArguments[0] > PARTY_SIZE - 1) - gFieldEffectArguments[0] = 0; - - FieldEffectStart(FLDEFF_USE_FLY); - task->data[0]++; + if (!PlayerHasFollowerNPC()) + { + taskState = 2; + } + else + { + FollowerNPCWalkIntoPlayerForLeaveMap(); + taskState++; + } } - if (!FieldEffectActiveListContains(FLDEFF_USE_FLY)) + if (taskState == 1) { - Overworld_ResetStateAfterFly(); - WarpIntoMap(); - SetMainCallback2(CB2_LoadMap); - gFieldCallback = FieldCallback_FlyIntoMap; - DestroyTask(taskId); + if (ObjectEventClearHeldMovementIfFinished(follower)) + { + FollowerNPCHideForLeaveMap(follower); + taskState++; + } + } + if (taskState == 2) + { + if (!fieldEffectStarted) + { + if (!IsWeatherNotFadingIn()) + return; + + gFieldEffectArguments[0] = GetCursorSelectionMonId(); + if ((int)gFieldEffectArguments[0] > PARTY_SIZE - 1) + gFieldEffectArguments[0] = 0; + + FieldEffectStart(FLDEFF_USE_FLY); + fieldEffectStarted = TRUE; + } + if (!FieldEffectActiveListContains(FLDEFF_USE_FLY)) + { + Overworld_ResetStateAfterFly(); + WarpIntoMap(); + SetMainCallback2(CB2_LoadMap); + gFieldCallback = FieldCallback_FlyIntoMap; + DestroyTask(taskId); + } } } +#undef taskState + static void FieldCallback_FlyIntoMap(void) { Overworld_PlaySpecialMapMusic(); @@ -1418,20 +1450,51 @@ static void FieldCallback_FlyIntoMap(void) gFieldCallback = NULL; } +#define taskState task->data[0] +#define tWaitPaletteFadeIn 0 +#define tWaitFieldEffectEnd 1 +#define tNPCFollowerFacePlayer 2 +#define tTaskEnd 3 + static void Task_FlyIntoMap(u8 taskId) { + struct ObjectEvent *player = &gObjectEvents[gPlayerAvatar.objectEventId]; + struct ObjectEvent *follower = &gObjectEvents[GetFollowerNPCObjectId()]; struct Task *task; task = &gTasks[taskId]; - if (task->data[0] == 0) + if (taskState == tWaitPaletteFadeIn) { if (gPaletteFade.active) { return; } FieldEffectStart(FLDEFF_FLY_IN); - task->data[0]++; + taskState++; } - if (!FieldEffectActiveListContains(FLDEFF_FLY_IN)) + if (taskState == tWaitFieldEffectEnd) + { + if (!FieldEffectActiveListContains(FLDEFF_FLY_IN)) + { + if (FNPC_NPC_FOLLOWER_SHOW_AFTER_LEAVE_ROUTE) + FollowerNPCReappearAfterLeaveMap(follower, player); + + taskState++; + } + } + if (taskState == tNPCFollowerFacePlayer) + { + if (PlayerHasFollowerNPC() && ObjectEventClearHeldMovementIfFinished(follower)) + { + if (FNPC_NPC_FOLLOWER_SHOW_AFTER_LEAVE_ROUTE) + FollowerNPCFaceAfterLeaveMap(); + taskState++; + } + else if (!PlayerHasFollowerNPC()) + { + taskState++; + } + } + if (taskState == tTaskEnd) { UnlockPlayerFieldControls(); UnfreezeObjectEvents(); @@ -1571,6 +1634,8 @@ static bool8 FallWarpEffect_End(struct Task *task) UnfreezeObjectEvents(); InstallCameraPanAheadCallback(); DestroyTask(FindTaskIdByFunc(Task_FallWarpFieldEffect)); + FollowerNPC_WarpSetEnd(); + return FALSE; } @@ -1633,7 +1698,9 @@ static bool8 EscalatorWarpOut_WaitForPlayer(struct Task *task) task->tState++; task->data[2] = 0; task->data[3] = 0; - if ((u8)task->tGoingUp == FALSE) + EscalatorMoveFollowerNPC(task->data[1]); + + if ((u8)task->data[1] == FALSE) { task->tState = 4; // jump to EscalatorWarpOut_Down_Ride } @@ -1755,6 +1822,7 @@ static bool8 EscalatorWarpIn_Init(struct Task *task) ObjectEventSetHeldMovement(objectEvent, GetFaceDirectionMovementAction(DIR_EAST)); PlayerGetDestCoords(&x, &y); behavior = MapGridGetMetatileBehaviorAt(x, y); + EscalatorMoveFollowerNPCFinish(); task->tState++; task->data[1] = 16; @@ -2005,12 +2073,22 @@ static bool8 LavaridgeGymB1FWarpEffect_Init(struct Task *task, struct ObjectEven task->data[1] = 1; task->data[0]++; if (objectEvent->localId == OBJ_EVENT_ID_PLAYER) // Hide follower before warping + { HideFollowerForFieldEffect(); + if (PlayerHasFollowerNPC() && gObjectEvents[GetFollowerNPCObjectId()].invisible == FALSE) + { + FollowerNPCWalkIntoPlayerForLeaveMap(); + CreateTask(Task_HideNPCFollowerAfterMovementFinish, 2); + } + } return TRUE; } static bool8 LavaridgeGymB1FWarpEffect_CameraShake(struct Task *task, struct ObjectEvent *objectEvent, struct Sprite *sprite) { + if (FindTaskIdByFunc(Task_HideNPCFollowerAfterMovementFinish) != TASK_NONE) + return FALSE; + SetCameraPanning(0, task->data[1]); task->data[1] = -task->data[1]; task->data[2]++; @@ -2201,12 +2279,22 @@ static bool8 LavaridgeGym1FWarpEffect_Init(struct Task *task, struct ObjectEvent objectEvent->noShadow = TRUE; task->data[0]++; if (objectEvent->localId == OBJ_EVENT_ID_PLAYER) // Hide follower before warping + { HideFollowerForFieldEffect(); + if (PlayerHasFollowerNPC() && gObjectEvents[GetFollowerNPCObjectId()].invisible == FALSE) + { + FollowerNPCWalkIntoPlayerForLeaveMap(); + CreateTask(Task_HideNPCFollowerAfterMovementFinish, 2); + } + } return FALSE; } static bool8 LavaridgeGym1FWarpEffect_AshPuff(struct Task *task, struct ObjectEvent *objectEvent, struct Sprite *sprite) { + if (FindTaskIdByFunc(Task_HideNPCFollowerAfterMovementFinish) != TASK_NONE) + return FALSE; + if (ObjectEventClearHeldMovementIfFinished(objectEvent)) { if (task->data[1] > 3) @@ -2278,11 +2366,18 @@ void SpriteCB_AshPuff(struct Sprite *sprite) FieldEffectStop(sprite, FLDEFF_ASH_PUFF); } -#define tState data[0] -#define tSpinDelay data[1] -#define tNumTurns data[2] -#define tTimer data[14] -#define tStartDir data[15] +#define tState data[0] +#define tSpinDelay data[1] +#define tNumTurns data[2] +#define tHideFollower data[3] +#define tTimer data[14] +#define tStartDir data[15] + +enum +{ + START_MOVEMENT, + WAIT_MOVEMENT_END +}; void StartEscapeRopeFieldEffect(void) { @@ -2299,11 +2394,40 @@ static void Task_EscapeRopeWarpOut(u8 taskId) static void EscapeRopeWarpOutEffect_Init(struct Task *task) { - task->tState++; + if (PlayerHasFollowerNPC()) + task->tState++; + else + task->tState += 2; + task->tTimer = 64; task->tStartDir = GetPlayerFacingDirection(); } +static void EscapeRopeWarpOutEffect_HideFollowerNPC(struct Task *task) +{ + struct ObjectEvent *follower = &gObjectEvents[GetFollowerNPCObjectId()]; + if (task->tHideFollower == START_MOVEMENT) + { + if (!PlayerHasFollowerNPC()) + { + task->tState++; + } + else + { + FollowerNPCWalkIntoPlayerForLeaveMap(); + task->tHideFollower = WAIT_MOVEMENT_END; + } + } + if (task->tHideFollower == WAIT_MOVEMENT_END) + { + if (ObjectEventClearHeldMovementIfFinished(follower)) + { + FollowerNPCHideForLeaveMap(follower); + task->tState++; + } + } +} + static void EscapeRopeWarpOutEffect_Spin(struct Task *task) { struct ObjectEvent *objectEvent; @@ -2362,33 +2486,66 @@ static void EscapeRopeWarpInEffect_Init(struct Task *task) { task->tState++; task->tStartDir = GetPlayerFacingDirection(); + task->data[3] = 0; } } static void EscapeRopeWarpInEffect_Spin(struct Task *task) { u8 spinDirections[5] = {DIR_SOUTH, DIR_WEST, DIR_EAST, DIR_NORTH, DIR_SOUTH}; - struct ObjectEvent *objectEvent = &gObjectEvents[gPlayerAvatar.objectEventId]; - if (task->tSpinDelay == 0 || (--task->tSpinDelay) == 0) + struct ObjectEvent *player = &gObjectEvents[gPlayerAvatar.objectEventId]; + struct ObjectEvent *follower = &gObjectEvents[GetFollowerNPCObjectId()]; + + if ((task->tSpinDelay == 0 || (--task->tSpinDelay) == 0) && task->data[3] == 0) { - if (ObjectEventIsMovementOverridden(objectEvent) && !ObjectEventClearHeldMovementIfFinished(objectEvent)) + if (ObjectEventIsMovementOverridden(player) && !ObjectEventClearHeldMovementIfFinished(player)) { return; } if (task->tNumTurns >= 32 && task->tStartDir == GetPlayerFacingDirection()) { - objectEvent->invisible = FALSE; - UnlockPlayerFieldControls(); - UnfreezeObjectEvents(); - DestroyTask(FindTaskIdByFunc(Task_EscapeRopeWarpIn)); - return; + task->data[3]++; } - ObjectEventSetHeldMovement(objectEvent, GetFaceDirectionMovementAction(spinDirections[objectEvent->facingDirection])); + if (task->data[3] == 0) + ObjectEventSetHeldMovement(player, GetFaceDirectionMovementAction(spinDirections[player->facingDirection])); + if (task->tNumTurns < 32) task->tNumTurns++; + task->tSpinDelay = task->tNumTurns >> 2; } - objectEvent->invisible ^= 1; + if (task->data[3] == 0) + player->invisible ^= 1; + + if (task->data[3] == 1) + { + if (FNPC_NPC_FOLLOWER_SHOW_AFTER_LEAVE_ROUTE) + FollowerNPCReappearAfterLeaveMap(follower, player); + + task->data[3]++; + } + if (task->data[3] == 2) + { + if (PlayerHasFollowerNPC() && ObjectEventClearHeldMovementIfFinished(follower)) + { + if (FNPC_NPC_FOLLOWER_SHOW_AFTER_LEAVE_ROUTE) + FollowerNPCFaceAfterLeaveMap(); + + task->data[3]++; + } + else if (!PlayerHasFollowerNPC()) + { + task->data[3]++; + } + } + if (task->data[3] == 3) + { + player->invisible = FALSE; + UnlockPlayerFieldControls(); + UnfreezeObjectEvents(); + DestroyTask(FindTaskIdByFunc(Task_EscapeRopeWarpIn)); + return; + } } #undef tState @@ -2573,25 +2730,49 @@ static void TeleportWarpInFieldEffect_SpinEnter(struct Task *task) task->data[0]++; task->data[1] = 1; task->data[2] = 0; + task->data[3] = 0; } } static void TeleportWarpInFieldEffect_SpinGround(struct Task *task) { + struct ObjectEvent *player = &gObjectEvents[gPlayerAvatar.objectEventId]; + struct ObjectEvent *follower = &gObjectEvents[GetFollowerNPCObjectId()]; + u8 spinDirections[5] = {DIR_SOUTH, DIR_WEST, DIR_EAST, DIR_NORTH, DIR_SOUTH}; - struct ObjectEvent *objectEvent = &gObjectEvents[gPlayerAvatar.objectEventId]; - if ((--task->data[1]) == 0) + if ((--task->data[1]) == 0 && task->data[3] == 0) { - ObjectEventTurn(objectEvent, spinDirections[objectEvent->facingDirection]); + ObjectEventTurn(player, spinDirections[player->facingDirection]); task->data[1] = 8; - if ((++task->data[2]) > 4 && task->data[14] == objectEvent->facingDirection) + if ((++task->data[2]) > 4 && task->data[14] == player->facingDirection) { - UnlockPlayerFieldControls(); - CameraObjectReset(); - UnfreezeObjectEvents(); - DestroyTask(FindTaskIdByFunc(Task_TeleportWarpIn)); + if (FNPC_NPC_FOLLOWER_SHOW_AFTER_LEAVE_ROUTE) + FollowerNPCReappearAfterLeaveMap(follower, player); + + task->data[3] = 1; } } + if (task->data[3] == 1) + { + if (PlayerHasFollowerNPC() && ObjectEventClearHeldMovementIfFinished(follower)) + { + if (FNPC_NPC_FOLLOWER_SHOW_AFTER_LEAVE_ROUTE) + FollowerNPCFaceAfterLeaveMap(); + + task->data[3]++; + } + else if (!PlayerHasFollowerNPC()) + { + task->data[3]++; + } + } + if (task->data[3] == 2) + { + UnlockPlayerFieldControls(); + CameraObjectReset(); + UnfreezeObjectEvents(); + DestroyTask(FindTaskIdByFunc(Task_TeleportWarpIn)); + } } // Task data for Task_FieldMoveShowMonOutDoors @@ -3096,6 +3277,8 @@ static void SurfFieldEffect_JumpOnSurfBlob(struct Task *task) ObjectEventSetGraphicsId(objectEvent, GetPlayerAvatarGraphicsIdByStateId(PLAYER_AVATAR_STATE_SURFING)); ObjectEventClearHeldMovementIfFinished(objectEvent); ObjectEventSetHeldMovement(objectEvent, GetJumpSpecialMovementAction(objectEvent->movementDirection)); + FollowerNPC_FollowerToWater(); + gFieldEffectArguments[0] = task->tDestX; gFieldEffectArguments[1] = task->tDestY; gFieldEffectArguments[2] = gPlayerAvatar.objectEventId; diff --git a/src/field_player_avatar.c b/src/field_player_avatar.c index 027fde989a..d264fb2581 100644 --- a/src/field_player_avatar.c +++ b/src/field_player_avatar.c @@ -9,6 +9,7 @@ #include "field_screen_effect.h" #include "field_player_avatar.h" #include "fieldmap.h" +#include "follower_npc.h" #include "menu.h" #include "metatile_behavior.h" #include "overworld.h" @@ -28,6 +29,7 @@ #include "constants/event_object_movement.h" #include "constants/field_effects.h" #include "constants/items.h" +#include "constants/metatile_behaviors.h" #include "constants/moves.h" #include "constants/songs.h" #include "constants/trainer_types.h" @@ -66,7 +68,6 @@ static bool8 TryInterruptObjectEventSpecialAnim(struct ObjectEvent *, u8); static void npc_clear_strange_bits(struct ObjectEvent *); static void MovePlayerAvatarUsingKeypadInput(u8, u16, u16); static void PlayerAllowForcedMovementIfMovingSameDirection(); -static bool8 TryDoMetatileBehaviorForcedMovement(); static u8 GetForcedMovementByMetatileBehavior(); static bool8 ForcedMovement_None(void); @@ -443,7 +444,7 @@ static void PlayerAllowForcedMovementIfMovingSameDirection(void) gPlayerAvatar.flags &= ~PLAYER_AVATAR_FLAG_CONTROLLABLE; } -static bool8 TryDoMetatileBehaviorForcedMovement(void) +bool8 TryDoMetatileBehaviorForcedMovement(void) { return sForcedMovementFuncs[GetForcedMovementByMetatileBehavior()](); } @@ -520,6 +521,10 @@ static bool8 DoForcedMovement(u8 direction, void (*moveFunc)(u8)) { playerAvatar->runningState = MOVING; moveFunc(direction); + if (PlayerHasFollowerNPC() + && gObjectEvents[GetFollowerNPCObjectId()].invisible == FALSE + && FindTaskIdByFunc(Task_MoveNPCFollowerAfterForcedMovement) == TASK_NONE) + CreateTask(Task_MoveNPCFollowerAfterForcedMovement, 3); return TRUE; } } @@ -826,7 +831,7 @@ static void PlayerNotOnBikeMoving(u8 direction, u16 heldKeys) } if (!(gPlayerAvatar.flags & PLAYER_AVATAR_FLAG_UNDERWATER) && (heldKeys & B_BUTTON) && FlagGet(FLAG_SYS_B_DASH) - && IsRunningDisallowed(gObjectEvents[gPlayerAvatar.objectEventId].currentMetatileBehavior) == 0) + && IsRunningDisallowed(gObjectEvents[gPlayerAvatar.objectEventId].currentMetatileBehavior) == 0 && !FollowerNPCComingThroughDoor()) { if (ObjectMovingOnRockStairs(&gObjectEvents[gPlayerAvatar.objectEventId], direction)) PlayerRunSlow(direction); @@ -917,7 +922,9 @@ static bool8 CanStopSurfing(s16 x, s16 y, u8 direction) { if ((gPlayerAvatar.flags & PLAYER_AVATAR_FLAG_SURFING) && MapGridGetElevationAt(x, y) == 3 - && GetObjectEventIdByPosition(x, y, 3) == OBJECT_EVENTS_COUNT) + && (GetObjectEventIdByPosition(x, y, 3) == OBJECT_EVENTS_COUNT + || GetObjectEventIdByPosition(x, y, 3) == GetFollowerNPCObjectId() + )) { CreateStopSurfingTask(direction); return TRUE; @@ -1205,6 +1212,22 @@ void PlayerOnBikeCollide(u8 direction) { PlayCollisionSoundIfNotFacingWarp(direction); PlayerSetAnimId(GetWalkInPlaceNormalMovementAction(direction), COPY_MOVE_WALK); + // Edge case: If the player stops at the top of a mud slide, but the NPC follower is still on a mud slide tile, + // move the follower into the player and hide them. + if (PlayerHasFollowerNPC()) + { + struct ObjectEvent *npcFollower = &gObjectEvents[GetFollowerNPCObjectId()]; + struct ObjectEvent *player = &gObjectEvents[gPlayerAvatar.objectEventId]; + + if (npcFollower->invisible == FALSE + && player->currentMetatileBehavior != MB_MUDDY_SLOPE + && npcFollower->currentMetatileBehavior == MB_MUDDY_SLOPE) + { + gPlayerAvatar.preventStep = TRUE; + ObjectEventSetHeldMovement(npcFollower, MOVEMENT_ACTION_WALK_FAST_UP); + CreateTask(Task_HideNPCFollowerAfterMovementFinish, 2); + } + } } void PlayerOnBikeCollideWithFarawayIslandMew(u8 direction) @@ -1596,6 +1619,7 @@ void InitPlayerAvatar(s16 x, s16 y, u8 direction, u8 gender) gPlayerAvatar.spriteId = objectEvent->spriteId; gPlayerAvatar.gender = gender; SetPlayerAvatarStateMask(PLAYER_AVATAR_FLAG_CONTROLLABLE | PLAYER_AVATAR_FLAG_ON_FOOT); + CreateFollowerNPCAvatar(); } void SetPlayerInvisibility(bool8 invisible) @@ -1845,6 +1869,7 @@ static void CreateStopSurfingTask(u8 direction) taskId = CreateTask(Task_StopSurfingInit, 0xFF); gTasks[taskId].data[0] = direction; Task_StopSurfingInit(taskId); + PrepareFollowerNPCDismountSurf(); } static void Task_StopSurfingInit(u8 taskId) diff --git a/src/field_screen_effect.c b/src/field_screen_effect.c index 726f5539b7..54e330149d 100644 --- a/src/field_screen_effect.c +++ b/src/field_screen_effect.c @@ -12,6 +12,7 @@ #include "field_screen_effect.h" #include "field_special_scene.h" #include "field_weather.h" +#include "follower_npc.h" #include "gpu_regs.h" #include "heal_location.h" #include "io_reg.h" @@ -48,8 +49,6 @@ static void FillPalBufferWhite(void); static void Task_ExitDoor(u8); static bool32 WaitForWeatherFadeIn(void); static void Task_SpinEnterWarp(u8 taskId); -static void Task_WarpAndLoadMap(u8 taskId); -static void Task_DoDoorWarp(u8 taskId); static void Task_EnableScriptAfterMusicFade(u8 taskId); static void ExitStairsMovement(s16*, s16*, s16*, s16*, s16*); @@ -125,7 +124,7 @@ void WarpFadeOutScreen(void) } } -static void SetPlayerVisibility(bool8 visible) +void SetPlayerVisibility(bool8 visible) { SetPlayerInvisibility(!visible); } @@ -293,6 +292,7 @@ void FieldCB_DefaultWarpExit(void) Overworld_PlaySpecialMapMusic(); WarpFadeInScreen(); SetUpWarpExitTask(); + FollowerNPC_WarpSetEnd(); LockPlayerFieldControls(); } @@ -341,6 +341,7 @@ static void Task_ExitDoor(u8 taskId) switch (task->tState) { case 0: + HideNPCFollower(); SetPlayerVisibility(FALSE); FreezeObjectEvents(); PlayerGetDestCoords(x, y); @@ -370,6 +371,8 @@ static void Task_ExitDoor(u8 taskId) case 3: if (task->data[1] < 0 || gTasks[task->data[1]].isActive != TRUE) { + FollowerNPC_SetIndicatorToComeOutDoor(); + FollowerNPC_WarpSetEnd(); UnfreezeObjectEvents(); task->tState = 4; } @@ -390,6 +393,7 @@ static void Task_ExitNonAnimDoor(u8 taskId) switch (task->tState) { case 0: + HideNPCFollower(); SetPlayerVisibility(FALSE); FreezeObjectEvents(); PlayerGetDestCoords(x, y); @@ -408,6 +412,14 @@ static void Task_ExitNonAnimDoor(u8 taskId) case 2: if (IsPlayerStandingStill()) { + s16 x, y; + + PlayerGetDestCoords(&x, &y); + if (!MetatileBehavior_IsDeepSouthWarp(MapGridGetMetatileBehaviorAt(x, y + 1))) + FollowerNPC_SetIndicatorToComeOutDoor(); + // TODO: Add specific follower door warp behavior for MB_DEEP_SOUTH_WARP. + + FollowerNPC_WarpSetEnd(); UnfreezeObjectEvents(); task->tState = 3; } @@ -661,7 +673,7 @@ void ReturnFromLinkRoom(void) CreateTask(Task_ReturnToWorldFromLinkRoom, 10); } -static void Task_WarpAndLoadMap(u8 taskId) +void Task_WarpAndLoadMap(u8 taskId) { struct Task *task = &gTasks[taskId]; @@ -692,16 +704,35 @@ static void Task_WarpAndLoadMap(u8 taskId) } } -static void Task_DoDoorWarp(u8 taskId) +#define tDoorTask data[1] + +enum +{ + DOORWARP_OPEN_DOOR, + DOORWARP_START_WALK_UP, + DOORWARP_HIDE_PLAYER, + DOORWARP_WAIT_DOOR_ANIM_TASK, + DOORWARP_DO_WARP +}; + +void Task_DoDoorWarp(u8 taskId) { struct Task *task = &gTasks[taskId]; s16 *x = &task->data[2]; s16 *y = &task->data[3]; + u8 playerObjId = gPlayerAvatar.objectEventId; + u8 followerObjId = GetFollowerNPCObjectId(); struct ObjectEvent *followerObject = GetFollowerObject(); switch (task->tState) { - case 0: + case DOORWARP_OPEN_DOOR: + // Stop running. + if (TestPlayerAvatarFlags(PLAYER_AVATAR_FLAG_DASH)) + SetPlayerAvatarTransitionFlags(PLAYER_AVATAR_FLAG_ON_FOOT); + + // Just in case came out and went right back in, reset follower NPC door state. + SetFollowerNPCData(FNPC_DATA_COME_OUT_DOOR, FNPC_DOOR_NONE); FreezeObjectEvents(); PlayerGetDestCoords(x, y); PlaySE(GetDoorSoundEffect(*x, *y - 1)); @@ -711,38 +742,49 @@ static void Task_DoDoorWarp(u8 taskId) ClearObjectEventMovement(followerObject, &gSprites[followerObject->spriteId]); ObjectEventSetHeldMovement(followerObject, MOVEMENT_ACTION_ENTER_POKEBALL); } - task->data[1] = FieldAnimateDoorOpen(*x, *y - 1); - task->tState = 1; + task->tDoorTask = FieldAnimateDoorOpen(*x, *y - 1); + task->tState = DOORWARP_START_WALK_UP; break; - case 1: - if (task->data[1] < 0 || gTasks[task->data[1]].isActive != TRUE) + case DOORWARP_START_WALK_UP: + if (task->tDoorTask < 0 || gTasks[task->tDoorTask].isActive != TRUE) { - u8 objEventId; - objEventId = GetObjectEventIdByLocalIdAndMap(OBJ_EVENT_ID_PLAYER, 0, 0); - ObjectEventClearHeldMovementIfActive(&gObjectEvents[objEventId]); - objEventId = GetObjectEventIdByLocalIdAndMap(OBJ_EVENT_ID_PLAYER, 0, 0); - ObjectEventSetHeldMovement(&gObjectEvents[objEventId], MOVEMENT_ACTION_WALK_NORMAL_UP); - task->tState = 2; + ObjectEventClearHeldMovementIfActive(&gObjectEvents[playerObjId]); + ObjectEventSetHeldMovement(&gObjectEvents[playerObjId], MOVEMENT_ACTION_WALK_NORMAL_UP); + + if (PlayerHasFollowerNPC() && !gObjectEvents[followerObjId].invisible) + { + u8 newState = DetermineFollowerNPCState(&gObjectEvents[followerObjId], MOVEMENT_ACTION_WALK_NORMAL_UP, + DetermineFollowerNPCDirection(&gObjectEvents[playerObjId], &gObjectEvents[followerObjId])); + ObjectEventClearHeldMovementIfActive(&gObjectEvents[followerObjId]); + ObjectEventSetHeldMovement(&gObjectEvents[followerObjId], newState); + } + + task->tState = DOORWARP_HIDE_PLAYER; } break; - case 2: + case DOORWARP_HIDE_PLAYER: if (IsPlayerStandingStill()) { - u8 objEventId; - task->data[1] = FieldAnimateDoorClose(*x, *y - 1); - objEventId = GetObjectEventIdByLocalIdAndMap(OBJ_EVENT_ID_PLAYER, 0, 0); - ObjectEventClearHeldMovementIfFinished(&gObjectEvents[objEventId]); + // Don't close door on NPC follower. + if (!PlayerHasFollowerNPC() || gObjectEvents[followerObjId].invisible) + task->tDoorTask = FieldAnimateDoorClose(*x, *y - 1); + + ObjectEventClearHeldMovementIfFinished(&gObjectEvents[playerObjId]); SetPlayerVisibility(FALSE); - task->tState = 3; + task->tState = DOORWARP_WAIT_DOOR_ANIM_TASK; } break; - case 3: - if (task->data[1] < 0 || gTasks[task->data[1]].isActive != TRUE) + case DOORWARP_WAIT_DOOR_ANIM_TASK: + if (task->tDoorTask < 0 || gTasks[task->tDoorTask].isActive != TRUE) + task->tState = DOORWARP_DO_WARP; + break; + case DOORWARP_DO_WARP: + if (PlayerHasFollowerNPC()) { - task->tState = 4; + ObjectEventClearHeldMovementIfActive(&gObjectEvents[followerObjId]); + ObjectEventSetHeldMovement(&gObjectEvents[followerObjId], MOVEMENT_ACTION_WALK_NORMAL_UP); } - break; - case 4: + TryFadeOutOldMapMusic(); WarpFadeOutScreen(); PlayRainStoppingSoundEffect(); @@ -1035,6 +1077,7 @@ static void Task_SpinEnterWarp(u8 taskId) case 1: if (WaitForWeatherFadeIn() && IsPlayerSpinEntranceActive() != TRUE) { + FollowerNPC_WarpSetEnd(); UnfreezeObjectEvents(); UnlockPlayerFieldControls(); DestroyTask(taskId); diff --git a/src/fldeff_teleport.c b/src/fldeff_teleport.c index c17dc9360b..83028c3949 100644 --- a/src/fldeff_teleport.c +++ b/src/fldeff_teleport.c @@ -2,6 +2,7 @@ #include "field_effect.h" #include "field_player_avatar.h" #include "fldeff.h" +#include "follower_npc.h" #include "party_menu.h" #include "overworld.h" #include "task.h" @@ -12,6 +13,9 @@ static void StartTeleportFieldEffect(void); bool8 SetUpFieldMove_Teleport(void) { + if (!CheckFollowerNPCFlag(FOLLOWER_NPC_FLAG_CAN_LEAVE_ROUTE)) + return FALSE; + if (Overworld_MapTypeAllowsTeleportAndFly(gMapHeader.mapType) == TRUE) { gFieldCallback2 = FieldCallback_PrepareFadeInForTeleport; diff --git a/src/follower_npc.c b/src/follower_npc.c new file mode 100644 index 0000000000..e363261d4e --- /dev/null +++ b/src/follower_npc.c @@ -0,0 +1,1689 @@ +#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 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_MAP_ID: + gSaveBlock3Ptr->NPCfollower.map.id = value; + break; + case FNPC_DATA_MAP_NUM: + gSaveBlock3Ptr->NPCfollower.map.number = value; + break; + case FNPC_DATA_MAP_GROUP: + gSaveBlock3Ptr->NPCfollower.map.group = 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_MAP_ID: + return gSaveBlock3Ptr->NPCfollower.map.id; + case FNPC_DATA_MAP_NUM: + return gSaveBlock3Ptr->NPCfollower.map.number; + case FNPC_DATA_MAP_GROUP: + return gSaveBlock3Ptr->NPCfollower.map.group; + 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 *ptr) +{ + struct ObjectEventTemplate npc; + struct ObjectEvent *follower; + u32 npcX, npcY; + u32 faceDirection; + u32 eventObjId; + const u8 *script; + u32 flag; + + // Only allow 1 follower NPC at a time. + if (PlayerHasFollowerNPC()) + return; + + for (eventObjId = 0; eventObjId < OBJECT_EVENTS_COUNT; eventObjId++) + { + if (!gObjectEvents[eventObjId].active || gObjectEvents[eventObjId].isPlayer) + continue; + + if (gObjectEvents[eventObjId].localId == localId) + { + 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 = ptr; + else + // Use the object's original script. + script = GetObjectEventScriptPointerByObjectEventId(eventObjId); + + npcX = gObjectEvents[eventObjId].currentCoords.x; + npcY = gObjectEvents[eventObjId].currentCoords.y; + faceDirection = gObjectEvents[eventObjId].facingDirection; + SetFollowerNPCData(FNPC_DATA_MAP_ID, gObjectEvents[eventObjId].localId); + RemoveObjectEvent(&gObjectEvents[eventObjId]); + FlagSet(flag); + + npc = *GetObjectEventTemplateByLocalIdAndMap(GetFollowerNPCData(FNPC_DATA_MAP_ID), 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, faceDirection); + 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_MAP_NUM, gSaveBlock1Ptr->location.mapNum); + SetFollowerNPCData(FNPC_DATA_MAP_GROUP, gSaveBlock1Ptr->location.mapGroup); + SetFollowerNPCScriptPointer(script); + SetFollowerNPCData(FNPC_DATA_EVENT_FLAG, flag); + SetFollowerNPCData(FNPC_DATA_FOLLOWER_FLAGS, followerFlags); + SetFollowerNPCData(FNPC_DATA_SURF_BLOB, FNPC_SURF_BLOB_NONE); + SetFollowerNPCData(FNPC_DATA_COME_OUT_DOOR, FNPC_DOOR_NONE); + + // 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 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 + +#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(); + + backupFollower = *follower; + backupFollower.graphicsId = newGraphicsId; + DestroySprite(&gSprites[oldSpriteId]); + RemoveObjectEvent(&gObjectEvents[GetFollowerNPCObjectId()]); + + clone = *GetObjectEventTemplateByLocalIdAndMap(GetFollowerNPCData(FNPC_DATA_MAP_ID), GetFollowerNPCData(FNPC_DATA_MAP_NUM), GetFollowerNPCData(FNPC_DATA_MAP_GROUP)); + 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; + + // 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) +{ + struct ObjectEvent *player; + struct ObjectEventTemplate clone; + + if (!PlayerHasFollowerNPC()) + return; + + player = &gObjectEvents[gPlayerAvatar.objectEventId]; + clone = *GetObjectEventTemplateByLocalIdAndMap(GetFollowerNPCData(FNPC_DATA_MAP_ID), GetFollowerNPCData(FNPC_DATA_MAP_NUM), GetFollowerNPCData(FNPC_DATA_MAP_GROUP)); + + clone.graphicsId = GetFollowerNPCSprite(); + clone.x = player->currentCoords.x - 7; + clone.y = player->currentCoords.y - 7; + clone.movementType = 0; + clone.localId = OBJ_EVENT_ID_NPC_FOLLOWER; + + 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, GetFollowerNPCData(FNPC_DATA_MAP_NUM), GetFollowerNPCData(FNPC_DATA_MAP_GROUP), 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 (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 (gMapHeader.mapType == MAP_TYPE_UNDERWATER) + { + TryUpdateFollowerNPCSpriteUnderwater(); + } + else + { + 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; + u32 toY; + + if (!PlayerHasFollowerNPC()) + return; + + player = &gObjectEvents[gPlayerAvatar.objectEventId]; + follower = &gObjectEvents[GetFollowerNPCObjectId()]; + + SetFollowerNPCData(FNPC_DATA_WARP_END, FNPC_WARP_REAPPEAR); + PlayerLogCoordinates(player); + + 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); + + if (gPlayerAvatar.flags & PLAYER_AVATAR_FLAG_ON_FOOT) + SetFollowerNPCSprite(FOLLOWER_NPC_SPRITE_INDEX_NORMAL); + + 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 (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); + + SetFollowerNPCData(FNPC_DATA_BATTLE_PARTNER, battlePartner); + TurnNPCIntoFollower(localId, flags, setScript, script); +} + +void ScriptDestroyFollowerNPC(struct ScriptContext *ctx) +{ + if (PlayerHasFollowerNPC()) + { + RemoveObjectEvent(&gObjectEvents[GetFollowerNPCData(FNPC_DATA_OBJ_ID)]); + FlagSet(GetFollowerNPCData(FNPC_DATA_EVENT_FLAG)); + ClearFollowerNPCData(); + } + + UpdateFollowingPokemon(); +} + +void ScriptFaceFollowerNPC(struct ScriptContext *ctx) +{ + if (PlayerHasFollowerNPC()) + { + u32 playerDirection, followerDirection; + struct ObjectEvent *player, *follower; + + player = &gObjectEvents[gPlayerAvatar.objectEventId]; + follower = &gObjectEvents[GetFollowerNPCData(FNPC_DATA_OBJ_ID)]; + 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) +{ + u32 walkSpeed = ScriptReadByte(ctx); + struct ObjectEvent *npc = &gObjectEvents[GetFollowerNPCObjectId()]; + + if (PlayerHasFollowerNPC() && 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) +{ + u32 newBattlePartner = ScriptReadHalfword(ctx); + + if (PlayerHasFollowerNPC()) + SetFollowerNPCData(FNPC_DATA_BATTLE_PARTNER, newBattlePartner); +} diff --git a/src/item_use.c b/src/item_use.c index c609614292..6f823aad8d 100644 --- a/src/item_use.c +++ b/src/item_use.c @@ -19,6 +19,7 @@ #include "field_screen_effect.h" #include "field_weather.h" #include "fldeff.h" +#include "follower_npc.h" #include "item.h" #include "item_menu.h" #include "item_use.h" @@ -263,7 +264,7 @@ void ItemUseOutOfBattle_Bike(u8 taskId) } else { - if (Overworld_IsBikingAllowed() == TRUE && IsBikingDisallowedByPlayer() == 0) + if (Overworld_IsBikingAllowed() && !IsBikingDisallowedByPlayer() && FollowerNPCCanBike()) { sItemUseOnFieldCB = ItemUseOnFieldCB_Bike; SetUpItemUseOnFieldCallback(taskId); @@ -281,6 +282,8 @@ static void ItemUseOnFieldCB_Bike(u8 taskId) GetOnOffBike(PLAYER_AVATAR_FLAG_MACH_BIKE); else // ACRO_BIKE GetOnOffBike(PLAYER_AVATAR_FLAG_ACRO_BIKE); + + FollowerNPC_HandleBike(); ScriptUnfreezeObjectEvents(); UnlockPlayerFieldControls(); DestroyTask(taskId); @@ -1073,6 +1076,9 @@ static void ItemUseOnFieldCB_EscapeRope(u8 taskId) bool8 CanUseDigOrEscapeRopeOnCurMap(void) { + if (!CheckFollowerNPCFlag(FOLLOWER_NPC_FLAG_CAN_LEAVE_ROUTE)) + return FALSE; + if (gMapHeader.allowEscaping) return TRUE; else diff --git a/src/load_save.c b/src/load_save.c index e4f77911fc..52305b1918 100644 --- a/src/load_save.c +++ b/src/load_save.c @@ -2,6 +2,7 @@ #include "malloc.h" #include "berry_powder.h" #include "fake_rtc.h" +#include "follower_npc.h" #include "item.h" #include "load_save.h" #include "main.h" @@ -10,6 +11,7 @@ #include "pokemon_storage_system.h" #include "random.h" #include "save_location.h" +#include "script_pokemon_util.h" #include "trainer_hill.h" #include "gba/flash_internal.h" #include "decoration_inventory.h" diff --git a/src/new_game.c b/src/new_game.c index bdc020f9df..b1d4617dce 100644 --- a/src/new_game.c +++ b/src/new_game.c @@ -47,6 +47,7 @@ #include "constants/map_groups.h" #include "constants/items.h" #include "difficulty.h" +#include "follower_npc.h" extern const u8 EventScript_ResetAllMapFlags[]; @@ -211,6 +212,7 @@ void NewGameInitData(void) SetCurrentDifficultyLevel(DIFFICULTY_NORMAL); ResetItemFlags(); ResetDexNav(); + ClearFollowerNPCData(); } static void ResetMiniGamesRecords(void) diff --git a/src/overworld.c b/src/overworld.c index e3dc63f1f8..2cdb6ddf8c 100644 --- a/src/overworld.c +++ b/src/overworld.c @@ -24,6 +24,7 @@ #include "field_weather.h" #include "fieldmap.h" #include "fldeff.h" +#include "follower_npc.h" #include "gpu_regs.h" #include "heal_location.h" #include "io_reg.h" @@ -68,6 +69,7 @@ #include "vs_seeker.h" #include "frontier_util.h" #include "constants/abilities.h" +#include "constants/event_object_movement.h" #include "constants/event_objects.h" #include "constants/layouts.h" #include "constants/map_types.h" @@ -458,6 +460,7 @@ static void Overworld_ResetStateAfterWhiteOut(void) VarSet(VAR_SHOULD_END_ABNORMAL_WEATHER, 0); VarSet(VAR_ABNORMAL_WEATHER_LOCATION, ABNORMAL_WEATHER_NONE); } + FollowerNPC_TryRemoveFollowerOnWhiteOut(); } static void UpdateMiscOverworldStates(void) @@ -1535,6 +1538,9 @@ static void DoCB1_Overworld(u16 newKeys, u16 heldKeys) PlayerStep(inputStruct.dpadDirection, newKeys, heldKeys); } } + // If stop running but keep holding B -> fix follower frame. + if (PlayerHasFollowerNPC() && (gPlayerAvatar.flags & PLAYER_AVATAR_FLAG_ON_FOOT) && IsPlayerStandingStill()) + ObjectEventSetHeldMovement(&gObjectEvents[GetFollowerNPCObjectId()], GetFaceDirectionAnimNum(gObjectEvents[GetFollowerNPCObjectId()].facingDirection)); } void CB1_Overworld(void) @@ -2228,6 +2234,7 @@ static bool32 ReturnToFieldLocal(u8 *state) case 1: InitViewGraphics(); TryLoadTrainerHillEReaderPalette(); + FollowerNPC_BindToSurfBlobOnReloadScreen(); (*state)++; break; case 2: @@ -2428,6 +2435,7 @@ static void InitObjectEventsLocal(void) TrySpawnObjectEvents(0, 0); UpdateFollowingPokemon(); TryRunOnWarpIntoMapScript(); + FollowerNPC_HandleSprite(); } static void InitObjectEventsReturnToField(void) diff --git a/src/party_menu.c b/src/party_menu.c index ebbee71ed0..cb085c5bea 100644 --- a/src/party_menu.c +++ b/src/party_menu.c @@ -25,6 +25,7 @@ #include "fieldmap.h" #include "fldeff.h" #include "fldeff_misc.h" +#include "follower_npc.h" #include "frontier_util.h" #include "gpu_regs.h" #include "graphics.h" @@ -518,6 +519,7 @@ static bool8 SetUpFieldMove_Dive(void); void TryItemHoldFormChange(struct Pokemon *mon); static void ShowMoveSelectWindow(u8 slot); static void Task_HandleWhichMoveInput(u8 taskId); +static void Task_HideFollowerNPCForTeleport(u8); // static const data #include "data/party_menu.h" @@ -4083,6 +4085,9 @@ bool8 FieldCallback_PrepareFadeInFromMenu(void) { FadeInFromBlack(); CreateTask(Task_FieldMoveWaitForFade, 8); + if (PlayerHasFollowerNPC()) + CreateTask(Task_HideFollowerNPCForTeleport, 0); + return TRUE; } @@ -4093,6 +4098,43 @@ bool8 FieldCallback_PrepareFadeInForTeleport(void) return FieldCallback_PrepareFadeInFromMenu(); } +#define taskState task->data[0] + +enum +{ + FNPC_WALK_INTO_PLAYER, + FNPC_WAIT_FOR_ANIM_FINISH +}; + +static void Task_HideFollowerNPCForTeleport(u8 taskId) +{ + struct ObjectEvent *follower = &gObjectEvents[GetFollowerNPCObjectId()]; + struct Task *task; + task = &gTasks[taskId]; + if (taskState == FNPC_WALK_INTO_PLAYER) + { + if (!PlayerHasFollowerNPC()) + { + DestroyTask(taskId); + } + else + { + FollowerNPCWalkIntoPlayerForLeaveMap(); + taskState = FNPC_WAIT_FOR_ANIM_FINISH; + } + } + if (taskState == FNPC_WAIT_FOR_ANIM_FINISH) + { + if (ObjectEventClearHeldMovementIfFinished(follower)) + { + FollowerNPCHideForLeaveMap(follower); + DestroyTask(taskId); + } + } +} + +#undef taskState + static void Task_FieldMoveWaitForFade(u8 taskId) { if (IsWeatherNotFadingIn() == TRUE) @@ -4130,6 +4172,9 @@ static void FieldCallback_Surf(void) static bool8 SetUpFieldMove_Surf(void) { + if (!CheckFollowerNPCFlag(FOLLOWER_NPC_FLAG_CAN_SURF)) + return FALSE; + if (PartyHasMonWithSurf() == TRUE && IsPlayerFacingSurfableFishableWater() == TRUE) { gFieldCallback2 = FieldCallback_PrepareFadeInFromMenu; @@ -4149,6 +4194,9 @@ static void DisplayCantUseSurfMessage(void) static bool8 SetUpFieldMove_Fly(void) { + if (!CheckFollowerNPCFlag(FOLLOWER_NPC_FLAG_CAN_LEAVE_ROUTE)) + return FALSE; + if (Overworld_MapTypeAllowsTeleportAndFly(gMapHeader.mapType) == TRUE) return TRUE; else @@ -4170,6 +4218,9 @@ static bool8 SetUpFieldMove_Waterfall(void) { s16 x, y; + if (!CheckFollowerNPCFlag(FOLLOWER_NPC_FLAG_CAN_WATERFALL)) + return FALSE; + GetXYCoordsOneStepInFrontOfPlayer(&x, &y); if (MetatileBehavior_IsWaterfall(MapGridGetMetatileBehaviorAt(x, y)) == TRUE && IsPlayerSurfingNorth() == TRUE) { @@ -4188,6 +4239,9 @@ static void FieldCallback_Dive(void) static bool8 SetUpFieldMove_Dive(void) { + if (!CheckFollowerNPCFlag(FOLLOWER_NPC_FLAG_CAN_DIVE)) + return FALSE; + gFieldEffectArguments[1] = TrySetDiveWarp(); if (gFieldEffectArguments[1] != 0) { @@ -7594,7 +7648,11 @@ static void Task_WaitAfterMultiPartnerPartySlideIn(u8 taskId) s16 *data = gTasks[taskId].data; // data[0] used as a timer afterwards rather than the x pos - if (++data[0] == 256) + if (FollowerNPCIsBattlePartner()) { + if (++data[0] == 128) + Task_ClosePartyMenu(taskId); + } + else if (++data[0] == 256) Task_ClosePartyMenu(taskId); } diff --git a/src/pokemon.c b/src/pokemon.c index cd1e44ef78..a67c99d654 100644 --- a/src/pokemon.c +++ b/src/pokemon.c @@ -19,6 +19,7 @@ #include "field_player_avatar.h" #include "field_specials.h" #include "field_weather.h" +#include "follower_npc.h" #include "graphics.h" #include "item.h" #include "caps.h" @@ -3462,7 +3463,8 @@ u8 GetMonsStateToDoubles_2(void) s32 aliveCount = 0; s32 i; - if (OW_DOUBLE_APPROACH_WITH_ONE_MON) + if (OW_DOUBLE_APPROACH_WITH_ONE_MON + || FollowerNPCIsBattlePartner()) return PLAYER_HAS_TWO_USABLE_MONS; for (i = 0; i < PARTY_SIZE; i++) diff --git a/src/scrcmd.c b/src/scrcmd.c index 3852632fac..f08763982b 100644 --- a/src/scrcmd.c +++ b/src/scrcmd.c @@ -24,6 +24,7 @@ #include "field_tasks.h" #include "field_weather.h" #include "fieldmap.h" +#include "follower_npc.h" #include "gpu_regs.h" #include "item.h" #include "lilycove_lady.h" @@ -56,6 +57,7 @@ #include "list_menu.h" #include "malloc.h" #include "constants/event_objects.h" +#include "constants/map_types.h" typedef u16 (*SpecialFunc)(void); typedef void (*NativeFunc)(struct ScriptContext *ctx); @@ -1494,7 +1496,29 @@ bool8 ScrCmd_resetobjectsubpriority(struct ScriptContext *ctx) bool8 ScrCmd_faceplayer(struct ScriptContext *ctx) { Script_RequestEffects(SCREFF_V1 | SCREFF_HARDWARE); + if (PlayerHasFollowerNPC() + && gObjectEvents[GetFollowerNPCObjectId()].invisible == FALSE + && gSelectedObjectEvent == GetFollowerNPCObjectId()) + { + struct ObjectEvent *npcFollower = &gObjectEvents[GetFollowerNPCObjectId()]; + switch (DetermineFollowerNPCDirection(&gObjectEvents[gPlayerAvatar.objectEventId], npcFollower)) + { + case DIR_NORTH: + ScriptMovement_StartObjectMovementScript(OBJ_EVENT_ID_NPC_FOLLOWER, npcFollower->mapGroup, npcFollower->mapNum, Common_Movement_FaceUp); + break; + case DIR_SOUTH: + ScriptMovement_StartObjectMovementScript(OBJ_EVENT_ID_NPC_FOLLOWER, npcFollower->mapGroup, npcFollower->mapNum, Common_Movement_FaceDown); + break; + case DIR_EAST: + ScriptMovement_StartObjectMovementScript(OBJ_EVENT_ID_NPC_FOLLOWER, npcFollower->mapGroup, npcFollower->mapNum, Common_Movement_FaceRight); + break; + case DIR_WEST: + ScriptMovement_StartObjectMovementScript(OBJ_EVENT_ID_NPC_FOLLOWER, npcFollower->mapGroup, npcFollower->mapNum, Common_Movement_FaceLeft); + break; + } + return FALSE; + } if (gObjectEvents[gSelectedObjectEvent].active) ObjectEventFaceOppositeDirection(&gObjectEvents[gSelectedObjectEvent], GetPlayerFacingDirection()); return FALSE; diff --git a/src/trainer_see.c b/src/trainer_see.c index e8b9c4f5ae..d743706dc4 100644 --- a/src/trainer_see.c +++ b/src/trainer_see.c @@ -5,6 +5,7 @@ #include "event_scripts.h" #include "field_effect.h" #include "field_player_avatar.h" +#include "follower_npc.h" #include "pokemon.h" #include "script.h" #include "script_movement.h" @@ -630,6 +631,9 @@ static void StartTrainerApproach(TaskFunc followupFunc) else taskId = gApproachingTrainers[1].taskId; + if (PlayerHasFollowerNPC() && (gPlayerAvatar.flags & PLAYER_AVATAR_FLAG_ON_FOOT)) + ObjectEventForceSetHeldMovement(&gObjectEvents[GetFollowerNPCObjectId()], GetFaceDirectionAnimNum(gObjectEvents[GetFollowerNPCObjectId()].facingDirection)); + taskFunc = Task_RunTrainerSeeFuncList; SetTaskFuncWithFollowupFunc(taskId, taskFunc, followupFunc); gTasks[taskId].tFuncId = TRSEE_EXCLAMATION; diff --git a/src/wild_encounter.c b/src/wild_encounter.c index dfb63e82d2..86923300f5 100644 --- a/src/wild_encounter.c +++ b/src/wild_encounter.c @@ -3,6 +3,7 @@ #include "pokemon.h" #include "metatile_behavior.h" #include "fieldmap.h" +#include "follower_npc.h" #include "random.h" #include "field_player_avatar.h" #include "event_data.h" @@ -859,8 +860,18 @@ void RockSmashWildEncounter(void) else if (WildEncounterCheck(wildPokemonInfo->encounterRate, TRUE) == TRUE && TryGenerateWildMon(wildPokemonInfo, WILD_AREA_ROCKS, WILD_CHECK_REPEL | WILD_CHECK_KEEN_EYE) == TRUE) { - BattleSetup_StartWildBattle(); - gSpecialVar_Result = TRUE; + if (TryDoDoubleWildBattle()) + { + struct Pokemon mon1 = gEnemyParty[0]; + TryGenerateWildMon(wildPokemonInfo, WILD_AREA_ROCKS, WILD_CHECK_REPEL | WILD_CHECK_KEEN_EYE); + gEnemyParty[1] = mon1; + BattleSetup_StartDoubleWildBattle(); + gSpecialVar_Result = TRUE; + } + else { + BattleSetup_StartWildBattle(); + gSpecialVar_Result = TRUE; + } } else { @@ -1233,6 +1244,9 @@ bool8 TryDoDoubleWildBattle(void) if (GetSafariZoneFlag() || (B_DOUBLE_WILD_REQUIRE_2_MONS == TRUE && GetMonsStateToDoubles() != PLAYER_HAS_TWO_USABLE_MONS)) return FALSE; + if (FollowerNPCIsBattlePartner() && FNPC_FLAG_PARTNER_WILD_BATTLES != 0 + && (FNPC_FLAG_PARTNER_WILD_BATTLES == FNPC_ALWAYS || FlagGet(FNPC_FLAG_PARTNER_WILD_BATTLES)) && FNPC_NPC_FOLLOWER_WILD_BATTLE_VS_2 == TRUE) + return TRUE; else if (B_FLAG_FORCE_DOUBLE_WILD != 0 && FlagGet(B_FLAG_FORCE_DOUBLE_WILD)) return TRUE; else if (B_DOUBLE_WILD_CHANCE != 0 && ((Random() % 100) + 1 <= B_DOUBLE_WILD_CHANCE))