feat: improved follower handling during scripted movements

This commit is contained in:
Ariel A 2024-10-20 03:15:38 -04:00
parent d80190fe10
commit 4056804575
9 changed files with 139 additions and 48 deletions

View File

@ -1944,3 +1944,11 @@
setvar VAR_0x8006, \item setvar VAR_0x8006, \item
special CreateEnemyEventMon special CreateEnemyEventMon
.endm .endm
@ hide any follower pokemon if present,
@ putting them into their pokeball;
@ by default waits for their movement to finish
.macro hidefollower wait=1
callfunc ScrFunc_hidefollower
.2byte \wait
.endm

View File

@ -33,6 +33,7 @@ EventScript_PkmnCenterNurse_IllTakeYourPkmn2::
return return
EventScript_PkmnCenterNurse_TakeAndHealPkmn:: EventScript_PkmnCenterNurse_TakeAndHealPkmn::
hidefollower 0
applymovement VAR_0x800B, Movement_PkmnCenterNurse_Turn applymovement VAR_0x800B, Movement_PkmnCenterNurse_Turn
waitmovement 0 waitmovement 0
dofieldeffect FLDEFF_POKECENTER_HEAL dofieldeffect FLDEFF_POKECENTER_HEAL

View File

@ -289,6 +289,12 @@
// instead of a normal pokeball // instead of a normal pokeball
#define OW_MON_POKEBALLS TRUE #define OW_MON_POKEBALLS TRUE
// New/old handling for followers during scripts;
// TRUE: Script collisions hide follower, FLAG_SAFE_FOLLOWER_MOVEMENT on by default
// (scripted player movement moves follower too!)
// FALSE: Script collisions unhandled, FLAG_SAFE_FOLLOWER_MOVEMENT off by default
#define OW_MON_SCRIPT_MOVEMENT TRUE
#define SHADOW_SIZE_S 0 #define SHADOW_SIZE_S 0
#define SHADOW_SIZE_M 1 #define SHADOW_SIZE_M 1
#define SHADOW_SIZE_L 2 #define SHADOW_SIZE_L 2

View File

@ -1643,7 +1643,9 @@
#define FLAG_ENABLE_MULTI_CORRIDOR_DOOR (SPECIAL_FLAGS_START + 0x2) #define FLAG_ENABLE_MULTI_CORRIDOR_DOOR (SPECIAL_FLAGS_START + 0x2)
#define FLAG_SPECIAL_FLAG_UNUSED_0x4003 (SPECIAL_FLAGS_START + 0x3) // Unused Flag #define FLAG_SPECIAL_FLAG_UNUSED_0x4003 (SPECIAL_FLAGS_START + 0x3) // Unused Flag
#define FLAG_STORING_ITEMS_IN_PYRAMID_BAG (SPECIAL_FLAGS_START + 0x4) #define FLAG_STORING_ITEMS_IN_PYRAMID_BAG (SPECIAL_FLAGS_START + 0x4)
#define FLAG_SAFE_FOLLOWER_MOVEMENT (SPECIAL_FLAGS_START + 0x5) // When set, applymovement does not put the follower inside a pokeball // When set, `applymovement` does not hide follower pokemon;
// Also, scripted movements on the player will move follower(s), too
#define FLAG_SAFE_FOLLOWER_MOVEMENT (SPECIAL_FLAGS_START + 0x5)
// FLAG_SPECIAL_FLAG_0x4005 - 0x407F also exist and are unused // FLAG_SPECIAL_FLAG_0x4005 - 0x407F also exist and are unused
#define SPECIAL_FLAGS_END (SPECIAL_FLAGS_START + 0x7F) #define SPECIAL_FLAGS_END (SPECIAL_FLAGS_START + 0x7F)
#define NUM_SPECIAL_FLAGS (SPECIAL_FLAGS_END - SPECIAL_FLAGS_START + 1) #define NUM_SPECIAL_FLAGS (SPECIAL_FLAGS_END - SPECIAL_FLAGS_START + 1)

View File

