diff --git a/graphics/battle_interface/last_used_ball_l_cycle.png b/graphics/battle_interface/last_used_ball_l_cycle.png new file mode 100644 index 000000000..58115f5a5 Binary files /dev/null and b/graphics/battle_interface/last_used_ball_l_cycle.png differ diff --git a/graphics/battle_interface/last_used_ball_r_cycle.png b/graphics/battle_interface/last_used_ball_r_cycle.png new file mode 100644 index 000000000..0a73148e1 Binary files /dev/null and b/graphics/battle_interface/last_used_ball_r_cycle.png differ diff --git a/include/battle.h b/include/battle.h index f471a86f5..131f192e9 100644 --- a/include/battle.h +++ b/include/battle.h @@ -1010,6 +1010,8 @@ extern u8 gBattleControllerData[MAX_BATTLERS_COUNT]; extern bool8 gHasFetchedBall; extern u8 gLastUsedBall; extern u16 gLastThrownBall; +extern u16 gBallToDisplay; +extern bool8 gLastUsedBallMenuPresent; extern u8 gPartyCriticalHits[PARTY_SIZE]; #endif // GUARD_BATTLE_H diff --git a/include/battle_interface.h b/include/battle_interface.h index e2937748a..0cd0df77c 100644 --- a/include/battle_interface.h +++ b/include/battle_interface.h @@ -105,6 +105,8 @@ bool32 CanThrowLastUsedBall(void); void TryHideLastUsedBall(void); void TryRestoreLastUsedBall(void); void TryAddLastUsedBallItemSprites(void); +void SwapBallToDisplay(bool32 sameBall); +void ArrowsChangeColorLastBallCycle(bool32 showArrows); void UpdateAbilityPopup(u8 battlerId); #endif // GUARD_BATTLE_INTERFACE_H diff --git a/include/config/battle.h b/include/config/battle.h index 68cab28b6..ae64647b6 100644 --- a/include/config/battle.h +++ b/include/config/battle.h @@ -191,6 +191,7 @@ #define B_CRITICAL_CAPTURE TRUE // If set to TRUE, Critical Capture will be enabled. #define B_LAST_USED_BALL TRUE // If TRUE, the "last used ball" feature from Gen 7 will be implemented #define B_LAST_USED_BALL_BUTTON R_BUTTON // If last used ball is implemented, this button (or button combo) will trigger throwing the last used ball. +#define B_LAST_USED_BALL_CYCLE TRUE // If TRUE, then holding B_LAST_USED_BALL_BUTTON while pressing the D-Pad cycles through the balls // Other settings #define B_DOUBLE_WILD_CHANCE 0 // % chance of encountering two Pokémon in a Wild Encounter. diff --git a/src/battle_controller_player.c b/src/battle_controller_player.c index eb47db444..d34938132 100644 --- a/src/battle_controller_player.c +++ b/src/battle_controller_player.c @@ -184,6 +184,9 @@ static void (*const sPlayerBufferCommands[CONTROLLER_CMDS_COUNT])(void) = [CONTROLLER_TERMINATOR_NOP] = PlayerCmdEnd }; +static EWRAM_DATA bool8 sAckBallUseBtn = FALSE; +static EWRAM_DATA bool8 sBallSwapped = FALSE; + // unknown unused data static const u8 sUnused[] = {0x48, 0x48, 0x20, 0x5a, 0x50, 0x50, 0x50, 0x58}; @@ -231,6 +234,49 @@ static void CompleteOnBankSpritePosX_0(void) PlayerBufferExecCompleted(); } +static u16 GetPrevBall(u16 ballId) +{ + u16 ballPrev; + u32 i, j; + CompactItemsInBagPocket(&gBagPockets[BALLS_POCKET]); + for (i = 0; i < gBagPockets[BALLS_POCKET].capacity; i++) + { + if (ballId == gBagPockets[BALLS_POCKET].itemSlots[i].itemId) + { + if (i <= 0) + { + for (j = gBagPockets[BALLS_POCKET].capacity - 1; j >= 0; j--) + { + ballPrev = gBagPockets[BALLS_POCKET].itemSlots[j].itemId; + if (ballPrev != ITEM_NONE) + return ballPrev; + } + } + i--; + return gBagPockets[BALLS_POCKET].itemSlots[i].itemId; + } + } +} + +static u16 GetNextBall(u16 ballId) +{ + u16 ballNext; + u32 i; + CompactItemsInBagPocket(&gBagPockets[BALLS_POCKET]); + for (i = 0; i < gBagPockets[BALLS_POCKET].capacity; i++) + { + if (ballId == gBagPockets[BALLS_POCKET].itemSlots[i].itemId) + { + i++; + ballNext = gBagPockets[BALLS_POCKET].itemSlots[i].itemId; + if (ballNext == ITEM_NONE) + return gBagPockets[BALLS_POCKET].itemSlots[0].itemId; // Zeroth slot + else + return ballNext; + } + } +} + static void HandleInputChooseAction(void) { u16 itemId = gBattleResources->bufferA[gActiveBattler][2] | (gBattleResources->bufferA[gActiveBattler][3] << 8); @@ -243,6 +289,62 @@ static void HandleInputChooseAction(void) else gPlayerDpadHoldFrames = 0; +#if B_LAST_USED_BALL == TRUE && B_LAST_USED_BALL_CYCLE == TRUE + if (!gLastUsedBallMenuPresent) + { + sAckBallUseBtn = FALSE; + } + else if (JOY_NEW(B_LAST_USED_BALL_BUTTON)) + { + sAckBallUseBtn = TRUE; + sBallSwapped = FALSE; + ArrowsChangeColorLastBallCycle(TRUE); + } + if (sAckBallUseBtn) + { + if (JOY_HELD(B_LAST_USED_BALL_BUTTON) && (JOY_NEW(DPAD_DOWN) || JOY_NEW(DPAD_RIGHT))) + { + bool8 sameBall = FALSE; + u16 nextBall = GetNextBall(gBallToDisplay); + sBallSwapped = TRUE; + if (gBallToDisplay == nextBall) + sameBall = TRUE; + else + gBallToDisplay = nextBall; + SwapBallToDisplay(sameBall); + PlaySE(SE_SELECT); + } + else if (JOY_HELD(B_LAST_USED_BALL_BUTTON) && (JOY_NEW(DPAD_UP) || JOY_NEW(DPAD_LEFT))) + { + bool8 sameBall = FALSE; + u16 prevBall = GetPrevBall(gBallToDisplay); + sBallSwapped = TRUE; + if (gBallToDisplay == prevBall) + sameBall = TRUE; + else + gBallToDisplay = prevBall; + SwapBallToDisplay(sameBall); + PlaySE(SE_SELECT); + } + else if (!JOY_HELD(B_LAST_USED_BALL_BUTTON) && sBallSwapped) + { + sAckBallUseBtn = FALSE; + sBallSwapped = FALSE; + ArrowsChangeColorLastBallCycle(FALSE); + } + else if (!JOY_HELD(B_LAST_USED_BALL_BUTTON) && CanThrowLastUsedBall()) + { + sAckBallUseBtn = FALSE; + PlaySE(SE_SELECT); + ArrowsChangeColorLastBallCycle(FALSE); + TryHideLastUsedBall(); + BtlController_EmitTwoReturnValues(1, B_ACTION_THROW_BALL, 0); + PlayerBufferExecCompleted(); + } + return; + } +#endif + if (JOY_NEW(A_BUTTON)) { PlaySE(SE_SELECT); @@ -333,7 +435,7 @@ static void HandleInputChooseAction(void) PlayerBufferExecCompleted(); } #endif -#if B_LAST_USED_BALL == TRUE +#if B_LAST_USED_BALL == TRUE && B_LAST_USED_BALL_CYCLE == FALSE else if (JOY_NEW(B_LAST_USED_BALL_BUTTON) && CanThrowLastUsedBall()) { PlaySE(SE_SELECT); diff --git a/src/battle_interface.c b/src/battle_interface.c index a087c715b..09b1200d2 100644 --- a/src/battle_interface.c +++ b/src/battle_interface.c @@ -3213,10 +3213,18 @@ static const struct OamData sOamData_LastUsedBall = .objMode = 0, .mosaic = 0, .bpp = 0, +#if B_LAST_USED_BALL_CYCLE == TRUE + .shape = SPRITE_SHAPE(32x64), +#else .shape = SPRITE_SHAPE(32x32), +#endif .x = 0, .matrixNum = 0, +#if B_LAST_USED_BALL_CYCLE == TRUE + .size = SPRITE_SIZE(32x64), +#else .size = SPRITE_SIZE(32x32), +#endif .tileNum = 0, .priority = 1, .paletteNum = 0, @@ -3234,7 +3242,11 @@ static const struct SpriteTemplate sSpriteTemplate_LastUsedBallWindow = .callback = SpriteCB_LastUsedBallWin }; -#if B_LAST_USED_BALL_BUTTON == R_BUTTON +#if B_LAST_USED_BALL_BUTTON == R_BUTTON && B_LAST_USED_BALL_CYCLE == TRUE + static const u8 ALIGNED(4) sLastUsedBallWindowGfx[] = INCBIN_U8("graphics/battle_interface/last_used_ball_r_cycle.4bpp"); +#elif B_LAST_USED_BALL_CYCLE == TRUE + static const u8 ALIGNED(4) sLastUsedBallWindowGfx[] = INCBIN_U8("graphics/battle_interface/last_used_ball_l_cycle.4bpp"); +#elif B_LAST_USED_BALL_BUTTON == R_BUTTON static const u8 ALIGNED(4) sLastUsedBallWindowGfx[] = INCBIN_U8("graphics/battle_interface/last_used_ball_r.4bpp"); #else static const u8 ALIGNED(4) sLastUsedBallWindowGfx[] = INCBIN_U8("graphics/battle_interface/last_used_ball_l.4bpp"); @@ -3247,12 +3259,19 @@ static const struct SpriteSheet sSpriteSheet_LastUsedBallWindow = #define LAST_USED_BALL_X_F 15 #define LAST_USED_BALL_X_0 -15 #define LAST_USED_BALL_Y ((IsDoubleBattle()) ? 78 : 68) +#define LAST_USED_BALL_Y_BNC ((IsDoubleBattle()) ? 76 : 66) #define LAST_BALL_WIN_X_F (LAST_USED_BALL_X_F - 1) #define LAST_BALL_WIN_X_0 (LAST_USED_BALL_X_0 - 1) #define LAST_USED_WIN_Y (LAST_USED_BALL_Y - 8) #define sHide data[0] +#define sTimer data[1] +#define sMoving data[2] +#define sBounce data[3] // 0 = Bounce down; 1 = Bounce up + +#define sState data[0] +#define sSameBall data[1] bool32 CanThrowLastUsedBall(void) { @@ -3263,7 +3282,7 @@ bool32 CanThrowLastUsedBall(void) return FALSE; if (gBattleTypeFlags & (BATTLE_TYPE_TRAINER | BATTLE_TYPE_FRONTIER)) return FALSE; - if (!CheckBagHasItem(gLastThrownBall, 1)) + if (!CheckBagHasItem(gBallToDisplay, 1)) return FALSE; return TRUE; @@ -3279,7 +3298,7 @@ void TryAddLastUsedBallItemSprites(void) // we're out of the last used ball, so just set it to the first ball in the bag // we have to compact the bag first bc it is typically only compacted when you open it CompactItemsInBagPocket(&gBagPockets[BALLS_POCKET]); - gLastThrownBall = gBagPockets[BALLS_POCKET].itemSlots[0].itemId; + gBallToDisplay = gBagPockets[BALLS_POCKET].itemSlots[0].itemId; } if (!CanThrowLastUsedBall()) @@ -3288,10 +3307,11 @@ void TryAddLastUsedBallItemSprites(void) // ball if (gBattleStruct->ballSpriteIds[0] == MAX_SPRITES) { - gBattleStruct->ballSpriteIds[0] = AddItemIconSprite(102, 102, gLastThrownBall); + gBattleStruct->ballSpriteIds[0] = AddItemIconSprite(102, 102, gBallToDisplay); gSprites[gBattleStruct->ballSpriteIds[0]].x = LAST_USED_BALL_X_0; gSprites[gBattleStruct->ballSpriteIds[0]].y = LAST_USED_BALL_Y; gSprites[gBattleStruct->ballSpriteIds[0]].sHide = FALSE; // restore + gLastUsedBallMenuPresent = TRUE; gSprites[gBattleStruct->ballSpriteIds[0]].callback = SpriteCB_LastUsedBall; } @@ -3306,7 +3326,11 @@ void TryAddLastUsedBallItemSprites(void) LAST_BALL_WIN_X_0, LAST_USED_WIN_Y, 5); gSprites[gBattleStruct->ballSpriteIds[1]].sHide = FALSE; // restore + gLastUsedBallMenuPresent = TRUE; } +#if B_LAST_USED_BALL_CYCLE == TRUE + ArrowsChangeColorLastBallCycle(0); //Default the arrows to be invisible +#endif #endif } @@ -3347,6 +3371,9 @@ static void SpriteCB_LastUsedBall(struct Sprite *sprite) { if (sprite->sHide) { + if (sprite->y < LAST_USED_BALL_Y) // Used to recover from an incomplete bounce before hiding the window + sprite->y++; + if (sprite->x != LAST_USED_BALL_X_0) sprite->x--; @@ -3373,14 +3400,19 @@ static void TryHideOrRestoreLastUsedBall(u8 caseId) gSprites[gBattleStruct->ballSpriteIds[0]].sHide = TRUE; // hide if (gBattleStruct->ballSpriteIds[1] != MAX_SPRITES) gSprites[gBattleStruct->ballSpriteIds[1]].sHide = TRUE; // hide + gLastUsedBallMenuPresent = FALSE; break; case 1: // restore if (gBattleStruct->ballSpriteIds[0] != MAX_SPRITES) gSprites[gBattleStruct->ballSpriteIds[0]].sHide = FALSE; // restore if (gBattleStruct->ballSpriteIds[1] != MAX_SPRITES) gSprites[gBattleStruct->ballSpriteIds[1]].sHide = FALSE; // restore + gLastUsedBallMenuPresent = TRUE; break; } +#if B_LAST_USED_BALL_CYCLE == TRUE + ArrowsChangeColorLastBallCycle(0); //Default the arrows to be invisible +#endif #endif } @@ -3400,3 +3432,120 @@ void TryRestoreLastUsedBall(void) TryAddLastUsedBallItemSprites(); #endif } + +static void SpriteCB_LastUsedBallBounce(struct Sprite *sprite) +{ + if ((sprite->sTimer++ % 4) != 0) // Change the image every 4 frame + return; + if (sprite->sBounce) + { + if (sprite->y > LAST_USED_BALL_Y_BNC) + sprite->y--; + else + sprite->sMoving = FALSE; + } + else + { + if (sprite->y < LAST_USED_BALL_Y) + sprite->y++; + else + sprite->sMoving = FALSE; + } +} + +static void Task_BounceBall(u8 taskId) +{ + struct Sprite *sprite = &gSprites[gBattleStruct->ballSpriteIds[0]]; + struct Task *task = &gTasks[taskId]; + switch(task->sState) + { + case 0: // Bounce up + sprite->sBounce = TRUE; + sprite->sMoving = TRUE; + sprite->callback = SpriteCB_LastUsedBallBounce; + if (task->sSameBall) + task->sState = 3; + else + task->sState = 1; + break; + case 1: // Destroy Icon + if (!sprite->sMoving) + { + DestroyLastUsedBallGfx(sprite); + task->sState++; + } // Passthrough + case 2: //Create New Icon + if (!sprite->inUse) + { + gBattleStruct->ballSpriteIds[0] = AddItemIconSprite(102, 102, gBallToDisplay); + gSprites[gBattleStruct->ballSpriteIds[0]].x = LAST_USED_BALL_X_F; + gSprites[gBattleStruct->ballSpriteIds[0]].y = LAST_USED_BALL_Y_BNC; + task->sState++; + } // Fallthrough + case 3: // Bounce Down + if (!sprite->sMoving) + { + sprite->sBounce = FALSE; + sprite->sMoving = TRUE; + sprite->callback = SpriteCB_LastUsedBallBounce; //Show and bounce down + task->sState++; + } + break; + case 4: // Destroy Task + if(!sprite->sMoving) + { + sprite->callback = SpriteCB_LastUsedBall; + DestroyTask(taskId); + } + } + if (!gLastUsedBallMenuPresent) + { + // Used to check if the R button was released before the animation was complete + sprite->callback = SpriteCB_LastUsedBall; + DestroyTask(taskId); + } +} + +void SwapBallToDisplay(bool32 sameBall) +{ + u8 taskId; + taskId = CreateTask(Task_BounceBall, 10); + gTasks[taskId].sSameBall = sameBall; +} + +void ArrowsChangeColorLastBallCycle(bool32 showArrows) +{ +#if B_LAST_USED_BALL == TRUE && B_LAST_USED_BALL_CYCLE == TRUE + u16 paletteNum = 16 + gSprites[gBattleStruct->ballSpriteIds[1]].oam.paletteNum; + struct PlttData *defaultPlttArrow; + struct PlttData *defaultPlttOutline; + struct PlttData *pltArrow; + struct PlttData *pltOutline; + if (gBattleStruct->ballSpriteIds[1] == MAX_SPRITES) + return; + paletteNum *= 16; + pltArrow = (struct PlttData *)&gPlttBufferFaded[paletteNum + 9]; // Arrow color is in idx 9 + pltOutline = (struct PlttData *)&gPlttBufferFaded[paletteNum + 8]; // Arrow outline is in idx 8 + if (!showArrows) //Make invisible + { + defaultPlttArrow = (struct PlttData *)&gPlttBufferFaded[paletteNum + 13]; // Background color is idx 13 + pltArrow->r = defaultPlttArrow->r; + pltArrow->g = defaultPlttArrow->g; + pltArrow->b = defaultPlttArrow->b; + pltOutline->r = defaultPlttArrow->r; + pltOutline->g = defaultPlttArrow->g; + pltOutline->b = defaultPlttArrow->b; + } + else // Make gray + { + defaultPlttArrow = (struct PlttData *)&gPlttBufferFaded[paletteNum + 11]; // Grey color is idx 11 + defaultPlttOutline = (struct PlttData *)&gPlttBufferFaded[paletteNum + 10]; //Light grey color for outline is idx 10 + pltArrow->r = defaultPlttArrow->r; + pltArrow->g = defaultPlttArrow->g; + pltArrow->b = defaultPlttArrow->b; + pltOutline->r = defaultPlttOutline->r; + pltOutline->g = defaultPlttOutline->g; + pltOutline->b = defaultPlttOutline->b; + } +#endif +} diff --git a/src/battle_main.c b/src/battle_main.c index e6ed4269d..e6640fda7 100644 --- a/src/battle_main.c +++ b/src/battle_main.c @@ -242,6 +242,8 @@ EWRAM_DATA struct TotemBoost gTotemBoosts[MAX_BATTLERS_COUNT] = {0}; EWRAM_DATA bool8 gHasFetchedBall = FALSE; EWRAM_DATA u8 gLastUsedBall = 0; EWRAM_DATA u16 gLastThrownBall = 0; +EWRAM_DATA u16 gBallToDisplay = 0; +EWRAM_DATA bool8 gLastUsedBallMenuPresent = FALSE; EWRAM_DATA u8 gPartyCriticalHits[PARTY_SIZE] = {0}; EWRAM_DATA static u8 sTriedEvolving = 0; diff --git a/src/battle_script_commands.c b/src/battle_script_commands.c index 646e28767..2ab173a49 100644 --- a/src/battle_script_commands.c +++ b/src/battle_script_commands.c @@ -15155,6 +15155,7 @@ static void Cmd_handleballthrow(void) u8 catchRate; gLastThrownBall = gLastUsedItem; + gBallToDisplay = gLastThrownBall; if (gBattleTypeFlags & BATTLE_TYPE_SAFARI) catchRate = gBattleStruct->safariCatchFactor * 1275 / 100; else diff --git a/src/battle_util.c b/src/battle_util.c index ea8ee4cde..2f2a65923 100644 --- a/src/battle_util.c +++ b/src/battle_util.c @@ -723,7 +723,7 @@ void HandleAction_ThrowBall(void) gBattlerAttacker = gBattlerByTurnOrder[gCurrentTurnActionNumber]; gBattle_BG0_X = 0; gBattle_BG0_Y = 0; - gLastUsedItem = gLastThrownBall; + gLastUsedItem = gBallToDisplay; RemoveBagItem(gLastUsedItem, 1); gBattlescriptCurrInstr = BattleScript_BallThrow; gCurrentActionFuncId = B_ACTION_EXEC_SCRIPT; diff --git a/sym_ewram.txt b/sym_ewram.txt index 914501a08..2d7a6e26e 100644 --- a/sym_ewram.txt +++ b/sym_ewram.txt @@ -149,3 +149,4 @@ .include "src/trainer_hill.o" .include "src/rayquaza_scene.o" .include "src/debug.o" + .include "src/battle_controller_player.o"