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 <root@LAPTOP-3SNV7DEQ>
Co-authored-by: Alex <93446519+AlexOn1ine@users.noreply.github.com>
This commit is contained in:
DizzyEggg 2023-12-20 15:26:28 +01:00 committed by GitHub
parent 8d238c88b9
commit 6137db102e
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
18 changed files with 514 additions and 66 deletions

View file

@ -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

View file

@ -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:

View file

@ -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

View file

@ -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;

View file

@ -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);

View file

@ -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);

View file

@ -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);

View file

@ -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[];

View file

@ -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.

View file

@ -3,5 +3,6 @@
void ReshowBattleScreenDummy(void);
void ReshowBattleScreenAfterMenu(void);
void CreateBattlerSprite(u32 battler);
#endif // GUARD_RESHOW_BATTLE_SCREEN_H

View file

@ -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;

View file

@ -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);

View file

@ -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);

View file

@ -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;

View file

@ -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;
}

View file

@ -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];

View file

@ -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)
{

View file

@ -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);
}
}