@ -161,6 +161,7 @@ u8 GetWalkInPlaceFastMovementAction(u32);
u8 GetWalkInPlaceNormalMovementAction(u32); u8 GetWalkInPlaceNormalMovementAction(u32);
u8 GetWalkInPlaceSlowMovementAction(u32); u8 GetWalkInPlaceSlowMovementAction(u32);
u8 GetCollisionAtCoords(struct ObjectEvent *, s16 x, s16 y, u32 dir); u8 GetCollisionAtCoords(struct ObjectEvent *, s16 x, s16 y, u32 dir);
u32 GetObjectObjectCollidesWith(struct ObjectEvent *objectEvent, s16 x, s16 y, bool32 addCoords);
void MoveCoords(u8 direction, s16 *x, s16 *y); void MoveCoords(u8 direction, s16 *x, s16 *y);
bool8 ObjectEventIsHeldMovementActive(struct ObjectEvent *); bool8 ObjectEventIsHeldMovementActive(struct ObjectEvent *);
u8 ObjectEventClearHeldMovementIfFinished(struct ObjectEvent *); u8 ObjectEventClearHeldMovementIfFinished(struct ObjectEvent *);

View File

@ -5222,9 +5222,7 @@ bool8 FollowablePlayerMovement_Step(struct ObjectEvent *objectEvent, struct Spri
s16 y; s16 y;
s16 targetX; s16 targetX;
s16 targetY; s16 targetY;
#ifdef MB_SIDEWAYS_STAIRS_RIGHT_SIDE
u32 playerAction = gObjectEvents[gPlayerAvatar.objectEventId].movementActionId; u32 playerAction = gObjectEvents[gPlayerAvatar.objectEventId].movementActionId;
#endif
targetX = gObjectEvents[gPlayerAvatar.objectEventId].previousCoords.x; targetX = gObjectEvents[gPlayerAvatar.objectEventId].previousCoords.x;
targetY = gObjectEvents[gPlayerAvatar.objectEventId].previousCoords.y; targetY = gObjectEvents[gPlayerAvatar.objectEventId].previousCoords.y;
@ -5240,8 +5238,8 @@ bool8 FollowablePlayerMovement_Step(struct ObjectEvent *objectEvent, struct Spri
if (objectEvent->invisible) { if (objectEvent->invisible) {
// Animate exiting pokeball // Animate exiting pokeball
// Player is jumping, but follower is invisible // don't emerge if player is jumping or moving via script
if (PlayerGetCopyableMovement() == COPY_MOVE_JUMP2) { if (PlayerGetCopyableMovement() == COPY_MOVE_JUMP2 || ArePlayerFieldControlsLocked()) {
sprite->sTypeFuncId = 0; // return to shadowing state sprite->sTypeFuncId = 0; // return to shadowing state
return FALSE; return FALSE;
} }
@ -5262,43 +5260,25 @@ bool8 FollowablePlayerMovement_Step(struct ObjectEvent *objectEvent, struct Spri
MoveCoords(direction, &x, &y); MoveCoords(direction, &x, &y);
#ifdef MB_SIDEWAYS_STAIRS_RIGHT_SIDE // https://github.com/ghoulslash/pokeemerald/tree/sideways_stairs #ifdef MB_SIDEWAYS_STAIRS_RIGHT_SIDE // https://github.com/ghoulslash/pokeemerald/tree/sideways_stairs
GetCollisionAtCoords(objectEvent, x, y, direction); // Sets directionOverwrite for stairs GetCollisionAtCoords(objectEvent, x, y, direction); // Sets directionOverwrite for stairs
if (GetLedgeJumpDirection(x, y, direction) != DIR_NONE) { #endif
if (GetLedgeJumpDirection(x, y, direction) != DIR_NONE)
// InitJumpRegular will set the proper speed // InitJumpRegular will set the proper speed
ObjectEventSetSingleMovement(objectEvent, sprite, GetJump2MovementAction(direction)); ObjectEventSetSingleMovement(objectEvent, sprite, GetJump2MovementAction(direction));
} else if (TestPlayerAvatarFlags(PLAYER_AVATAR_FLAG_DASH)) { else if (playerAction >= MOVEMENT_ACTION_WALK_SLOW_DOWN && playerAction <= MOVEMENT_ACTION_WALK_SLOW_RIGHT) {
// Set follow speed according to player's speed if (TestPlayerAvatarFlags(PLAYER_AVATAR_FLAG_DASH)) // on sideways stairs
if (playerAction >= MOVEMENT_ACTION_RUN_DOWN_SLOW && playerAction <= MOVEMENT_ACTION_RUN_RIGHT_SLOW)
objectEvent->movementActionId = GetWalkNormalMovementAction(direction); objectEvent->movementActionId = GetWalkNormalMovementAction(direction);
else else
objectEvent->movementActionId = GetWalkFastMovementAction(direction);
} else if (PlayerGetCopyableMovement() == COPY_MOVE_JUMP2) {
ObjectEventSetSingleMovement(objectEvent, sprite, GetWalkSlowMovementAction(direction));
} else {
if (playerAction >= MOVEMENT_ACTION_WALK_SLOW_DOWN && playerAction <= MOVEMENT_ACTION_WALK_SLOW_RIGHT) {
ObjectEventSetSingleMovement(objectEvent, sprite, GetWalkSlowMovementAction(direction)); ObjectEventSetSingleMovement(objectEvent, sprite, GetWalkSlowMovementAction(direction));
} else {
objectEvent->movementActionId = GetWalkNormalMovementAction(direction);
if (OW_MON_BOBBING == TRUE)
sprite->y2 = -1;
}
}
sprite->sActionFuncId = 0;
#else
if (GetLedgeJumpDirection(x, y, direction) != DIR_NONE) {
// InitJumpRegular will set the proper speed
ObjectEventSetSingleMovement(objectEvent, sprite, GetJump2MovementAction(direction));
} else if (TestPlayerAvatarFlags(PLAYER_AVATAR_FLAG_DASH)) {
// Set follow speed according to player's speed
ObjectEventSetSingleMovement(objectEvent, sprite, GetWalkFastMovementAction(direction));
} else if (PlayerGetCopyableMovement() == COPY_MOVE_JUMP2) { } else if (PlayerGetCopyableMovement() == COPY_MOVE_JUMP2) {
// If *player* jumps, make step take twice as long
ObjectEventSetSingleMovement(objectEvent, sprite, GetWalkSlowMovementAction(direction)); ObjectEventSetSingleMovement(objectEvent, sprite, GetWalkSlowMovementAction(direction));
} else if (gSprites[gPlayerAvatar.spriteId].data[4] == MOVE_SPEED_FAST_1) {
objectEvent->movementActionId = GetWalkFastMovementAction(direction);
} else { } else {
ObjectEventSetSingleMovement(objectEvent, sprite, GetWalkNormalMovementAction(direction)); objectEvent->movementActionId = GetWalkNormalMovementAction(direction);
if (OW_MON_BOBBING == TRUE) if (OW_MON_BOBBING == TRUE)
sprite->y2 = -1; sprite->y2 = -1;
} }
#endif sprite->sActionFuncId = 0;
objectEvent->singleMovementActive = 1; objectEvent->singleMovementActive = 1;
sprite->sTypeFuncId = 2; sprite->sTypeFuncId = 2;
return TRUE; return TRUE;
@ -5823,13 +5803,17 @@ static bool8 IsMetatileDirectionallyImpassable(struct ObjectEvent *objectEvent,
return FALSE; return FALSE;
} }
static bool8 DoesObjectCollideWithObjectAt(struct ObjectEvent *objectEvent, s16 x, s16 y) u32 GetObjectObjectCollidesWith(struct ObjectEvent *objectEvent, s16 x, s16 y, bool32 addCoords) {
{
u8 i; u8 i;
struct ObjectEvent *curObject; struct ObjectEvent *curObject;
if (objectEvent->localId == OBJ_EVENT_ID_FOLLOWER) if (objectEvent->localId == OBJ_EVENT_ID_FOLLOWER)
return FALSE; // follower cannot collide with other objects, but they can collide with it return OBJECT_EVENTS_COUNT; // follower cannot collide with other objects, but they can collide with it
if (addCoords) {
x += objectEvent->currentCoords.x;
y += objectEvent->currentCoords.y;
}
for (i = 0; i < OBJECT_EVENTS_COUNT; i++) for (i = 0; i < OBJECT_EVENTS_COUNT; i++)
{ {
@ -5839,11 +5823,16 @@ static bool8 DoesObjectCollideWithObjectAt(struct ObjectEvent *objectEvent, s16
if ((curObject->currentCoords.x == x && curObject->currentCoords.y == y) || (curObject->previousCoords.x == x && curObject->previousCoords.y == y)) if ((curObject->currentCoords.x == x && curObject->currentCoords.y == y) || (curObject->previousCoords.x == x && curObject->previousCoords.y == y))
{ {
if (AreElevationsCompatible(objectEvent->currentElevation, curObject->currentElevation)) if (AreElevationsCompatible(objectEvent->currentElevation, curObject->currentElevation))
return TRUE; return i;
} }
} }
} }
return FALSE; return OBJECT_EVENTS_COUNT;
}
static bool8 DoesObjectCollideWithObjectAt(struct ObjectEvent *objectEvent, s16 x, s16 y)
{
return (GetObjectObjectCollidesWith(objectEvent, x, y, FALSE) < OBJECT_EVENTS_COUNT);
} }
bool8 IsBerryTreeSparkling(u8 localId, u8 mapNum, u8 mapGroup) bool8 IsBerryTreeSparkling(u8 localId, u8 mapNum, u8 mapGroup)
@ -5972,6 +5961,16 @@ bool8 ObjectEventIsHeldMovementActive(struct ObjectEvent *objectEvent)
return FALSE; return FALSE;
} }
static const u8 sActionIdToCopyableMovement[] = {
[MOVEMENT_ACTION_FACE_DOWN ... MOVEMENT_ACTION_FACE_RIGHT] = COPY_MOVE_FACE,
[MOVEMENT_ACTION_WALK_SLOW_DOWN ... MOVEMENT_ACTION_WALK_NORMAL_RIGHT] = COPY_MOVE_WALK,
[MOVEMENT_ACTION_JUMP_2_DOWN ... MOVEMENT_ACTION_JUMP_2_RIGHT] = COPY_MOVE_JUMP2,
[MOVEMENT_ACTION_WALK_FAST_DOWN ... MOVEMENT_ACTION_WALK_FAST_RIGHT] = COPY_MOVE_WALK,
[MOVEMENT_ACTION_RIDE_WATER_CURRENT_DOWN ... MOVEMENT_ACTION_PLAYER_RUN_RIGHT] = COPY_MOVE_WALK,
[MOVEMENT_ACTION_NONE] = COPY_MOVE_NONE,
};
bool8 ObjectEventSetHeldMovement(struct ObjectEvent *objectEvent, u8 movementActionId) bool8 ObjectEventSetHeldMovement(struct ObjectEvent *objectEvent, u8 movementActionId)
{ {
if (ObjectEventIsMovementOverridden(objectEvent)) if (ObjectEventIsMovementOverridden(objectEvent))
@ -5982,6 +5981,16 @@ bool8 ObjectEventSetHeldMovement(struct ObjectEvent *objectEvent, u8 movementAct
objectEvent->heldMovementActive = TRUE; objectEvent->heldMovementActive = TRUE;
objectEvent->heldMovementFinished = FALSE; objectEvent->heldMovementFinished = FALSE;
gSprites[objectEvent->spriteId].sActionFuncId = 0; gSprites[objectEvent->spriteId].sActionFuncId = 0;
// When player is moved via script, set copyable movement
// for any followers via a lookup table
if (ArePlayerFieldControlsLocked() &&
objectEvent->isPlayer &&
FlagGet(FLAG_SAFE_FOLLOWER_MOVEMENT))
{
objectEvent->extra.playerCopyableMovement = sActionIdToCopyableMovement[objectEvent->movementActionId];
}
return FALSE; return FALSE;
} }
@ -6004,6 +6013,15 @@ void ObjectEventClearHeldMovement(struct ObjectEvent *objectEvent)
objectEvent->heldMovementFinished = FALSE; objectEvent->heldMovementFinished = FALSE;
gSprites[objectEvent->spriteId].sTypeFuncId = 0; gSprites[objectEvent->spriteId].sTypeFuncId = 0;
gSprites[objectEvent->spriteId].sActionFuncId = 0; gSprites[objectEvent->spriteId].sActionFuncId = 0;
// When player is moved via script, set copyable movement
// for any followers via a lookup table
if (ArePlayerFieldControlsLocked() &&
objectEvent->isPlayer &&
FlagGet(FLAG_SAFE_FOLLOWER_MOVEMENT))
{
objectEvent->extra.playerCopyableMovement = sActionIdToCopyableMovement[objectEvent->movementActionId];
}
} }
u8 ObjectEventCheckHeldMovementStatus(struct ObjectEvent *objectEvent) u8 ObjectEventCheckHeldMovementStatus(struct ObjectEvent *objectEvent)

