Stair Warps
Credit to ghoulslash. The easy implementation is to pull from the repo
FRLG include a stair warp effect where the player walks up/down stairs at the beginning/end of the warp sequence. This ports that feature from FRLG to emerald:
Add new metatile behaviors
1. First, open include/constants/metatile_behaviors.h. Replace any 4 unused metatile behaviors (I chose MB_UNUSED_EB through MB_UNUSED_EE):
#define MB_UP_RIGHT_STAIR_WARP 0xEB
#define MB_UP_LEFT_STAIR_WARP 0xEC
#define MB_DOWN_RIGHT_STAIR_WARP 0xED
#define MB_DOWN_LEFT_STAIR_WARP 0xEE
2. Next, open src/metatile_behavior.c. Replace the elements in sTileBitAttributes
for your metatile behaviours:
[MB_UP_RIGHT_STAIR_WARP] = TILE_ATTRIBUTES(FALSE, FALSE, FALSE),
[MB_UP_LEFT_STAIR_WARP] = TILE_ATTRIBUTES(FALSE, FALSE, FALSE),
[MB_DOWN_RIGHT_STAIR_WARP] = TILE_ATTRIBUTES(FALSE, FALSE, FALSE),
[MB_DOWN_LEFT_STAIR_WARP] = TILE_ATTRIBUTES(FALSE, FALSE, FALSE),
3. At the bottom of the file, add the following 5 functions:
bool8 MetatileBehavior_IsDirectionalUpRightStairWarp(u8 metatileBehavior)
{
if(metatileBehavior == MB_UP_RIGHT_STAIR_WARP)
return TRUE;
else
return FALSE;
}
bool8 MetatileBehavior_IsDirectionalUpLeftStairWarp(u8 metatileBehavior)
{
if (metatileBehavior == MB_UP_LEFT_STAIR_WARP)
return TRUE;
else
return FALSE;
}
bool8 MetatileBehavior_IsDirectionalDownRightStairWarp(u8 metatileBehavior)
{
if (metatileBehavior == MB_DOWN_RIGHT_STAIR_WARP)
return TRUE;
else
return FALSE;
}
bool8 MetatileBehavior_IsDirectionalDownLeftStairWarp(u8 metatileBehavior)
{
if (metatileBehavior == MB_DOWN_LEFT_STAIR_WARP)
return TRUE;
else
return FALSE;
}
bool8 MetatileBehavior_IsDirectionalStairWarp(u8 metatileBehavior)
{
if (metatileBehavior >= MB_UP_RIGHT_STAIR_WARP && metatileBehavior <= MB_DOWN_LEFT_STAIR_WARP)
return TRUE;
else
return FALSE;
}
Globally define the metatile behavior functions
Open include/metatile_behavior.h. Add the following to the bottom:
bool8 MetatileBehavior_IsDirectionalUpRightStairWarp(u8 metatileBehavior);
bool8 MetatileBehavior_IsDirectionalUpLeftStairWarp(u8 metatileBehavior);
bool8 MetatileBehavior_IsDirectionalDownRightStairWarp(u8 metatileBehavior);
bool8 MetatileBehavior_IsDirectionalDownLeftStairWarp(u8 metatileBehavior);
bool8 MetatileBehavior_IsDirectionalStairWarp(u8 metatileBehavior);
Add a new stair warp collision
This allows us to ignore the impassable tiles when we try to warp on the stairs. Open include/global.fieldmap.h. Add COLLISION_STAIR_WARP
after the enum define COLLISION_HORIZONTAL_RAIL,
Add the collision exclusion
Open src/field_player_avatar.c.
1. First, let's add #include "field_screen_effect.h"
to the top of the file.
2. Next, find static u8 CheckForPlayerAvatarCollision(u8 direction)
. Before the line, MoveCoords(direction, &x, &y);
, add the following two lines:
if (IsDirectionalStairWarpMetatileBehavior(MapGridGetMetatileBehaviorAt(x, y), direction))
return COLLISION_STAIR_WARP;
3. Finally, find static void PlayerNotOnBikeMoving(u8 direction, u16 heldKeys)
. Replace the if (collision)
code block with:
if (collision)
{
if (collision == COLLISION_LEDGE_JUMP)
{
PlayerJumpLedge(direction);
return;
}
else if (collision == COLLISION_OBJECT_EVENT && IsPlayerCollidingWithFarawayIslandMew(direction))
{
PlayerNotOnBikeCollideWithFarawayIslandMew(direction);
return;
}
else if (collision == COLLISION_STAIR_WARP)
{
PlayerFaceDirection(direction);
}
else
{
u8 adjustedCollision = collision - COLLISION_STOP_SURFING;
if (adjustedCollision > 3)
PlayerNotOnBikeCollide(direction);
return;
}
}
Add the warp arrow
Open src/field_control_avatar.c and find the function TryArrowWarp
. Replace everything with the following:
static bool8 TryArrowWarp(struct MapPosition *position, u16 metatileBehavior, u8 direction)
{
s8 warpEventId = GetWarpEventAtMapPosition(&gMapHeader, position);
u16 delay;
if (warpEventId != -1)
{
if (IsArrowWarpMetatileBehavior(metatileBehavior, direction) == TRUE)
{
StoreInitialPlayerAvatarState();
SetupWarp(&gMapHeader, warpEventId, position);
DoWarp();
return TRUE;
}
else if (IsDirectionalStairWarpMetatileBehavior(metatileBehavior, direction) == TRUE)
{
delay = 0;
if (gPlayerAvatar.flags & (PLAYER_AVATAR_FLAG_MACH_BIKE | PLAYER_AVATAR_FLAG_ACRO_BIKE))
{
SetPlayerAvatarTransitionFlags(PLAYER_AVATAR_FLAG_ON_FOOT);
delay = 12;
}
StoreInitialPlayerAvatarState();
SetupWarp(&gMapHeader, warpEventId, position);
DoStairWarp(metatileBehavior, delay);
return TRUE;
}
}
return FALSE;
}
Add DoStairWarp
This is the meat of the code implementation. Let's start by opening src/field_screen_effect.c.
1. Find the function static void SetUpWarpExitTask(void)
. Before else if (MetatileBehavior_IsNonAnimDoor(behavior) == TRUE)
, Add the following:
else if (MetatileBehavior_IsDirectionalStairWarp(behavior) == TRUE)
func = Task_ExitStairs;
2. Also, add static void Task_ExitStairs(u8 taskId);
to the top of the file underneath static void Task_EnableScriptAfterMusicFade(u8 taskId);
.
3. At the bottom of the file, let's add a bunch of functions:
static void GetStairsMovementDirection(u8 a0, s16 *a1, s16 *a2)
{
if (MetatileBehavior_IsDirectionalUpRightStairWarp(a0))
{
*a1 = 16;
*a2 = -10;
}
else if (MetatileBehavior_IsDirectionalUpLeftStairWarp(a0))
{
*a1 = -17;
*a2 = -10;
}
else if (MetatileBehavior_IsDirectionalDownRightStairWarp(a0))
{
*a1 = 17;
*a2 = 3;
}
else if (MetatileBehavior_IsDirectionalDownLeftStairWarp(a0))
{
*a1 = -17;
*a2 = 3;
}
else
{
*a1 = 0;
*a2 = 0;
}
}
static bool8 WaitStairExitMovementFinished(s16 *a0, s16 *a1, s16 *a2, s16 *a3, s16 *a4)
{
struct Sprite *sprite;
sprite = &gSprites[gPlayerAvatar.spriteId];
if (*a4 != 0)
{
*a2 += *a0;
*a3 += *a1;
sprite->pos2.x = *a2 >> 5;
sprite->pos2.y = *a3 >> 5;
(*a4)--;
return TRUE;
}
else
{
sprite->pos2.x = 0;
sprite->pos2.y = 0;
return FALSE;
}
}
static void ExitStairsMovement(s16 *a0, s16 *a1, s16 *a2, s16 *a3, s16 *a4)
{
s16 x, y;
u8 behavior;
s32 r1;
struct Sprite *sprite;
PlayerGetDestCoords(&x, &y);
behavior = MapGridGetMetatileBehaviorAt(x, y);
if (MetatileBehavior_IsDirectionalDownRightStairWarp(behavior) || MetatileBehavior_IsDirectionalUpRightStairWarp(behavior))
r1 = 3;
else
r1 = 4;
ObjectEventForceSetHeldMovement(&gObjectEvents[gPlayerAvatar.objectEventId], GetWalkInPlaceSlowMovementAction(r1));
GetStairsMovementDirection(behavior, a0, a1);
*a2 = *a0 * 16;
*a3 = *a1 * 16;
*a4 = 16;
sprite = &gSprites[gPlayerAvatar.spriteId];
sprite->pos2.x = *a2 >> 5;
sprite->pos2.y = *a3 >> 5;
*a0 *= -1;
*a1 *= -1;
}
static void Task_ExitStairs(u8 taskId)
{
s16 * data = gTasks[taskId].data;
switch (data[0])
{
default:
if (WaitForWeatherFadeIn() == TRUE)
{
CameraObjectReset1();
ScriptContext2_Disable();
DestroyTask(taskId);
}
break;
case 0:
Overworld_PlaySpecialMapMusic();
WarpFadeInScreen();
ScriptContext2_Enable();
ExitStairsMovement(&data[1], &data[2], &data[3], &data[4], &data[5]);
data[0]++;
break;
case 1:
if (!WaitStairExitMovementFinished(&data[1], &data[2], &data[3], &data[4], &data[5]))
data[0]++;
break;
}
}
bool8 IsDirectionalStairWarpMetatileBehavior(u16 metatileBehavior, u8 playerDirection)
{
switch (playerDirection)
{
case DIR_WEST:
if (MetatileBehavior_IsDirectionalUpLeftStairWarp(metatileBehavior))
return TRUE;
if (MetatileBehavior_IsDirectionalDownLeftStairWarp(metatileBehavior))
return TRUE;
break;
case DIR_EAST:
if (MetatileBehavior_IsDirectionalUpRightStairWarp(metatileBehavior))
return TRUE;
if (MetatileBehavior_IsDirectionalDownRightStairWarp(metatileBehavior))
return TRUE;
break;
}
return FALSE;
}
static void ForceStairsMovement(u16 a0, s16 *a1, s16 *a2)
{
ObjectEventForceSetHeldMovement(&gObjectEvents[gPlayerAvatar.objectEventId], GetWalkInPlaceNormalMovementAction(GetPlayerFacingDirection()));
GetStairsMovementDirection(a0, a1, a2);
}
static void UpdateStairsMovement(s16 a0, s16 a1, s16 *a2, s16 *a3, s16 *a4)
{
struct Sprite *playerSpr = &gSprites[gPlayerAvatar.spriteId];
struct ObjectEvent *playerObj = &gObjectEvents[gPlayerAvatar.objectEventId];
if (a1 > 0 || *a4 > 6)
*a3 += a1;
*a2 += a0;
(*a4)++;
playerSpr->pos2.x = *a2 >> 5;
playerSpr->pos2.y = *a3 >> 5;
if (playerObj->heldMovementFinished)
ObjectEventForceSetHeldMovement(playerObj, GetWalkInPlaceNormalMovementAction(GetPlayerFacingDirection()));
}
static void Task_StairWarp(u8 taskId)
{
s16 * data = gTasks[taskId].data;
struct ObjectEvent *playerObj = &gObjectEvents[gPlayerAvatar.objectEventId];
struct Sprite *playerSpr = &gSprites[gPlayerAvatar.spriteId];
switch (data[0])
{
case 0:
ScriptContext2_Enable();
FreezeObjectEvents();
CameraObjectReset2();
data[0]++;
break;
case 1:
if (!ObjectEventIsMovementOverridden(playerObj) || ObjectEventClearHeldMovementIfFinished(playerObj))
{
if (data[15] != 0)
data[15]--;
else
{
TryFadeOutOldMapMusic();
PlayRainStoppingSoundEffect();
playerSpr->oam.priority = 1;
ForceStairsMovement(data[1], &data[2], &data[3]);
PlaySE(SE_KAIDAN);
data[0]++;
}
}
break;
case 2:
UpdateStairsMovement(data[2], data[3], &data[4], &data[5], &data[6]);
data[15]++;
if (data[15] >= 12)
{
WarpFadeOutScreen();
data[0]++;
}
break;
case 3:
UpdateStairsMovement(data[2], data[3], &data[4], &data[5], &data[6]);
if (!PaletteFadeActive() && BGMusicStopped())
data[0]++;
break;
default:
gFieldCallback = FieldCB_DefaultWarpExit;
WarpIntoMap();
SetMainCallback2(CB2_LoadMap);
DestroyTask(taskId);
break;
}
}
void DoStairWarp(u16 metatileBehavior, u16 delay)
{
u8 taskId = CreateTask(Task_StairWarp, 10);
gTasks[taskId].data[1] = metatileBehavior;
gTasks[taskId].data[15] = delay;
Task_StairWarp(taskId);
}
Globally define two stair warp functions
Open include/field_screen_effect.h. At the bottom, add the following:
void DoStairWarp(u16 metatileBehavior, u16 delay);
bool8 IsDirectionalStairWarpMetatileBehavior(u16 metatileBehavior, u8 playerDirection);
Adjust the player's movement direction on stair warps
Open src/overworld.c and find the function GetAdjustedInitialDirection
. Before the line else if ((playerStruct->transitionFlags == PLAYER_AVATAR_FLAG_UNDERWATER && (etc...)
, add the following:
else if (MetatileBehavior_IsDirectionalUpRightStairWarp(metatileBehavior) == TRUE || MetatileBehavior_IsDirectionalDownRightStairWarp(metatileBehavior) == TRUE)
return DIR_WEST;
else if (MetatileBehavior_IsDirectionalUpLeftStairWarp(metatileBehavior) == TRUE || MetatileBehavior_IsDirectionalDownLeftStairWarp(metatileBehavior) == TRUE)
return DIR_EAST;