diff --git a/.github/ISSUE_TEMPLATE/01_battle_engine_bugs.yaml b/.github/ISSUE_TEMPLATE/01_battle_engine_bugs.yaml index c4404a039..558f249e4 100644 --- a/.github/ISSUE_TEMPLATE/01_battle_engine_bugs.yaml +++ b/.github/ISSUE_TEMPLATE/01_battle_engine_bugs.yaml @@ -23,8 +23,9 @@ body: label: Version description: What version of pokeemerald-expansion are you using as a base? options: - - 1.5.1 (Default) + - 1.5.2 (Default) - upcoming (Edge) + - 1.5.1 - 1.5.0 - 1.4.3 - 1.4.2 diff --git a/.github/ISSUE_TEMPLATE/02_battle_ai_issues.yaml b/.github/ISSUE_TEMPLATE/02_battle_ai_issues.yaml index a24757fc7..157177dce 100644 --- a/.github/ISSUE_TEMPLATE/02_battle_ai_issues.yaml +++ b/.github/ISSUE_TEMPLATE/02_battle_ai_issues.yaml @@ -23,8 +23,9 @@ body: label: Version description: What version of pokeemerald-expansion are you using as a base? options: - - 1.5.1 (Default) + - 1.5.2 (Default) - upcoming (Edge) + - 1.5.1 - 1.5.0 - 1.4.3 - 1.4.2 diff --git a/.github/ISSUE_TEMPLATE/04_other_errors.yaml b/.github/ISSUE_TEMPLATE/04_other_errors.yaml index e25695c0b..a73096bab 100644 --- a/.github/ISSUE_TEMPLATE/04_other_errors.yaml +++ b/.github/ISSUE_TEMPLATE/04_other_errors.yaml @@ -23,8 +23,9 @@ body: label: Version description: What version of pokeemerald-expansion are you using as a base? options: - - 1.5.1 (Default) + - 1.5.2 (Default) - upcoming (Edge) + - 1.5.1 - 1.5.0 - 1.4.3 - 1.4.2 diff --git a/asm/macros/battle_script.inc b/asm/macros/battle_script.inc index cd90d9b5a..25898a7d8 100644 --- a/asm/macros/battle_script.inc +++ b/asm/macros/battle_script.inc @@ -1318,7 +1318,7 @@ .2byte \holdEffect .4byte \jumpInstr .endm - + .macro dostockpilestatchangeswearoff, battler:req, statChangeInstr:req callnative BS_DoStockpileStatChangesWearOff .byte \battler @@ -1354,7 +1354,7 @@ .macro setsnow callnative BS_SetSnow .endm - + .macro setzeffect callnative BS_SetZEffect .endm @@ -1364,12 +1364,6 @@ callnative BS_TrySymbiosis .endm - @ returns TRUE or FALSE to gBattleCommunication[0] - .macro canteleport battler:req - callnative BS_CanTeleport - .byte \battler - .endm - @ returns B_SIDE_x to gBattleCommunication[0] .macro getbattlerside battler:req callnative BS_GetBattlerSide @@ -2088,7 +2082,7 @@ .macro swapsidestatuses various BS_ATTACKER, VARIOUS_SWAP_SIDE_STATUSES .endm - + .macro swapstats stat:req various BS_ATTACKER, VARIOUS_SWAP_STATS .byte \stat @@ -2189,6 +2183,11 @@ jumpifbyte CMP_COMMON_BITS, gMoveResultFlags, MOVE_RESULT_NO_EFFECT, \jumpInstr .endm + .macro jumpifside battler:req, side:req, equalJumpInstr:req + getbattlerside \battler + jumpifbyte CMP_EQUAL, gBattleCommunication, \side, \equalJumpInstr + .endm + .macro jumpifbattletype flags:req, jumpInstr:req jumpifword CMP_COMMON_BITS, gBattleTypeFlags, \flags, \jumpInstr .endm diff --git a/data/battle_scripts_1.s b/data/battle_scripts_1.s index d46e72e41..244d23c75 100644 --- a/data/battle_scripts_1.s +++ b/data/battle_scripts_1.s @@ -4023,6 +4023,8 @@ BattleScript_MoveMissedDoDamage:: .if B_CRASH_IF_TARGET_IMMUNE < GEN_4 jumpifhalfword CMP_COMMON_BITS, gMoveResultFlags, MOVE_RESULT_DOESNT_AFFECT_FOE, BattleScript_MoveEnd .endif + moveendcase MOVEEND_PROTECT_LIKE_EFFECT @ Spiky Shield's damage happens before recoil. + jumpifhasnohp BS_ATTACKER, BattleScript_MoveEnd printstring STRINGID_PKMNCRASHED waitmessage B_WAIT_TIME_LONG damagecalc @@ -5300,15 +5302,14 @@ BattleScript_EffectHurricane: BattleScript_EffectTeleport: attackcanceler attackstring - ppreduce .if B_TELEPORT_BEHAVIOR >= GEN_7 - canteleport BS_ATTACKER - jumpifbyte CMP_EQUAL, gBattleCommunication, TRUE, BattleScript_EffectTeleportNew - goto BattleScript_ButItFailed + jumpifbattletype BATTLE_TYPE_TRAINER, BattleScript_EffectBatonPass + jumpifside BS_ATTACKER, B_SIDE_PLAYER, BattleScript_EffectBatonPass .else jumpifbattletype BATTLE_TYPE_TRAINER, BattleScript_ButItFailed .endif BattleScript_EffectTeleportTryToRunAway: + ppreduce getifcantrunfrombattle BS_ATTACKER jumpifbyte CMP_EQUAL, gBattleCommunication, BATTLE_RUN_FORBIDDEN, BattleScript_ButItFailed jumpifbyte CMP_EQUAL, gBattleCommunication, BATTLE_RUN_FAILURE, BattleScript_PrintAbilityMadeIneffective @@ -5319,29 +5320,6 @@ BattleScript_EffectTeleportTryToRunAway: setoutcomeonteleport BS_ATTACKER goto BattleScript_MoveEnd -BattleScript_EffectTeleportNew: - getbattlerside BS_ATTACKER - jumpifbyte CMP_EQUAL, gBattleCommunication, B_SIDE_OPPONENT, BattleScript_EffectTeleportTryToRunAway - attackanimation - waitanimation - openpartyscreen BS_ATTACKER, BattleScript_EffectTeleportNewEnd - switchoutabilities BS_ATTACKER - waitstate - switchhandleorder BS_ATTACKER, 2 - returntoball BS_ATTACKER - getswitchedmondata BS_ATTACKER - switchindataupdate BS_ATTACKER - hpthresholds BS_ATTACKER - trytoclearprimalweather - printstring STRINGID_EMPTYSTRING3 - waitmessage 1 - printstring STRINGID_SWITCHINMON - switchinanim BS_ATTACKER, TRUE - waitstate - switchineffects BS_ATTACKER -BattleScript_EffectTeleportNewEnd: - goto BattleScript_MoveEnd - BattleScript_EffectBeatUp:: attackcanceler accuracycheck BattleScript_PrintMoveMissed, ACC_CURR_MOVE diff --git a/include/battle.h b/include/battle.h index 163245184..d36d59216 100644 --- a/include/battle.h +++ b/include/battle.h @@ -212,6 +212,7 @@ struct SideTimer u8 toxicSpikesAmount; u8 stealthRockAmount; u8 stickyWebAmount; + u8 stickyWebBattlerId; u8 stickyWebBattlerSide; // Used for Court Change u8 auroraVeilTimer; u8 auroraVeilBattlerId; @@ -652,7 +653,6 @@ struct BattleStruct u8 forcedSwitch:4; // For each battler u8 switchInAbilityPostponed:4; // To not activate against an empty field, each bit for battler u8 ballSpriteIds[2]; // item gfx, window gfx - u8 stickyWebUser; u8 appearedInBattle; // Bitfield to track which Pokemon appeared in battle. Used for Burmy's form change u8 skyDropTargets[MAX_BATTLERS_COUNT]; // For Sky Drop, to account for if multiple Pokemon use Sky Drop in a double battle. // When using a move which hits multiple opponents which is then bounced by a target, we need to make sure, the move hits both opponents, the one with bounce, and the one without. diff --git a/include/battle_util.h b/include/battle_util.h index a82010e01..8a1bce851 100644 --- a/include/battle_util.h +++ b/include/battle_util.h @@ -211,14 +211,12 @@ void BufferStatChange(u8 battlerId, u8 statId, u8 stringId); bool32 BlocksPrankster(u16 move, u8 battlerPrankster, u8 battlerDef, bool32 checkTarget); u16 GetUsedHeldItem(u8 battler); bool32 IsBattlerWeatherAffected(u8 battlerId, u32 weatherFlags); -u32 ApplyWeatherDamageMultiplier(u8 battlerAtk, u16 move, u8 moveType, u32 dmg, u16 holdEffectAtk, u16 holdEffectDef); u32 GetBattlerMoveTargetType(u8 battlerId, u16 move); bool32 CanTargetBattler(u8 battlerAtk, u8 battlerDef, u16 move); bool8 IsMoveAffectedByParentalBond(u16 move, u8 battlerId); void CopyMonLevelAndBaseStatsToBattleMon(u32 battler, struct Pokemon *mon); void CopyMonAbilityAndTypesToBattleMon(u32 battler, struct Pokemon *mon); void RecalcBattlerStats(u32 battler, struct Pokemon *mon); -void MulModifier(u16 *modifier, u16 val); bool32 IsAlly(u32 battlerAtk, u32 battlerDef); // Ability checks @@ -247,9 +245,4 @@ u8 GetBattlerGender(u8 battlerId); bool8 AreBattlersOfOppositeGender(u8 battler1, u8 battler2); u32 CalcSecondaryEffectChance(u8 battlerId, u8 secondaryEffectChance); -static inline u32 ApplyModifier(uq4_12_t modifier, u32 val) -{ - return UQ_4_12_TO_INT((modifier * val) + UQ_4_12_ROUND); -} - #endif // GUARD_BATTLE_UTIL_H diff --git a/include/fpmath.h b/include/fpmath.h index 987c59d5a..6e3edd64e 100644 --- a/include/fpmath.h +++ b/include/fpmath.h @@ -52,10 +52,30 @@ static inline uq4_12_t uq4_12_multiply(uq4_12_t a, uq4_12_t b) return (product + UQ_4_12_ROUND) >> UQ_4_12_SHIFT; } +static inline uq4_12_t uq4_12_multiply_half_down(uq4_12_t a, uq4_12_t b) +{ + u32 product = (u32) a * b; + return (product + UQ_4_12_ROUND - 1) >> UQ_4_12_SHIFT; +} + static inline uq4_12_t uq4_12_divide(uq4_12_t dividend, uq4_12_t divisor) { if (divisor == UQ_4_12(0.0)) return UQ_4_12(0); return (dividend << UQ_4_12_SHIFT) / divisor; } +// Multiplies value by the UQ_4_12 number modifier. +// Returns an integer, rounded to nearest (rounding down on n.5) +static inline u32 uq4_12_multiply_by_int_half_down(uq4_12_t modifier, u32 value) +{ + return UQ_4_12_TO_INT((modifier * value) + UQ_4_12_ROUND - 1); +} + +// Multiplies value by the UQ_4_12 number modifier. +// Returns an integer, rounded to nearest (rounding up on n.5) +static inline u32 uq4_12_multiply_by_int_half_up(uq4_12_t modifier, u32 value) +{ + return UQ_4_12_TO_INT((modifier * value) + UQ_4_12_ROUND); +} + #endif // FPMATH_H_ diff --git a/src/battle_controllers.c b/src/battle_controllers.c index d7ca18c6a..50058cb83 100644 --- a/src/battle_controllers.c +++ b/src/battle_controllers.c @@ -151,7 +151,7 @@ static void InitSinglePlayerBtlControllers(void) gBattlerPartyIndexes[0] = 0; gBattlerPartyIndexes[1] = 0; - if (BATTLE_TWO_VS_ONE_OPPONENT) + if (BATTLE_TWO_VS_ONE_OPPONENT || WILD_DOUBLE_BATTLE) { gBattlerPartyIndexes[2] = 3; gBattlerPartyIndexes[3] = 1; diff --git a/src/battle_main.c b/src/battle_main.c index b39634eef..576bb17a8 100644 --- a/src/battle_main.c +++ b/src/battle_main.c @@ -3188,7 +3188,10 @@ static void BattleStartClearSetData(void) gBattleStruct->mega.triggerSpriteId = 0xFF; gBattleStruct->burst.triggerSpriteId = 0xFF; - gBattleStruct->stickyWebUser = 0xFF; + for (i = 0; i < ARRAY_COUNT(gSideTimers); i++) + { + gSideTimers[i].stickyWebBattlerId = 0xFF; + } gBattleStruct->appearedInBattle = 0; gBattleStruct->beatUpSlot = 0; @@ -3294,8 +3297,12 @@ void SwitchInClearSetData(void) gBattleStruct->lastMoveFailed &= ~(gBitTable[gActiveBattler]); gBattleStruct->palaceFlags &= ~(gBitTable[gActiveBattler]); - if (gActiveBattler == gBattleStruct->stickyWebUser) - gBattleStruct->stickyWebUser = 0xFF; // Switched into sticky web user slot so reset it + for (i = 0; i < ARRAY_COUNT(gSideTimers); i++) + { + // Switched into sticky web user slot, so reset stored battler ID + if (gSideTimers[i].stickyWebBattlerId == gActiveBattler) + gSideTimers[i].stickyWebBattlerId = 0xFF; + } for (i = 0; i < gBattlersCount; i++) { @@ -3408,8 +3415,12 @@ void FaintClearSetData(void) gBattleStruct->palaceFlags &= ~(gBitTable[gActiveBattler]); - if (gActiveBattler == gBattleStruct->stickyWebUser) - gBattleStruct->stickyWebUser = 0xFF; // User of sticky web fainted, so reset the stored battler ID + for (i = 0; i < ARRAY_COUNT(gSideTimers); i++) + { + // User of sticky web fainted, so reset the stored battler ID + if (gSideTimers[i].stickyWebBattlerId == gActiveBattler) + gSideTimers[i].stickyWebBattlerId = 0xFF; + } for (i = 0; i < gBattlersCount; i++) { @@ -4605,7 +4616,11 @@ static void HandleTurnActionSelectionState(void) { // if we choose to throw a ball with our second mon, skip the action of the first // (if we have chosen throw ball with first, second's is already skipped) - gChosenActionByBattler[GetBattlerAtPosition(B_POSITION_PLAYER_LEFT)] = B_ACTION_NOTHING_FAINTED; + // if throwing a ball in a wild battle with an in-game partner, skip partner's turn when throwing a ball + if (gBattleTypeFlags & BATTLE_TYPE_INGAME_PARTNER) + gChosenActionByBattler[GetBattlerAtPosition(B_POSITION_PLAYER_RIGHT)] = B_ACTION_NOTHING_FAINTED; + else + gChosenActionByBattler[GetBattlerAtPosition(B_POSITION_PLAYER_LEFT)] = B_ACTION_NOTHING_FAINTED; } gBattleMainFunc = SetActionsAndBattlersTurnOrder; diff --git a/src/battle_message.c b/src/battle_message.c index 811395cb8..c410c8d4b 100644 --- a/src/battle_message.c +++ b/src/battle_message.c @@ -142,8 +142,8 @@ static const u8 sText_PkmnRaisedSpDefALittle[] = _("{B_ATK_PREFIX2}'s {B_CURRENT static const u8 sText_PkmnRaisedDef[] = _("{B_ATK_PREFIX2}'s {B_CURRENT_MOVE}\nraised DEFENSE!"); static const u8 sText_PkmnRaisedDefALittle[] = _("{B_ATK_PREFIX2}'s {B_CURRENT_MOVE}\nraised DEFENSE a little!"); static const u8 sText_PkmnCoveredByVeil[] = _("{B_ATK_PREFIX2}'s party is covered\nby a veil!"); -static const u8 sText_PkmnUsedSafeguard[] = _("{B_DEF_NAME_WITH_PREFIX}'s party is protected\nby SAFEGUARD!"); -static const u8 sText_PkmnSafeguardExpired[] = _("{B_ATK_PREFIX3}'s party is no longer\nprotected by SAFEGUARD!"); +static const u8 sText_PkmnUsedSafeguard[] = _("{B_DEF_NAME_WITH_PREFIX}'s party is protected\nby Safeguard!"); +static const u8 sText_PkmnSafeguardExpired[] = _("{B_ATK_PREFIX3}'s party is no longer\nprotected by Safeguard!"); static const u8 sText_PkmnWentToSleep[] = _("{B_ATK_NAME_WITH_PREFIX} went\nto sleep!"); static const u8 sText_PkmnSleptHealthy[] = _("{B_ATK_NAME_WITH_PREFIX} slept and\nbecame healthy!"); static const u8 sText_PkmnWhippedWhirlwind[] = _("{B_ATK_NAME_WITH_PREFIX} whipped\nup a whirlwind!"); @@ -2705,7 +2705,7 @@ void BufferStringBattle(u16 stringID) { if (gBattleTypeFlags & BATTLE_TYPE_LEGENDARY) stringPtr = sText_LegendaryPkmnAppeared; - else if (gBattleTypeFlags & BATTLE_TYPE_DOUBLE && IsValidForBattle(&gEnemyParty[gBattlerPartyIndexes[BATTLE_PARTNER(gActiveBattler)]])) // interesting, looks like they had something planned for wild double battles + else if (gBattleTypeFlags & BATTLE_TYPE_DOUBLE && IsValidForBattle(&gEnemyParty[gBattlerPartyIndexes[GetBattlerAtPosition(B_POSITION_OPPONENT_RIGHT)]])) stringPtr = sText_TwoWildPkmnAppeared; else if (gBattleTypeFlags & BATTLE_TYPE_WALLY_TUTORIAL) stringPtr = sText_WildPkmnAppearedPause; diff --git a/src/battle_script_commands.c b/src/battle_script_commands.c index f42bdae7d..18c28d709 100644 --- a/src/battle_script_commands.c +++ b/src/battle_script_commands.c @@ -1414,7 +1414,7 @@ static void Cmd_attackcanceler(void) return; } - // Z-moves and Max Moves bypass protection, but deal reduced damage (factored in CalcFinalDmg) + // Z-moves and Max Moves bypass protection, but deal reduced damage (factored in AccumulateOtherModifiers) if (gBattleStruct->zmove.active && IS_BATTLER_PROTECTED(gBattlerTarget)) { BattleScriptPush(cmd->nextInstr); @@ -8551,8 +8551,9 @@ static bool32 IsTeatimeAffected(u32 battlerId) #define UPDATE_COURTCHANGED_BATTLER(structField)\ { \ - sideTimerPlayer->structField ^= BIT_SIDE; \ - sideTimerOpp->structField ^= BIT_SIDE; \ + temp = sideTimerPlayer->structField; \ + sideTimerPlayer->structField = BATTLE_OPPOSITE(sideTimerOpp->structField); \ + sideTimerOpp->structField = BATTLE_OPPOSITE(temp); \ } \ static bool32 CourtChangeSwapSideStatuses(void) @@ -8587,9 +8588,7 @@ static bool32 CourtChangeSwapSideStatuses(void) UPDATE_COURTCHANGED_BATTLER(auroraVeilBattlerId); UPDATE_COURTCHANGED_BATTLER(tailwindBattlerId); UPDATE_COURTCHANGED_BATTLER(luckyChantBattlerId); - - // For Mirror Armor only - gBattleStruct->stickyWebUser = gBattlerAttacker; + UPDATE_COURTCHANGED_BATTLER(stickyWebBattlerId); // Track which side originally set the Sticky Web SWAP(sideTimerPlayer->stickyWebBattlerSide, sideTimerOpp->stickyWebBattlerSide, temp); @@ -8633,33 +8632,6 @@ static void HandleScriptMegaPrimalBurst(u32 caseId, u32 battlerId, u32 type) } } -static bool32 CanTeleport(u8 battlerId) -{ - struct Pokemon *party = GetBattlerParty(battlerId); - u32 species, count, i; - - for (i = 0; i < PARTY_SIZE; i++) - { - species = GetMonData(&party[i], MON_DATA_SPECIES_OR_EGG); - if (species != SPECIES_NONE && species != SPECIES_EGG && GetMonData(&party[i], MON_DATA_HP) != 0) - count++; - } - - switch (GetBattlerSide(battlerId)) - { - case B_SIDE_OPPONENT: - if (count == 1 || gBattleTypeFlags & BATTLE_TYPE_DOUBLE) - return FALSE; - break; - case B_SIDE_PLAYER: - if (count == 1 || (gBattleTypeFlags & BATTLE_TYPE_DOUBLE && count <= 2)) - return FALSE; - break; - } - - return TRUE; -} - // Return True if the order was changed, and false if the order was not changed(for example because the target would move after the attacker anyway). static bool32 ChangeOrderTargetAfterAttacker(void) { @@ -10615,8 +10587,8 @@ static void Cmd_various(void) // If Pokémon which set up Sticky Web is not on the field, no Pokémon have their Speed lowered." gBattlerAttacker = gBattlerTarget; // Initialize 'fail' condition SET_STATCHANGER(STAT_SPEED, 1, TRUE); - if (gBattleStruct->stickyWebUser != 0xFF) - gBattlerAttacker = gBattleStruct->stickyWebUser; + if (gSideTimers[GetBattlerSide(gActiveBattler)].stickyWebBattlerId != 0xFF) + gBattlerAttacker = gSideTimers[GetBattlerSide(gActiveBattler)].stickyWebBattlerId; break; } case VARIOUS_CUT_1_3_HP_RAISE_STATS: @@ -13878,9 +13850,9 @@ static void Cmd_setstickyweb(void) else { gSideStatuses[targetSide] |= SIDE_STATUS_STICKY_WEB; + gSideTimers[targetSide].stickyWebBattlerId = gBattlerAttacker; // For Mirror Armor gSideTimers[targetSide].stickyWebBattlerSide = GetBattlerSide(gBattlerAttacker); // For Court Change/Defiant - set this to the user's side gSideTimers[targetSide].stickyWebAmount = 1; - gBattleStruct->stickyWebUser = gBattlerAttacker; // For Mirror Armor gBattlescriptCurrInstr = cmd->nextInstr; } } @@ -16145,13 +16117,6 @@ void BS_GetBattlerSide(void) gBattlescriptCurrInstr = cmd->nextInstr; } -void BS_CanTeleport(void) -{ - NATIVE_ARGS(u8 battler); - gBattleCommunication[0] = CanTeleport(cmd->battler); - gBattlescriptCurrInstr = cmd->nextInstr; -} - void BS_TrySymbiosis(void) { NATIVE_ARGS(); diff --git a/src/battle_util.c b/src/battle_util.c index 5e50dc29c..6f069a95d 100644 --- a/src/battle_util.c +++ b/src/battle_util.c @@ -3823,14 +3823,15 @@ u8 AtkCanceller_UnableToUseMove2(void) bool8 HasNoMonsToSwitch(u8 battler, u8 partyIdBattlerOn1, u8 partyIdBattlerOn2) { - u8 playerId, flankId; + u32 i, side, playerId, flankId; struct Pokemon *party; - s32 i; if (!(gBattleTypeFlags & BATTLE_TYPE_DOUBLE)) return FALSE; - if (BATTLE_TWO_VS_ONE_OPPONENT && GetBattlerSide(battler) == B_SIDE_OPPONENT) + side = GetBattlerSide(battler); + + if (BATTLE_TWO_VS_ONE_OPPONENT && side == B_SIDE_OPPONENT) { flankId = GetBattlerAtPosition(B_POSITION_OPPONENT_LEFT); playerId = GetBattlerAtPosition(B_POSITION_OPPONENT_RIGHT); @@ -3843,9 +3844,7 @@ bool8 HasNoMonsToSwitch(u8 battler, u8 partyIdBattlerOn1, u8 partyIdBattlerOn2) for (i = 0; i < PARTY_SIZE; i++) { - if (GetMonData(&party[i], MON_DATA_HP) != 0 - && GetMonData(&party[i], MON_DATA_SPECIES_OR_EGG) != SPECIES_NONE - && GetMonData(&party[i], MON_DATA_SPECIES_OR_EGG) != SPECIES_EGG + if (IsValidForBattle(&party[i]) && i != partyIdBattlerOn1 && i != partyIdBattlerOn2 && i != *(gBattleStruct->monToSwitchIntoId + flankId) && i != playerId[gBattleStruct->monToSwitchIntoId]) break; @@ -3855,22 +3854,41 @@ bool8 HasNoMonsToSwitch(u8 battler, u8 partyIdBattlerOn1, u8 partyIdBattlerOn2) else if (gBattleTypeFlags & BATTLE_TYPE_INGAME_PARTNER) { party = GetBattlerParty(battler); - - playerId = ((battler & BIT_FLANK) / 2); - for (i = playerId * MULTI_PARTY_SIZE; i < playerId * MULTI_PARTY_SIZE + MULTI_PARTY_SIZE; i++) + if (side == B_SIDE_OPPONENT && WILD_DOUBLE_BATTLE) { - if (GetMonData(&party[i], MON_DATA_HP) != 0 - && GetMonData(&party[i], MON_DATA_SPECIES_OR_EGG) != SPECIES_NONE - && GetMonData(&party[i], MON_DATA_SPECIES_OR_EGG) != SPECIES_EGG) - break; + flankId = GetBattlerAtPosition(B_POSITION_OPPONENT_LEFT); + playerId = GetBattlerAtPosition(B_POSITION_OPPONENT_RIGHT); + + if (partyIdBattlerOn1 == PARTY_SIZE) + partyIdBattlerOn1 = gBattlerPartyIndexes[flankId]; + if (partyIdBattlerOn2 == PARTY_SIZE) + partyIdBattlerOn2 = gBattlerPartyIndexes[playerId]; + + for (i = 0; i < PARTY_SIZE; i++) + { + if (IsValidForBattle(&party[i]) + && i != partyIdBattlerOn1 && i != partyIdBattlerOn2 + && i != *(gBattleStruct->monToSwitchIntoId + flankId) && i != playerId[gBattleStruct->monToSwitchIntoId]) + break; + } + return (i == PARTY_SIZE); + } + else + { + playerId = ((battler & BIT_FLANK) / 2); + for (i = playerId * MULTI_PARTY_SIZE; i < playerId * MULTI_PARTY_SIZE + MULTI_PARTY_SIZE; i++) + { + if (IsValidForBattle(&party[i])) + break; + } + return (i == playerId * MULTI_PARTY_SIZE + MULTI_PARTY_SIZE); } - return (i == playerId * MULTI_PARTY_SIZE + MULTI_PARTY_SIZE); } else if (gBattleTypeFlags & BATTLE_TYPE_MULTI) { if (gBattleTypeFlags & BATTLE_TYPE_TOWER_LINK_MULTI) { - if (GetBattlerSide(battler) == B_SIDE_PLAYER) + if (side == B_SIDE_PLAYER) { party = gPlayerParty; flankId = GetBattlerMultiplayerId(battler); @@ -3894,14 +3912,12 @@ bool8 HasNoMonsToSwitch(u8 battler, u8 partyIdBattlerOn1, u8 partyIdBattlerOn2) for (i = playerId * MULTI_PARTY_SIZE; i < playerId * MULTI_PARTY_SIZE + MULTI_PARTY_SIZE; i++) { - if (GetMonData(&party[i], MON_DATA_HP) != 0 - && GetMonData(&party[i], MON_DATA_SPECIES_OR_EGG) != SPECIES_NONE - && GetMonData(&party[i], MON_DATA_SPECIES_OR_EGG) != SPECIES_EGG) + if (IsValidForBattle(&party[i])) break; } return (i == playerId * MULTI_PARTY_SIZE + MULTI_PARTY_SIZE); } - else if ((gBattleTypeFlags & BATTLE_TYPE_TWO_OPPONENTS) && GetBattlerSide(battler) == B_SIDE_OPPONENT) + else if ((gBattleTypeFlags & BATTLE_TYPE_TWO_OPPONENTS) && side == B_SIDE_OPPONENT) { party = gEnemyParty; @@ -3912,16 +3928,14 @@ bool8 HasNoMonsToSwitch(u8 battler, u8 partyIdBattlerOn1, u8 partyIdBattlerOn2) for (i = playerId; i < playerId + MULTI_PARTY_SIZE; i++) { - if (GetMonData(&party[i], MON_DATA_HP) != 0 - && GetMonData(&party[i], MON_DATA_SPECIES_OR_EGG) != SPECIES_NONE - && GetMonData(&party[i], MON_DATA_SPECIES_OR_EGG) != SPECIES_EGG) + if (IsValidForBattle(&party[i])) break; } return (i == playerId + 3); } else { - if (GetBattlerSide(battler) == B_SIDE_OPPONENT) + if (side == B_SIDE_OPPONENT) { flankId = GetBattlerAtPosition(B_POSITION_OPPONENT_LEFT); playerId = GetBattlerAtPosition(B_POSITION_OPPONENT_RIGHT); @@ -3941,9 +3955,7 @@ bool8 HasNoMonsToSwitch(u8 battler, u8 partyIdBattlerOn1, u8 partyIdBattlerOn2) for (i = 0; i < PARTY_SIZE; i++) { - if (GetMonData(&party[i], MON_DATA_HP) != 0 - && GetMonData(&party[i], MON_DATA_SPECIES_OR_EGG) != SPECIES_NONE - && GetMonData(&party[i], MON_DATA_SPECIES_OR_EGG) != SPECIES_EGG + if (IsValidForBattle(&party[i]) && i != partyIdBattlerOn1 && i != partyIdBattlerOn2 && i != *(gBattleStruct->monToSwitchIntoId + flankId) && i != playerId[gBattleStruct->monToSwitchIntoId]) break; @@ -7954,7 +7966,7 @@ u8 IsMonDisobedient(void) if (IsBattlerModernFatefulEncounter(gBattlerAttacker)) // only false if illegal Mew or Deoxys { - if (gBattleTypeFlags & BATTLE_TYPE_INGAME_PARTNER && GetBattlerPosition(gBattlerAttacker) == 2) + if (gBattleTypeFlags & BATTLE_TYPE_INGAME_PARTNER && GetBattlerPosition(gBattlerAttacker) == B_POSITION_PLAYER_RIGHT) return 0; if (gBattleTypeFlags & BATTLE_TYPE_FRONTIER) return 0; @@ -8694,6 +8706,85 @@ static u32 CalcMoveBasePowerAfterModifiers(u16 move, u8 battlerAtk, u8 battlerDe u16 atkAbility = GetBattlerAbility(battlerAtk); u16 defAbility = GetBattlerAbility(battlerDef); + // move effect + switch (gBattleMoves[move].effect) + { + case EFFECT_FACADE: + if (gBattleMons[battlerAtk].status1 & (STATUS1_BURN | STATUS1_PSN_ANY | STATUS1_PARALYSIS | STATUS1_FROSTBITE)) + modifier = uq4_12_multiply(modifier, UQ_4_12(2.0)); + break; + case EFFECT_BRINE: + if (gBattleMons[battlerDef].hp <= (gBattleMons[battlerDef].maxHP / 2)) + modifier = uq4_12_multiply(modifier, UQ_4_12(2.0)); + break; + case EFFECT_BARB_BARRAGE: + case EFFECT_VENOSHOCK: + if (gBattleMons[battlerDef].status1 & STATUS1_PSN_ANY) + modifier = uq4_12_multiply(modifier, UQ_4_12(2.0)); + break; + case EFFECT_RETALIATE: + if (gSideTimers[atkSide].retaliateTimer == 1) + modifier = uq4_12_multiply(modifier, UQ_4_12(2.0)); + break; + case EFFECT_SOLAR_BEAM: + if (IsBattlerWeatherAffected(battlerAtk, (B_WEATHER_HAIL | B_WEATHER_SANDSTORM | B_WEATHER_RAIN | B_WEATHER_SNOW))) + modifier = uq4_12_multiply(modifier, UQ_4_12(0.5)); + break; + case EFFECT_STOMPING_TANTRUM: + if (gBattleStruct->lastMoveFailed & gBitTable[battlerAtk]) + modifier = uq4_12_multiply(modifier, UQ_4_12(2.0)); + break; + case EFFECT_BULLDOZE: + case EFFECT_MAGNITUDE: + case EFFECT_EARTHQUAKE: + if (gFieldStatuses & STATUS_FIELD_GRASSY_TERRAIN && !(gStatuses3[battlerDef] & STATUS3_SEMI_INVULNERABLE)) + modifier = uq4_12_multiply(modifier, UQ_4_12(0.5)); + break; + case EFFECT_KNOCK_OFF: + #if B_KNOCK_OFF_DMG >= GEN_6 + if (gBattleMons[battlerDef].item != ITEM_NONE + && CanBattlerGetOrLoseItem(battlerDef, gBattleMons[battlerDef].item)) + modifier = uq4_12_multiply(modifier, UQ_4_12(1.5)); + #endif + break; + } + +#if B_TERRAIN_TYPE_BOOST >= GEN_8 + #define TERRAIN_TYPE_BOOST UQ_4_12(1.3) +#else + #define TERRAIN_TYPE_BOOST UQ_4_12(1.5) +#endif + + // various effects + if (gProtectStructs[battlerAtk].helpingHand) + modifier = uq4_12_multiply(modifier, UQ_4_12(1.5)); + if (gSpecialStatuses[battlerAtk].gemBoost) + modifier = uq4_12_multiply(modifier, UQ_4_12(1.0) + sPercentToModifier[gSpecialStatuses[battlerAtk].gemParam]); + if (gStatuses3[battlerAtk] & STATUS3_CHARGED_UP && moveType == TYPE_ELECTRIC) + modifier = uq4_12_multiply(modifier, UQ_4_12(2.0)); + if (gStatuses3[battlerAtk] & STATUS3_ME_FIRST) + modifier = uq4_12_multiply(modifier, UQ_4_12(1.5)); + if (IsBattlerTerrainAffected(battlerAtk, STATUS_FIELD_GRASSY_TERRAIN) && moveType == TYPE_GRASS) + modifier = uq4_12_multiply(modifier, TERRAIN_TYPE_BOOST); + if (IsBattlerTerrainAffected(battlerDef, STATUS_FIELD_MISTY_TERRAIN) && moveType == TYPE_DRAGON) + modifier = uq4_12_multiply(modifier, UQ_4_12(0.5)); + if (IsBattlerTerrainAffected(battlerAtk, STATUS_FIELD_ELECTRIC_TERRAIN) && moveType == TYPE_ELECTRIC) + modifier = uq4_12_multiply(modifier, TERRAIN_TYPE_BOOST); + if (IsBattlerTerrainAffected(battlerAtk, STATUS_FIELD_PSYCHIC_TERRAIN) && moveType == TYPE_PSYCHIC) + modifier = uq4_12_multiply(modifier, TERRAIN_TYPE_BOOST); + #if B_SPORT_TURNS >= GEN_6 + if ((moveType == TYPE_ELECTRIC && gFieldStatuses & STATUS_FIELD_MUDSPORT) + || (moveType == TYPE_FIRE && gFieldStatuses & STATUS_FIELD_WATERSPORT)) + #else + if ((moveType == TYPE_ELECTRIC && AbilityBattleEffects(ABILITYEFFECT_FIELD_SPORT, 0, 0, ABILITYEFFECT_MUD_SPORT, 0)) + || (moveType == TYPE_FIRE && AbilityBattleEffects(ABILITYEFFECT_FIELD_SPORT, 0, 0, ABILITYEFFECT_WATER_SPORT, 0))) + #endif + #if B_SPORT_DMG_REDUCTION >= GEN_5 + modifier = uq4_12_multiply(modifier, UQ_4_12(0.23)); + #else + modifier = uq4_12_multiply(modifier, UQ_4_12(0.5)); + #endif + // attacker's abilities switch (atkAbility) { @@ -8890,16 +8981,6 @@ static u32 CalcMoveBasePowerAfterModifiers(u16 move, u8 battlerAtk, u8 battlerDe if (moveType == TYPE_FIRE) modifier = uq4_12_multiply(modifier, UQ_4_12(1.25)); break; - case ABILITY_FLUFFY: - if (IsMoveMakingContact(move, battlerAtk)) - { - modifier = uq4_12_multiply(modifier, UQ_4_12(0.5)); - if (updateFlags) - RecordAbilityBattle(battlerDef, defAbility); - } - if (moveType == TYPE_FIRE) - modifier = uq4_12_multiply(modifier, UQ_4_12(2.0)); - break; case ABILITY_PROTOSYNTHESIS: { u8 defHighestStat = GetHighestStatId(battlerDef); @@ -8993,86 +9074,7 @@ static u32 CalcMoveBasePowerAfterModifiers(u16 move, u8 battlerAtk, u8 battlerDe modifier = uq4_12_multiply(modifier, UQ_4_12(1.1)); break; } - - // move effect - switch (gBattleMoves[move].effect) - { - case EFFECT_FACADE: - if (gBattleMons[battlerAtk].status1 & (STATUS1_BURN | STATUS1_PSN_ANY | STATUS1_PARALYSIS | STATUS1_FROSTBITE)) - modifier = uq4_12_multiply(modifier, UQ_4_12(2.0)); - break; - case EFFECT_BRINE: - if (gBattleMons[battlerDef].hp <= (gBattleMons[battlerDef].maxHP / 2)) - modifier = uq4_12_multiply(modifier, UQ_4_12(2.0)); - break; - case EFFECT_BARB_BARRAGE: - case EFFECT_VENOSHOCK: - if (gBattleMons[battlerDef].status1 & STATUS1_PSN_ANY) - modifier = uq4_12_multiply(modifier, UQ_4_12(2.0)); - break; - case EFFECT_RETALIATE: - if (gSideTimers[atkSide].retaliateTimer == 1) - modifier = uq4_12_multiply(modifier, UQ_4_12(2.0)); - break; - case EFFECT_SOLAR_BEAM: - if (IsBattlerWeatherAffected(battlerAtk, (B_WEATHER_HAIL | B_WEATHER_SANDSTORM | B_WEATHER_RAIN | B_WEATHER_SNOW))) - modifier = uq4_12_multiply(modifier, UQ_4_12(0.5)); - break; - case EFFECT_STOMPING_TANTRUM: - if (gBattleStruct->lastMoveFailed & gBitTable[battlerAtk]) - modifier = uq4_12_multiply(modifier, UQ_4_12(2.0)); - break; - case EFFECT_BULLDOZE: - case EFFECT_MAGNITUDE: - case EFFECT_EARTHQUAKE: - if (gFieldStatuses & STATUS_FIELD_GRASSY_TERRAIN && !(gStatuses3[battlerDef] & STATUS3_SEMI_INVULNERABLE)) - modifier = uq4_12_multiply(modifier, UQ_4_12(0.5)); - break; - case EFFECT_KNOCK_OFF: - #if B_KNOCK_OFF_DMG >= GEN_6 - if (gBattleMons[battlerDef].item != ITEM_NONE - && CanBattlerGetOrLoseItem(battlerDef, gBattleMons[battlerDef].item)) - modifier = uq4_12_multiply(modifier, UQ_4_12(1.5)); - #endif - break; - } - -#if B_TERRAIN_TYPE_BOOST >= GEN_8 - #define TERRAIN_TYPE_BOOST UQ_4_12(1.3) -#else - #define TERRAIN_TYPE_BOOST UQ_4_12(1.5) -#endif - - // various effects - if (gProtectStructs[battlerAtk].helpingHand) - modifier = uq4_12_multiply(modifier, UQ_4_12(1.5)); - if (gSpecialStatuses[battlerAtk].gemBoost) - modifier = uq4_12_multiply(modifier, UQ_4_12(1.0) + sPercentToModifier[gSpecialStatuses[battlerAtk].gemParam]); - if (gStatuses3[battlerAtk] & STATUS3_CHARGED_UP && moveType == TYPE_ELECTRIC) - modifier = uq4_12_multiply(modifier, UQ_4_12(2.0)); - if (gStatuses3[battlerAtk] & STATUS3_ME_FIRST) - modifier = uq4_12_multiply(modifier, UQ_4_12(1.5)); - if (gFieldStatuses & STATUS_FIELD_GRASSY_TERRAIN && moveType == TYPE_GRASS && IsBattlerGrounded(battlerAtk) && !(gStatuses3[battlerAtk] & STATUS3_SEMI_INVULNERABLE)) - modifier = uq4_12_multiply(modifier, TERRAIN_TYPE_BOOST); - if (gFieldStatuses & STATUS_FIELD_MISTY_TERRAIN && moveType == TYPE_DRAGON && IsBattlerGrounded(battlerDef) && !(gStatuses3[battlerDef] & STATUS3_SEMI_INVULNERABLE)) - modifier = uq4_12_multiply(modifier, UQ_4_12(0.5)); - if (gFieldStatuses & STATUS_FIELD_ELECTRIC_TERRAIN && moveType == TYPE_ELECTRIC && IsBattlerGrounded(battlerAtk) && !(gStatuses3[battlerAtk] & STATUS3_SEMI_INVULNERABLE)) - modifier = uq4_12_multiply(modifier, TERRAIN_TYPE_BOOST); - if (gFieldStatuses & STATUS_FIELD_PSYCHIC_TERRAIN && moveType == TYPE_PSYCHIC && IsBattlerGrounded(battlerAtk) && !(gStatuses3[battlerAtk] & STATUS3_SEMI_INVULNERABLE)) - modifier = uq4_12_multiply(modifier, TERRAIN_TYPE_BOOST); - #if B_SPORT_TURNS >= GEN_6 - if ((moveType == TYPE_ELECTRIC && gFieldStatuses & STATUS_FIELD_MUDSPORT) - || (moveType == TYPE_FIRE && gFieldStatuses & STATUS_FIELD_WATERSPORT)) - #else - if ((moveType == TYPE_ELECTRIC && AbilityBattleEffects(ABILITYEFFECT_FIELD_SPORT, 0, 0, ABILITYEFFECT_MUD_SPORT, 0)) - || (moveType == TYPE_FIRE && AbilityBattleEffects(ABILITYEFFECT_FIELD_SPORT, 0, 0, ABILITYEFFECT_WATER_SPORT, 0))) - #endif - #if B_SPORT_DMG_REDUCTION >= GEN_5 - modifier = uq4_12_multiply(modifier, UQ_4_12(0.23)); - #else - modifier = uq4_12_multiply(modifier, UQ_4_12(0.5)); - #endif - return ApplyModifier(modifier, basePower); + return uq4_12_multiply_by_int_half_down(modifier, basePower); } #undef TERRAIN_TYPE_BOOST @@ -9136,39 +9138,39 @@ static u32 CalcAttackStat(u16 move, u8 battlerAtk, u8 battlerDef, u8 moveType, b case ABILITY_HUGE_POWER: case ABILITY_PURE_POWER: if (IS_MOVE_PHYSICAL(move)) - modifier = uq4_12_multiply(modifier, UQ_4_12(2.0)); + modifier = uq4_12_multiply_half_down(modifier, UQ_4_12(2.0)); break; case ABILITY_SLOW_START: if (gDisableStructs[battlerAtk].slowStartTimer != 0) - modifier = uq4_12_multiply(modifier, UQ_4_12(0.5)); + modifier = uq4_12_multiply_half_down(modifier, UQ_4_12(0.5)); break; case ABILITY_SOLAR_POWER: if (IS_MOVE_SPECIAL(move) && IsBattlerWeatherAffected(battlerAtk, B_WEATHER_SUN)) - modifier = uq4_12_multiply(modifier, UQ_4_12(1.5)); + modifier = uq4_12_multiply_half_down(modifier, UQ_4_12(1.5)); break; case ABILITY_DEFEATIST: if (gBattleMons[battlerAtk].hp <= (gBattleMons[battlerAtk].maxHP / 2)) - modifier = uq4_12_multiply(modifier, UQ_4_12(0.5)); + modifier = uq4_12_multiply_half_down(modifier, UQ_4_12(0.5)); break; case ABILITY_FLASH_FIRE: if (moveType == TYPE_FIRE && gBattleResources->flags->flags[battlerAtk] & RESOURCE_FLAG_FLASH_FIRE) - modifier = uq4_12_multiply(modifier, UQ_4_12(1.5)); + modifier = uq4_12_multiply_half_down(modifier, UQ_4_12(1.5)); break; case ABILITY_SWARM: if (moveType == TYPE_BUG && gBattleMons[battlerAtk].hp <= (gBattleMons[battlerAtk].maxHP / 3)) - modifier = uq4_12_multiply(modifier, UQ_4_12(1.5)); + modifier = uq4_12_multiply_half_down(modifier, UQ_4_12(1.5)); break; case ABILITY_TORRENT: if (moveType == TYPE_WATER && gBattleMons[battlerAtk].hp <= (gBattleMons[battlerAtk].maxHP / 3)) - modifier = uq4_12_multiply(modifier, UQ_4_12(1.5)); + modifier = uq4_12_multiply_half_down(modifier, UQ_4_12(1.5)); break; case ABILITY_BLAZE: if (moveType == TYPE_FIRE && gBattleMons[battlerAtk].hp <= (gBattleMons[battlerAtk].maxHP / 3)) - modifier = uq4_12_multiply(modifier, UQ_4_12(1.5)); + modifier = uq4_12_multiply_half_down(modifier, UQ_4_12(1.5)); break; case ABILITY_OVERGROW: if (moveType == TYPE_GRASS && gBattleMons[battlerAtk].hp <= (gBattleMons[battlerAtk].maxHP / 3)) - modifier = uq4_12_multiply(modifier, UQ_4_12(1.5)); + modifier = uq4_12_multiply_half_down(modifier, UQ_4_12(1.5)); break; #if B_PLUS_MINUS_INTERACTION >= GEN_5 case ABILITY_PLUS: @@ -9177,34 +9179,34 @@ static u32 CalcAttackStat(u16 move, u8 battlerAtk, u8 battlerDef, u8 moveType, b { u32 partnerAbility = GetBattlerAbility(BATTLE_PARTNER(battlerAtk)); if (partnerAbility == ABILITY_PLUS || partnerAbility == ABILITY_MINUS) - modifier = uq4_12_multiply(modifier, UQ_4_12(1.5)); + modifier = uq4_12_multiply_half_down(modifier, UQ_4_12(1.5)); } break; #else case ABILITY_PLUS: if (IS_MOVE_SPECIAL(move) && IsBattlerAlive(BATTLE_PARTNER(battlerAtk)) && GetBattlerAbility(BATTLE_PARTNER(battlerAtk)) == ABILITY_MINUS) - modifier = uq4_12_multiply(modifier, UQ_4_12(1.5)); + modifier = uq4_12_multiply_half_down(modifier, UQ_4_12(1.5)); break; case ABILITY_MINUS: if (IS_MOVE_SPECIAL(move) && IsBattlerAlive(BATTLE_PARTNER(battlerAtk)) && GetBattlerAbility(BATTLE_PARTNER(battlerAtk)) == ABILITY_PLUS) - modifier = uq4_12_multiply(modifier, UQ_4_12(1.5)); + modifier = uq4_12_multiply_half_down(modifier, UQ_4_12(1.5)); break; #endif case ABILITY_FLOWER_GIFT: if (gBattleMons[battlerAtk].species == SPECIES_CHERRIM_SUNSHINE && IsBattlerWeatherAffected(battlerAtk, B_WEATHER_SUN) && IS_MOVE_PHYSICAL(move)) - modifier = uq4_12_multiply(modifier, UQ_4_12(1.5)); + modifier = uq4_12_multiply_half_down(modifier, UQ_4_12(1.5)); break; case ABILITY_HUSTLE: if (IS_MOVE_PHYSICAL(move)) - modifier = uq4_12_multiply(modifier, UQ_4_12(1.5)); + modifier = uq4_12_multiply_half_down(modifier, UQ_4_12(1.5)); break; case ABILITY_STAKEOUT: if (gDisableStructs[battlerDef].isFirstTurn == 2) // just switched in - modifier = uq4_12_multiply(modifier, UQ_4_12(2.0)); + modifier = uq4_12_multiply_half_down(modifier, UQ_4_12(2.0)); break; case ABILITY_GUTS: if (gBattleMons[battlerAtk].status1 & STATUS1_ANY && IS_MOVE_PHYSICAL(move)) - modifier = uq4_12_multiply(modifier, UQ_4_12(1.5)); + modifier = uq4_12_multiply_half_down(modifier, UQ_4_12(1.5)); break; } @@ -9214,15 +9216,11 @@ static u32 CalcAttackStat(u16 move, u8 battlerAtk, u8 battlerDef, u8 moveType, b case ABILITY_THICK_FAT: if (moveType == TYPE_FIRE || moveType == TYPE_ICE) { - modifier = uq4_12_multiply(modifier, UQ_4_12(0.5)); + modifier = uq4_12_multiply_half_down(modifier, UQ_4_12(0.5)); if (updateFlags) RecordAbilityBattle(battlerDef, ABILITY_THICK_FAT); } break; - case ABILITY_ICE_SCALES: - if (IS_MOVE_SPECIAL(move)) - modifier = uq4_12_multiply(modifier, UQ_4_12(0.5)); - break; } // ally's abilities @@ -9232,7 +9230,7 @@ static u32 CalcAttackStat(u16 move, u8 battlerAtk, u8 battlerDef, u8 moveType, b { case ABILITY_FLOWER_GIFT: if (gBattleMons[BATTLE_PARTNER(battlerAtk)].species == SPECIES_CHERRIM_SUNSHINE && IsBattlerWeatherAffected(BATTLE_PARTNER(battlerAtk), B_WEATHER_SUN) && IS_MOVE_PHYSICAL(move)) - modifier = uq4_12_multiply(modifier, UQ_4_12(1.5)); + modifier = uq4_12_multiply_half_down(modifier, UQ_4_12(1.5)); break; } } @@ -9242,34 +9240,34 @@ static u32 CalcAttackStat(u16 move, u8 battlerAtk, u8 battlerDef, u8 moveType, b { case HOLD_EFFECT_THICK_CLUB: if ((atkBaseSpeciesId == SPECIES_CUBONE || atkBaseSpeciesId == SPECIES_MAROWAK) && IS_MOVE_PHYSICAL(move)) - modifier = uq4_12_multiply(modifier, UQ_4_12(2.0)); + modifier = uq4_12_multiply_half_down(modifier, UQ_4_12(2.0)); break; case HOLD_EFFECT_DEEP_SEA_TOOTH: if (gBattleMons[battlerAtk].species == SPECIES_CLAMPERL && IS_MOVE_SPECIAL(move)) - modifier = uq4_12_multiply(modifier, UQ_4_12(2.0)); + modifier = uq4_12_multiply_half_down(modifier, UQ_4_12(2.0)); break; case HOLD_EFFECT_LIGHT_BALL: if (atkBaseSpeciesId == SPECIES_PIKACHU) - modifier = uq4_12_multiply(modifier, UQ_4_12(2.0)); + modifier = uq4_12_multiply_half_down(modifier, UQ_4_12(2.0)); break; case HOLD_EFFECT_CHOICE_BAND: if (IS_MOVE_PHYSICAL(move)) - modifier = uq4_12_multiply(modifier, UQ_4_12(1.5)); + modifier = uq4_12_multiply_half_down(modifier, UQ_4_12(1.5)); break; case HOLD_EFFECT_CHOICE_SPECS: if (IS_MOVE_SPECIAL(move)) - modifier = uq4_12_multiply(modifier, UQ_4_12(1.5)); + modifier = uq4_12_multiply_half_down(modifier, UQ_4_12(1.5)); break; } // The offensive stats of a Player's Pokémon are boosted by x1.1 (+10%) if they have the 1st badge and 7th badges. // Having the 1st badge boosts physical attack while having the 7th badge boosts special attack. if (ShouldGetStatBadgeBoost(FLAG_BADGE01_GET, battlerAtk) && IS_MOVE_PHYSICAL(move)) - modifier = uq4_12_multiply(modifier, UQ_4_12(1.1)); + modifier = uq4_12_multiply_half_down(modifier, UQ_4_12(1.1)); if (ShouldGetStatBadgeBoost(FLAG_BADGE07_GET, battlerAtk) && IS_MOVE_SPECIAL(move)) - modifier = uq4_12_multiply(modifier, UQ_4_12(1.1)); + modifier = uq4_12_multiply_half_down(modifier, UQ_4_12(1.1)); - return ApplyModifier(modifier, atkStat); + return uq4_12_multiply_by_int_half_down(modifier, atkStat); } static bool32 CanEvolve(u32 species) @@ -9343,7 +9341,7 @@ static u32 CalcDefenseStat(u16 move, u8 battlerAtk, u8 battlerDef, u8 moveType, case ABILITY_MARVEL_SCALE: if (gBattleMons[battlerDef].status1 & STATUS1_ANY && usesDefStat) { - modifier = uq4_12_multiply(modifier, UQ_4_12(1.5)); + modifier = uq4_12_multiply_half_down(modifier, UQ_4_12(1.5)); if (updateFlags) RecordAbilityBattle(battlerDef, ABILITY_MARVEL_SCALE); } @@ -9351,7 +9349,7 @@ static u32 CalcDefenseStat(u16 move, u8 battlerAtk, u8 battlerDef, u8 moveType, case ABILITY_FUR_COAT: if (usesDefStat) { - modifier = uq4_12_multiply(modifier, UQ_4_12(2.0)); + modifier = uq4_12_multiply_half_down(modifier, UQ_4_12(2.0)); if (updateFlags) RecordAbilityBattle(battlerDef, ABILITY_FUR_COAT); } @@ -9359,22 +9357,18 @@ static u32 CalcDefenseStat(u16 move, u8 battlerAtk, u8 battlerDef, u8 moveType, case ABILITY_GRASS_PELT: if (gFieldStatuses & STATUS_FIELD_GRASSY_TERRAIN && usesDefStat) { - modifier = uq4_12_multiply(modifier, UQ_4_12(1.5)); + modifier = uq4_12_multiply_half_down(modifier, UQ_4_12(1.5)); if (updateFlags) RecordAbilityBattle(battlerDef, ABILITY_GRASS_PELT); } break; case ABILITY_FLOWER_GIFT: if (gBattleMons[battlerDef].species == SPECIES_CHERRIM_SUNSHINE && IsBattlerWeatherAffected(battlerDef, B_WEATHER_SUN) && !usesDefStat) - modifier = uq4_12_multiply(modifier, UQ_4_12(1.5)); - break; - case ABILITY_PUNK_ROCK: - if (gBattleMoves[move].soundMove) - modifier = uq4_12_multiply(modifier, UQ_4_12(2.0)); + modifier = uq4_12_multiply_half_down(modifier, UQ_4_12(1.5)); break; case ABILITY_PURIFYING_SALT: if (gBattleMoves[move].type == TYPE_GHOST) - modifier = uq4_12_multiply(modifier, UQ_4_12(2.0)); + modifier = uq4_12_multiply_half_down(modifier, UQ_4_12(2.0)); break; } @@ -9385,7 +9379,7 @@ static u32 CalcDefenseStat(u16 move, u8 battlerAtk, u8 battlerDef, u8 moveType, { case ABILITY_FLOWER_GIFT: if (gBattleMons[BATTLE_PARTNER(battlerDef)].species == SPECIES_CHERRIM_SUNSHINE && IsBattlerWeatherAffected(BATTLE_PARTNER(battlerDef), B_WEATHER_SUN) && !usesDefStat) - modifier = uq4_12_multiply(modifier, UQ_4_12(1.5)); + modifier = uq4_12_multiply_half_down(modifier, UQ_4_12(1.5)); break; } } @@ -9395,240 +9389,374 @@ static u32 CalcDefenseStat(u16 move, u8 battlerAtk, u8 battlerDef, u8 moveType, { case HOLD_EFFECT_DEEP_SEA_SCALE: if (gBattleMons[battlerDef].species == SPECIES_CLAMPERL && !usesDefStat) - modifier = uq4_12_multiply(modifier, UQ_4_12(2.0)); + modifier = uq4_12_multiply_half_down(modifier, UQ_4_12(2.0)); break; case HOLD_EFFECT_METAL_POWDER: if (gBattleMons[battlerDef].species == SPECIES_DITTO && usesDefStat && !(gBattleMons[battlerDef].status2 & STATUS2_TRANSFORMED)) - modifier = uq4_12_multiply(modifier, UQ_4_12(2.0)); + modifier = uq4_12_multiply_half_down(modifier, UQ_4_12(2.0)); break; case HOLD_EFFECT_EVIOLITE: if (CanEvolve(gBattleMons[battlerDef].species)) - modifier = uq4_12_multiply(modifier, UQ_4_12(1.5)); + modifier = uq4_12_multiply_half_down(modifier, UQ_4_12(1.5)); break; case HOLD_EFFECT_ASSAULT_VEST: if (!usesDefStat) - modifier = uq4_12_multiply(modifier, UQ_4_12(1.5)); + modifier = uq4_12_multiply_half_down(modifier, UQ_4_12(1.5)); break; #if B_SOUL_DEW_BOOST <= GEN_6 case HOLD_EFFECT_SOUL_DEW: if ((gBattleMons[battlerDef].species == SPECIES_LATIAS || gBattleMons[battlerDef].species == SPECIES_LATIOS) && !(gBattleTypeFlags & BATTLE_TYPE_FRONTIER) && !usesDefStat) - modifier = uq4_12_multiply(modifier, UQ_4_12(1.5)); + modifier = uq4_12_multiply_half_down(modifier, UQ_4_12(1.5)); break; #endif } // sandstorm sp.def boost for rock types if (IS_BATTLER_OF_TYPE(battlerDef, TYPE_ROCK) && gBattleWeather & B_WEATHER_SANDSTORM && WEATHER_HAS_EFFECT && !usesDefStat) - modifier = uq4_12_multiply(modifier, UQ_4_12(1.5)); + modifier = uq4_12_multiply_half_down(modifier, UQ_4_12(1.5)); // snow def boost for ice types if (IS_BATTLER_OF_TYPE(battlerDef, TYPE_ICE) && gBattleWeather & B_WEATHER_SNOW && WEATHER_HAS_EFFECT && usesDefStat) - modifier = uq4_12_multiply(modifier, UQ_4_12(1.5)); + modifier = uq4_12_multiply_half_down(modifier, UQ_4_12(1.5)); // The defensive stats of a Player's Pokémon are boosted by x1.1 (+10%) if they have the 5th badge and 7th badges. // Having the 5th badge boosts physical defense while having the 7th badge boosts special defense. if (ShouldGetStatBadgeBoost(FLAG_BADGE05_GET, battlerDef) && IS_MOVE_PHYSICAL(move)) - modifier = uq4_12_multiply(modifier, UQ_4_12(1.1)); + modifier = uq4_12_multiply_half_down(modifier, UQ_4_12(1.1)); if (ShouldGetStatBadgeBoost(FLAG_BADGE07_GET, battlerDef) && IS_MOVE_SPECIAL(move)) - modifier = uq4_12_multiply(modifier, UQ_4_12(1.1)); + modifier = uq4_12_multiply_half_down(modifier, UQ_4_12(1.1)); - return ApplyModifier(modifier, defStat); + return uq4_12_multiply_by_int_half_down(modifier, defStat); } -static u32 CalcFinalDmg(u32 dmg, u16 move, u8 battlerAtk, u8 battlerDef, u8 moveType, uq4_12_t typeEffectivenessModifier, bool32 isCrit, bool32 updateFlags) +// base damage formula before adding any modifiers +static inline s32 CalculateBaseDamage(u32 power, u32 userFinalAttack, u32 level, u32 targetFinalDefense) { - u32 percentBoost; - u32 abilityAtk = GetBattlerAbility(battlerAtk); - u32 abilityDef = GetBattlerAbility(battlerDef); - u32 defSide = GET_BATTLER_SIDE(battlerDef); - uq4_12_t finalModifier = UQ_4_12(1.0); - u16 itemDef = gBattleMons[battlerDef].item; - u16 holdEffectAtk = GetBattlerHoldEffect(battlerAtk, TRUE); - u16 holdEffectDef = GetBattlerHoldEffect(battlerDef, TRUE); + return power * userFinalAttack * (2 * level / 5 + 2) / targetFinalDefense / 50 + 2; +} - // check multiple targets in double battle +#if B_MULTIPLE_TARGETS_DMG >= GEN_4 + #define V_MULTIPLE_TARGETS_DMG UQ_4_12(0.75) +#else + #define V_MULTIPLE_TARGETS_DMG UQ_4_12(0.5) +#endif + +#if B_CRIT_MULTIPLIER >= GEN_6 + #define V_CRIT_MULTIPLIER UQ_4_12(1.5) +#else + #define V_CRIT_MULTIPLIER UQ_4_12(2.0) +#endif + +#if B_BURN_FACADE_DMG >= GEN_6 + #define FACADE_PREVENTS_BURN_MALUS(move) (gBattleMoves[move].effect == EFFECT_FACADE) +#else + #define FACADE_PREVENTS_BURN_MALUS(move) (FALSE) +#endif + +#if B_PARENTAL_BOND_DMG < GEN_7 + #define V_PARENTAL_BOND_DMG UQ_4_12(0.5) +#else + #define V_PARENTAL_BOND_DMG UQ_4_12(0.25) +#endif + +static inline uq4_12_t GetTargetDamageModifier(u32 move, u32 battlerAtk, u32 battlerDef) +{ if (GetMoveTargetCount(move, battlerAtk, battlerDef) >= 2) - #if B_MULTIPLE_TARGETS_DMG >= GEN_4 - finalModifier = uq4_12_multiply(finalModifier, UQ_4_12(0.75)); - #else - finalModifier = uq4_12_multiply(finalModifier, UQ_4_12(0.5)); - #endif + return V_MULTIPLE_TARGETS_DMG; + return UQ_4_12(1.0); +} - // take type effectiveness - finalModifier = uq4_12_multiply(finalModifier, typeEffectivenessModifier); +static inline uq4_12_t GetParentalBondModifier(u32 battlerAtk) +{ + if (gSpecialStatuses[battlerAtk].parentalBondState != PARENTAL_BOND_2ND_HIT) + return UQ_4_12(1.0); + return V_PARENTAL_BOND_DMG; +} - // check crit - if (isCrit) - #if B_CRIT_MULTIPLIER >= GEN_6 - dmg = ApplyModifier(UQ_4_12(1.5), dmg); - #else - dmg = ApplyModifier(UQ_4_12(2.0), dmg); - #endif +static inline uq4_12_t GetSameTypeAttackBonusModifier(u32 battlerAtk, u32 moveType, u32 move, u32 abilityAtk) +{ + if (!IS_BATTLER_OF_TYPE(battlerAtk, moveType) || move == MOVE_STRUGGLE || move == MOVE_NONE) + return UQ_4_12(1.0); + return (abilityAtk == ABILITY_ADAPTABILITY) ? UQ_4_12(2.0) : UQ_4_12(1.5); +} - // check burn - if (gBattleMons[battlerAtk].status1 & STATUS1_BURN && IS_MOVE_PHYSICAL(move) - #if B_BURN_FACADE_DMG >= GEN_6 - && gBattleMoves[move].effect != EFFECT_FACADE - #endif +// Utility Umbrella holders take normal damage from what would be rain- and sun-weakened attacks. +static uq4_12_t GetWeatherDamageModifier(u32 battlerAtk, u32 move, u32 moveType, u32 holdEffectAtk, u32 holdEffectDef) +{ + if (!WEATHER_HAS_EFFECT) + return UQ_4_12(1.0); + if (gBattleMoves[move].effect == EFFECT_HYDRO_STEAM && (gBattleWeather & B_WEATHER_SUN) && holdEffectAtk != HOLD_EFFECT_UTILITY_UMBRELLA) + return UQ_4_12(1.5); + if (holdEffectDef == HOLD_EFFECT_UTILITY_UMBRELLA) + return UQ_4_12(1.0); + + if (gBattleWeather & B_WEATHER_RAIN) + { + if (moveType != TYPE_FIRE && moveType != TYPE_WATER) + return UQ_4_12(1.0); + return (moveType == TYPE_FIRE) ? UQ_4_12(0.5) : UQ_4_12(1.5); + } + if (gBattleWeather & B_WEATHER_SUN) + { + if (moveType != TYPE_FIRE && moveType != TYPE_WATER) + return UQ_4_12(1.0); + return (moveType == TYPE_WATER) ? UQ_4_12(0.5) : UQ_4_12(1.5); + } + return UQ_4_12(1.0); +} + +static inline uq4_12_t GetBurnOrFrostBiteModifier(u32 battlerAtk, u32 move, u32 abilityAtk) +{ + if (gBattleMons[battlerAtk].status1 & STATUS1_BURN + && IS_MOVE_PHYSICAL(move) + && !FACADE_PREVENTS_BURN_MALUS(move) && abilityAtk != ABILITY_GUTS) - dmg = ApplyModifier(UQ_4_12(0.5), dmg); - - // check frostbite - if (gBattleMons[battlerAtk].status1 & STATUS1_FROSTBITE && IS_MOVE_SPECIAL(move) - #if B_BURN_FACADE_DMG >= GEN_6 - && gBattleMoves[move].effect != EFFECT_FACADE - #endif + return UQ_4_12(0.5); + if (gBattleMons[battlerAtk].status1 & STATUS1_FROSTBITE + && IS_MOVE_SPECIAL(move) + && !FACADE_PREVENTS_BURN_MALUS(move) && abilityAtk != ABILITY_GUTS) - dmg = ApplyModifier(UQ_4_12(0.5), dmg); + return UQ_4_12(0.5); + return UQ_4_12(1.0); +} - // check weather - dmg = ApplyWeatherDamageMultiplier(battlerAtk, move, moveType, dmg, holdEffectAtk, holdEffectDef); +static inline uq4_12_t GetCriticalModifier(bool32 isCrit) +{ + return isCrit ? V_CRIT_MULTIPLIER : UQ_4_12(1.0); +} - // check stab - if (IS_BATTLER_OF_TYPE(battlerAtk, moveType) && move != MOVE_STRUGGLE && move != MOVE_NONE) - { - if (abilityAtk == ABILITY_ADAPTABILITY) - finalModifier = uq4_12_multiply(finalModifier, UQ_4_12(2.0)); - else - finalModifier = uq4_12_multiply(finalModifier, UQ_4_12(1.5)); - } - - // Collision Course, Electro Drift - if (gBattleMoves[move].effect == EFFECT_COLLISION_COURSE && typeEffectivenessModifier >= UQ_4_12(2.0)) - finalModifier = uq4_12_multiply(finalModifier, UQ_4_12(1.3333)); - - // reflect, light screen, aurora veil - if (((gSideStatuses[defSide] & SIDE_STATUS_REFLECT && IS_MOVE_PHYSICAL(move)) - || (gSideStatuses[defSide] & SIDE_STATUS_LIGHTSCREEN && IS_MOVE_SPECIAL(move)) - || (gSideStatuses[defSide] & SIDE_STATUS_AURORA_VEIL)) - && abilityAtk != ABILITY_INFILTRATOR - && !(isCrit) - && !gProtectStructs[battlerAtk].confusionSelfDmg) - { - if (gBattleTypeFlags & BATTLE_TYPE_DOUBLE) - finalModifier = uq4_12_multiply(finalModifier, UQ_4_12(0.66)); - else - finalModifier = uq4_12_multiply(finalModifier, UQ_4_12(0.5)); - } - - // Parental Bond Second Strike - if (gSpecialStatuses[battlerAtk].parentalBondState == PARENTAL_BOND_2ND_HIT) - { - if (B_PARENTAL_BOND_DMG < GEN_7) - finalModifier = uq4_12_multiply(finalModifier, UQ_4_12(0.5)); - else - finalModifier = uq4_12_multiply(finalModifier, UQ_4_12(0.25)); - } - - // Z-Moves and Max Moves bypass Protect and do 25% of their original damage +static inline uq4_12_t GetZMoveAgainstProtectionModifier(u32 battlerDef) +{ if (gBattleStruct->zmove.active && IS_BATTLER_PROTECTED(battlerDef)) - { - finalModifier = uq4_12_multiply(finalModifier, UQ_4_12(0.25)); - } + return UQ_4_12(0.25); + return UQ_4_12(1.0); +} - // attacker's abilities +static inline uq4_12_t GetMinimizeModifier(u32 move, u32 battlerDef) +{ + if (gBattleMoves[move].minimizeDoubleDamage && gStatuses3[battlerDef] & STATUS3_MINIMIZED) + return UQ_4_12(2.0); + return UQ_4_12(1.0); +} + +static inline uq4_12_t GetUndergroundModifier(u32 move, u32 battlerDef) +{ + if (gBattleMoves[move].damagesUnderground && gStatuses3[battlerDef] & STATUS3_UNDERGROUND) + return UQ_4_12(2.0); + return UQ_4_12(1.0); +} + +static inline uq4_12_t GetDiveModifier(u32 move, u32 battlerDef) +{ + if (gBattleMoves[move].damagesUnderwater && gStatuses3[battlerDef] & STATUS3_UNDERWATER) + return UQ_4_12(2.0); + return UQ_4_12(1.0); +} + +static inline uq4_12_t GetAirborneModifier(u32 move, u32 battlerDef) +{ + if (gBattleMoves[move].damagesAirborneDoubleDamage && gStatuses3[battlerDef] & STATUS3_ON_AIR) + return UQ_4_12(2.0); + return UQ_4_12(1.0); +} + +static inline uq4_12_t GetScreensModifier(u32 move, u32 battlerAtk, u32 battlerDef, bool32 isCrit) +{ + u32 sideStatus = gSideStatuses[GET_BATTLER_SIDE(battlerDef)]; + bool32 lightScreen = (sideStatus & SIDE_STATUS_LIGHTSCREEN) && IS_MOVE_SPECIAL(move); + bool32 reflect = (sideStatus & SIDE_STATUS_REFLECT) && IS_MOVE_PHYSICAL(move); + bool32 auroraVeil = sideStatus & SIDE_STATUS_AURORA_VEIL; + u32 abilityAtk = GetBattlerAbility(battlerAtk); + + if (isCrit || abilityAtk == ABILITY_INFILTRATOR || gProtectStructs[battlerAtk].confusionSelfDmg) + return UQ_4_12(1.0); + if (reflect || lightScreen || auroraVeil) + return (gBattleTypeFlags & BATTLE_TYPE_DOUBLE) ? UQ_4_12(0.667) : UQ_4_12(0.5); + return UQ_4_12(1.0); +} + +static inline uq4_12_t GetCollisionCourseElectroDriftModifier(u32 move, uq4_12_t typeEffectivenessModifier) +{ + if (gBattleMoves[move].effect == EFFECT_COLLISION_COURSE && typeEffectivenessModifier >= UQ_4_12(2.0)) + return UQ_4_12(1.3333); + return UQ_4_12(1.0); +} + +static inline uq4_12_t GetAttackerAbilitiesModifier(u32 battlerAtk, uq4_12_t typeEffectivenessModifier, bool32 isCrit) +{ + u32 abilityAtk = GetBattlerAbility(battlerAtk); switch (abilityAtk) { - case ABILITY_TINTED_LENS: - if (typeEffectivenessModifier <= UQ_4_12(0.5)) - finalModifier = uq4_12_multiply(finalModifier, UQ_4_12(2.0)); + case ABILITY_NEUROFORCE: + if (typeEffectivenessModifier >= UQ_4_12(2.0)) + return UQ_4_12(1.25); break; case ABILITY_SNIPER: if (isCrit) - finalModifier = uq4_12_multiply(finalModifier, UQ_4_12(1.5)); + return UQ_4_12(1.5); break; - case ABILITY_NEUROFORCE: - if (typeEffectivenessModifier >= UQ_4_12(2.0)) - finalModifier = uq4_12_multiply(finalModifier, UQ_4_12(1.25)); + case ABILITY_TINTED_LENS: + if (typeEffectivenessModifier <= UQ_4_12(0.5)) + return UQ_4_12(2.0); break; } + return UQ_4_12(1.0); +} - // target's abilities +static inline uq4_12_t GetDefenderAbilitiesModifier(u32 move, u32 moveType, u32 battlerAtk, u32 battlerDef, uq4_12_t typeEffectivenessModifier) +{ + u32 abilityDef = GetBattlerAbility(battlerDef); switch (abilityDef) { case ABILITY_MULTISCALE: case ABILITY_SHADOW_SHIELD: if (BATTLER_MAX_HP(battlerDef)) - finalModifier = uq4_12_multiply(finalModifier, UQ_4_12(0.5)); + return UQ_4_12(0.5); break; case ABILITY_FILTER: case ABILITY_SOLID_ROCK: case ABILITY_PRISM_ARMOR: if (typeEffectivenessModifier >= UQ_4_12(2.0)) - finalModifier = uq4_12_multiply(finalModifier, UQ_4_12(0.75)); + return UQ_4_12(0.75); + break; + case ABILITY_FLUFFY: + if (!IsMoveMakingContact(move, battlerAtk) && moveType == TYPE_FIRE) + return UQ_4_12(2.0); + if (IsMoveMakingContact(move, battlerAtk) && moveType != TYPE_FIRE) + return UQ_4_12(0.5); + break; + case ABILITY_PUNK_ROCK: + if (gBattleMoves[move].soundMove) + return UQ_4_12(0.5); + break; + case ABILITY_ICE_SCALES: + if (IS_MOVE_SPECIAL(move)) + return UQ_4_12(0.5); break; } + return UQ_4_12(1.0); +} - // target's ally's abilities - if (IsBattlerAlive(BATTLE_PARTNER(battlerDef))) +static inline uq4_12_t GetDefenderPartnerAbilitiesModifier(u32 battlerPartnerDef) +{ + if (!IsBattlerAlive(battlerPartnerDef)) + return UQ_4_12(1.0); + + switch (GetBattlerAbility(battlerPartnerDef)) { - switch (GetBattlerAbility(BATTLE_PARTNER(battlerDef))) - { - case ABILITY_FRIEND_GUARD: - finalModifier = uq4_12_multiply(finalModifier, UQ_4_12(0.75)); - break; - } + case ABILITY_FRIEND_GUARD: + return UQ_4_12(0.75); + break; } + return UQ_4_12(1.0); +} - // attacker's hold effect +static inline uq4_12_t GetAttackerItemsModifier(u32 battlerAtk, uq4_12_t typeEffectivenessModifier) +{ + u32 holdEffectAtk = GetBattlerHoldEffect(battlerAtk, TRUE); + u32 percentBoost; switch (holdEffectAtk) { case HOLD_EFFECT_METRONOME: percentBoost = min((gBattleStruct->sameMoveTurns[battlerAtk] * GetBattlerHoldEffectParam(battlerAtk)), 100); - finalModifier = uq4_12_multiply(finalModifier, UQ_4_12(1.0) + sPercentToModifier[percentBoost]); + return sPercentToModifier[percentBoost]; break; case HOLD_EFFECT_EXPERT_BELT: if (typeEffectivenessModifier >= UQ_4_12(2.0)) - finalModifier = uq4_12_multiply(finalModifier, UQ_4_12(1.2)); + return UQ_4_12(1.2); break; case HOLD_EFFECT_LIFE_ORB: - finalModifier = uq4_12_multiply(finalModifier, UQ_4_12(1.3)); + return UQ_4_12(1.3); break; } + return UQ_4_12(1.0); +} + +static inline uq4_12_t GetDefenderItemsModifier(u32 moveType, u32 battlerDef, uq4_12_t typeEffectivenessModifier, bool32 updateFlags) +{ + u32 holdEffectDef = GetBattlerHoldEffect(battlerDef, TRUE); + u32 holdEffectDefParam = GetBattlerHoldEffectParam(battlerDef); + u32 itemDef = gBattleMons[battlerDef].item; + u32 abilityDef = GetBattlerAbility(battlerDef); - // target's hold effect switch (holdEffectDef) { - // berries reducing dmg case HOLD_EFFECT_RESIST_BERRY: - if (moveType == GetBattlerHoldEffectParam(battlerDef) - && (moveType == TYPE_NORMAL || typeEffectivenessModifier >= UQ_4_12(2.0)) - && !UnnerveOn(battlerDef, itemDef)) + if (UnnerveOn(battlerDef, itemDef)) + return UQ_4_12(1.0); + if (moveType == holdEffectDefParam && (moveType == TYPE_NORMAL || typeEffectivenessModifier >= UQ_4_12(2.0))) { - if (abilityDef == ABILITY_RIPEN) - finalModifier = uq4_12_multiply(finalModifier, UQ_4_12(0.25)); - else - finalModifier = uq4_12_multiply(finalModifier, UQ_4_12(0.5)); if (updateFlags) gSpecialStatuses[battlerDef].berryReduced = TRUE; + return (abilityDef == ABILITY_RIPEN) ? UQ_4_12(0.25) : UQ_4_12(0.5); } break; } - - if (gBattleMoves[move].minimizeDoubleDamage && gStatuses3[battlerDef] & STATUS3_MINIMIZED) - finalModifier = uq4_12_multiply(finalModifier, UQ_4_12(2.0)); - if (gBattleMoves[move].damagesUnderground && gStatuses3[battlerDef] & STATUS3_UNDERGROUND) - finalModifier = uq4_12_multiply(finalModifier, UQ_4_12(2.0)); - if (gBattleMoves[move].damagesUnderwater && gStatuses3[battlerDef] & STATUS3_UNDERWATER) - finalModifier = uq4_12_multiply(finalModifier, UQ_4_12(2.0)); - if (gBattleMoves[move].damagesAirborneDoubleDamage && gStatuses3[battlerDef] & STATUS3_ON_AIR) - finalModifier = uq4_12_multiply(finalModifier, UQ_4_12(2.0)); - - dmg = ApplyModifier(finalModifier, dmg); - if (dmg == 0) - dmg = 1; - - return dmg; + return UQ_4_12(1.0); } -static s32 DoMoveDamageCalc(u16 move, u8 battlerAtk, u8 battlerDef, u8 moveType, s32 fixedBasePower, +#define DAMAGE_MULTIPLY_MODIFIER(modifier) do { \ + finalModifier = uq4_12_multiply_half_down(modifier, finalModifier); \ +} while (0) + +// Calculates the "other" modifier which accounts for held items, abilities, +// or very specific interactions of moves that are not handled in the basic +// damage calculation. It is implemented as described by bulbapedia: +// https://bulbapedia.bulbagarden.net/wiki/Damage#Generation_V_onward +// Please Note: Fixed Point Multiplication is not associative. +// The order of operations is relevant. +static uq4_12_t GetOtherModifiers(u32 move, u32 moveType, u32 battlerAtk, u32 battlerDef, bool32 isCrit, uq4_12_t typeEffectivenessModifier, bool32 updateFlags) +{ + u32 abilityAtk = GetBattlerAbility(battlerAtk); + uq4_12_t finalModifier = UQ_4_12(1.0); + u32 battlerDefPartner = BATTLE_PARTNER(battlerDef); + u32 unmodifiedAttackerSpeed = gBattleMons[battlerAtk].speed; + u32 unmodifiedDefenderSpeed = gBattleMons[battlerDef].speed; + //TODO: Behemoth Blade, Behemoth Bash, Dynamax Cannon (Dynamax) + DAMAGE_MULTIPLY_MODIFIER(GetMinimizeModifier(move, battlerDef)); + DAMAGE_MULTIPLY_MODIFIER(GetUndergroundModifier(move, battlerDef)); + DAMAGE_MULTIPLY_MODIFIER(GetDiveModifier(move, battlerDef)); + DAMAGE_MULTIPLY_MODIFIER(GetAirborneModifier(move, battlerDef)); + DAMAGE_MULTIPLY_MODIFIER(GetScreensModifier(move, battlerAtk, battlerDef, isCrit)); + DAMAGE_MULTIPLY_MODIFIER(GetCollisionCourseElectroDriftModifier(move, typeEffectivenessModifier)); + + if (unmodifiedAttackerSpeed >= unmodifiedDefenderSpeed) + { + DAMAGE_MULTIPLY_MODIFIER(GetAttackerAbilitiesModifier(battlerAtk, typeEffectivenessModifier, isCrit)); + DAMAGE_MULTIPLY_MODIFIER(GetDefenderAbilitiesModifier(move, moveType, battlerAtk, battlerDef, typeEffectivenessModifier)); + DAMAGE_MULTIPLY_MODIFIER(GetDefenderPartnerAbilitiesModifier(battlerDefPartner)); + DAMAGE_MULTIPLY_MODIFIER(GetAttackerItemsModifier(battlerAtk, typeEffectivenessModifier)); + DAMAGE_MULTIPLY_MODIFIER(GetDefenderItemsModifier(moveType, battlerDef, typeEffectivenessModifier, updateFlags)); + } + else + { + DAMAGE_MULTIPLY_MODIFIER(GetDefenderAbilitiesModifier(move, moveType, battlerAtk, battlerDef, typeEffectivenessModifier)); + DAMAGE_MULTIPLY_MODIFIER(GetDefenderPartnerAbilitiesModifier(battlerDefPartner)); + DAMAGE_MULTIPLY_MODIFIER(GetAttackerAbilitiesModifier(battlerAtk, typeEffectivenessModifier, isCrit)); + DAMAGE_MULTIPLY_MODIFIER(GetDefenderItemsModifier(moveType, battlerDef, typeEffectivenessModifier, updateFlags)); + DAMAGE_MULTIPLY_MODIFIER(GetAttackerItemsModifier(battlerAtk, typeEffectivenessModifier)); + } + return finalModifier; +} + +#undef DAMAGE_ACCUMULATE_MULTIPLIER + +#define DAMAGE_APPLY_MODIFIER(modifier) do { \ + dmg = uq4_12_multiply_by_int_half_down(modifier, dmg); \ +} while (0) + +static s32 DoMoveDamageCalc(u32 move, u32 battlerAtk, u32 battlerDef, u32 moveType, s32 fixedBasePower, bool32 isCrit, bool32 randomFactor, bool32 updateFlags, uq4_12_t typeEffectivenessModifier) { s32 dmg; + u32 userFinalAttack; + u32 targetFinalDefense; + u32 holdEffectAtk = GetBattlerHoldEffect(battlerAtk, TRUE); + u32 holdEffectDef = GetBattlerHoldEffect(battlerDef, TRUE); + u32 abilityAtk = GetBattlerAbility(battlerAtk); - // Don't calculate damage if the move has no effect on target. - if (typeEffectivenessModifier == UQ_4_12(0)) + if (typeEffectivenessModifier == UQ_4_12(0.0)) return 0; if (fixedBasePower) @@ -9636,29 +9764,34 @@ static s32 DoMoveDamageCalc(u16 move, u8 battlerAtk, u8 battlerDef, u8 moveType, else gBattleMovePower = CalcMoveBasePowerAfterModifiers(move, battlerAtk, battlerDef, moveType, updateFlags); - // long dmg basic formula - dmg = ((gBattleMons[battlerAtk].level * 2) / 5) + 2; - dmg *= gBattleMovePower; - dmg *= CalcAttackStat(move, battlerAtk, battlerDef, moveType, isCrit, updateFlags); - dmg /= CalcDefenseStat(move, battlerAtk, battlerDef, moveType, isCrit, updateFlags); - dmg = (dmg / 50) + 2; + userFinalAttack = CalcAttackStat(move, battlerAtk, battlerDef, moveType, isCrit, updateFlags); + targetFinalDefense = CalcDefenseStat(move, battlerAtk, battlerDef, moveType, isCrit, updateFlags); - // Calculate final modifiers. - dmg = CalcFinalDmg(dmg, move, battlerAtk, battlerDef, moveType, typeEffectivenessModifier, isCrit, updateFlags); - - // Add a random factor. + dmg = CalculateBaseDamage(gBattleMovePower, userFinalAttack, gBattleMons[battlerAtk].level, targetFinalDefense); + DAMAGE_APPLY_MODIFIER(GetTargetDamageModifier(move, battlerAtk, battlerDef)); + DAMAGE_APPLY_MODIFIER(GetParentalBondModifier(battlerAtk)); + DAMAGE_APPLY_MODIFIER(GetWeatherDamageModifier(battlerAtk, move, moveType, holdEffectAtk, holdEffectDef)); + DAMAGE_APPLY_MODIFIER(GetCriticalModifier(isCrit)); + // TODO: Glaive Rush (Gen IX effect) if (randomFactor) { dmg *= 100 - RandomUniform(RNG_DAMAGE_MODIFIER, 0, 15); dmg /= 100; } + DAMAGE_APPLY_MODIFIER(GetSameTypeAttackBonusModifier(battlerAtk, moveType, move, abilityAtk)); + DAMAGE_APPLY_MODIFIER(typeEffectivenessModifier); + DAMAGE_APPLY_MODIFIER(GetBurnOrFrostBiteModifier(battlerAtk, move, abilityAtk)); + DAMAGE_APPLY_MODIFIER(GetZMoveAgainstProtectionModifier(battlerDef)); + DAMAGE_APPLY_MODIFIER(GetOtherModifiers(move, moveType, battlerAtk, battlerDef, isCrit, typeEffectivenessModifier, updateFlags)); + if (dmg == 0) dmg = 1; - return dmg; } +#undef DAMAGE_APPLY_MODIFIER + s32 CalculateMoveDamage(u16 move, u8 battlerAtk, u8 battlerDef, u8 moveType, s32 fixedBasePower, bool32 isCrit, bool32 randomFactor, bool32 updateFlags) { return DoMoveDamageCalc(move, battlerAtk, battlerDef, moveType, fixedBasePower, isCrit, randomFactor, @@ -10840,34 +10973,6 @@ bool32 IsBattlerWeatherAffected(u8 battlerId, u32 weatherFlags) return FALSE; } -// Utility Umbrella holders take normal damage from what would be rain- and sun-weakened attacks. -u32 ApplyWeatherDamageMultiplier(u8 battlerAtk, u16 move, u8 moveType, u32 dmg, u16 holdEffectAtk, u16 holdEffectDef) -{ - if (WEATHER_HAS_EFFECT) - { - if (gBattleMoves[move].effect == EFFECT_HYDRO_STEAM && (gBattleWeather & B_WEATHER_SUN) && holdEffectAtk != HOLD_EFFECT_UTILITY_UMBRELLA) - dmg = ApplyModifier(UQ_4_12(1.5), dmg); - else if (holdEffectDef != HOLD_EFFECT_UTILITY_UMBRELLA) - { - if (gBattleWeather & B_WEATHER_RAIN) - { - if (moveType == TYPE_FIRE) - dmg = ApplyModifier(UQ_4_12(0.5), dmg); - else if (moveType == TYPE_WATER) - dmg = ApplyModifier(UQ_4_12(1.5), dmg); - } - else if (gBattleWeather & B_WEATHER_SUN) - { - if (moveType == TYPE_FIRE) - dmg = ApplyModifier(UQ_4_12(1.5), dmg); - else if (moveType == TYPE_WATER) - dmg = ApplyModifier(UQ_4_12(0.5), dmg); - } - } - } - return dmg; -} - // Gets move target before redirection effects etc. are applied // Possible return values are defined in battle.h following MOVE_TARGET_SELECTED u32 GetBattlerMoveTargetType(u8 battlerId, u16 move) diff --git a/src/script_pokemon_util.c b/src/script_pokemon_util.c old mode 100755 new mode 100644 diff --git a/test/ability_contrary.c b/test/ability_contrary.c index 52347f379..799cb1116 100644 --- a/test/ability_contrary.c +++ b/test/ability_contrary.c @@ -26,7 +26,7 @@ SINGLE_BATTLE_TEST("Contrary raises Attack when Intimidated", s16 damage) HP_BAR(player, captureDamage: &results[i].damage); } FINALLY { - EXPECT_MUL_EQ(results[1].damage, Q_4_12(2.125), results[0].damage); + EXPECT_MUL_EQ(results[1].damage, Q_4_12(2.25), results[0].damage); } } diff --git a/test/ability_dry_skin.c b/test/ability_dry_skin.c index 028076d5a..59f99760c 100644 --- a/test/ability_dry_skin.c +++ b/test/ability_dry_skin.c @@ -36,15 +36,24 @@ SINGLE_BATTLE_TEST("Dry Skin increases damage taken from Fire-type moves by 25%" PARAMETRIZE { ability = ABILITY_DRY_SKIN; } GIVEN { ASSUME(gBattleMoves[MOVE_EMBER].type == TYPE_FIRE); - PLAYER(SPECIES_WOBBUFFET); - OPPONENT(SPECIES_PARASECT) { Ability(ability); } + ASSUME(gBattleMoves[MOVE_EMBER].power == 40); + ASSUME(gSpeciesInfo[SPECIES_PARASECT].types[0] == TYPE_BUG); + ASSUME(gSpeciesInfo[SPECIES_PARASECT].types[1] == TYPE_GRASS); + ASSUME(gSpeciesInfo[SPECIES_WOBBUFFET].types[0] == TYPE_PSYCHIC); + ASSUME(gSpeciesInfo[SPECIES_WOBBUFFET].types[1] == TYPE_PSYCHIC); + PLAYER(SPECIES_WOBBUFFET) { SpAttack(71); } + OPPONENT(SPECIES_PARASECT) { Ability(ability); SpDefense(165); } } WHEN { TURN { MOVE(player, MOVE_EMBER); } } SCENE { MESSAGE("Wobbuffet used Ember!"); HP_BAR(opponent, captureDamage: &results[i].damage); } FINALLY { - EXPECT_MUL_EQ(results[0].damage, Q_4_12(1.25), results[1].damage); + // Due to numerics related to rounding on each applied multiplier, + // the ability effect doesn't manifest as a 25% damage increase, but as a ~31% damage increase in this case. + // Values obtained from https://calc.pokemonshowdown.com (Neutral nature and 0 IVs on both sides) + EXPECT_EQ(results[0].damage, 52); + EXPECT_EQ(results[1].damage, 68); } } diff --git a/test/ability_fluffy.c b/test/ability_fluffy.c new file mode 100644 index 000000000..238045e25 --- /dev/null +++ b/test/ability_fluffy.c @@ -0,0 +1,66 @@ +#include "global.h" +#include "test_battle.h" + +ASSUMPTIONS +{ + ASSUME(gBattleMoves[MOVE_TACKLE].makesContact); + ASSUME(gBattleMoves[MOVE_EMBER].type == TYPE_FIRE); + ASSUME(gBattleMoves[MOVE_TACKLE].makesContact); + ASSUME(gBattleMoves[MOVE_FIRE_PUNCH].makesContact); + ASSUME(gBattleMoves[MOVE_FIRE_PUNCH].type == TYPE_FIRE); + ASSUME(P_GEN_7_POKEMON == TRUE); +} + +SINGLE_BATTLE_TEST("Fluffy halves damage taken from moves that make direct contact", s16 damage) +{ + u32 ability; + PARAMETRIZE { ability = ABILITY_KLUTZ; } + PARAMETRIZE { ability = ABILITY_FLUFFY; } + GIVEN { + PLAYER(SPECIES_WOBBUFFET); + OPPONENT(SPECIES_STUFFUL) { Ability(ability); } + } WHEN { + TURN { MOVE(player, MOVE_TACKLE); } + } SCENE { + MESSAGE("Wobbuffet used Tackle!"); + HP_BAR(opponent, captureDamage: &results[i].damage); + } FINALLY { + EXPECT_MUL_EQ(results[0].damage, UQ_4_12(0.5), results[1].damage); + } +} + +SINGLE_BATTLE_TEST("Fluffy doubles damage taken from fire type moves", s16 damage) +{ + u32 ability; + PARAMETRIZE { ability = ABILITY_KLUTZ; } + PARAMETRIZE { ability = ABILITY_FLUFFY; } + GIVEN { + PLAYER(SPECIES_WOBBUFFET); + OPPONENT(SPECIES_STUFFUL) { Ability(ability); } + } WHEN { + TURN { MOVE(player, MOVE_EMBER); } + } SCENE { + MESSAGE("Wobbuffet used Ember!"); + HP_BAR(opponent, captureDamage: &results[i].damage); + } FINALLY { + EXPECT_MUL_EQ(results[0].damage, UQ_4_12(2.0), results[1].damage); + } +} + +SINGLE_BATTLE_TEST("Fluffy does not alter damage of fire-type moves that make direct contact", s16 damage) +{ + u32 ability; + PARAMETRIZE { ability = ABILITY_KLUTZ; } + PARAMETRIZE { ability = ABILITY_FLUFFY; } + GIVEN { + PLAYER(SPECIES_WOBBUFFET); + OPPONENT(SPECIES_STUFFUL) { Ability(ability); } + } WHEN { + TURN { MOVE(player, MOVE_FIRE_PUNCH); } + } SCENE { + MESSAGE("Wobbuffet used Fire Punch!"); + HP_BAR(opponent, captureDamage: &results[i].damage); + } FINALLY { + EXPECT_EQ(results[0].damage, results[1].damage); + } +} diff --git a/test/ability_mirror_armor.c b/test/ability_mirror_armor.c new file mode 100644 index 000000000..7e15e029b --- /dev/null +++ b/test/ability_mirror_armor.c @@ -0,0 +1,202 @@ +#include "global.h" +#include "test_battle.h" + +ASSUMPTIONS +{ + ASSUME(P_GEN_8_POKEMON == TRUE); +} + +SINGLE_BATTLE_TEST("Mirror Armor lowers a stat of the attacking pokemon") +{ + u16 move, statId; + + PARAMETRIZE { move = MOVE_LEER; statId = STAT_DEF; } + PARAMETRIZE { move = MOVE_GROWL; statId = STAT_ATK; } + PARAMETRIZE { move = MOVE_SWEET_SCENT; statId = STAT_EVASION; } + PARAMETRIZE { move = MOVE_SAND_ATTACK; statId = STAT_ACC; } + PARAMETRIZE { move = MOVE_CONFIDE; statId = STAT_SPATK; } + PARAMETRIZE { move = MOVE_FAKE_TEARS; statId = STAT_SPDEF; } + + GIVEN { + PLAYER(SPECIES_CORVIKNIGHT) {Ability(ABILITY_MIRROR_ARMOR);} + OPPONENT(SPECIES_WYNAUT); + } WHEN { + TURN { MOVE(opponent, move); } + } SCENE { + ABILITY_POPUP(player, ABILITY_MIRROR_ARMOR); + ANIMATION(ANIM_TYPE_GENERAL, B_ANIM_STATS_CHANGE, opponent); + switch (statId) + { + case STAT_DEF: + MESSAGE("Foe Wynaut's Defense fell!"); + break; + case STAT_ATK: + MESSAGE("Foe Wynaut's Attack fell!"); + break; + case STAT_EVASION: + MESSAGE("Foe Wynaut's evasiveness harshly fell!"); + break; + case STAT_ACC: + MESSAGE("Foe Wynaut's accuracy fell!"); + break; + case STAT_SPATK: + MESSAGE("Foe Wynaut's Sp. Atk fell!"); + break; + case STAT_SPDEF: + MESSAGE("Foe Wynaut's Sp. Def harshly fell!"); + break; + } + } THEN { + EXPECT_EQ(player->statStages[statId], DEFAULT_STAT_STAGE); + EXPECT_EQ(opponent->statStages[statId], (statId == STAT_SPDEF || statId == STAT_EVASION) ? DEFAULT_STAT_STAGE - 2 : DEFAULT_STAT_STAGE - 1); + } +} + +SINGLE_BATTLE_TEST("Mirror Armor triggers even if the attacking Pokemon also has Mirror Armor ability") +{ + GIVEN { + PLAYER(SPECIES_CORVIKNIGHT) { Ability(ABILITY_MIRROR_ARMOR); } + OPPONENT(SPECIES_CORVIKNIGHT) { Ability(ABILITY_MIRROR_ARMOR); } + } WHEN { + TURN { MOVE(opponent, MOVE_LEER); } + } SCENE { + MESSAGE("Foe Corviknigh used Leer!"); + ABILITY_POPUP(player, ABILITY_MIRROR_ARMOR); + NOT ANIMATION(ANIM_TYPE_GENERAL, B_ANIM_STATS_CHANGE, player); + ANIMATION(ANIM_TYPE_GENERAL, B_ANIM_STATS_CHANGE, opponent); + MESSAGE("Foe Corviknigh's Defense fell!"); + } THEN { + EXPECT_EQ(player->statStages[STAT_DEF], DEFAULT_STAT_STAGE); + EXPECT_EQ(opponent->statStages[STAT_DEF], DEFAULT_STAT_STAGE - 1); + } +} + +SINGLE_BATTLE_TEST("Mirror Armor doesn't lower the stats of an attacking Pokemon with the Clear Body ability") +{ + GIVEN { + PLAYER(SPECIES_CORVIKNIGHT) { Ability(ABILITY_MIRROR_ARMOR); } + OPPONENT(SPECIES_WYNAUT) { Ability(ABILITY_CLEAR_BODY); } + } WHEN { + TURN { MOVE(opponent, MOVE_LEER); } + } SCENE { + MESSAGE("Foe Wynaut used Leer!"); + ABILITY_POPUP(player, ABILITY_MIRROR_ARMOR); + ABILITY_POPUP(opponent, ABILITY_CLEAR_BODY); + MESSAGE("Foe Wynaut's Clear Body prevents stat loss!"); + } THEN { + EXPECT_EQ(player->statStages[STAT_DEF], DEFAULT_STAT_STAGE); + EXPECT_EQ(opponent->statStages[STAT_DEF], DEFAULT_STAT_STAGE); + } +} + +SINGLE_BATTLE_TEST("Mirror Armor lowers the Attack of Pokemon with Intimidate") +{ + GIVEN { + PLAYER(SPECIES_CORVIKNIGHT) { Ability(ABILITY_MIRROR_ARMOR); } + OPPONENT(SPECIES_GYARADOS) { Ability(ABILITY_INTIMIDATE); } + } WHEN { + TURN {} + } SCENE { + ABILITY_POPUP(opponent, ABILITY_INTIMIDATE); + ABILITY_POPUP(player, ABILITY_MIRROR_ARMOR); + ANIMATION(ANIM_TYPE_GENERAL, B_ANIM_STATS_CHANGE, opponent); + MESSAGE("Foe Gyarados's Attack fell!"); + } THEN { + EXPECT_EQ(player->statStages[STAT_ATK], DEFAULT_STAT_STAGE); + EXPECT_EQ(opponent->statStages[STAT_ATK], DEFAULT_STAT_STAGE - 1); + } +} + +// Unsure whether this should or should not fail, as Showdown has conflicting information. Needs testing in gen8 games. +SINGLE_BATTLE_TEST("Mirror Armor doesn't lower the stats of an attacking Pokemon behind Substitute") +{ + KNOWN_FAILING; + GIVEN { + PLAYER(SPECIES_CORVIKNIGHT) { Ability(ABILITY_MIRROR_ARMOR); } + OPPONENT(SPECIES_WYNAUT); + } WHEN { + TURN { MOVE(opponent, MOVE_SUBSTITUTE); } + TURN { MOVE(opponent, MOVE_LEER); } + } SCENE { + MESSAGE("Foe Wynaut used Substitute!"); + ANIMATION(ANIM_TYPE_MOVE, MOVE_SUBSTITUTE, opponent); + MESSAGE("Foe Wynaut used Leer!"); + ABILITY_POPUP(player, ABILITY_MIRROR_ARMOR); + NOT ANIMATION(ANIM_TYPE_GENERAL, B_ANIM_STATS_CHANGE, opponent); + } THEN { + EXPECT_EQ(player->statStages[STAT_DEF], DEFAULT_STAT_STAGE); + EXPECT_EQ(opponent->statStages[STAT_DEF], DEFAULT_STAT_STAGE); + } +} + +SINGLE_BATTLE_TEST("Mirror Armor raises the stat of an attacking Pokemon with Contrary") +{ + GIVEN { + PLAYER(SPECIES_CORVIKNIGHT) {Ability(ABILITY_MIRROR_ARMOR);} + OPPONENT(SPECIES_SHUCKLE) {Ability(ABILITY_CONTRARY);} + } WHEN { + TURN { MOVE(opponent, MOVE_LEER); } + } SCENE { + MESSAGE("Foe Shuckle used Leer!"); + ABILITY_POPUP(player, ABILITY_MIRROR_ARMOR); + ANIMATION(ANIM_TYPE_GENERAL, B_ANIM_STATS_CHANGE, opponent); + MESSAGE("Foe Shuckle's Defense rose!"); + } THEN { + EXPECT_EQ(player->statStages[STAT_DEF], DEFAULT_STAT_STAGE); + EXPECT_EQ(opponent->statStages[STAT_DEF], DEFAULT_STAT_STAGE + 1); + } +} + +SINGLE_BATTLE_TEST("Mirror Armor doesn't lower the stat of the attacking Pokemon if it is already at -6") +{ + GIVEN { + PLAYER(SPECIES_CORVIKNIGHT) {Ability(ABILITY_MIRROR_ARMOR);} + OPPONENT(SPECIES_WYNAUT); + } WHEN { + TURN { MOVE(player, MOVE_SCREECH); } + TURN { MOVE(player, MOVE_SCREECH); } + TURN { MOVE(player, MOVE_SCREECH); } + TURN { MOVE(opponent, MOVE_LEER); } + } SCENE { + MESSAGE("Corviknigh used Screech!"); + MESSAGE("Corviknigh used Screech!"); + MESSAGE("Corviknigh used Screech!"); + MESSAGE("Foe Wynaut used Leer!"); + ABILITY_POPUP(player, ABILITY_MIRROR_ARMOR); + NOT ANIMATION(ANIM_TYPE_GENERAL, B_ANIM_STATS_CHANGE, opponent); + MESSAGE("Foe Wynaut's Defense won't go lower!"); + } THEN { + EXPECT_EQ(player->statStages[STAT_DEF], DEFAULT_STAT_STAGE); + EXPECT_EQ(opponent->statStages[STAT_DEF], MIN_STAT_STAGE); + } +} + +// This behaviour needs to be verified in the actual games. Currently it's written to follow Showdown's logic. +DOUBLE_BATTLE_TEST("Mirror Armor lowers Speed of the partner Pokemon after Court Change was used by the opponent after it set up Sticky Web") +{ + KNOWN_FAILING; + GIVEN { + ASSUME(gBattleMoves[MOVE_STICKY_WEB].effect == EFFECT_STICKY_WEB); + ASSUME(gBattleMoves[MOVE_COURT_CHANGE].effect == EFFECT_COURT_CHANGE); + PLAYER(SPECIES_WOBBUFFET); + PLAYER(SPECIES_WOBBUFFET); + PLAYER(SPECIES_CORVIKNIGHT) {Ability(ABILITY_MIRROR_ARMOR); Item(ITEM_IRON_BALL); } + OPPONENT(SPECIES_WYNAUT); + OPPONENT(SPECIES_WYNAUT); + OPPONENT(SPECIES_WYNAUT); + } WHEN { + TURN { MOVE(playerLeft, MOVE_STICKY_WEB); } + TURN { MOVE(opponentLeft, MOVE_COURT_CHANGE); } + TURN { SWITCH(playerRight, 2);} + TURN { } + } SCENE { + MESSAGE("Wobbuffet used Sticky Web!"); + MESSAGE("Foe Wynaut used Court Change!"); + MESSAGE("Foe Wynaut swapped the battle effects affecting each side!"); + MESSAGE("Go! Corviknigh!"); + MESSAGE("Corviknigh was caught in a Sticky Web!"); + ABILITY_POPUP(playerRight, ABILITY_MIRROR_ARMOR); + ANIMATION(ANIM_TYPE_GENERAL, B_ANIM_STATS_CHANGE, playerLeft); + MESSAGE("Wobbuffet's Speed fell!"); + } +} diff --git a/test/ability_swarm.c b/test/ability_swarm.c index 7709e976e..7e4211119 100644 --- a/test/ability_swarm.c +++ b/test/ability_swarm.c @@ -8,13 +8,21 @@ SINGLE_BATTLE_TEST("Swarm boosts Bug-type moves in a pinch", s16 damage) PARAMETRIZE { hp = 33; } GIVEN { ASSUME(gBattleMoves[MOVE_BUG_BITE].type == TYPE_BUG); - PLAYER(SPECIES_LEDYBA) { Ability(ABILITY_SWARM); MaxHP(99); HP(hp); } - OPPONENT(SPECIES_WOBBUFFET); + ASSUME(gBattleMoves[MOVE_BUG_BITE].power == 60); + ASSUME(gSpeciesInfo[SPECIES_LEDYBA].types[0] == TYPE_BUG); + ASSUME(gSpeciesInfo[SPECIES_WOBBUFFET].types[0] == TYPE_PSYCHIC); + ASSUME(gSpeciesInfo[SPECIES_WOBBUFFET].types[1] == TYPE_PSYCHIC); + PLAYER(SPECIES_LEDYBA) { Ability(ABILITY_SWARM); MaxHP(99); HP(hp); Attack(45); } + OPPONENT(SPECIES_WOBBUFFET) { Defense(121); } } WHEN { TURN { MOVE(player, MOVE_BUG_BITE); } } SCENE { HP_BAR(opponent, captureDamage: &results[i].damage); } FINALLY { - EXPECT_MUL_EQ(results[0].damage, Q_4_12(1.5), results[1].damage); + // Due to numerics related to rounding on each applied multiplier, + // the 50% move power increase doesn't manifest as a 50% damage increase, but as a 44% damage increase in this case. + // Values obtained from https://calc.pokemonshowdown.com (Neutral nature and 0 IVs on both sides) + EXPECT_EQ(results[0].damage, 50); + EXPECT_EQ(results[1].damage, 72); } } diff --git a/test/damage_formula.c b/test/damage_formula.c new file mode 100644 index 000000000..73d919330 --- /dev/null +++ b/test/damage_formula.c @@ -0,0 +1,78 @@ +#include "global.h" +#include "test_battle.h" + +// From https://bulbapedia.bulbagarden.net/wiki/Damage#Example + +SINGLE_BATTLE_TEST("Damage calculation matches Gen5+") +{ + s16 dmg; + s16 expectedDamage; + PARAMETRIZE { expectedDamage = 196; } + PARAMETRIZE { expectedDamage = 192; } + PARAMETRIZE { expectedDamage = 192; } + PARAMETRIZE { expectedDamage = 192; } + PARAMETRIZE { expectedDamage = 184; } + PARAMETRIZE { expectedDamage = 184; } + PARAMETRIZE { expectedDamage = 184; } + PARAMETRIZE { expectedDamage = 180; } + PARAMETRIZE { expectedDamage = 180; } + PARAMETRIZE { expectedDamage = 180; } + PARAMETRIZE { expectedDamage = 172; } + PARAMETRIZE { expectedDamage = 172; } + PARAMETRIZE { expectedDamage = 172; } + PARAMETRIZE { expectedDamage = 168; } + PARAMETRIZE { expectedDamage = 168; } + PARAMETRIZE { expectedDamage = 168; } + GIVEN { + PLAYER(SPECIES_GLACEON) { Level(75); Attack(123); } + OPPONENT(SPECIES_GARCHOMP) { Defense(163); } + } WHEN { + TURN { + MOVE(player, MOVE_ICE_FANG, WITH_RNG(RNG_DAMAGE_MODIFIER, i)); + } + } + SCENE{ + MESSAGE("Glaceon used Ice Fang!"); + HP_BAR(opponent, captureDamage: &dmg); + } + THEN{ + EXPECT_EQ(expectedDamage, dmg); + } +} + +SINGLE_BATTLE_TEST("Damage calculation matches Gen5+ (Muscle Band, crit)") +{ + s16 dmg; + s16 expectedDamage; + PARAMETRIZE { expectedDamage = 324; } + PARAMETRIZE { expectedDamage = 316; } + PARAMETRIZE { expectedDamage = 312; } + PARAMETRIZE { expectedDamage = 312; } + PARAMETRIZE { expectedDamage = 304; } + PARAMETRIZE { expectedDamage = 304; } + PARAMETRIZE { expectedDamage = 300; } + PARAMETRIZE { expectedDamage = 300; } + PARAMETRIZE { expectedDamage = 292; } + PARAMETRIZE { expectedDamage = 292; } + PARAMETRIZE { expectedDamage = 288; } + PARAMETRIZE { expectedDamage = 288; } + PARAMETRIZE { expectedDamage = 280; } + PARAMETRIZE { expectedDamage = 276; } + PARAMETRIZE { expectedDamage = 276; } + PARAMETRIZE { expectedDamage = 268; } + GIVEN { + PLAYER(SPECIES_GLACEON) { Level(75); Attack(123); Item(ITEM_MUSCLE_BAND); } + OPPONENT(SPECIES_GARCHOMP) { Defense(163); } + } WHEN { + TURN { + MOVE(player, MOVE_ICE_FANG, WITH_RNG(RNG_DAMAGE_MODIFIER, i), criticalHit: TRUE); + } + } + SCENE{ + MESSAGE("Glaceon used Ice Fang!"); + HP_BAR(opponent, captureDamage: &dmg); + } + THEN{ + EXPECT_EQ(expectedDamage, dmg); + } +} diff --git a/test/hold_effect_berserk_gene.c b/test/hold_effect_berserk_gene.c index 8a473e8da..981881e74 100644 --- a/test/hold_effect_berserk_gene.c +++ b/test/hold_effect_berserk_gene.c @@ -112,7 +112,7 @@ SINGLE_BATTLE_TEST("Berserk Gene does not confuse when Safeguard is active") } SCENE { ANIMATION(ANIM_TYPE_GENERAL, B_ANIM_STATS_CHANGE, player); MESSAGE("Using Berserk Gene, the Attack of Wobbuffet sharply rose!"); - MESSAGE("Wobbuffet's party is protected by SAFEGUARD!"); + MESSAGE("Wobbuffet's party is protected by Safeguard!"); NOT MESSAGE("Wobbuffet became confused!"); } } diff --git a/test/move_effect_court_change.c b/test/move_effect_court_change.c new file mode 100644 index 000000000..9d03efddf --- /dev/null +++ b/test/move_effect_court_change.c @@ -0,0 +1,153 @@ +#include "global.h" +#include "test_battle.h" + +ASSUMPTIONS +{ + ASSUME(gBattleMoves[MOVE_COURT_CHANGE].effect == EFFECT_COURT_CHANGE); +} + +DOUBLE_BATTLE_TEST("Court Change swaps entry hazards used by the opponent") +{ + GIVEN { + PLAYER(SPECIES_WYNAUT); + PLAYER(SPECIES_WYNAUT); + PLAYER(SPECIES_WYNAUT); + OPPONENT(SPECIES_WOBBUFFET); + OPPONENT(SPECIES_WOBBUFFET); + OPPONENT(SPECIES_WOBBUFFET); + } WHEN { + TURN { MOVE(opponentLeft, MOVE_STICKY_WEB); MOVE(opponentRight, MOVE_STEALTH_ROCK); } + TURN { MOVE(opponentLeft, MOVE_SPIKES); MOVE(opponentRight, MOVE_TOXIC_SPIKES); } + TURN { MOVE(playerLeft, MOVE_COURT_CHANGE); } + TURN { SWITCH(playerLeft, 2); SWITCH(opponentLeft, 2); } + } SCENE { + MESSAGE("Foe Wobbuffet used Sticky Web!"); + MESSAGE("Foe Wobbuffet used Stealth Rock!"); + MESSAGE("Foe Wobbuffet used Spikes!"); + MESSAGE("Foe Wobbuffet used Toxic Spikes!"); + MESSAGE("Wynaut used Court Change!"); + MESSAGE("Wynaut swapped the battle effects affecting each side!"); + MESSAGE("Go! Wynaut!"); + NONE_OF { + MESSAGE("Wynaut is hurt by spikes!"); + MESSAGE("Pointed stones dug into Wynaut!"); + MESSAGE("Wynaut was poisoned!"); + MESSAGE("Wynaut was caught in a Sticky Web!"); + } + MESSAGE("2 sent out Wobbuffet!"); + MESSAGE("Foe Wobbuffet is hurt by spikes!"); + MESSAGE("Pointed stones dug into Foe Wobbuffet!"); + MESSAGE("Foe Wobbuffet was poisoned!"); + MESSAGE("Foe Wobbuffet was caught in a Sticky Web!"); + } +} + +DOUBLE_BATTLE_TEST("Court Change swaps entry hazards used by the player") +{ + GIVEN { + PLAYER(SPECIES_WOBBUFFET); + PLAYER(SPECIES_WOBBUFFET); + PLAYER(SPECIES_WOBBUFFET); + OPPONENT(SPECIES_WYNAUT); + OPPONENT(SPECIES_WYNAUT); + OPPONENT(SPECIES_WYNAUT); + } WHEN { + TURN { MOVE(playerLeft, MOVE_STICKY_WEB); MOVE(playerRight, MOVE_STEALTH_ROCK); } + TURN { MOVE(playerLeft, MOVE_SPIKES); MOVE(playerRight, MOVE_TOXIC_SPIKES); } + TURN { MOVE(opponentLeft, MOVE_COURT_CHANGE); } + TURN { SWITCH(opponentLeft, 2); SWITCH(playerLeft, 2); } + } SCENE { + MESSAGE("Wobbuffet used Sticky Web!"); + MESSAGE("Wobbuffet used Stealth Rock!"); + MESSAGE("Wobbuffet used Spikes!"); + MESSAGE("Wobbuffet used Toxic Spikes!"); + MESSAGE("Foe Wynaut used Court Change!"); + MESSAGE("Foe Wynaut swapped the battle effects affecting each side!"); + MESSAGE("Go! Wobbuffet!"); + MESSAGE("Wobbuffet is hurt by spikes!"); + MESSAGE("Pointed stones dug into Wobbuffet!"); + MESSAGE("Wobbuffet was poisoned!"); + MESSAGE("Wobbuffet was caught in a Sticky Web!"); + MESSAGE("2 sent out Wynaut!"); + NONE_OF { + MESSAGE("Foe Wynaut is hurt by spikes!"); + MESSAGE("Pointed stones dug into Foe Wynaut!"); + MESSAGE("Foe Wynaut was poisoned!"); + MESSAGE("Foe Wynaut was caught in a Sticky Web!"); + } + } +} + +DOUBLE_BATTLE_TEST("Court Change used by the player swaps Mist, Safeguard, Lucky Chant, Reflect, Light Screen, Tailwind") +{ + GIVEN { + PLAYER(SPECIES_WYNAUT); + PLAYER(SPECIES_WYNAUT); + PLAYER(SPECIES_WYNAUT); + OPPONENT(SPECIES_WOBBUFFET); + OPPONENT(SPECIES_WOBBUFFET); + OPPONENT(SPECIES_WOBBUFFET); + } WHEN { + TURN { MOVE(opponentLeft, MOVE_MIST); MOVE(opponentRight, MOVE_SAFEGUARD); } + TURN { MOVE(opponentLeft, MOVE_LUCKY_CHANT); MOVE(opponentRight, MOVE_REFLECT); } + TURN { MOVE(opponentLeft, MOVE_LIGHT_SCREEN); MOVE(opponentRight, MOVE_TAILWIND); } + TURN { MOVE(playerLeft, MOVE_COURT_CHANGE); } + TURN { } + TURN { } + TURN { } + TURN { } + } SCENE { + MESSAGE("Foe Wobbuffet used Mist!"); + MESSAGE("Foe Wobbuffet used Safeguard!"); + MESSAGE("Foe Wobbuffet used Lucky Chant!"); + MESSAGE("Foe Wobbuffet used Reflect!"); + MESSAGE("Foe Wobbuffet used Light Screen!"); + MESSAGE("Foe Wobbuffet used Tailwind!"); + MESSAGE("Wynaut used Court Change!"); + MESSAGE("Wynaut swapped the battle effects affecting each side!"); + // The effects now end for the player side. + MESSAGE("Ally's Mist wore off!"); + MESSAGE("Ally's party is no longer protected by Safeguard!"); + MESSAGE("Ally's Reflect wore off!"); + MESSAGE("Your team's Lucky Chant wore off!"); + MESSAGE("Your team's tailwind petered out!"); + MESSAGE("Ally's Light Screen wore off!"); + } +} + +DOUBLE_BATTLE_TEST("Court Change used by the opponent swaps Mist, Safeguard, Lucky Chant, Reflect, Light Screen, Tailwind") +{ + GIVEN { + PLAYER(SPECIES_WOBBUFFET); + PLAYER(SPECIES_WOBBUFFET); + PLAYER(SPECIES_WOBBUFFET); + OPPONENT(SPECIES_WYNAUT); + OPPONENT(SPECIES_WYNAUT); + OPPONENT(SPECIES_WYNAUT); + } WHEN { + TURN { MOVE(playerLeft, MOVE_MIST); MOVE(playerRight, MOVE_SAFEGUARD); } + TURN { MOVE(playerLeft, MOVE_LUCKY_CHANT); MOVE(playerRight, MOVE_REFLECT); } + TURN { MOVE(playerLeft, MOVE_LIGHT_SCREEN); MOVE(playerRight, MOVE_TAILWIND); } + TURN { MOVE(opponentLeft, MOVE_COURT_CHANGE); } + TURN { } + TURN { } + TURN { } + TURN { } + } SCENE { + MESSAGE("Wobbuffet used Mist!"); + MESSAGE("Wobbuffet used Safeguard!"); + MESSAGE("Wobbuffet used Lucky Chant!"); + MESSAGE("Wobbuffet used Reflect!"); + MESSAGE("Wobbuffet used Light Screen!"); + MESSAGE("Wobbuffet used Tailwind!"); + MESSAGE("Foe Wynaut used Court Change!"); + MESSAGE("Foe Wynaut swapped the battle effects affecting each side!"); + // The effects now end for the player side. + MESSAGE("Foe's Mist wore off!"); + MESSAGE("Foe's party is no longer protected by Safeguard!"); + MESSAGE("Foe's Reflect wore off!"); + MESSAGE("The opposing team's Lucky Chant wore off!"); + MESSAGE("The opposing team's tailwind petered out!"); + MESSAGE("Foe's Light Screen wore off!"); + } +} diff --git a/test/move_effect_defog.c b/test/move_effect_defog.c index b55d5eab6..055f3f61a 100644 --- a/test/move_effect_defog.c +++ b/test/move_effect_defog.c @@ -125,7 +125,7 @@ DOUBLE_BATTLE_TEST("Defog lowers evasiveness by 1 and removes Mist and Safeguard STATUS_ICON(opponentRight, badPoison: TRUE); } else { - MESSAGE("Foe Wobbuffet's party is protected by SAFEGUARD!"); + MESSAGE("Foe Wobbuffet's party is protected by Safeguard!"); NOT STATUS_ICON(opponentRight, badPoison: TRUE); } } diff --git a/test/move_effect_recoil_if_miss.c b/test/move_effect_recoil_if_miss.c index 471c8a262..26ddee622 100644 --- a/test/move_effect_recoil_if_miss.c +++ b/test/move_effect_recoil_if_miss.c @@ -8,7 +8,6 @@ ASSUMPTIONS SINGLE_BATTLE_TEST("Jump Kick has 50% recoil on miss") { - s16 recoil; GIVEN { PLAYER(SPECIES_WOBBUFFET); OPPONENT(SPECIES_WOBBUFFET); @@ -25,7 +24,6 @@ SINGLE_BATTLE_TEST("Jump Kick has 50% recoil on miss") SINGLE_BATTLE_TEST("Jump Kick has 50% recoil on protect") { - s16 recoil; GIVEN { ASSUME(!gBattleMoves[MOVE_JUMP_KICK].ignoresProtect); PLAYER(SPECIES_WOBBUFFET); @@ -55,3 +53,48 @@ SINGLE_BATTLE_TEST("Jump Kick has no recoil if no target") NOT HP_BAR(player, damage: maxHP / 2); } } + +SINGLE_BATTLE_TEST("Jump Kick's recoil happens after Spiky Shield damage and Pokemon can faint from either of these") +{ + s16 hp, maxHp = 256; + bool32 faintOnSpiky = FALSE, faintOnJumpKick = FALSE; + + PARAMETRIZE { hp = maxHp; } + PARAMETRIZE { hp = maxHp / 2; faintOnJumpKick = TRUE; } // Faints after Jump Kick's recoil + PARAMETRIZE { hp = maxHp / 8; faintOnSpiky = TRUE; } // Faints after Spiky Shield's recoil + + GIVEN { + ASSUME(gBattleMoves[MOVE_SPIKY_SHIELD].effect == EFFECT_PROTECT); + PLAYER(SPECIES_WOBBUFFET) { HP(hp); MaxHP(maxHp); } + PLAYER(SPECIES_WYNAUT); + OPPONENT(SPECIES_WOBBUFFET); + } WHEN { + if (!faintOnJumpKick && !faintOnSpiky) { + TURN { MOVE(opponent, MOVE_SPIKY_SHIELD); MOVE(player, MOVE_JUMP_KICK, hit: FALSE); } + } else { + TURN { MOVE(opponent, MOVE_SPIKY_SHIELD); MOVE(player, MOVE_JUMP_KICK, hit: FALSE); SEND_OUT(player, 1); } + } + TURN { ; } + } SCENE { + ANIMATION(ANIM_TYPE_MOVE, MOVE_SPIKY_SHIELD, opponent); + MESSAGE("Wobbuffet used Jump Kick!"); + MESSAGE("Foe Wobbuffet protected itself!"); + HP_BAR(player, damage: maxHp / 8); + MESSAGE("Wobbuffet was hurt by Foe Wobbuffet's Spiky Shield!"); + if (faintOnSpiky){ + MESSAGE("Wobbuffet fainted!"); + MESSAGE("Go! Wynaut!"); + NONE_OF { + MESSAGE("Wobbuffet kept going and crashed!"); + HP_BAR(player); + } + } else { + MESSAGE("Wobbuffet kept going and crashed!"); + HP_BAR(player); + if (faintOnJumpKick) { + MESSAGE("Wobbuffet fainted!"); + MESSAGE("Go! Wynaut!"); + } + } + } +} diff --git a/test/move_effect_sticky_web.c b/test/move_effect_sticky_web.c new file mode 100644 index 000000000..f0a6eb8d8 --- /dev/null +++ b/test/move_effect_sticky_web.c @@ -0,0 +1,237 @@ +#include "global.h" +#include "test_battle.h" + +ASSUMPTIONS +{ + ASSUME(gBattleMoves[MOVE_STICKY_WEB].effect == EFFECT_STICKY_WEB); +} + +SINGLE_BATTLE_TEST("Sticky Web lowers Speed by 1 on switch-in") +{ + GIVEN { + PLAYER(SPECIES_WOBBUFFET); + OPPONENT(SPECIES_WOBBUFFET); + OPPONENT(SPECIES_WYNAUT); + } WHEN { + TURN { MOVE(player, MOVE_STICKY_WEB); } + TURN { SWITCH(opponent, 1); } + TURN {} + } SCENE { + ANIMATION(ANIM_TYPE_MOVE, MOVE_STICKY_WEB, player); + MESSAGE("A sticky web spreads out on the ground around the opposing team!"); + MESSAGE("2 sent out Wynaut!"); + MESSAGE("Foe Wynaut was caught in a Sticky Web!"); + ANIMATION(ANIM_TYPE_GENERAL, B_ANIM_STATS_CHANGE, opponent); + MESSAGE("Foe Wynaut's Speed fell!"); + } +} + +SINGLE_BATTLE_TEST("Sticky Web can only be set up 1 time") +{ + GIVEN { + PLAYER(SPECIES_WOBBUFFET); + OPPONENT(SPECIES_WOBBUFFET); + } WHEN { + TURN { MOVE(player, MOVE_STICKY_WEB); } + TURN { MOVE(player, MOVE_STICKY_WEB); } + } SCENE { + MESSAGE("Wobbuffet used Sticky Web!"); + ANIMATION(ANIM_TYPE_MOVE, MOVE_STICKY_WEB, player); + MESSAGE("A sticky web spreads out on the ground around the opposing team!"); + + MESSAGE("Wobbuffet used Sticky Web!"); + NOT ANIMATION(ANIM_TYPE_MOVE, MOVE_STICKY_WEB, player); + MESSAGE("But it failed!"); + } +} + + +DOUBLE_BATTLE_TEST("Sticky Web lowers Speed by 1 in a double battle after Explosion fainting both mons") +{ + GIVEN { + ASSUME(gBattleMoves[MOVE_EXPLOSION].effect == EFFECT_EXPLOSION); + PLAYER(SPECIES_WOBBUFFET) {Speed(5);} + PLAYER(SPECIES_WOBBUFFET) {HP(1500); Speed(10);} + PLAYER(SPECIES_WOBBUFFET) {Speed(10);} + OPPONENT(SPECIES_WOBBUFFET) {HP(1); Speed(1);} + OPPONENT(SPECIES_WOBBUFFET) {HP(1); Speed(1);} + OPPONENT(SPECIES_WYNAUT) {Speed(10);} + OPPONENT(SPECIES_WYNAUT) {Speed(10);} + } WHEN { + TURN { MOVE(playerRight, MOVE_STICKY_WEB); MOVE(playerLeft, MOVE_EXPLOSION); SEND_OUT(playerLeft, 2); SEND_OUT(opponentLeft, 2); SEND_OUT(opponentRight, 3); } + TURN {} + } SCENE { + ANIMATION(ANIM_TYPE_MOVE, MOVE_STICKY_WEB, playerRight); + MESSAGE("A sticky web spreads out on the ground around the opposing team!"); + ANIMATION(ANIM_TYPE_MOVE, MOVE_EXPLOSION, playerLeft); + MESSAGE("2 sent out Wynaut!"); + MESSAGE("Foe Wynaut was caught in a Sticky Web!"); + ANIMATION(ANIM_TYPE_GENERAL, B_ANIM_STATS_CHANGE, opponentLeft); + MESSAGE("Foe Wynaut's Speed fell!"); + MESSAGE("2 sent out Wynaut!"); + MESSAGE("Foe Wynaut was caught in a Sticky Web!"); + ANIMATION(ANIM_TYPE_GENERAL, B_ANIM_STATS_CHANGE, opponentRight); + MESSAGE("Foe Wynaut's Speed fell!"); + } +} + +SINGLE_BATTLE_TEST("Sticky Web raises Speed by 1 for a Pokemon with Contrary") +{ + GIVEN { + PLAYER(SPECIES_WOBBUFFET); + OPPONENT(SPECIES_WOBBUFFET); + OPPONENT(SPECIES_SHUCKLE) { Ability(ABILITY_CONTRARY); } + } WHEN { + TURN { MOVE(player, MOVE_STICKY_WEB); } + TURN { SWITCH(opponent, 1); } + TURN {} + } SCENE { + ANIMATION(ANIM_TYPE_MOVE, MOVE_STICKY_WEB, player); + MESSAGE("A sticky web spreads out on the ground around the opposing team!"); + MESSAGE("2 sent out Shuckle!"); + MESSAGE("Foe Shuckle was caught in a Sticky Web!"); + ANIMATION(ANIM_TYPE_GENERAL, B_ANIM_STATS_CHANGE, opponent); + MESSAGE("Foe Shuckle's Speed rose!"); + } +} + +#define BATTLER_OPPONENT (opponentSetUpper == 0 ? opponentLeft : opponentRight) +#define BATTLER_PLAYER (playerSetUpper == 0 ? playerLeft : playerRight) + +DOUBLE_BATTLE_TEST("Sticky Web has correct interactions with Mirror Armor - the battler which set up Sticky Web has its Speed lowered instead") +{ + u8 playerSetUpper, opponentSetUpper; // 0 left, 1 right + + PARAMETRIZE {playerSetUpper = 0; opponentSetUpper = 0; } + PARAMETRIZE {playerSetUpper = 0; opponentSetUpper = 1; } + PARAMETRIZE {playerSetUpper = 1; opponentSetUpper = 0; } + PARAMETRIZE {playerSetUpper = 1; opponentSetUpper = 1; } + + GIVEN { + ASSUME(P_GEN_8_POKEMON == TRUE); + PLAYER(SPECIES_SQUIRTLE); + PLAYER(SPECIES_CHARMANDER); + PLAYER(SPECIES_CORVIKNIGHT) { Ability(ABILITY_MIRROR_ARMOR); Item(ITEM_IRON_BALL); } // Iron Ball, so that flying type Corviknight is affected by Sticky Web. + OPPONENT(SPECIES_CATERPIE); + OPPONENT(SPECIES_WEEDLE); + } WHEN { + TURN { MOVE(BATTLER_OPPONENT, MOVE_STICKY_WEB); } + TURN { MOVE(BATTLER_PLAYER, MOVE_STICKY_WEB); } + TURN { SWITCH(playerRight, 2); } + } SCENE { + ANIMATION(ANIM_TYPE_MOVE, MOVE_STICKY_WEB, BATTLER_OPPONENT); + MESSAGE("A sticky web spreads out on the ground around your team!"); + ANIMATION(ANIM_TYPE_MOVE, MOVE_STICKY_WEB, BATTLER_PLAYER); + MESSAGE("A sticky web spreads out on the ground around the opposing team!"); + + MESSAGE("Go! Corviknigh!"); + MESSAGE("Corviknigh was caught in a Sticky Web!"); + ABILITY_POPUP(playerRight, ABILITY_MIRROR_ARMOR); + ANIMATION(ANIM_TYPE_GENERAL, B_ANIM_STATS_CHANGE, BATTLER_OPPONENT); + if (opponentSetUpper == 0) { + MESSAGE("Foe Caterpie's Speed fell!"); + } else { + MESSAGE("Foe Weedle's Speed fell!"); + } + } +} + +#undef BATTLER_OPPONENT +#undef BATTLER_PLAYER + +DOUBLE_BATTLE_TEST("Sticky Web has correct interactions with Mirror Armor - no one has their Speed lowered if the set upper switched") +{ + u16 speedPlayer, speedOpponent; + + // We need to make sure Sticky Web user saves for both sides, so it doesn't matter who sets it first. + PARAMETRIZE { speedPlayer = 5; speedOpponent = 10; } + PARAMETRIZE { speedPlayer = 10; speedOpponent = 5; } + + GIVEN { + ASSUME(P_GEN_8_POKEMON == TRUE); + PLAYER(SPECIES_SQUIRTLE) { Speed(speedPlayer); } + PLAYER(SPECIES_CHARMANDER) { Speed(speedPlayer); } + PLAYER(SPECIES_CORVIKNIGHT) { Ability(ABILITY_MIRROR_ARMOR); Item(ITEM_IRON_BALL); Speed(speedOpponent); } // Iron Ball, so that flying type Corviknight is affected by Sticky Web. + OPPONENT(SPECIES_CATERPIE) { Speed(speedOpponent); } + OPPONENT(SPECIES_WEEDLE) { Speed(speedOpponent); } + OPPONENT(SPECIES_PIDGEY) { Speed(speedOpponent); } // Flying type,so not affected by Sticky Web. + } WHEN { + TURN { MOVE(opponentLeft, MOVE_STICKY_WEB); MOVE(playerRight, MOVE_STICKY_WEB); } + TURN { SWITCH(opponentLeft, 2); } + TURN { SWITCH(playerRight, 2); } + } SCENE { + if (speedPlayer > speedOpponent) { + ANIMATION(ANIM_TYPE_MOVE, MOVE_STICKY_WEB, playerRight); + MESSAGE("A sticky web spreads out on the ground around the opposing team!"); + ANIMATION(ANIM_TYPE_MOVE, MOVE_STICKY_WEB, opponentLeft); + MESSAGE("A sticky web spreads out on the ground around your team!"); + } else { + ANIMATION(ANIM_TYPE_MOVE, MOVE_STICKY_WEB, opponentLeft); + MESSAGE("A sticky web spreads out on the ground around your team!"); + ANIMATION(ANIM_TYPE_MOVE, MOVE_STICKY_WEB, playerRight); + MESSAGE("A sticky web spreads out on the ground around the opposing team!"); + } + + MESSAGE("Go! Corviknigh!"); + MESSAGE("Corviknigh was caught in a Sticky Web!"); + ABILITY_POPUP(playerRight, ABILITY_MIRROR_ARMOR); + NOT ANIMATION(ANIM_TYPE_GENERAL, B_ANIM_STATS_CHANGE, opponentLeft); + } THEN { + EXPECT_EQ(playerLeft->statStages[STAT_SPEED], DEFAULT_STAT_STAGE); + EXPECT_EQ(playerRight->statStages[STAT_SPEED], DEFAULT_STAT_STAGE); + EXPECT_EQ(opponentLeft->statStages[STAT_SPEED], DEFAULT_STAT_STAGE); + EXPECT_EQ(opponentRight->statStages[STAT_SPEED], DEFAULT_STAT_STAGE); + } +} + +DOUBLE_BATTLE_TEST("Sticky Web has correct interactions with Mirror Armor - no one has their Speed lowered if the set upper fainted") +{ + bool8 hasReplacement; + + // We need to make sure Sticky Web user saves for both sides, so it doesn't matter who sets it first. + PARAMETRIZE {hasReplacement = TRUE;} + PARAMETRIZE {hasReplacement = FALSE;} + + GIVEN { + ASSUME(P_GEN_8_POKEMON == TRUE); + ASSUME(gBattleMoves[MOVE_MEMENTO].effect == EFFECT_MEMENTO); + PLAYER(SPECIES_SQUIRTLE) {Speed(5); } + PLAYER(SPECIES_CHARMANDER) {Speed(5); } + PLAYER(SPECIES_CORVIKNIGHT) {Ability(ABILITY_MIRROR_ARMOR); Item(ITEM_IRON_BALL); Speed(5); } // Iron Ball, so that flying type Corviknight is affected by Sticky Web. + OPPONENT(SPECIES_CATERPIE) {Speed(7); } + OPPONENT(SPECIES_WEEDLE) {Speed(7); } + if (hasReplacement) { + OPPONENT(SPECIES_PIDGEY) {Speed(7); } + } + + } WHEN { + TURN { MOVE(opponentLeft, MOVE_STICKY_WEB); } + if (hasReplacement) { + TURN { MOVE(opponentLeft, MOVE_MEMENTO, target:playerLeft); SEND_OUT(opponentLeft, 2); } + } else { + TURN { MOVE(opponentLeft, MOVE_MEMENTO, target:playerLeft);} + } + TURN { SWITCH(playerRight, 2); } + } SCENE { + ANIMATION(ANIM_TYPE_MOVE, MOVE_STICKY_WEB, opponentLeft); + MESSAGE("A sticky web spreads out on the ground around your team!"); + + ANIMATION(ANIM_TYPE_MOVE, MOVE_MEMENTO, opponentLeft); + MESSAGE("Foe Caterpie fainted!"); + if (hasReplacement) { + MESSAGE("2 sent out Pidgey!"); + } + + MESSAGE("Go! Corviknigh!"); + MESSAGE("Corviknigh was caught in a Sticky Web!"); + ABILITY_POPUP(playerRight, ABILITY_MIRROR_ARMOR); + NOT ANIMATION(ANIM_TYPE_GENERAL, B_ANIM_STATS_CHANGE, opponentLeft); + } THEN { + if (hasReplacement) { + EXPECT_EQ(opponentLeft->statStages[STAT_SPEED], DEFAULT_STAT_STAGE); + } + EXPECT_EQ(playerLeft->statStages[STAT_SPEED], DEFAULT_STAT_STAGE); + EXPECT_EQ(playerRight->statStages[STAT_SPEED], DEFAULT_STAT_STAGE); + EXPECT_EQ(opponentRight->statStages[STAT_SPEED], DEFAULT_STAT_STAGE); + } +} diff --git a/test/move_effect_teleport.c b/test/move_effect_teleport.c new file mode 100644 index 000000000..9c8a16d4b --- /dev/null +++ b/test/move_effect_teleport.c @@ -0,0 +1,61 @@ +#include "global.h" +#include "test_battle.h" + +ASSUMPTIONS +{ + ASSUME(gBattleMoves[MOVE_TELEPORT].effect == EFFECT_TELEPORT); +} + +SINGLE_BATTLE_TEST("Teleport fails when there is no pokemon to switch in") +{ + GIVEN { + PLAYER(SPECIES_WOBBUFFET); + OPPONENT(SPECIES_WOBBUFFET); + } WHEN { + TURN { MOVE(opponent, MOVE_TELEPORT); } + } SCENE { + MESSAGE("But it failed!"); + } +} + +SINGLE_BATTLE_TEST("Teleport fails when there no alive pokemon left") +{ + GIVEN { + PLAYER(SPECIES_WOBBUFFET); + OPPONENT(SPECIES_WOBBUFFET); + OPPONENT(SPECIES_WYNAUT) { HP(0); } + } WHEN { + TURN { MOVE(opponent, MOVE_TELEPORT); } + } SCENE { + MESSAGE("But it failed!"); + } +} + +SINGLE_BATTLE_TEST("Teleport forces the pokemon to switch out") +{ + GIVEN { + PLAYER(SPECIES_WOBBUFFET); + OPPONENT(SPECIES_WOBBUFFET); + OPPONENT(SPECIES_WYNAUT); + } WHEN { + TURN { MOVE(opponent, MOVE_TELEPORT); SEND_OUT(opponent, 1); } + } SCENE { + ANIMATION(ANIM_TYPE_MOVE, MOVE_TELEPORT, opponent); + MESSAGE("2 sent out Wynaut!"); + } +} + +SINGLE_BATTLE_TEST("Teleport does not fail if the user is trapped") +{ + GIVEN { + PLAYER(SPECIES_WOBBUFFET); + OPPONENT(SPECIES_WOBBUFFET); + OPPONENT(SPECIES_WYNAUT); + } WHEN { + TURN { MOVE(player, MOVE_FIRE_SPIN); MOVE(opponent, MOVE_TELEPORT); SEND_OUT(opponent, 1); } + } SCENE { + ANIMATION(ANIM_TYPE_MOVE, MOVE_FIRE_SPIN, player); + ANIMATION(ANIM_TYPE_MOVE, MOVE_TELEPORT, opponent); + MESSAGE("2 sent out Wynaut!"); + } +} diff --git a/test/status3.c b/test/status3.c new file mode 100644 index 000000000..0331883bd --- /dev/null +++ b/test/status3.c @@ -0,0 +1,90 @@ +#include "global.h" +#include "test_battle.h" + +ASSUMPTIONS { + ASSUME(gBattleMoves[MOVE_MINIMIZE].effect == EFFECT_MINIMIZE); + ASSUME(gBattleMoves[MOVE_STEAMROLLER].minimizeDoubleDamage); + ASSUME(gBattleMoves[MOVE_EARTHQUAKE].damagesUnderground); + ASSUME(gBattleMoves[MOVE_SURF].damagesUnderwater); + ASSUME(gBattleMoves[MOVE_TWISTER].damagesAirborneDoubleDamage); +} + +SINGLE_BATTLE_TEST("Minimize causes the target to take double damage from certain moves", s16 damage) +{ + bool32 useMinimize; + PARAMETRIZE { useMinimize = FALSE; } + PARAMETRIZE { useMinimize = TRUE; } + GIVEN { + PLAYER(SPECIES_WOBBUFFET) { Speed(1); } + OPPONENT(SPECIES_WOBBUFFET) { Speed(2); } + } WHEN { + if (useMinimize) + TURN { MOVE(opponent, MOVE_MINIMIZE); MOVE(player, MOVE_STEAMROLLER); } + else + TURN { MOVE(player, MOVE_STEAMROLLER); } + } SCENE { + HP_BAR(opponent, captureDamage: &results[i].damage); + } FINALLY { + EXPECT_MUL_EQ(results[0].damage, UQ_4_12(2.0), results[1].damage); + } +} + +SINGLE_BATTLE_TEST("Being underground causes the target to take double damage from certain moves", s16 damage) +{ + bool32 useDig; + PARAMETRIZE { useDig = FALSE; } + PARAMETRIZE { useDig = TRUE; } + GIVEN { + PLAYER(SPECIES_WOBBUFFET) { Speed(1); } + OPPONENT(SPECIES_WOBBUFFET) { Speed(2); } + } WHEN { + if (useDig) + TURN { MOVE(opponent, MOVE_DIG); MOVE(player, MOVE_EARTHQUAKE); } + else + TURN { MOVE(player, MOVE_EARTHQUAKE); } + } SCENE { + HP_BAR(opponent, captureDamage: &results[i].damage); + } FINALLY { + EXPECT_MUL_EQ(results[0].damage, UQ_4_12(2.0), results[1].damage); + } +} + +SINGLE_BATTLE_TEST("Being underwater causes the target to take double damage from certain moves", s16 damage) +{ + bool32 useDive; + PARAMETRIZE { useDive = FALSE; } + PARAMETRIZE { useDive = TRUE; } + GIVEN { + PLAYER(SPECIES_WOBBUFFET) { Speed(1); } + OPPONENT(SPECIES_WOBBUFFET) { Speed(2); } + } WHEN { + if (useDive) + TURN { MOVE(opponent, MOVE_DIVE); MOVE(player, MOVE_SURF); } + else + TURN { MOVE(player, MOVE_SURF); } + } SCENE { + HP_BAR(opponent, captureDamage: &results[i].damage); + } FINALLY { + EXPECT_MUL_EQ(results[0].damage, UQ_4_12(2.0), results[1].damage); + } +} + +SINGLE_BATTLE_TEST("Being airborne causes the target to take double damage from certain moves", s16 damage) +{ + bool32 useDive; + PARAMETRIZE { useDive = FALSE; } + PARAMETRIZE { useDive = TRUE; } + GIVEN { + PLAYER(SPECIES_WOBBUFFET) { Speed(1); } + OPPONENT(SPECIES_WOBBUFFET) { Speed(2); } + } WHEN { + if (useDive) + TURN { MOVE(opponent, MOVE_FLY); MOVE(player, MOVE_TWISTER); } + else + TURN { MOVE(player, MOVE_TWISTER); } + } SCENE { + HP_BAR(opponent, captureDamage: &results[i].damage); + } FINALLY { + EXPECT_MUL_EQ(results[0].damage, UQ_4_12(2.0), results[1].damage); + } +} diff --git a/test/test_runner.c b/test/test_runner.c index d91ad02d1..3f1352e5a 100644 --- a/test/test_runner.c +++ b/test/test_runner.c @@ -2,6 +2,7 @@ #include "global.h" #include "characters.h" #include "gpu_regs.h" +#include "load_save.h" #include "main.h" #include "malloc.h" #include "random.h" @@ -114,6 +115,10 @@ void CB2_TestRunner(void) return; } + MoveSaveBlocks_ResetHeap(); + ClearSav1(); + ClearSav2(); + gIntrTable[7] = Intr_Timer2; // The current test restarted the ROM (e.g. by jumping to NULL). diff --git a/test/weather_snow.c b/test/weather_snow.c index 93deb1432..bee4759eb 100644 --- a/test/weather_snow.c +++ b/test/weather_snow.c @@ -81,7 +81,6 @@ SINGLE_BATTLE_TEST("Snow halves the power of Solar Beam", s16 damage) SINGLE_BATTLE_TEST("Snow halves the power of Solar Blade", s16 damage) { u16 move; - KNOWN_FAILING; // fails bc the bp of solar blade gets rounded up which leads to slightly incorrect calcs down the line PARAMETRIZE{ move = MOVE_CELEBRATE; } PARAMETRIZE{ move = MOVE_SNOWSCAPE; } GIVEN {