View File

@ -997,6 +997,20 @@ bool8 ScrCmd_fadeinbgm(struct ScriptContext *ctx)
return FALSE; return FALSE;
} }
struct ObjectEvent * ScriptHideFollower(void) {
struct ObjectEvent *obj = GetFollowerObject();
if (obj == NULL || obj->invisible)
return NULL;
ClearObjectEventMovement(obj, &gSprites[obj->spriteId]);
gSprites[obj->spriteId].animCmdIndex = 0; // Reset start frame of animation
// Note: ScriptMovement_ returns TRUE on error
if (ScriptMovement_StartObjectMovementScript(obj->localId, obj->mapGroup, obj->mapNum, EnterPokeballMovement))
return NULL;
return obj;
}
bool8 ScrCmd_applymovement(struct ScriptContext *ctx) bool8 ScrCmd_applymovement(struct ScriptContext *ctx)
{ {
u16 localId = VarGet(ScriptReadHalfword(ctx)); u16 localId = VarGet(ScriptReadHalfword(ctx));
@ -1010,17 +1024,11 @@ bool8 ScrCmd_applymovement(struct ScriptContext *ctx)
} }
ScriptMovement_StartObjectMovementScript(localId, gSaveBlock1Ptr->location.mapNum, gSaveBlock1Ptr->location.mapGroup, movementScript); ScriptMovement_StartObjectMovementScript(localId, gSaveBlock1Ptr->location.mapNum, gSaveBlock1Ptr->location.mapGroup, movementScript);
sMovingNpcId = localId; sMovingNpcId = localId;
objEvent = GetFollowerObject(); if (localId != OBJ_EVENT_ID_FOLLOWER &&
// Force follower into pokeball !FlagGet(FLAG_SAFE_FOLLOWER_MOVEMENT)
if (localId != OBJ_EVENT_ID_FOLLOWER && (movementScript < Common_Movement_FollowerSafeStart || movementScript > Common_Movement_FollowerSafeEnd))
&& !FlagGet(FLAG_SAFE_FOLLOWER_MOVEMENT)
&& (movementScript < Common_Movement_FollowerSafeStart || movementScript > Common_Movement_FollowerSafeEnd)
&& (objEvent = GetFollowerObject())
&& !objEvent->invisible)
{ {
ClearObjectEventMovement(objEvent, &gSprites[objEvent->spriteId]); ScriptHideFollower();
gSprites[objEvent->spriteId].animCmdIndex = 0; // Reset start frame of animation
ScriptMovement_StartObjectMovementScript(OBJ_EVENT_ID_FOLLOWER, gSaveBlock1Ptr->location.mapNum, gSaveBlock1Ptr->location.mapGroup, EnterPokeballMovement);
} }
return FALSE; return FALSE;
} }
@ -2369,3 +2377,23 @@ bool8 ScrCmd_warpwhitefade(struct ScriptContext *ctx)
ResetInitialPlayerAvatarState(); ResetInitialPlayerAvatarState();
return TRUE; return TRUE;
} }
bool8 ScrFunc_hidefollower(struct ScriptContext *ctx) {
bool16 wait = VarGet(ScriptReadHalfword(ctx));
struct ObjectEvent *obj;
if ((obj = ScriptHideFollower()) != NULL && wait) {
sMovingNpcId = obj->localId;
sMovingNpcMapGroup = obj->mapGroup;
sMovingNpcMapNum = obj->mapNum;
SetupNativeScript(ctx, WaitForMovementFinish);
}
// Just in case, prevent `applymovement`
// from hiding the follower again
if (obj)
FlagSet(FLAG_SAFE_FOLLOWER_MOVEMENT);
// execute next script command with no delay
return TRUE;
}

