diff --git a/src/data/ability.ts b/src/data/ability.ts index 780c4a515cd..3c73bb47c47 100644 --- a/src/data/ability.ts +++ b/src/data/ability.ts @@ -3614,22 +3614,19 @@ export class MoodyAbAttr extends PostTurnAbAttr { } } -export class PostTurnStatStageChangeAbAttr extends PostTurnAbAttr { - private stats: BattleStat[]; - private stages: number; +export class SpeedBoostAbAttr extends PostTurnAbAttr { - constructor(stats: BattleStat[], stages: number) { + constructor() { super(true); - - this.stats = Array.isArray(stats) - ? stats - : [ stats ]; - this.stages = stages; } applyPostTurn(pokemon: Pokemon, passive: boolean, simulated: boolean, args: any[]): boolean { if (!simulated) { - pokemon.scene.unshiftPhase(new StatStageChangePhase(pokemon.scene, pokemon.getBattlerIndex(), true, this.stats, this.stages)); + if (!pokemon.turnData.switchedInThisTurn && !pokemon.turnData.failedRunAway) { + pokemon.scene.unshiftPhase(new StatStageChangePhase(pokemon.scene, pokemon.getBattlerIndex(), true, [ Stat.SPD ], 1)); + } else { + return false; + } } return true; } @@ -5011,7 +5008,7 @@ export function initAbilities() { .attr(PostSummonWeatherChangeAbAttr, WeatherType.RAIN) .attr(PostBiomeChangeWeatherChangeAbAttr, WeatherType.RAIN), new Ability(Abilities.SPEED_BOOST, 3) - .attr(PostTurnStatStageChangeAbAttr, [ Stat.SPD ], 1), + .attr(SpeedBoostAbAttr), new Ability(Abilities.BATTLE_ARMOR, 3) .attr(BlockCritAbAttr) .ignorable(), diff --git a/src/enums/switch-type.ts b/src/enums/switch-type.ts index b25ba6ad119..752c0902636 100644 --- a/src/enums/switch-type.ts +++ b/src/enums/switch-type.ts @@ -3,6 +3,8 @@ * or {@linkcode SwitchSummonPhase} will carry out. */ export enum SwitchType { + /** Switchout specifically for when combat starts and the player is prompted if they will switch Pokemon */ + INITIAL_SWITCH, /** Basic switchout where the Pokemon to switch in is selected */ SWITCH, /** Transfers stat stages and other effects from the returning Pokemon to the switched in Pokemon */ diff --git a/src/field/pokemon.ts b/src/field/pokemon.ts index 5c49e18058b..321532fffa7 100644 --- a/src/field/pokemon.ts +++ b/src/field/pokemon.ts @@ -5143,6 +5143,8 @@ export class PokemonTurnData { public statStagesDecreased: boolean = false; public moveEffectiveness: TypeDamageMultiplier | null = null; public combiningPledge?: Moves; + public switchedInThisTurn: boolean = false; + public failedRunAway: boolean = false; } export enum AiType { diff --git a/src/phases/attempt-run-phase.ts b/src/phases/attempt-run-phase.ts index e0dd7fa72fd..b4768dc9a26 100644 --- a/src/phases/attempt-run-phase.ts +++ b/src/phases/attempt-run-phase.ts @@ -10,6 +10,10 @@ import { NewBattlePhase } from "./new-battle-phase"; import { PokemonPhase } from "./pokemon-phase"; export class AttemptRunPhase extends PokemonPhase { + + /** For testing purposes: this is to force the pokemon to fail and escape */ + public forceFailEscape = false; + constructor(scene: BattleScene, fieldIndex: number) { super(scene, fieldIndex); } @@ -28,7 +32,7 @@ export class AttemptRunPhase extends PokemonPhase { applyAbAttrs(RunSuccessAbAttr, playerPokemon, null, false, escapeChance); - if (playerPokemon.randSeedInt(100) < escapeChance.value) { + if (playerPokemon.randSeedInt(100) < escapeChance.value && !this.forceFailEscape) { this.scene.playSound("se/flee"); this.scene.queueMessage(i18next.t("battle:runAwaySuccess"), null, true, 500); @@ -51,6 +55,7 @@ export class AttemptRunPhase extends PokemonPhase { this.scene.pushPhase(new BattleEndPhase(this.scene)); this.scene.pushPhase(new NewBattlePhase(this.scene)); } else { + playerPokemon.turnData.failedRunAway = true; this.scene.queueMessage(i18next.t("battle:runAwayCannotEscape"), null, true, 500); } diff --git a/src/phases/check-switch-phase.ts b/src/phases/check-switch-phase.ts index 8849d304435..5e459d0e6b5 100644 --- a/src/phases/check-switch-phase.ts +++ b/src/phases/check-switch-phase.ts @@ -51,7 +51,7 @@ export class CheckSwitchPhase extends BattlePhase { this.scene.ui.setMode(Mode.CONFIRM, () => { this.scene.ui.setMode(Mode.MESSAGE); this.scene.tryRemovePhase(p => p instanceof PostSummonPhase && p.player && p.fieldIndex === this.fieldIndex); - this.scene.unshiftPhase(new SwitchPhase(this.scene, SwitchType.SWITCH, this.fieldIndex, false, true)); + this.scene.unshiftPhase(new SwitchPhase(this.scene, SwitchType.INITIAL_SWITCH, this.fieldIndex, false, true)); this.end(); }, () => { this.scene.ui.setMode(Mode.MESSAGE); diff --git a/src/phases/switch-summon-phase.ts b/src/phases/switch-summon-phase.ts index 37652b3cfa4..c7e7bbe011e 100644 --- a/src/phases/switch-summon-phase.ts +++ b/src/phases/switch-summon-phase.ts @@ -64,10 +64,8 @@ export class SwitchSummonPhase extends SummonPhase { } const pokemon = this.getPokemon(); - (this.player ? this.scene.getEnemyField() : this.scene.getPlayerField()).forEach(enemyPokemon => enemyPokemon.removeTagsBySourceId(pokemon.id)); - - if (this.switchType === SwitchType.SWITCH) { + if (this.switchType === SwitchType.SWITCH || this.switchType === SwitchType.INITIAL_SWITCH) { const substitute = pokemon.getTag(SubstituteTag); if (substitute) { this.scene.tweens.add({ @@ -186,6 +184,11 @@ export class SwitchSummonPhase extends SummonPhase { } } + if (this.switchType !== SwitchType.INITIAL_SWITCH) { + pokemon.resetTurnData(); + pokemon.turnData.switchedInThisTurn = true; + } + this.lastPokemon?.resetSummonData(); this.scene.triggerPokemonFormChange(pokemon, SpeciesFormChangeActiveTrigger, true); diff --git a/src/test/abilities/speed_boost.test.ts b/src/test/abilities/speed_boost.test.ts new file mode 100644 index 00000000000..dd2e83aaa88 --- /dev/null +++ b/src/test/abilities/speed_boost.test.ts @@ -0,0 +1,125 @@ +import { Stat } from "#enums/stat"; +import { Abilities } from "#enums/abilities"; +import { Moves } from "#enums/moves"; +import { Species } from "#enums/species"; +import GameManager from "#test/utils/gameManager"; +import Phaser from "phaser"; +import { afterEach, beforeAll, beforeEach, describe, expect, it } from "vitest"; +import { CommandPhase } from "#app/phases/command-phase"; +import { Command } from "#app/ui/command-ui-handler"; +import { AttemptRunPhase } from "#app/phases/attempt-run-phase"; + +describe("Abilities - Speed Boost", () => { + let phaserGame: Phaser.Game; + let game: GameManager; + + beforeAll(() => { + phaserGame = new Phaser.Game({ + type: Phaser.HEADLESS, + }); + }); + + afterEach(() => { + game.phaseInterceptor.restoreOg(); + }); + + beforeEach(() => { + game = new GameManager(phaserGame); + + game.override + .battleType("single") + .enemySpecies(Species.DRAGAPULT) + .ability(Abilities.SPEED_BOOST) + .enemyMoveset(Moves.SPLASH) + .moveset([ Moves.SPLASH, Moves.U_TURN ]); + }); + + it("should increase speed by 1 stage at end of turn", + async () => { + await game.classicMode.startBattle(); + + const playerPokemon = game.scene.getPlayerPokemon()!; + game.move.select(Moves.SPLASH); + await game.toNextTurn(); + + expect(playerPokemon.getStatStage(Stat.SPD)).toBe(1); + }); + + it("should not trigger this turn if pokemon was switched into combat via attack, but the turn after", + async () => { + await game.classicMode.startBattle([ + Species.SHUCKLE, + Species.NINJASK + ]); + + game.move.select(Moves.U_TURN); + game.doSelectPartyPokemon(1); + await game.toNextTurn(); + const playerPokemon = game.scene.getPlayerPokemon()!; + expect(playerPokemon.getStatStage(Stat.SPD)).toBe(0); + + game.move.select(Moves.SPLASH); + await game.toNextTurn(); + expect(playerPokemon.getStatStage(Stat.SPD)).toBe(1); + }); + + it("checking back to back swtiches", + async () => { + await game.classicMode.startBattle([ + Species.SHUCKLE, + Species.NINJASK + ]); + + game.move.select(Moves.U_TURN); + game.doSelectPartyPokemon(1); + await game.toNextTurn(); + let playerPokemon = game.scene.getPlayerPokemon()!; + expect(playerPokemon.getStatStage(Stat.SPD)).toBe(0); + + game.move.select(Moves.U_TURN); + game.doSelectPartyPokemon(1); + await game.toNextTurn(); + playerPokemon = game.scene.getPlayerPokemon()!; + expect(playerPokemon.getStatStage(Stat.SPD)).toBe(0); + + game.move.select(Moves.SPLASH); + await game.toNextTurn(); + expect(playerPokemon.getStatStage(Stat.SPD)).toBe(1); + }); + + it("should not trigger this turn if pokemon was switched into combat via normal switch, but the turn after", + async () => { + await game.classicMode.startBattle([ + Species.SHUCKLE, + Species.NINJASK + ]); + + game.doSwitchPokemon(1); + await game.toNextTurn(); + const playerPokemon = game.scene.getPlayerPokemon()!; + expect(playerPokemon.getStatStage(Stat.SPD)).toBe(0); + + game.move.select(Moves.SPLASH); + await game.toNextTurn(); + expect(playerPokemon.getStatStage(Stat.SPD)).toBe(1); + }); + + it("should not trigger if pokemon fails to escape", + async () => { + await game.classicMode.startBattle([ Species.SHUCKLE ]); + + const commandPhase = game.scene.getCurrentPhase() as CommandPhase; + commandPhase.handleCommand(Command.RUN, 0); + const runPhase = game.scene.getCurrentPhase() as AttemptRunPhase; + runPhase.forceFailEscape = true; + await game.phaseInterceptor.to(AttemptRunPhase); + await game.toNextTurn(); + + const playerPokemon = game.scene.getPlayerPokemon()!; + expect(playerPokemon.getStatStage(Stat.SPD)).toBe(0); + + game.move.select(Moves.SPLASH); + await game.toNextTurn(); + expect(playerPokemon.getStatStage(Stat.SPD)).toBe(1); + }); +});