2 Stair Warps
Jaizu edited this page 2020-08-27 12:38:46 +02:00

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;