From 6137db102e83c5cf2a178cc0f8c62b5f0c35b9ed Mon Sep 17 00:00:00 2001 From: DizzyEggg Date: Wed, 20 Dec 2023 15:26:28 +0100 Subject: [PATCH] Ally Switch (#3533) * ally switch move animation * Ally Switch anim done * ally switch test and improve animation * derp * add ally switch known failing test for ally targeting moves * moves which targetted ally fail after ally switch * ally switch works like protect --------- Co-authored-by: root Co-authored-by: Alex <93446519+AlexOn1ine@users.noreply.github.com> --- asm/macros/battle_script.inc | 9 + data/battle_anim_scripts.s | 15 +- data/battle_scripts_1.s | 3 + include/battle.h | 5 +- include/battle_anim.h | 2 +- include/battle_interface.h | 1 + include/battle_main.h | 1 + include/battle_scripts.h | 1 + include/config/battle.h | 1 + include/reshow_battle_screen.h | 1 + src/battle_anim.c | 4 +- src/battle_anim_effects_1.c | 247 +++++++++++++++++++++----- src/battle_interface.c | 1 - src/battle_main.c | 19 ++ src/battle_script_commands.c | 45 ++++- src/battle_util.c | 5 + src/reshow_battle_screen.c | 11 +- test/battle/move_effect/ally_switch.c | 209 ++++++++++++++++++++++ 18 files changed, 514 insertions(+), 66 deletions(-) create mode 100644 test/battle/move_effect/ally_switch.c diff --git a/asm/macros/battle_script.inc b/asm/macros/battle_script.inc index de509588bc..d155aebf66 100644 --- a/asm/macros/battle_script.inc +++ b/asm/macros/battle_script.inc @@ -1312,6 +1312,15 @@ .byte \battler .4byte \jumpInstr .endm + + .macro allyswitchswapbattlers + callnative BS_AllySwitchSwapBattler + .endm + + .macro allyswitchfailchance jumpInstr:req + callnative BS_AllySwitchFailChance + .4byte \jumpInstr + .endm .macro jumpifholdeffect battler:req, holdEffect:req, jumpInstr:req callnative BS_JumpIfHoldEffect diff --git a/data/battle_anim_scripts.s b/data/battle_anim_scripts.s index e101540851..4cdb0ecb99 100644 --- a/data/battle_anim_scripts.s +++ b/data/battle_anim_scripts.s @@ -5625,6 +5625,11 @@ Move_QUICK_GUARD: end Move_ALLY_SWITCH: + call SetPsychicBackground + createvisualtask AnimTask_AllySwitchAttacker, 2 + createvisualtask AnimTask_AllySwitchPartner, 2 + call DoubleTeamAnimRet + call UnsetPsychicBg end Move_SCALD: @@ -19348,9 +19353,8 @@ Move_TELEPORT: call UnsetPsychicBg waitforvisualfinish end - -Move_DOUBLE_TEAM: - createvisualtask AnimTask_DoubleTeam, 2 + +DoubleTeamAnimRet: setalpha 12, 8 monbg ANIM_ATK_PARTNER playsewithpan SE_M_DOUBLE_TEAM, SOUND_PAN_ATTACKER @@ -19374,6 +19378,11 @@ Move_DOUBLE_TEAM: clearmonbg ANIM_ATK_PARTNER blendoff delay 1 + return + +Move_DOUBLE_TEAM: + createvisualtask AnimTask_DoubleTeam, 2 + call DoubleTeamAnimRet end Move_MINIMIZE: diff --git a/data/battle_scripts_1.s b/data/battle_scripts_1.s index 664ef32280..974a4ace76 100644 --- a/data/battle_scripts_1.s +++ b/data/battle_scripts_1.s @@ -1521,8 +1521,11 @@ BattleScript_EffectAllySwitch: attackstring ppreduce jumpifnoally BS_ATTACKER, BattleScript_ButItFailed + allyswitchfailchance BattleScript_ButItFailed attackanimation waitanimation + @ The actual data/gfx swap happens in the move animation. Here it's just the gBattlerAttacker / scripting battler change + allyswitchswapbattlers printstring STRINGID_ALLYSWITCHPOSITION waitmessage B_WAIT_TIME_LONG goto BattleScript_MoveEnd diff --git a/include/battle.h b/include/battle.h index a8f9ad2308..7185bf340c 100644 --- a/include/battle.h +++ b/include/battle.h @@ -67,8 +67,8 @@ struct DisableStruct u32 transformedMonOtId; u16 disabledMove; u16 encoredMove; - u8 protectUses; - u8 stockpileCounter; + u8 protectUses:4; + u8 stockpileCounter:4; s8 stockpileDef; s8 stockpileSpDef; s8 stockpileBeforeDef; @@ -163,6 +163,7 @@ struct ProtectStruct u16 silkTrapped:1; u16 eatMirrorHerb:1; u16 activateOpportunist:2; // 2 - to copy stats. 1 - stats copied (do not repeat). 0 - no stats to copy + u16 usedAllySwitch:1; u32 physicalDmg; u32 specialDmg; u8 physicalBattlerId; diff --git a/include/battle_anim.h b/include/battle_anim.h index 7aaa099518..672b640cc9 100644 --- a/include/battle_anim.h +++ b/include/battle_anim.h @@ -134,7 +134,7 @@ void SetBattlerSpriteYOffsetFromRotation(u8 spriteId); u32 GetBattlePalettesMask(bool8 battleBackground, bool8 attacker, bool8 target, bool8 attackerPartner, bool8 targetPartner, bool8 anim1, bool8 anim2); u32 GetBattleMonSpritePalettesMask(u8 playerLeft, u8 playerRight, u8 opponentLeft, u8 opponentRight); u8 GetSpritePalIdxByBattler(u8 battler); -s16 CloneBattlerSpriteWithBlend(u8); +s16 CloneBattlerSpriteWithBlend(u8 animBattler); void DestroySpriteWithActiveSheet(struct Sprite *); u8 CreateInvisibleSpriteCopy(int, u8, int); void AnimLoadCompressedBgTilemapHandleContest(struct BattleAnimBgData *, const void *, bool32); diff --git a/include/battle_interface.h b/include/battle_interface.h index af8f0dc117..b26205d810 100644 --- a/include/battle_interface.h +++ b/include/battle_interface.h @@ -103,6 +103,7 @@ bool32 IsBurstTriggerSpriteActive(void); void HideBurstTriggerSprite(void); void DestroyBurstTriggerSprite(void); void MegaIndicator_LoadSpritesGfx(void); +void MegaIndicator_SetVisibilities(u32 healthboxId, bool32 invisible); u8 CreatePartyStatusSummarySprites(u8 battler, struct HpAndStatus *partyInfo, bool8 skipPlayer, bool8 isBattleStart); void Task_HidePartyStatusSummary(u8 taskId); void UpdateHealthboxAttribute(u8 healthboxSpriteId, struct Pokemon *mon, u8 elementId); diff --git a/include/battle_main.h b/include/battle_main.h index 4943f69a70..b171b2f289 100644 --- a/include/battle_main.h +++ b/include/battle_main.h @@ -57,6 +57,7 @@ void SwitchInClearSetData(u32 battler); const u8* FaintClearSetData(u32 battler); void BattleTurnPassed(void); u8 IsRunningFromBattleImpossible(u32 battler); +void SwitchTwoBattlersInParty(u32 battler, u32 battler2); void SwitchPartyOrder(u32 battlerId); void SwapTurnOrder(u8 id1, u8 id2); u32 GetBattlerTotalSpeedStatArgs(u32 battler, u32 ability, u32 holdEffect); diff --git a/include/battle_scripts.h b/include/battle_scripts.h index 4790998cc4..bc534b69af 100644 --- a/include/battle_scripts.h +++ b/include/battle_scripts.h @@ -13,6 +13,7 @@ extern const u8 BattleScript_MoveMissedPause[]; extern const u8 BattleScript_MoveMissed[]; extern const u8 BattleScript_FlingFailConsumeItem[]; extern const u8 BattleScript_FailedFromAtkString[]; +extern const u8 BattleScript_FailedFromAtkCanceler[]; extern const u8 BattleScript_ButItFailed[]; extern const u8 BattleScript_StatUp[]; extern const u8 BattleScript_StatDown[]; diff --git a/include/config/battle.h b/include/config/battle.h index a3dc007ae3..fd390debf0 100644 --- a/include/config/battle.h +++ b/include/config/battle.h @@ -111,6 +111,7 @@ #define B_WIDE_GUARD GEN_LATEST // In Gen5 only, Quick Guard has a chance to fail if used consecutively. #define B_QUICK_GUARD GEN_LATEST // In Gen5 only, Wide Guard has a chance to fail if used consecutively. #define B_IMPRISON GEN_LATEST // In Gen5+, Imprison doesn't fail if opposing pokemon don't have any moves the user knows. +#define B_ALLY_SWITCH_FAIL_CHANCE GEN_LATEST // In Gen9, using Ally Switch consecutively decreases the chance of success for each consecutive use. // Ability settings #define B_EXPANDED_ABILITY_NAMES TRUE // If TRUE, ability names are increased from 12 characters to 16 characters. diff --git a/include/reshow_battle_screen.h b/include/reshow_battle_screen.h index 174fb4157f..07958bf6c0 100644 --- a/include/reshow_battle_screen.h +++ b/include/reshow_battle_screen.h @@ -3,5 +3,6 @@ void ReshowBattleScreenDummy(void); void ReshowBattleScreenAfterMenu(void); +void CreateBattlerSprite(u32 battler); #endif // GUARD_RESHOW_BATTLE_SCREEN_H diff --git a/src/battle_anim.c b/src/battle_anim.c index d2216e5155..a80cc3d1f7 100644 --- a/src/battle_anim.c +++ b/src/battle_anim.c @@ -237,7 +237,9 @@ void LaunchBattleAnimation(u32 animType, u32 animId) if (gTestRunnerEnabled) { TestRunner_Battle_RecordAnimation(animType, animId); - if (gTestRunnerHeadless) + // Play Transform and Ally Switch even in Headless as these move animations also change mon data. + if (gTestRunnerHeadless + && !(animType == ANIM_TYPE_MOVE && (animId == MOVE_TRANSFORM || animId == MOVE_ALLY_SWITCH))) { gAnimScriptCallback = Nop; gAnimScriptActive = FALSE; diff --git a/src/battle_anim_effects_1.c b/src/battle_anim_effects_1.c index b79c94a6a3..188ec915d6 100644 --- a/src/battle_anim_effects_1.c +++ b/src/battle_anim_effects_1.c @@ -9,21 +9,16 @@ #include "math_util.h" #include "palette.h" #include "random.h" +#include "reshow_battle_screen.h" #include "scanline_effect.h" #include "sound.h" #include "trig.h" #include "util.h" +#include "constants/abilities.h" #include "constants/rgb.h" #include "constants/songs.h" #include "constants/moves.h" -struct { - s16 startX; - s16 startY; - s16 targetX; - s16 targetY; -} static EWRAM_DATA sFrenzyPlantRootData = {0}; // Debug? Written to but never read. - static void AnimMovePowderParticle_Step(struct Sprite *); static void AnimSolarBeamSmallOrb(struct Sprite *); static void AnimSolarBeamSmallOrb_Step(struct Sprite *); @@ -4256,10 +4251,6 @@ static void AnimFrenzyPlantRoot(struct Sprite *sprite) StartSpriteAnim(sprite, gBattleAnimArgs[4]); sprite->data[2] = gBattleAnimArgs[5]; sprite->callback = AnimRootFlickerOut; - sFrenzyPlantRootData.startX = sprite->x; - sFrenzyPlantRootData.startY = sprite->y; - sFrenzyPlantRootData.targetX = targetX; - sFrenzyPlantRootData.targetY = targetY; } static void AnimRootFlickerOut(struct Sprite *sprite) @@ -6486,78 +6477,238 @@ static void AnimHornHit_Step(struct Sprite *sprite) DestroyAnimSprite(sprite); } -void AnimTask_DoubleTeam(u8 taskId) -{ - u16 i; - int obj; - u16 r3; - u16 r4; - struct Task *task = &gTasks[taskId]; - task->data[0] = GetAnimBattlerSpriteId(ANIM_ATTACKER); - task->data[1] = AllocSpritePalette(ANIM_TAG_BENT_SPOON); - r3 = OBJ_PLTT_ID(task->data[1]); - r4 = OBJ_PLTT_ID2(gSprites[task->data[0]].oam.paletteNum); - for (i = 1; i < 16; i++) - gPlttBufferUnfaded[r3 + i] = gPlttBufferUnfaded[r4 + i]; +// Double Team and Ally Switch. +#define tBattlerSpriteId data[0] +#define tSpoonPal data[1] +#define tBlendSpritesCount data[3] +#define tBattlerId data[4] +#define tIsAllySwitch data[5] - BlendPalette(r3, 16, 11, RGB_BLACK); - task->data[3] = 0; - i = 0; - while (i < 2 && (obj = CloneBattlerSpriteWithBlend(0)) >= 0) +#define sCounter data[0] +#define sSinIndex data[1] +#define sTaskId data[2] +#define sCounter2 data[3] +#define sSinAmplitude data[4] +#define sSinIndexMod data[5] +#define sBattlerFlank data[6] + +void PrepareDoubleTeamAnim(u32 taskId, u32 animBattler, bool32 forAllySwitch) +{ + s32 i, spriteId; + u16 palOffsetBattler, palOffsetSpoon; + struct Task *task = &gTasks[taskId]; + + task->tBattlerSpriteId = GetAnimBattlerSpriteId(animBattler); + task->tSpoonPal = AllocSpritePalette(ANIM_TAG_BENT_SPOON); + task->tBattlerId = GetAnimBattlerId(animBattler); + task->tIsAllySwitch = forAllySwitch; + palOffsetSpoon = OBJ_PLTT_ID(task->tSpoonPal); + palOffsetBattler = OBJ_PLTT_ID2(gSprites[task->tBattlerSpriteId].oam.paletteNum); + for (i = 1; i < 16; i++) + gPlttBufferUnfaded[palOffsetSpoon + i] = gPlttBufferUnfaded[palOffsetBattler + i]; + + BlendPalette(palOffsetSpoon, 16, 11, RGB_BLACK); + task->tBlendSpritesCount = 0; + for (i = 0; i < ((forAllySwitch == TRUE) ? 1 : 2); i++) { - gSprites[obj].oam.paletteNum = task->data[1]; - gSprites[obj].data[0] = 0; - gSprites[obj].data[1] = i << 7; - gSprites[obj].data[2] = taskId; - gSprites[obj].callback = AnimDoubleTeam; - task->data[3]++; - i++; + spriteId = CloneBattlerSpriteWithBlend(animBattler); + if (spriteId < 0) + break; + gSprites[spriteId].oam.paletteNum = task->tSpoonPal; + gSprites[spriteId].sCounter = 0; + gSprites[spriteId].sSinIndex = i << 7; + gSprites[spriteId].sTaskId = taskId; + // Which direction + if (gBattleAnimAttacker & BIT_FLANK) + gSprites[spriteId].sBattlerFlank = (animBattler != ANIM_ATTACKER); + else + gSprites[spriteId].sBattlerFlank = (animBattler == ANIM_ATTACKER); + gSprites[spriteId].callback = AnimDoubleTeam; + task->tBlendSpritesCount++; } task->func = AnimTask_DoubleTeam_Step; - if (GetBattlerSpriteBGPriorityRank(gBattleAnimAttacker) == 1) + if (GetBattlerSpriteBGPriorityRank(task->tBattlerId) == 1) ClearGpuRegBits(REG_OFFSET_DISPCNT, DISPCNT_BG1_ON); else ClearGpuRegBits(REG_OFFSET_DISPCNT, DISPCNT_BG2_ON); } +void AnimTask_DoubleTeam(u8 taskId) +{ + PrepareDoubleTeamAnim(taskId, ANIM_ATTACKER, FALSE); +} + +static inline void SwapStructData(void *s1, void *s2, void *data, u32 size) +{ + memcpy(data, s1, size); + memcpy(s1, s2, size); + memcpy(s2, data, size); +} + +static void ReloadBattlerSprites(u32 battler, struct Pokemon *party) +{ + BattleLoadMonSpriteGfx(&party[gBattlerPartyIndexes[battler]], battler); + CreateBattlerSprite(battler); + UpdateHealthboxAttribute(gHealthboxSpriteIds[battler], &party[gBattlerPartyIndexes[battler]], HEALTHBOX_ALL); + // If battler is mega evolved / primal reversed, hide the sprite until the move animation finishes. + MegaIndicator_SetVisibilities(gHealthboxSpriteIds[battler], TRUE); +} + +static void AnimTask_AllySwitchDataSwap(u8 taskId) +{ + s32 i, j; + struct Pokemon *party; + u32 temp; + u32 battlerAtk = gBattlerAttacker; + u32 battlerPartner = BATTLE_PARTNER(battlerAtk); + + void *data = Alloc(0x200); + if (data == NULL) + { + SoftReset(1); + } + + SwapStructData(&gBattleMons[battlerAtk], &gBattleMons[battlerPartner], data, sizeof(struct BattlePokemon)); + SwapStructData(&gDisableStructs[battlerAtk], &gDisableStructs[battlerPartner], data, sizeof(struct DisableStruct)); + SwapStructData(&gSpecialStatuses[battlerAtk], &gSpecialStatuses[battlerPartner], data, sizeof(struct SpecialStatus)); + SwapStructData(&gProtectStructs[battlerAtk], &gProtectStructs[battlerPartner], data, sizeof(struct ProtectStruct)); + SwapStructData(&gBattleSpritesDataPtr->battlerData[battlerAtk], &gBattleSpritesDataPtr->battlerData[battlerPartner], data, sizeof(struct BattleSpriteInfo)); + + SWAP(gTransformedPersonalities[battlerAtk], gTransformedPersonalities[battlerPartner], temp); + SWAP(gTransformedOtIds[battlerAtk], gTransformedOtIds[battlerPartner], temp); + SWAP(gStatuses3[battlerAtk], gStatuses3[battlerPartner], temp); + SWAP(gStatuses4[battlerAtk], gStatuses4[battlerPartner], temp); + SWAP(gBattleStruct->chosenMovePositions[battlerAtk], gBattleStruct->chosenMovePositions[battlerPartner], temp); + SWAP(gChosenMoveByBattler[battlerAtk], gChosenMoveByBattler[battlerPartner], temp); + SWAP(gBattleStruct->moveTarget[battlerAtk], gBattleStruct->moveTarget[battlerPartner], temp); + SWAP(gMoveSelectionCursor[battlerAtk], gMoveSelectionCursor[battlerPartner], temp); + // Swap turn order, so that all the battlers take action + SWAP(gChosenActionByBattler[battlerAtk], gChosenActionByBattler[battlerPartner], temp); + for (i = 0; i < MAX_BATTLERS_COUNT; i++) + { + if (gBattlerByTurnOrder[i] == battlerAtk || gBattlerByTurnOrder[i] == battlerPartner) + { + for (j = i + 1; j < MAX_BATTLERS_COUNT; j++) + { + if (gBattlerByTurnOrder[j] == battlerAtk || gBattlerByTurnOrder[j] == battlerPartner) + break; + } + SWAP(gBattlerByTurnOrder[i], gBattlerByTurnOrder[j], temp); + break; + } + } + + party = GetBattlerParty(battlerAtk); + SwitchTwoBattlersInParty(battlerAtk, battlerPartner); + SWAP(gBattlerPartyIndexes[battlerAtk], gBattlerPartyIndexes[battlerPartner], temp); + + // For Snipe Shot and abilities Stalwart/Propeller Tail - keep the original target. + for (i = 0; i < MAX_BATTLERS_COUNT; i++) + { + u16 ability = GetBattlerAbility(i); + if (gChosenMoveByBattler[i] == MOVE_SNIPE_SHOT || ability == ABILITY_PROPELLER_TAIL || ability == ABILITY_STALWART) + gBattleStruct->moveTarget[i] ^= BIT_FLANK; + } + + // For some reason the order in which the sprites are created matters. Looks like an issue with the sprite system, potentially with the Sprite Template. + if ((battlerAtk & BIT_FLANK) != 0) + { + ReloadBattlerSprites(battlerAtk, party); + ReloadBattlerSprites(battlerPartner, party); + } + else + { + ReloadBattlerSprites(battlerPartner, party); + ReloadBattlerSprites(battlerAtk, party); + } + + Free(data); + + gBattleScripting.battler = battlerPartner; + DestroyAnimVisualTask(taskId); +} + static void AnimTask_DoubleTeam_Step(u8 taskId) { struct Task *task = &gTasks[taskId]; - if (!task->data[3]) + if (task->tBlendSpritesCount == 0) { - if (GetBattlerSpriteBGPriorityRank(gBattleAnimAttacker) == 1) + if (GetBattlerSpriteBGPriorityRank(task->tBattlerId) == 1) SetGpuRegBits(REG_OFFSET_DISPCNT, DISPCNT_BG1_ON); else SetGpuRegBits(REG_OFFSET_DISPCNT, DISPCNT_BG2_ON); FreeSpritePaletteByTag(ANIM_TAG_BENT_SPOON); - DestroyAnimVisualTask(taskId); + // Swap attacker and partner data-wise and visually + if (task->tIsAllySwitch && task->tBattlerId == BATTLE_PARTNER(gBattlerAttacker)) + gTasks[taskId].func = AnimTask_AllySwitchDataSwap; + else + DestroyAnimVisualTask(taskId); } } static void AnimDoubleTeam(struct Sprite *sprite) { - if (++sprite->data[3] > 1) + if (++sprite->sCounter2 > 1) { - sprite->data[3] = 0; - sprite->data[0]++; + sprite->sCounter2 = 0; + sprite->sCounter++; } - if (sprite->data[0] > 64) + if (sprite->sCounter > 64) { - gTasks[sprite->data[2]].data[3]--; + gTasks[sprite->sTaskId].tBlendSpritesCount--; + // If Ally Switch - destroy the mon sprites, they'll be created again later. + if (gTasks[sprite->sTaskId].tIsAllySwitch && gTasks[sprite->sTaskId].tBattlerId == BATTLE_PARTNER(gBattlerAttacker)) + { + DestroySprite(&gSprites[gBattlerSpriteIds[gBattlerAttacker]]); + DestroySprite(&gSprites[gBattlerSpriteIds[BATTLE_PARTNER(gBattlerAttacker)]]); + } DestroySpriteWithActiveSheet(sprite); } else { - sprite->data[4] = gSineTable[sprite->data[0]] / 6; - sprite->data[5] = gSineTable[sprite->data[0]] / 13; - sprite->data[1] = (sprite->data[1] + sprite->data[5]) & 0xFF; - sprite->x2 = Sin(sprite->data[1], sprite->data[4]); + sprite->sSinAmplitude = gSineTable[sprite->sCounter] / 6; + sprite->sSinIndexMod = gSineTable[sprite->sCounter] / 13; + sprite->sSinIndex = (sprite->sSinIndex + sprite->sSinIndexMod) & 0xFF; + sprite->x2 = Sin(sprite->sSinIndex, sprite->sSinAmplitude); + if (gTasks[sprite->sTaskId].tIsAllySwitch) + { + if (sprite->sBattlerFlank) + sprite->x2 = abs(sprite->x2); + else + sprite->x2 = -(abs(sprite->x2)); + } } } +void AnimTask_AllySwitchAttacker(u8 taskId) +{ + PrepareDoubleTeamAnim(taskId, ANIM_ATTACKER, TRUE); + gSprites[gBattlerSpriteIds[gBattlerAttacker]].invisible = TRUE; + gSprites[gBattlerSpriteIds[BATTLE_PARTNER(gBattlerAttacker)]].invisible = TRUE; +} + +void AnimTask_AllySwitchPartner(u8 taskId) +{ + PrepareDoubleTeamAnim(taskId, ANIM_ATK_PARTNER, TRUE); +} + +#undef tBattlerSpriteId +#undef tSpoonPal +#undef tBlendSpritesCount +#undef tBattlerId +#undef tIsAllySwitch + +#undef sCounter +#undef sSinIndex +#undef sTaskId +#undef sCounter2 +#undef sSinAmplitude +#undef sSinIndexMod +#undef sBattlerFlank + static void AnimSuperFang(struct Sprite *sprite) { StoreSpriteCallbackInData6(sprite, DestroyAnimSprite); diff --git a/src/battle_interface.c b/src/battle_interface.c index d3751f702f..b07cb3ee07 100644 --- a/src/battle_interface.c +++ b/src/battle_interface.c @@ -195,7 +195,6 @@ static void SpriteCB_StatusSummaryBalls_OnSwitchout(struct Sprite *); static void SpriteCb_MegaTrigger(struct Sprite *); static void SpriteCb_BurstTrigger(struct Sprite *); -static void MegaIndicator_SetVisibilities(u32 healthboxId, bool32 invisible); static void MegaIndicator_UpdateLevel(u32 healthboxId, u32 level); static void MegaIndicator_CreateSprite(u32 battlerId, u32 healthboxSpriteId); static void MegaIndicator_UpdateOamPriority(u32 healthboxId, u32 oamPriority); diff --git a/src/battle_main.c b/src/battle_main.c index e0fba09ed0..dd313a41de 100644 --- a/src/battle_main.c +++ b/src/battle_main.c @@ -3974,6 +3974,25 @@ u8 IsRunningFromBattleImpossible(u32 battler) return BATTLE_RUN_SUCCESS; } +void SwitchTwoBattlersInParty(u32 battler, u32 battler2) +{ + s32 i; + u32 partyId1, partyId2; + + for (i = 0; i < (int)ARRAY_COUNT(gBattlePartyCurrentOrder); i++) + gBattlePartyCurrentOrder[i] = *(battler * 3 + i + (u8 *)(gBattleStruct->battlerPartyOrders)); + + partyId1 = GetPartyIdFromBattlePartyId(gBattlerPartyIndexes[battler]); + partyId2 = GetPartyIdFromBattlePartyId(gBattlerPartyIndexes[battler2]); + SwitchPartyMonSlots(partyId1, partyId2); + + for (i = 0; i < (int)ARRAY_COUNT(gBattlePartyCurrentOrder); i++) + { + *(battler * 3 + i + (u8 *)(gBattleStruct->battlerPartyOrders)) = gBattlePartyCurrentOrder[i]; + *(BATTLE_PARTNER(battler) * 3 + i + (u8 *)(gBattleStruct->battlerPartyOrders)) = gBattlePartyCurrentOrder[i]; + } +} + void SwitchPartyOrder(u32 battler) { s32 i; diff --git a/src/battle_script_commands.c b/src/battle_script_commands.c index a95f948c7e..2b46485ebc 100644 --- a/src/battle_script_commands.c +++ b/src/battle_script_commands.c @@ -2182,6 +2182,7 @@ static void Cmd_attackanimation(void) if ((gHitMarker & (HITMARKER_NO_ANIMATIONS | HITMARKER_DISABLE_ANIMATION)) && gCurrentMove != MOVE_TRANSFORM && gCurrentMove != MOVE_SUBSTITUTE + && gCurrentMove != MOVE_ALLY_SWITCH // In a wild double battle gotta use the teleport animation if two wild pokemon are alive. && !(gCurrentMove == MOVE_TELEPORT && WILD_DOUBLE_BATTLE && GetBattlerSide(gBattlerAttacker) == B_SIDE_OPPONENT && IsBattlerAlive(BATTLE_PARTNER(gBattlerAttacker)))) { @@ -10600,17 +10601,22 @@ static void Cmd_various(void) gBattlescriptCurrInstr = cmd->nextInstr; } +static void TryResetProtectUseCounter(u32 battler) +{ + u32 lastMove = gLastResultingMoves[battler]; + if (lastMove == MOVE_UNAVAILABLE + || (!gBattleMoves[lastMove].protectionMove && (B_ALLY_SWITCH_FAIL_CHANCE >= GEN_9 && gBattleMoves[lastMove].effect != EFFECT_ALLY_SWITCH))) + gDisableStructs[battler].protectUses = 0; +} + static void Cmd_setprotectlike(void) { CMD_ARGS(); bool32 fail = TRUE; bool32 notLastTurn = TRUE; - u32 lastMove = gLastResultingMoves[gBattlerAttacker]; - - if (lastMove == MOVE_UNAVAILABLE || !(gBattleMoves[lastMove].protectionMove)) - gDisableStructs[gBattlerAttacker].protectUses = 0; + TryResetProtectUseCounter(gBattlerAttacker); if (gCurrentTurnActionNumber == (gBattlersCount - 1)) notLastTurn = FALSE; @@ -16481,3 +16487,34 @@ void BS_TryTriggerStatusForm(void) } gBattlescriptCurrInstr = cmd->nextInstr; } + +void BS_AllySwitchSwapBattler(void) +{ + NATIVE_ARGS(); + + gBattleScripting.battler = gBattlerAttacker; + gBattlerAttacker ^= BIT_FLANK; + gProtectStructs[gBattlerAttacker].usedAllySwitch = TRUE; + gBattlescriptCurrInstr = cmd->nextInstr; +} + +void BS_AllySwitchFailChance(void) +{ + NATIVE_ARGS(const u8 *failInstr); + + if (B_ALLY_SWITCH_FAIL_CHANCE >= GEN_9) + { + TryResetProtectUseCounter(gBattlerAttacker); + if (sProtectSuccessRates[gDisableStructs[gBattlerAttacker].protectUses] < Random()) + { + gDisableStructs[gBattlerAttacker].protectUses = 0; + gBattlescriptCurrInstr = cmd->failInstr; + return; + } + else + { + gDisableStructs[gBattlerAttacker].protectUses++; + } + } + gBattlescriptCurrInstr = cmd->nextInstr; +} diff --git a/src/battle_util.c b/src/battle_util.c index 25a84b8bab..b30eff7c18 100644 --- a/src/battle_util.c +++ b/src/battle_util.c @@ -520,6 +520,11 @@ void HandleAction_UseMove(void) gBattlescriptCurrInstr = BattleScript_MoveUsedLoafingAround; } } + // Edge case: moves targeting the ally fail after a successful Ally Switch. + else if (moveTarget == MOVE_TARGET_ALLY && gProtectStructs[BATTLE_PARTNER(gBattlerAttacker)].usedAllySwitch) + { + gBattlescriptCurrInstr = BattleScript_FailedFromAtkCanceler; + } else { gBattlescriptCurrInstr = gBattleScriptsForMoveEffects[gBattleMoves[gCurrentMove].effect]; diff --git a/src/reshow_battle_screen.c b/src/reshow_battle_screen.c index 38999e1c79..82d8542152 100644 --- a/src/reshow_battle_screen.c +++ b/src/reshow_battle_screen.c @@ -18,9 +18,8 @@ // this file's functions static void CB2_ReshowBattleScreenAfterMenu(void); -static bool8 LoadBattlerSpriteGfx(u8 battlerId); -static void CreateBattlerSprite(u8 battlerId); -static void CreateHealthboxSprite(u8 battlerId); +static bool8 LoadBattlerSpriteGfx(u32 battler); +static void CreateHealthboxSprite(u32 battler); static void ClearBattleBgCntBaseBlocks(void); void ReshowBattleScreenDummy(void) @@ -180,7 +179,7 @@ static void ClearBattleBgCntBaseBlocks(void) regBgcnt2->charBaseBlock = 0; } -static bool8 LoadBattlerSpriteGfx(u8 battler) +static bool8 LoadBattlerSpriteGfx(u32 battler) { if (battler < gBattlersCount) { @@ -205,7 +204,7 @@ static bool8 LoadBattlerSpriteGfx(u8 battler) return TRUE; } -static void CreateBattlerSprite(u8 battler) +void CreateBattlerSprite(u32 battler) { if (battler < gBattlersCount) { @@ -271,7 +270,7 @@ static void CreateBattlerSprite(u8 battler) } } -static void CreateHealthboxSprite(u8 battler) +static void CreateHealthboxSprite(u32 battler) { if (battler < gBattlersCount) { diff --git a/test/battle/move_effect/ally_switch.c b/test/battle/move_effect/ally_switch.c new file mode 100644 index 0000000000..3271fd0cbe --- /dev/null +++ b/test/battle/move_effect/ally_switch.c @@ -0,0 +1,209 @@ +#include "global.h" +#include "test/battle.h" + +ASSUMPTIONS +{ + ASSUME(gBattleMoves[MOVE_ALLY_SWITCH].effect == EFFECT_ALLY_SWITCH); +} + +SINGLE_BATTLE_TEST("Ally Switch fails in a single battle") +{ + GIVEN { + PLAYER(SPECIES_WOBBUFFET); + OPPONENT(SPECIES_WOBBUFFET); + } WHEN { + TURN { MOVE(player, MOVE_ALLY_SWITCH); } + } SCENE { + MESSAGE("Wobbuffet used Ally Switch!"); + NOT ANIMATION(ANIM_TYPE_MOVE, MOVE_ALLY_SWITCH, player); + MESSAGE("But it failed!"); + } +} + +DOUBLE_BATTLE_TEST("Ally Switch fails if there is no partner") +{ + GIVEN { + PLAYER(SPECIES_WOBBUFFET); + PLAYER(SPECIES_WOBBUFFET) { HP(1); } + OPPONENT(SPECIES_WOBBUFFET); + OPPONENT(SPECIES_WOBBUFFET); + } WHEN { + TURN { MOVE(opponentLeft, MOVE_TACKLE, target:playerRight); } + TURN { MOVE(playerLeft, MOVE_ALLY_SWITCH); } + } SCENE { + MESSAGE("Wobbuffet fainted!"); + MESSAGE("Wobbuffet used Ally Switch!"); + NOT ANIMATION(ANIM_TYPE_MOVE, MOVE_ALLY_SWITCH, playerLeft); + MESSAGE("But it failed!"); + } +} + +DOUBLE_BATTLE_TEST("Ally Switch changes the position of battlers") +{ + GIVEN { + ASSUME(gBattleMoves[MOVE_SCREECH].effect == EFFECT_DEFENSE_DOWN_2); + ASSUME(gBattleMoves[MOVE_SCREECH].target == MOVE_TARGET_SELECTED); + PLAYER(SPECIES_WOBBUFFET) { Speed(5); } // Wobb is playerLeft, but it'll be Wynaut after Ally Switch + PLAYER(SPECIES_WYNAUT) { Speed(4); } + OPPONENT(SPECIES_KADABRA) { Speed(3); } + OPPONENT(SPECIES_ABRA) { Speed(2); } + } WHEN { + TURN { MOVE(playerLeft, MOVE_ALLY_SWITCH); MOVE(opponentLeft, MOVE_SCREECH, target:playerLeft); MOVE(opponentRight, MOVE_SCREECH, target:playerLeft); } + } SCENE { + MESSAGE("Wobbuffet used Ally Switch!"); + ANIMATION(ANIM_TYPE_MOVE, MOVE_ALLY_SWITCH, playerLeft); + MESSAGE("Wobbuffet and Wynaut switched places!"); + + MESSAGE("Foe Kadabra used Screech!"); + ANIMATION(ANIM_TYPE_GENERAL, B_ANIM_STATS_CHANGE, playerLeft); + MESSAGE("Wynaut's Defense harshly fell!"); + + MESSAGE("Foe Abra used Screech!"); + ANIMATION(ANIM_TYPE_GENERAL, B_ANIM_STATS_CHANGE, playerLeft); + MESSAGE("Wynaut's Defense harshly fell!"); + } THEN { + EXPECT_EQ(playerLeft->speed, 4); + EXPECT_EQ(playerLeft->species, SPECIES_WYNAUT); + EXPECT_EQ(playerRight->speed, 5); + EXPECT_EQ(playerRight->species, SPECIES_WOBBUFFET); + } +} + +DOUBLE_BATTLE_TEST("Ally Switch does not redirect the target of Snipe Shot") +{ + GIVEN { + ASSUME(gBattleMoves[MOVE_SNIPE_SHOT].effect == EFFECT_SNIPE_SHOT); + PLAYER(SPECIES_WOBBUFFET); // Wobb is playerLeft, but it'll be Wynaut after Ally Switch + PLAYER(SPECIES_WYNAUT); + OPPONENT(SPECIES_KADABRA); + OPPONENT(SPECIES_ABRA); + } WHEN { + TURN { MOVE(playerLeft, MOVE_ALLY_SWITCH); MOVE(opponentLeft, MOVE_SNIPE_SHOT, target:playerLeft); } // Kadabra targets Wobb and Snipe Shot ignores Ally Switch position change. + } SCENE { + MESSAGE("Wobbuffet used Ally Switch!"); + ANIMATION(ANIM_TYPE_MOVE, MOVE_ALLY_SWITCH, playerLeft); + MESSAGE("Wobbuffet and Wynaut switched places!"); + + MESSAGE("Foe Kadabra used Snipe Shot!"); + ANIMATION(ANIM_TYPE_MOVE, MOVE_SNIPE_SHOT, opponentLeft); + HP_BAR(playerRight); + } +} + +DOUBLE_BATTLE_TEST("Ally Switch does not redirect moves done by pokemon with Stalwart and Propeller Tail") +{ + u16 ability; + PARAMETRIZE { ability = ABILITY_STALWART; } + PARAMETRIZE { ability = ABILITY_PROPELLER_TAIL; } + PARAMETRIZE { ability = ABILITY_TELEPATHY; } + + GIVEN { + PLAYER(SPECIES_WOBBUFFET); // Wobb is playerLeft, but it'll be Wynaut after Ally Switch + PLAYER(SPECIES_WYNAUT); + OPPONENT(SPECIES_KADABRA) { Ability(ability); } + OPPONENT(SPECIES_ABRA); + } WHEN { + TURN { MOVE(playerLeft, MOVE_ALLY_SWITCH); MOVE(opponentLeft, MOVE_TACKLE, target:playerRight); } // Kadabra targets playerRight Wynaut. + } SCENE { + MESSAGE("Wobbuffet used Ally Switch!"); + ANIMATION(ANIM_TYPE_MOVE, MOVE_ALLY_SWITCH, playerLeft); + MESSAGE("Wobbuffet and Wynaut switched places!"); + + MESSAGE("Foe Kadabra used Tackle!"); + ANIMATION(ANIM_TYPE_MOVE, MOVE_TACKLE, opponentLeft); + HP_BAR((ability == ABILITY_STALWART || ability == ABILITY_PROPELLER_TAIL) ? playerLeft : playerRight); + } +} + +DOUBLE_BATTLE_TEST("Ally Switch has no effect on parnter's chosen move") +{ + u16 chosenMove; + struct BattlePokemon *chosenTarget = NULL; + + PARAMETRIZE { chosenMove = MOVE_TACKLE; chosenTarget = opponentLeft; } + PARAMETRIZE { chosenMove = MOVE_TACKLE; chosenTarget = opponentRight; } + PARAMETRIZE { chosenMove = MOVE_POUND; chosenTarget = opponentLeft; } + PARAMETRIZE { chosenMove = MOVE_POUND; chosenTarget = opponentRight; } + + GIVEN { + PLAYER(SPECIES_WOBBUFFET); + PLAYER(SPECIES_WYNAUT) { Moves(MOVE_TACKLE, MOVE_POUND, MOVE_CELEBRATE, MOVE_SCRATCH); } + OPPONENT(SPECIES_KADABRA); + OPPONENT(SPECIES_ABRA); + } WHEN { + TURN { MOVE(playerLeft, MOVE_ALLY_SWITCH); MOVE(playerRight, chosenMove, target:chosenTarget); } + } SCENE { + MESSAGE("Wobbuffet used Ally Switch!"); + ANIMATION(ANIM_TYPE_MOVE, MOVE_ALLY_SWITCH, playerLeft); + MESSAGE("Wobbuffet and Wynaut switched places!"); + + ANIMATION(ANIM_TYPE_MOVE, chosenMove, playerLeft); + HP_BAR(chosenTarget); + } +} + +DOUBLE_BATTLE_TEST("Ally Switch - move fails if the target was ally which changed position") +{ + u32 move = MOVE_NONE; + + PARAMETRIZE { move = MOVE_COACHING; } + PARAMETRIZE { move = MOVE_AROMATIC_MIST; } + PARAMETRIZE { move = MOVE_HOLD_HANDS; } + + GIVEN { + PLAYER(SPECIES_WOBBUFFET); + PLAYER(SPECIES_WYNAUT); + OPPONENT(SPECIES_KADABRA); + OPPONENT(SPECIES_ABRA); + } WHEN { + TURN { MOVE(playerLeft, MOVE_ALLY_SWITCH); MOVE(playerRight, move, target:playerLeft); } + } SCENE { + MESSAGE("Wobbuffet used Ally Switch!"); + ANIMATION(ANIM_TYPE_MOVE, MOVE_ALLY_SWITCH, playerLeft); + MESSAGE("Wobbuffet and Wynaut switched places!"); + + NOT ANIMATION(ANIM_TYPE_MOVE, move, playerLeft); + MESSAGE("But it failed!"); + } +} + +// Verified on Showdown, even though Bulbapedia says otherwise. +DOUBLE_BATTLE_TEST("Acupressure works after ally used Ally Switch") +{ + struct BattlePokemon *battlerTarget = NULL; + + PARAMETRIZE { battlerTarget = playerLeft; } + PARAMETRIZE { battlerTarget = playerRight; } + + GIVEN { + PLAYER(SPECIES_WOBBUFFET); + PLAYER(SPECIES_WYNAUT); + OPPONENT(SPECIES_KADABRA); + OPPONENT(SPECIES_ABRA); + } WHEN { + TURN { MOVE(playerLeft, MOVE_ALLY_SWITCH); MOVE(playerRight, MOVE_ACUPRESSURE, target:battlerTarget); } + } SCENE { + MESSAGE("Wobbuffet used Ally Switch!"); + ANIMATION(ANIM_TYPE_MOVE, MOVE_ALLY_SWITCH, playerLeft); + MESSAGE("Wobbuffet and Wynaut switched places!"); + + ANIMATION(ANIM_TYPE_MOVE, MOVE_ACUPRESSURE); + ANIMATION(ANIM_TYPE_GENERAL, B_ANIM_STATS_CHANGE, battlerTarget); + NOT MESSAGE("But it failed!"); + } +} + +DOUBLE_BATTLE_TEST("Ally Switch increases the Protect-like moves counter") +{ + GIVEN { + ASSUME(B_ALLY_SWITCH_FAIL_CHANCE >= GEN_9); + PLAYER(SPECIES_WOBBUFFET); + PLAYER(SPECIES_WOBBUFFET); + OPPONENT(SPECIES_WOBBUFFET); + OPPONENT(SPECIES_WOBBUFFET); + } WHEN { + TURN { MOVE(playerLeft, MOVE_ALLY_SWITCH); } + } THEN { + EXPECT(gDisableStructs[B_POSITION_PLAYER_RIGHT].protectUses == 1); + } +}