## Stair Warps Credit to ghoulslash. The easy implementation is to pull from the [repo](https://github.com/ghoulslash/pokeemerald/tree/stair_warps) 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](../blob/master/include/constants/metatile_behaviors.h). Replace any 4 unused metatile behaviors (I chose MB_UNUSED_EB through MB_UNUSED_EE): ```c #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](../blob/master/src/metatile_behavior.c). Replace the elements in `sTileBitAttributes` for your metatile behaviours: ```c [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: ```c 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](../blob/master/include/metatile_behavior.h). Add the following to the bottom: ```c 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](../blob/master/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](../blob/master/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: ```c 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: ```c 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](../blob/master/src/field_control_avatar.c) and find the function `TryArrowWarp`. Replace everything with the following: ```c 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](../blob/master/src/field_screen_effect.c). **1.** Find the function `static void SetUpWarpExitTask(void)`. Before `else if (MetatileBehavior_IsNonAnimDoor(behavior) == TRUE)`, Add the following: ```c 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: ```c 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](../blob/master/include/field_screen_effect.h). At the bottom, add the following: ```c 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](../blob/master/src/overworld.c) and find the function `GetAdjustedInitialDirection`. Before the line `else if ((playerStruct->transitionFlags == PLAYER_AVATAR_FLAG_UNDERWATER && (etc...)`, add the following: ```c 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; ```