diff --git a/docs/changelogs/1.9.x/1.9.0.md b/docs/changelogs/1.9.x/1.9.0.md index 16947711d3e2..0d39f109ed69 100644 --- a/docs/changelogs/1.9.x/1.9.0.md +++ b/docs/changelogs/1.9.x/1.9.0.md @@ -43,7 +43,7 @@ * Migration script available in `migration_scripts/egg_move_refactor.py` by @AlexOn1ine in https://github.com/rh-hideout/pokeemerald-expansion/pull/5040 * Added documentation to `STATIC_ASSERTS` used by the `BoxPokemon` after 1.8.0 by @pkmnsnfrn in https://github.com/rh-hideout/pokeemerald-expansion/pull/4294 * #### Competitive-formatted parties by @mrgriffin in https://github.com/rh-hideout/pokeemerald-expansion/pull/3545 - * Can be disabled by setting `COMPETITIVE_PARTY_SYNTAX` to `FALSE` in `include/config/general.h`. + * Can be disabled by setting `COMPETITIVE_PARTY_SYNTAX` to `FALSE` in `include/config/general.h`. If migrating from 1.8, remove the first and last lines from `src/data/trainers.h` (`const struct Trainer gTrainers[] = {` and `};` respectively). * Introduces `trainerproc`, a tool which converts Competitive-formatted parties into Trainer Control-formatted parties. * If you made custom changes to the following files and want to use this new format, ***Do not accept the incoming changes for them.*** Instead, use the migration script present in `migration_scripts/convert_parties.py`: - `src/data/trainers.h` diff --git a/graphics/pokemon/urshifu/front.png b/graphics/pokemon/urshifu/front.png index cbb462be2ea0..468ba31f7090 100644 Binary files a/graphics/pokemon/urshifu/front.png and b/graphics/pokemon/urshifu/front.png differ diff --git a/graphics/pokemon/urshifu/rapid_strike_style/front.png b/graphics/pokemon/urshifu/rapid_strike_style/front.png index 09d72971424b..d06637f4e173 100644 Binary files a/graphics/pokemon/urshifu/rapid_strike_style/front.png and b/graphics/pokemon/urshifu/rapid_strike_style/front.png differ diff --git a/include/battle.h b/include/battle.h index c48dc7883504..1aa26a9470be 100644 --- a/include/battle.h +++ b/include/battle.h @@ -795,6 +795,7 @@ struct BattleStruct u8 quickClawRandom[MAX_BATTLERS_COUNT]; u8 quickDrawRandom[MAX_BATTLERS_COUNT]; u8 shellSideArmCategory[MAX_BATTLERS_COUNT][MAX_BATTLERS_COUNT]; + u8 speedTieBreaks; // MAX_BATTLERS_COUNT! values. u8 boosterEnergyActivates; u8 distortedTypeMatchups; u8 categoryOverride; // for Z-Moves and Max Moves diff --git a/include/battle_main.h b/include/battle_main.h index 59a515b50843..eb0af5aa776a 100644 --- a/include/battle_main.h +++ b/include/battle_main.h @@ -75,6 +75,7 @@ s8 GetChosenMovePriority(u32 battlerId); s8 GetMovePriority(u32 battlerId, u16 move); s32 GetWhichBattlerFasterArgs(u32 battler1, u32 battler2, bool32 ignoreChosenMoves, u32 ability1, u32 ability2, u32 holdEffectBattler1, u32 holdEffectBattler2, u32 speedBattler1, u32 speedBattler2, s32 priority1, s32 priority2); +s32 GetWhichBattlerFasterOrTies(u32 battler1, u32 battler2, bool32 ignoreChosenMoves); s32 GetWhichBattlerFaster(u32 battler1, u32 battler2, bool32 ignoreChosenMoves); void RunBattleScriptCommands_PopCallbacksStack(void); void RunBattleScriptCommands(void); diff --git a/src/battle_ai_main.c b/src/battle_ai_main.c index 08b8e4335cec..63bab46ec92d 100644 --- a/src/battle_ai_main.c +++ b/src/battle_ai_main.c @@ -2722,7 +2722,7 @@ static s32 AI_TryToFaint(u32 battlerAtk, u32 battlerDef, u32 move, s32 score) ADJUST_SCORE(SLOW_KILL); } else if (CanTargetFaintAi(battlerDef, battlerAtk) - && GetWhichBattlerFaster(battlerAtk, battlerDef, TRUE) != AI_IS_FASTER + && GetWhichBattlerFasterOrTies(battlerAtk, battlerDef, TRUE) != AI_IS_FASTER && GetMovePriority(battlerAtk, move) > 0) { ADJUST_SCORE(LAST_CHANCE); @@ -4117,7 +4117,7 @@ static u32 AI_CalcMoveEffectScore(u32 battlerAtk, u32 battlerDef, u32 move) if (IsStatBoostingBerry(item) && aiData->hpPercents[battlerAtk] > 60) ADJUST_SCORE(WEAK_EFFECT); else if (ShouldRestoreHpBerry(battlerAtk, item) && !CanAIFaintTarget(battlerAtk, battlerDef, 0) - && ((GetWhichBattlerFaster(battlerAtk, battlerDef, TRUE) == 1 && CanTargetFaintAiWithMod(battlerDef, battlerAtk, 0, 0)) + && ((GetWhichBattlerFasterOrTies(battlerAtk, battlerDef, TRUE) == 1 && CanTargetFaintAiWithMod(battlerDef, battlerAtk, 0, 0)) || !CanTargetFaintAiWithMod(battlerDef, battlerAtk, toHeal, 0))) ADJUST_SCORE(WEAK_EFFECT); // Recycle healing berry if we can't otherwise faint the target and the target wont kill us after we activate the berry } diff --git a/src/battle_main.c b/src/battle_main.c index 2b95bad97563..9df19b9566cf 100644 --- a/src/battle_main.c +++ b/src/battle_main.c @@ -120,6 +120,7 @@ static void SpriteCB_UnusedBattleInit(struct Sprite *sprite); static void SpriteCB_UnusedBattleInit_Main(struct Sprite *sprite); static u32 Crc32B (const u8 *data, u32 size); static u32 GeneratePartyHash(const struct Trainer *trainer, u32 i); +static s32 Factorial(s32); EWRAM_DATA u16 gBattle_BG0_X = 0; EWRAM_DATA u16 gBattle_BG0_Y = 0; @@ -3807,6 +3808,8 @@ static void TryDoEventsBeforeFirstTurn(void) } #endif // TESTING + gBattleStruct->speedTieBreaks = RandomUniform(RNG_SPEED_TIE, 0, Factorial(MAX_BATTLERS_COUNT) - 1); + for (i = 0; i < gBattlersCount; i++) gBattlerByTurnOrder[i] = i; for (i = 0; i < gBattlersCount - 1; i++) @@ -3977,6 +3980,8 @@ void BattleTurnPassed(void) { s32 i; + gBattleStruct->speedTieBreaks = RandomUniform(RNG_SPEED_TIE, 0, Factorial(MAX_BATTLERS_COUNT) - 1); + TurnValuesCleanUp(TRUE); if (gBattleOutcome == 0) { @@ -4865,9 +4870,10 @@ s32 GetWhichBattlerFasterArgs(u32 battler1, u32 battler2, bool32 ignoreChosenMov strikesFirst = 1; else { - if (speedBattler1 == speedBattler2 && Random() & 1) + if (speedBattler1 == speedBattler2) { - strikesFirst = 0; // same speeds, same priorities + // same speeds, same priorities + strikesFirst = 0; } else if (speedBattler1 < speedBattler2) { @@ -4898,7 +4904,7 @@ s32 GetWhichBattlerFasterArgs(u32 battler1, u32 battler2, bool32 ignoreChosenMov return strikesFirst; } -s32 GetWhichBattlerFaster(u32 battler1, u32 battler2, bool32 ignoreChosenMoves) +s32 GetWhichBattlerFasterOrTies(u32 battler1, u32 battler2, bool32 ignoreChosenMoves) { s32 priority1 = 0, priority2 = 0; u32 ability1 = GetBattlerAbility(battler1); @@ -4916,8 +4922,60 @@ s32 GetWhichBattlerFaster(u32 battler1, u32 battler2, bool32 ignoreChosenMoves) priority2 = GetChosenMovePriority(battler2); } - return GetWhichBattlerFasterArgs(battler1, battler2, ignoreChosenMoves, ability1, ability2, - holdEffectBattler1, holdEffectBattler2, speedBattler1, speedBattler2, priority1, priority2); + return GetWhichBattlerFasterArgs( + battler1, battler2, + ignoreChosenMoves, + ability1, ability2, + holdEffectBattler1, holdEffectBattler2, + speedBattler1, speedBattler2, + priority1, priority2 + ); +} + +// 24 == MAX_BATTLERS_COUNT!. +// These are the possible orders if all the battlers speed tie. An order +// is chosen at the start of the turn. +static const u8 sBattlerOrders[24][4] = +{ + { 0, 1, 2, 3 }, + { 0, 1, 3, 2 }, + { 0, 2, 1, 3 }, + { 0, 2, 3, 1 }, + { 0, 3, 1, 2 }, + { 0, 3, 2, 1 }, + { 1, 0, 2, 3 }, + { 1, 0, 3, 2 }, + { 1, 2, 0, 3 }, + { 1, 2, 3, 0 }, + { 1, 3, 0, 2 }, + { 1, 3, 2, 0 }, + { 2, 0, 1, 3 }, + { 2, 0, 3, 1 }, + { 2, 1, 0, 3 }, + { 2, 1, 3, 0 }, + { 2, 3, 0, 1 }, + { 2, 3, 1, 0 }, + { 3, 0, 1, 2 }, + { 3, 0, 2, 1 }, + { 3, 1, 0, 2 }, + { 3, 1, 2, 0 }, + { 3, 2, 0, 1 }, + { 3, 2, 1, 0 }, +}; + +s32 GetWhichBattlerFaster(u32 battler1, u32 battler2, bool32 ignoreChosenMoves) +{ + s32 strikesFirst = GetWhichBattlerFasterOrTies(battler1, battler2, ignoreChosenMoves); + if (strikesFirst == 0) + { + s32 order1 = sBattlerOrders[gBattleStruct->speedTieBreaks][battler1]; + s32 order2 = sBattlerOrders[gBattleStruct->speedTieBreaks][battler2]; + if (order1 < order2) + strikesFirst = 1; + else + strikesFirst = -1; + } + return strikesFirst; } static void SetActionsAndBattlersTurnOrder(void) @@ -5890,3 +5948,11 @@ bool32 IsWildMonSmart(void) return FALSE; #endif } + +static s32 Factorial(s32 n) +{ + s32 f = 1, i; + for (i = 2; i <= n; i++) + f *= i; + return f; +} diff --git a/test/battle/ability/comatose.c b/test/battle/ability/comatose.c index bd991c258eb9..cc65e9afac7e 100644 --- a/test/battle/ability/comatose.c +++ b/test/battle/ability/comatose.c @@ -34,7 +34,10 @@ SINGLE_BATTLE_TEST("Comatose may be suppressed if pokemon transformed into a pok PARAMETRIZE { move = MOVE_THUNDER_WAVE; } GIVEN { - PLAYER(SPECIES_KOMALA) { Ability(ABILITY_COMATOSE); Speed(30); } + // FIXME: Explicit moves currently required here because Ditto + // expects to find Celebrate in slot 1 during the second turn + // (after transforming). + PLAYER(SPECIES_KOMALA) { Ability(ABILITY_COMATOSE); Speed(30); Moves(MOVE_CELEBRATE, MOVE_GASTRO_ACID, move); } OPPONENT(SPECIES_DITTO) { Speed(20); } } WHEN { TURN { MOVE(player, MOVE_GASTRO_ACID); MOVE(opponent, MOVE_TRANSFORM); } diff --git a/test/battle/move.c b/test/battle/move.c index 936a821081b6..228a09a7c665 100644 --- a/test/battle/move.c +++ b/test/battle/move.c @@ -66,10 +66,9 @@ SINGLE_BATTLE_TEST("Turn order is determined by Speed if priority ties") } } -SINGLE_BATTLE_TEST("Turn order is determined randomly if priority and Speed tie") +SINGLE_BATTLE_TEST("Turn order is determined randomly if priority and Speed tie [singles]") { - KNOWN_FAILING; // The algorithm is significantly biased. - PASSES_RANDOMLY(1, 2); + PASSES_RANDOMLY(1, 2, RNG_SPEED_TIE); GIVEN { PLAYER(SPECIES_WOBBUFFET) { Speed(1); } OPPONENT(SPECIES_WOBBUFFET) { Speed(1); } @@ -81,6 +80,60 @@ SINGLE_BATTLE_TEST("Turn order is determined randomly if priority and Speed tie" } } +DOUBLE_BATTLE_TEST("Turn order is determined randomly if priority and Speed tie [doubles]") +{ + struct BattlePokemon *order[4] = { NULL, NULL, NULL, NULL }; + u32 a, b, c, d; + + // TODO: Test all of these in a single PASSES_RANDOMLY pass rather + // than 24 PARAMETRIZEd passes. + PARAMETRIZE { a = 0; b = 1; c = 2; d = 3; } + PARAMETRIZE { a = 0; b = 1; c = 3; d = 2; } + PARAMETRIZE { a = 0; b = 2; c = 1; d = 3; } + PARAMETRIZE { a = 0; b = 2; c = 3; d = 1; } + PARAMETRIZE { a = 0; b = 3; c = 1; d = 2; } + PARAMETRIZE { a = 0; b = 3; c = 2; d = 1; } + PARAMETRIZE { a = 1; b = 0; c = 2; d = 3; } + PARAMETRIZE { a = 1; b = 0; c = 3; d = 2; } + PARAMETRIZE { a = 1; b = 2; c = 0; d = 3; } + PARAMETRIZE { a = 1; b = 2; c = 3; d = 0; } + PARAMETRIZE { a = 1; b = 3; c = 0; d = 2; } + PARAMETRIZE { a = 1; b = 3; c = 2; d = 0; } + PARAMETRIZE { a = 2; b = 0; c = 1; d = 3; } + PARAMETRIZE { a = 2; b = 0; c = 3; d = 1; } + PARAMETRIZE { a = 2; b = 1; c = 0; d = 3; } + PARAMETRIZE { a = 2; b = 1; c = 3; d = 0; } + PARAMETRIZE { a = 2; b = 3; c = 0; d = 1; } + PARAMETRIZE { a = 2; b = 3; c = 1; d = 0; } + PARAMETRIZE { a = 3; b = 0; c = 1; d = 2; } + PARAMETRIZE { a = 3; b = 0; c = 2; d = 1; } + PARAMETRIZE { a = 3; b = 1; c = 0; d = 2; } + PARAMETRIZE { a = 3; b = 1; c = 2; d = 0; } + PARAMETRIZE { a = 3; b = 2; c = 0; d = 1; } + PARAMETRIZE { a = 3; b = 2; c = 1; d = 0; } + + order[a] = playerLeft; + order[b] = playerRight; + order[c] = opponentLeft; + order[d] = opponentRight; + + PASSES_RANDOMLY(1, 24, RNG_SPEED_TIE); + + GIVEN { + PLAYER(SPECIES_WOBBUFFET) { Speed(1); } + PLAYER(SPECIES_WYNAUT) { Speed(1); } + OPPONENT(SPECIES_WOBBUFFET) { Speed(1); } + OPPONENT(SPECIES_WYNAUT) { Speed(1); } + } WHEN { + TURN { MOVE(playerLeft, MOVE_SPLASH); MOVE(playerRight, MOVE_SPLASH); MOVE(opponentLeft, MOVE_SPLASH); MOVE(opponentRight, MOVE_SPLASH); } + } SCENE { + ANIMATION(ANIM_TYPE_MOVE, MOVE_SPLASH, order[0]); + ANIMATION(ANIM_TYPE_MOVE, MOVE_SPLASH, order[1]); + ANIMATION(ANIM_TYPE_MOVE, MOVE_SPLASH, order[2]); + ANIMATION(ANIM_TYPE_MOVE, MOVE_SPLASH, order[3]); + } +} + SINGLE_BATTLE_TEST("Critical hits occur at a 1/24 rate") { ASSUME(B_CRIT_CHANCE >= GEN_7);