mirror of
https://github.com/pret/pokefirered.git
synced 2026-04-25 15:28:53 -05:00
Merge pull request #139 from MiyazakiTheFalse/codex/add-chase-overworld-subsystem-for-chaser-events
Add overworld chase chasers with spawn/despawn and pursuit updates
This commit is contained in:
commit
7bf2de20f4
|
|
@ -1,6 +1,6 @@
|
|||
# Chase boundary policy
|
||||
|
||||
This document defines where an **active chase** is allowed to continue in the overworld.
|
||||
This document defines where an **active chase** is allowed to continue in the overworld and how chase visuals move while the chase state is active.
|
||||
|
||||
## Continuation gate
|
||||
|
||||
|
|
@ -38,6 +38,26 @@ Encounter-eligible tiles are used for **re-engagement attempts only** (forced ch
|
|||
- Active chase can persist while crossing safe/non-encounter tiles (for example, route path tiles) as long as map context remains valid.
|
||||
- Forced chase encounter generation resumes once the player steps back onto an encounter-eligible tile before chase timeout expires.
|
||||
|
||||
## Overworld chaser movement policy
|
||||
|
||||
When `ChaseStamina_IsChaseActive()` is true, a chase-overworld layer drives 1..`ChaseStamina_GetActiveChasers()` visual object events.
|
||||
|
||||
- Chasers use a reserved local-id range (`230..231`) and a dedicated chase graphics id (`OBJ_EVENT_GFX_MEOWTH`).
|
||||
- Spawn point is biased behind the player when possible; each chaser then pursues every overworld frame.
|
||||
- Target coordinate selection prefers the live player object-event coordinates and falls back to `gSaveBlock1Ptr->pos` when needed.
|
||||
- Movement picks a primary axis toward player distance, then tries fallback directions (secondary axis, current facing, opposite facing).
|
||||
- Every attempted step is validated with object collision checks; if all checks fail the chaser turns in place toward the player instead of forcing illegal movement.
|
||||
|
||||
This policy keeps chasers visible and responsive while avoiding collision softlocks.
|
||||
|
||||
## Edge cases and invalidation handling
|
||||
|
||||
- **Chase termination (`EndChase`)**: all spawned chaser object events are explicitly removed immediately.
|
||||
- **Map transitions / warps**: transition hooks despawn all chase object events before map context changes settle.
|
||||
- **Context mismatch safety**: if spawned chasers belong to a previous map context, the chase-overworld layer invalidates and despawns them before any updates.
|
||||
- **Doors/buildings/safe hubs**: entering non-chase contexts still terminates the chase state per core policy above, and visual chasers are cleaned up with that termination.
|
||||
- **Ledges / blocked terrain**: collision fallback can choose alternate legal directions or no-step facing updates, preventing repeated illegal moves.
|
||||
|
||||
## Suppression safety rule
|
||||
|
||||
`ChaseStamina_ShouldSuppressRandomEncounters()` re-checks map-context validity. If the map context is invalid, it clears chase state and returns `FALSE` so random-encounter suppression cannot persist outside chase-valid zones.
|
||||
|
|
@ -52,4 +72,4 @@ State-machine coverage should include the following sequence:
|
|||
4. Player returns to grass before timeout.
|
||||
5. Forced chase re-engagement is again eligible on encounter-valid terrain.
|
||||
|
||||
Future tuning should update this document alongside `src/chase_stamina.c` so map/terrain rules stay explicit.
|
||||
Future tuning should update this document alongside `src/chase_stamina.c` and `src/chase_overworld.c` so map, terrain, and visual pursuit behavior stay explicit.
|
||||
|
|
|
|||
10
include/chase_overworld.h
Normal file
10
include/chase_overworld.h
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
#ifndef GUARD_CHASE_OVERWORLD_H
|
||||
#define GUARD_CHASE_OVERWORLD_H
|
||||
|
||||
#include "global.h"
|
||||
|
||||
void ChaseOverworld_UpdateOverworldFrame(bool8 tookStep);
|
||||
void ChaseOverworld_OnMapTransition(const struct WarpData *from, const struct WarpData *to);
|
||||
void ChaseOverworld_OnChaseEnded(void);
|
||||
|
||||
#endif // GUARD_CHASE_OVERWORLD_H
|
||||
209
src/chase_overworld.c
Normal file
209
src/chase_overworld.c
Normal file
|
|
@ -0,0 +1,209 @@
|
|||
#include "global.h"
|
||||
#include "chase_overworld.h"
|
||||
#include "chase_stamina.h"
|
||||
#include "event_object_movement.h"
|
||||
#include "fieldmap.h"
|
||||
#include "constants/event_object_movement.h"
|
||||
#include "constants/event_objects.h"
|
||||
#include "constants/maps.h"
|
||||
|
||||
#define CHASE_OVERWORLD_MAX_CHASERS 2
|
||||
#define CHASE_OVERWORLD_LOCAL_ID_BASE 230
|
||||
#define CHASE_OVERWORLD_GFX_ID OBJ_EVENT_GFX_MEOWTH
|
||||
|
||||
static EWRAM_DATA bool8 sChasersSpawned = FALSE;
|
||||
static EWRAM_DATA u8 sSpawnedMapGroup = MAP_GROUP(MAP_UNDEFINED);
|
||||
static EWRAM_DATA u8 sSpawnedMapNum = MAP_NUM(MAP_UNDEFINED);
|
||||
|
||||
static void DespawnChasers(void)
|
||||
{
|
||||
u8 i;
|
||||
|
||||
for (i = 0; i < CHASE_OVERWORLD_MAX_CHASERS; i++)
|
||||
RemoveObjectEventByLocalIdAndMap(CHASE_OVERWORLD_LOCAL_ID_BASE + i, sSpawnedMapNum, sSpawnedMapGroup);
|
||||
|
||||
sChasersSpawned = FALSE;
|
||||
sSpawnedMapGroup = MAP_GROUP(MAP_UNDEFINED);
|
||||
sSpawnedMapNum = MAP_NUM(MAP_UNDEFINED);
|
||||
}
|
||||
|
||||
static bool8 IsSpawnContextValid(void)
|
||||
{
|
||||
return sChasersSpawned
|
||||
&& sSpawnedMapGroup == gSaveBlock1Ptr->location.mapGroup
|
||||
&& sSpawnedMapNum == gSaveBlock1Ptr->location.mapNum;
|
||||
}
|
||||
|
||||
static u8 GetMoveDirectionTowardTarget(s16 fromX, s16 fromY, s16 toX, s16 toY, u8 preferredDir)
|
||||
{
|
||||
if (fromX < toX)
|
||||
return DIR_EAST;
|
||||
if (fromX > toX)
|
||||
return DIR_WEST;
|
||||
if (fromY < toY)
|
||||
return DIR_SOUTH;
|
||||
if (fromY > toY)
|
||||
return DIR_NORTH;
|
||||
|
||||
return preferredDir;
|
||||
}
|
||||
|
||||
static bool8 TryQueueChaserStep(struct ObjectEvent *objectEvent, s16 targetX, s16 targetY)
|
||||
{
|
||||
s16 dx;
|
||||
s16 dy;
|
||||
u8 i;
|
||||
u8 primaryDir;
|
||||
u8 secondaryDir;
|
||||
u8 fallbackDirs[4];
|
||||
|
||||
primaryDir = DIR_NONE;
|
||||
secondaryDir = DIR_NONE;
|
||||
dx = objectEvent->currentCoords.x - targetX;
|
||||
if (dx < 0)
|
||||
dx = -dx;
|
||||
dy = objectEvent->currentCoords.y - targetY;
|
||||
if (dy < 0)
|
||||
dy = -dy;
|
||||
|
||||
if (dx >= dy)
|
||||
{
|
||||
if (objectEvent->currentCoords.x < targetX)
|
||||
primaryDir = DIR_EAST;
|
||||
else if (objectEvent->currentCoords.x > targetX)
|
||||
primaryDir = DIR_WEST;
|
||||
|
||||
if (objectEvent->currentCoords.y < targetY)
|
||||
secondaryDir = DIR_SOUTH;
|
||||
else if (objectEvent->currentCoords.y > targetY)
|
||||
secondaryDir = DIR_NORTH;
|
||||
}
|
||||
else
|
||||
{
|
||||
if (objectEvent->currentCoords.y < targetY)
|
||||
primaryDir = DIR_SOUTH;
|
||||
else if (objectEvent->currentCoords.y > targetY)
|
||||
primaryDir = DIR_NORTH;
|
||||
|
||||
if (objectEvent->currentCoords.x < targetX)
|
||||
secondaryDir = DIR_EAST;
|
||||
else if (objectEvent->currentCoords.x > targetX)
|
||||
secondaryDir = DIR_WEST;
|
||||
}
|
||||
|
||||
if (primaryDir == DIR_NONE)
|
||||
primaryDir = GetMoveDirectionTowardTarget(objectEvent->currentCoords.x, objectEvent->currentCoords.y, targetX, targetY, objectEvent->facingDirection);
|
||||
|
||||
fallbackDirs[0] = primaryDir;
|
||||
fallbackDirs[1] = secondaryDir;
|
||||
fallbackDirs[2] = objectEvent->facingDirection;
|
||||
fallbackDirs[3] = GetOppositeDirection(objectEvent->facingDirection);
|
||||
|
||||
for (i = 0; i < ARRAY_COUNT(fallbackDirs); i++)
|
||||
{
|
||||
u8 direction = fallbackDirs[i];
|
||||
s16 testX = objectEvent->currentCoords.x;
|
||||
s16 testY = objectEvent->currentCoords.y;
|
||||
|
||||
if (direction == DIR_NONE)
|
||||
continue;
|
||||
|
||||
MoveCoords(direction, &testX, &testY);
|
||||
if (GetCollisionAtCoords(objectEvent, testX, testY, direction) == COLLISION_NONE)
|
||||
return ObjectEventSetHeldMovement(objectEvent, GetWalkNormalMovementAction(direction));
|
||||
}
|
||||
|
||||
ObjectEventTurn(objectEvent, GetMoveDirectionTowardTarget(objectEvent->currentCoords.x, objectEvent->currentCoords.y, targetX, targetY, objectEvent->facingDirection));
|
||||
return FALSE;
|
||||
}
|
||||
|
||||
static void SpawnOrSyncChasers(void)
|
||||
{
|
||||
u8 i;
|
||||
u8 playerObjectEventId;
|
||||
u8 activeChasers = ChaseStamina_GetActiveChasers();
|
||||
s16 playerX = gSaveBlock1Ptr->pos.x;
|
||||
s16 playerY = gSaveBlock1Ptr->pos.y;
|
||||
|
||||
if (TryGetObjectEventIdByLocalIdAndMap(LOCALID_PLAYER, 0, 0, &playerObjectEventId))
|
||||
{
|
||||
playerX = gObjectEvents[playerObjectEventId].currentCoords.x;
|
||||
playerY = gObjectEvents[playerObjectEventId].currentCoords.y;
|
||||
}
|
||||
|
||||
if (activeChasers > CHASE_OVERWORLD_MAX_CHASERS)
|
||||
activeChasers = CHASE_OVERWORLD_MAX_CHASERS;
|
||||
|
||||
if (!sChasersSpawned)
|
||||
{
|
||||
sChasersSpawned = TRUE;
|
||||
sSpawnedMapGroup = gSaveBlock1Ptr->location.mapGroup;
|
||||
sSpawnedMapNum = gSaveBlock1Ptr->location.mapNum;
|
||||
}
|
||||
|
||||
for (i = 0; i < CHASE_OVERWORLD_MAX_CHASERS; i++)
|
||||
{
|
||||
u8 localId = CHASE_OVERWORLD_LOCAL_ID_BASE + i;
|
||||
u8 objectEventId;
|
||||
|
||||
if (i >= activeChasers)
|
||||
{
|
||||
RemoveObjectEventByLocalIdAndMap(localId, sSpawnedMapNum, sSpawnedMapGroup);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!TryGetObjectEventIdByLocalIdAndMap(localId, sSpawnedMapNum, sSpawnedMapGroup, &objectEventId))
|
||||
{
|
||||
s16 spawnX = playerX - (i + 2);
|
||||
s16 spawnY = playerY + 1;
|
||||
u8 elevation = MapGridGetElevationAt(spawnX, spawnY);
|
||||
|
||||
SpawnSpecialObjectEventParameterized(CHASE_OVERWORLD_GFX_ID, MOVEMENT_TYPE_FACE_DOWN, localId, spawnX + MAP_OFFSET, spawnY + MAP_OFFSET, elevation);
|
||||
if (!TryGetObjectEventIdByLocalIdAndMap(localId, sSpawnedMapNum, sSpawnedMapGroup, &objectEventId))
|
||||
continue;
|
||||
}
|
||||
|
||||
if (ObjectEventIsHeldMovementActive(&gObjectEvents[objectEventId]))
|
||||
{
|
||||
if (!ObjectEventClearHeldMovementIfFinished(&gObjectEvents[objectEventId]))
|
||||
continue;
|
||||
}
|
||||
|
||||
TryQueueChaserStep(&gObjectEvents[objectEventId], playerX, playerY);
|
||||
}
|
||||
}
|
||||
|
||||
void ChaseOverworld_UpdateOverworldFrame(bool8 tookStep)
|
||||
{
|
||||
(void)tookStep;
|
||||
|
||||
if (!ChaseStamina_IsChaseActive())
|
||||
{
|
||||
if (sChasersSpawned)
|
||||
DespawnChasers();
|
||||
return;
|
||||
}
|
||||
|
||||
if (sChasersSpawned && !IsSpawnContextValid())
|
||||
{
|
||||
DespawnChasers();
|
||||
return;
|
||||
}
|
||||
|
||||
SpawnOrSyncChasers();
|
||||
}
|
||||
|
||||
void ChaseOverworld_OnMapTransition(const struct WarpData *from, const struct WarpData *to)
|
||||
{
|
||||
(void)from;
|
||||
(void)to;
|
||||
|
||||
if (sChasersSpawned)
|
||||
DespawnChasers();
|
||||
}
|
||||
|
||||
void ChaseOverworld_OnChaseEnded(void)
|
||||
{
|
||||
if (sChasersSpawned)
|
||||
DespawnChasers();
|
||||
}
|
||||
|
|
@ -1,5 +1,6 @@
|
|||
#include "global.h"
|
||||
#include "battle.h"
|
||||
#include "chase_overworld.h"
|
||||
#include "chase_stamina.h"
|
||||
#include "fieldmap.h"
|
||||
#include "field_player_avatar.h"
|
||||
|
|
@ -143,6 +144,7 @@ static void EndChase(void)
|
|||
sActiveChasers = 0;
|
||||
sChaseStepsRemaining = 0;
|
||||
sChaseReengageStepCountdown = 0;
|
||||
ChaseOverworld_OnChaseEnded();
|
||||
}
|
||||
|
||||
static void StartChase(u8 initialChasers, u16 initialSteps)
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
#include "global.h"
|
||||
#include "gflib.h"
|
||||
#include "bike.h"
|
||||
#include "chase_overworld.h"
|
||||
#include "chase_stamina.h"
|
||||
#include "coord_event_weather.h"
|
||||
#include "daycare.h"
|
||||
|
|
@ -205,6 +206,7 @@ int ProcessPlayerFieldInput(struct FieldInput *input)
|
|||
FieldClearPlayerInput(&gFieldInputRecord);
|
||||
gFieldInputRecord.dpadDirection = input->dpadDirection;
|
||||
ChaseStamina_UpdateOverworldFrame(input->tookStep);
|
||||
ChaseOverworld_UpdateOverworldFrame(input->tookStep);
|
||||
|
||||
if (CheckForTrainersWantingBattle() == TRUE)
|
||||
return TRUE;
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@
|
|||
#include "gflib.h"
|
||||
#include "bg_regs.h"
|
||||
#include "cable_club.h"
|
||||
#include "chase_overworld.h"
|
||||
#include "chase_stamina.h"
|
||||
#include "credits.h"
|
||||
#include "corpse_run.h"
|
||||
|
|
@ -796,6 +797,7 @@ void LoadMapFromCameraTransition(u8 mapGroup, u8 mapNum)
|
|||
ApplyCurrentWarp();
|
||||
to = gSaveBlock1Ptr->location;
|
||||
ChaseStamina_OnMapTransition(&from, &to);
|
||||
ChaseOverworld_OnMapTransition(&from, &to);
|
||||
LoadCurrentMapData();
|
||||
LoadObjEventTemplatesFromHeader();
|
||||
TrySetMapSaveWarpStatus();
|
||||
|
|
@ -832,6 +834,7 @@ static void LoadMapFromWarp(bool32 unused)
|
|||
|
||||
LoadCurrentMapData();
|
||||
ChaseStamina_OnMapTransition(&from, &gSaveBlock1Ptr->location);
|
||||
ChaseOverworld_OnMapTransition(&from, &gSaveBlock1Ptr->location);
|
||||
LoadObjEventTemplatesFromHeader();
|
||||
isOutdoors = IsMapTypeOutdoors(gMapHeader.mapType);
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user