View File

@ -4,6 +4,7 @@
#include "mystery_gift.h" #include "mystery_gift.h"
#include "util.h" #include "util.h"
#include "constants/event_objects.h" #include "constants/event_objects.h"
#include "constants/flags.h"
#include "constants/map_scripts.h" #include "constants/map_scripts.h"
#define RAM_SCRIPT_MAGIC 51 #define RAM_SCRIPT_MAGIC 51
@ -243,6 +244,8 @@ void ScriptContext_SetupScript(const u8 *ptr)
InitScriptContext(&sGlobalScriptContext, gScriptCmdTable, gScriptCmdTableEnd); InitScriptContext(&sGlobalScriptContext, gScriptCmdTable, gScriptCmdTableEnd);
SetupBytecodeScript(&sGlobalScriptContext, ptr); SetupBytecodeScript(&sGlobalScriptContext, ptr);
LockPlayerFieldControls(); LockPlayerFieldControls();
if (OW_MON_SCRIPT_MOVEMENT)
FlagSet(FLAG_SAFE_FOLLOWER_MOVEMENT);
sGlobalScriptContextStatus = CONTEXT_RUNNING; sGlobalScriptContextStatus = CONTEXT_RUNNING;
} }

View File

@ -1,6 +1,7 @@
#include "global.h" #include "global.h"
#include "script_movement.h" #include "script_movement.h"
#include "event_object_movement.h" #include "event_object_movement.h"
#include "event_scripts.h"
#include "task.h" #include "task.h"
#include "util.h" #include "util.h"
#include "constants/event_objects.h" #include "constants/event_objects.h"
@ -205,13 +206,34 @@ static void ScriptMovement_MoveObjects(u8 taskId)
} }
} }
// from event_object_movement
#define sTypeFuncId data[1]
#define sTimer data[5]
static void ScriptMovement_TakeStep(u8 taskId, u8 moveScrId, u8 objEventId, const u8 *movementScript) static void ScriptMovement_TakeStep(u8 taskId, u8 moveScrId, u8 objEventId, const u8 *movementScript)
{ {
u8 nextMoveActionId; u8 nextMoveActionId;
struct ObjectEvent *obj = &gObjectEvents[objEventId];
if (ObjectEventIsHeldMovementActive(&gObjectEvents[objEventId]) if (ObjectEventIsHeldMovementActive(obj) &&
&& !ObjectEventClearHeldMovementIfFinished(&gObjectEvents[objEventId])) !ObjectEventClearHeldMovementIfFinished(obj))
{
// If, while undergoing scripted movement,
// a non-player object collides with an active follower pokemon,
// put that follower into a pokeball
// (sTimer helps limit this expensive check to once per step)
if (OW_MON_SCRIPT_MOVEMENT &&
gSprites[obj->spriteId].sTimer == 1 &&
(objEventId = GetObjectObjectCollidesWith(obj, 0, 0, TRUE)) < OBJECT_EVENTS_COUNT &&
// switch `obj` to follower
((obj = &gObjectEvents[objEventId])->movementType == MOVEMENT_TYPE_FOLLOW_PLAYER) &&
gSprites[obj->spriteId].sTypeFuncId != 0)
{
ClearObjectEventMovement(obj, &gSprites[obj->spriteId]);
ScriptMovement_StartObjectMovementScript(obj->localId, obj->mapNum, obj->mapGroup, EnterPokeballMovement);
}
return; return;
}
nextMoveActionId = *movementScript; nextMoveActionId = *movementScript;
if (nextMoveActionId == MOVEMENT_ACTION_STEP_END) if (nextMoveActionId == MOVEMENT_ACTION_STEP_END)
@ -229,3 +251,5 @@ static void ScriptMovement_TakeStep(u8 taskId, u8 moveScrId, u8 objEventId, cons
} }
} }
#undef sTypeFuncId
#undef sTimer