From 063aa24588b7c4afb0c5a90370bc20021385369d Mon Sep 17 00:00:00 2001 From: Mason Date: Thu, 11 Jul 2024 15:16:58 -0400 Subject: [PATCH 1/8] [Bug] Toxic Spikes implementation issues fixed Adjusted MoveEffectPhase.start() so that ENEMY_SIDE targeted moves no longer occur twice per use in double battles. Updated Toxic Orb test to no longer expect a tick of damage turn 1. Fixed Toxic/Poison dealing damage immediately when applied. Fixed Hazards not persisting through save Added unit tests Fixed flyout not displaying correct number of Spikes/Toxic Spikes after a refresh --- src/data/arena-tag.ts | 37 +++ src/phases/check-status-effect-phase.ts | 23 ++ src/phases/move-effect-phase.ts | 17 ++ src/phases/obtain-status-effect-phase.ts | 4 - src/phases/turn-start-phase.ts | 9 +- src/system/arena-data.ts | 8 +- src/system/game-data.ts | 17 +- src/test/items/toxic_orb.test.ts | 16 +- src/test/moves/toxic_spikes.test.ts | 294 +++++++++++++++++++++++ 9 files changed, 402 insertions(+), 23 deletions(-) create mode 100644 src/phases/check-status-effect-phase.ts create mode 100644 src/test/moves/toxic_spikes.test.ts diff --git a/src/data/arena-tag.ts b/src/data/arena-tag.ts index 3394df827fb..c545b04e733 100644 --- a/src/data/arena-tag.ts +++ b/src/data/arena-tag.ts @@ -64,6 +64,18 @@ export abstract class ArenaTag { ? allMoves[this.sourceMove].name : null; } + + /** + * When given a arena tag or json representing one, load the data for it. + * This is meant to be inherited from by any arena tag with custom attributes + * @param {ArenaTag | any} source An arena tag + */ + loadTag(source : ArenaTag | any) : void { + this.turnCount = source.turnCount; + this.sourceMove = source.sourceMove; + this.sourceId = source.sourceId; + this.side = source.side; + } } /** @@ -557,6 +569,12 @@ export class ArenaTrapTag extends ArenaTag { getMatchupScoreMultiplier(pokemon: Pokemon): number { return pokemon.isGrounded() ? 1 : Phaser.Math.Linear(0, 1 / Math.pow(2, this.layers), Math.min(pokemon.getHpRatio(), 0.5) * 2); } + + loadTag(source: any): void { + super.loadTag(source); + this.layers = source.layers; + this.maxLayers = source.maxLayers; + } } /** @@ -905,6 +923,12 @@ class HappyHourTag extends ArenaTag { } } +class NoneTag extends ArenaTag { + constructor() { + super(ArenaTagType.NONE, 0, undefined, undefined); + } +} + export function getArenaTag(tagType: ArenaTagType, turnCount: integer, sourceMove: Moves | undefined, sourceId: integer, targetIndex?: BattlerIndex, side: ArenaTagSide = ArenaTagSide.BOTH): ArenaTag | null { switch (tagType) { case ArenaTagType.MIST: @@ -954,3 +978,16 @@ export function getArenaTag(tagType: ArenaTagType, turnCount: integer, sourceMov return null; } } + +/** + * When given a battler tag or json representing one, creates an actual ArenaTag object with the same data. + * @param {ArenaTag | any} source An arena tag + * @return {ArenaTag} The valid arena tag + */ +export function loadArenaTag(source: ArenaTag | any): ArenaTag { + const tag = getArenaTag(source.tagType, source.turnCount, source.sourceMove, source.sourceId, source.targetIndex, source.side) + ?? new NoneTag(); + tag.loadTag(source); + return tag; +} + diff --git a/src/phases/check-status-effect-phase.ts b/src/phases/check-status-effect-phase.ts new file mode 100644 index 00000000000..8dafdfed227 --- /dev/null +++ b/src/phases/check-status-effect-phase.ts @@ -0,0 +1,23 @@ +import {PostTurnStatusEffectPhase} from "#app/phases/post-turn-status-effect-phase"; +import {Phase} from "#app/phase"; +import {BattlerIndex} from "#app/battle"; +import BattleScene from "#app/battle-scene"; + +export class CheckStatusEffectPhase extends Phase { + private order : BattlerIndex[]; + constructor(scene : BattleScene, order : BattlerIndex[]) { + super(scene); + this.scene = scene; + this.order = order; + } + + start() { + const field = this.scene.getField(); + for (const o of this.order) { + if (field[o].status && field[o].status.isPostTurn()) { + this.scene.unshiftPhase(new PostTurnStatusEffectPhase(this.scene, o)); + } + } + this.end(); + } +} diff --git a/src/phases/move-effect-phase.ts b/src/phases/move-effect-phase.ts index a5ac913cc5d..175e8fc3bd9 100644 --- a/src/phases/move-effect-phase.ts +++ b/src/phases/move-effect-phase.ts @@ -14,6 +14,7 @@ import { PokemonMultiHitModifier, FlinchChanceModifier, EnemyAttackStatusEffectC import i18next from "i18next"; import * as Utils from "#app/utils.js"; import { PokemonPhase } from "./pokemon-phase"; +import {Abilities} from "#enums/abilities"; export class MoveEffectPhase extends PokemonPhase { public move: PokemonMove; @@ -123,6 +124,11 @@ export class MoveEffectPhase extends PokemonPhase { new MoveAnim(move.id as Moves, user, this.getTarget()?.getBattlerIndex()!).play(this.scene, () => { // TODO: is the bang correct here? /** Has the move successfully hit a target (for damage) yet? */ let hasHit: boolean = false; + + /** Hazards bounce if any opposing Pokémon has the ability **/ + /** TODO: Update to support future Magic Coat / Magic Bounce implementation **/ + let hazardBounce: boolean = false; + for (const target of targets) { /** * If the move missed a target, stop all future hits against that target @@ -157,6 +163,17 @@ export class MoveEffectPhase extends PokemonPhase { /** Does this phase represent the invoked move's first strike? */ const firstHit = (user.turnData.hitsLeft === user.turnData.hitCount); + /** Check if current target is affected by Magic Bounce or used Magic Coat + * If they did, then field hazards should be bounced back in their entirety + */ + const bounceStatus = target.hasAbility(Abilities.MAGIC_BOUNCE) || (this.scene.currentBattle.turnCommands[target.getBattlerIndex()]?.move?.move === Moves.MAGIC_COAT); + hazardBounce = hazardBounce && bounceStatus; + + // Prevent ENEMY_SIDE targeted moves from occurring twice in double battles + if (move.moveTarget === MoveTarget.ENEMY_SIDE && target !== targets[targets.length-1]) { + continue; + } + // Only log the move's result on the first strike if (firstHit) { user.pushMoveHistory(moveHistoryEntry); diff --git a/src/phases/obtain-status-effect-phase.ts b/src/phases/obtain-status-effect-phase.ts index ac6e66a2e9f..9f15de84ca8 100644 --- a/src/phases/obtain-status-effect-phase.ts +++ b/src/phases/obtain-status-effect-phase.ts @@ -6,7 +6,6 @@ import { StatusEffect } from "#app/enums/status-effect.js"; import Pokemon from "#app/field/pokemon.js"; import { getPokemonNameWithAffix } from "#app/messages.js"; import { PokemonPhase } from "./pokemon-phase"; -import { PostTurnStatusEffectPhase } from "./post-turn-status-effect-phase"; export class ObtainStatusEffectPhase extends PokemonPhase { private statusEffect: StatusEffect | undefined; @@ -33,9 +32,6 @@ export class ObtainStatusEffectPhase extends PokemonPhase { pokemon.updateInfo(true); new CommonBattleAnim(CommonAnim.POISON + (this.statusEffect! - 1), pokemon).play(this.scene, () => { this.scene.queueMessage(getStatusEffectObtainText(this.statusEffect, getPokemonNameWithAffix(pokemon), this.sourceText ?? undefined)); - if (pokemon.status?.isPostTurn()) { - this.scene.pushPhase(new PostTurnStatusEffectPhase(this.scene, this.battlerIndex)); - } this.end(); }); return; diff --git a/src/phases/turn-start-phase.ts b/src/phases/turn-start-phase.ts index 1320cb6235c..68d62602389 100644 --- a/src/phases/turn-start-phase.ts +++ b/src/phases/turn-start-phase.ts @@ -13,10 +13,10 @@ import { BerryPhase } from "./berry-phase"; import { FieldPhase } from "./field-phase"; import { MoveHeaderPhase } from "./move-header-phase"; import { MovePhase } from "./move-phase"; -import { PostTurnStatusEffectPhase } from "./post-turn-status-effect-phase"; import { SwitchSummonPhase } from "./switch-summon-phase"; import { TurnEndPhase } from "./turn-end-phase"; import { WeatherEffectPhase } from "./weather-effect-phase"; +import {CheckStatusEffectPhase} from "#app/phases/check-status-effect-phase"; export class TurnStartPhase extends FieldPhase { constructor(scene: BattleScene) { @@ -153,11 +153,8 @@ export class TurnStartPhase extends FieldPhase { this.scene.pushPhase(new WeatherEffectPhase(this.scene)); - for (const o of order) { - if (field[o].status && field[o].status.isPostTurn()) { - this.scene.pushPhase(new PostTurnStatusEffectPhase(this.scene, o)); - } - } + /** Add a new phase to check who should be taking status damage */ + this.scene.pushPhase(new CheckStatusEffectPhase(this.scene, order)); this.scene.pushPhase(new BerryPhase(this.scene)); this.scene.pushPhase(new TurnEndPhase(this.scene)); diff --git a/src/system/arena-data.ts b/src/system/arena-data.ts index 886129edcf6..682ade58e48 100644 --- a/src/system/arena-data.ts +++ b/src/system/arena-data.ts @@ -1,5 +1,5 @@ import { Arena } from "../field/arena"; -import { ArenaTag } from "../data/arena-tag"; +import {ArenaTag, loadArenaTag} from "../data/arena-tag"; import { Biome } from "#enums/biome"; import { Weather } from "../data/weather"; import { Terrain } from "#app/data/terrain.js"; @@ -15,6 +15,10 @@ export default class ArenaData { this.biome = sourceArena ? sourceArena.biomeType : source.biome; this.weather = sourceArena ? sourceArena.weather : source.weather ? new Weather(source.weather.weatherType, source.weather.turnsLeft) : null; this.terrain = sourceArena ? sourceArena.terrain : source.terrain ? new Terrain(source.terrain.terrainType, source.terrain.turnsLeft) : null; - this.tags = sourceArena ? sourceArena.tags : []; + this.tags = []; + + if (source.tags) { + this.tags = source.tags.map(t => loadArenaTag(t)); + } } } diff --git a/src/system/game-data.ts b/src/system/game-data.ts index a9acd80fdee..78bb2523a64 100644 --- a/src/system/game-data.ts +++ b/src/system/game-data.ts @@ -44,6 +44,8 @@ import { WeatherType } from "#app/enums/weather-type.js"; import { TerrainType } from "#app/data/terrain.js"; import { OutdatedPhase } from "#app/phases/outdated-phase.js"; import { ReloadSessionPhase } from "#app/phases/reload-session-phase.js"; +import {TagAddedEvent} from "#app/events/arena"; +import {ArenaTrapTag} from "#app/data/arena-tag"; export const defaultStarterSpecies: Species[] = [ Species.BULBASAUR, Species.CHARMANDER, Species.SQUIRTLE, @@ -966,8 +968,19 @@ export class GameData { scene.arena.terrain = sessionData.arena.terrain; scene.arena.eventTarget.dispatchEvent(new TerrainChangedEvent(TerrainType.NONE, scene.arena.terrain?.terrainType!, scene.arena.terrain?.turnsLeft!)); // TODO: is this bang correct? - // TODO - //scene.arena.tags = sessionData.arena.tags; + + scene.arena.tags = sessionData.arena.tags; + if (scene.arena.tags) { + for (const tag of scene.arena.tags) { + if (tag instanceof ArenaTrapTag) { + const { tagType, side, turnCount, layers, maxLayers } = tag as ArenaTrapTag; + scene.arena.eventTarget.dispatchEvent(new TagAddedEvent(tagType, side, turnCount, layers, maxLayers)); + } else { + scene.arena.eventTarget.dispatchEvent(new TagAddedEvent(tag.tagType, tag.side, tag.turnCount)); + } + } + } + const modifiersModule = await import("../modifier/modifier"); diff --git a/src/test/items/toxic_orb.test.ts b/src/test/items/toxic_orb.test.ts index dc54a5a1c36..83259b67866 100644 --- a/src/test/items/toxic_orb.test.ts +++ b/src/test/items/toxic_orb.test.ts @@ -9,10 +9,10 @@ import { Moves } from "#enums/moves"; import { Species } from "#enums/species"; import Phaser from "phaser"; import { afterEach, beforeAll, beforeEach, describe, expect, it } from "vitest"; -import { CommandPhase } from "#app/phases/command-phase.js"; -import { EnemyCommandPhase } from "#app/phases/enemy-command-phase.js"; -import { MessagePhase } from "#app/phases/message-phase.js"; -import { TurnEndPhase } from "#app/phases/turn-end-phase.js"; +import {CommandPhase} from "#app/phases/command-phase"; +import {EnemyCommandPhase} from "#app/phases/enemy-command-phase"; +import {TurnEndPhase} from "#app/phases/turn-end-phase"; +import {MessagePhase} from "#app/phases/message-phase"; describe("Items - Toxic orb", () => { @@ -71,10 +71,8 @@ describe("Items - Toxic orb", () => { await game.phaseInterceptor.run(MessagePhase); const message = game.textInterceptor.getLatestMessage(); expect(message).toContain("was badly poisoned by the Toxic Orb"); - await game.phaseInterceptor.run(MessagePhase); - const message2 = game.textInterceptor.getLatestMessage(); - expect(message2).toContain("is hurt"); - expect(message2).toContain("by poison"); expect(game.scene.getParty()[0].status!.effect).toBe(StatusEffect.TOXIC); - }, 20000); + // Damage should not have ticked yet. + expect(game.scene.getParty()[0].status!.turnCount).toBe(0); + }, 2000); }); diff --git a/src/test/moves/toxic_spikes.test.ts b/src/test/moves/toxic_spikes.test.ts new file mode 100644 index 00000000000..2d7a10f21cd --- /dev/null +++ b/src/test/moves/toxic_spikes.test.ts @@ -0,0 +1,294 @@ +import { CommandPhase } from "#app/phases/command-phase"; +import GameManager from "#test/utils/gameManager"; +import { Abilities } from "#enums/abilities"; +import { Moves } from "#enums/moves"; +import { Species } from "#enums/species"; +import Phaser from "phaser"; +import { afterEach, beforeAll, beforeEach, describe, expect, it } from "vitest"; +import {StatusEffect} from "#app/data/status-effect"; +import {ArenaTagType} from "#enums/arena-tag-type"; +import {ArenaTrapTag} from "#app/data/arena-tag"; +import {decrypt, encrypt, GameData, SessionSaveData} from "#app/system/game-data"; + + + +// I am frankly, not that comfortable with the testing system, these could prob be more precise +describe("Moves - Toxic Spikes", () => { + 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.scene.battleStyle = 1; + game.override.battleType("single"); + game.override.enemySpecies(Species.RATTATA); + game.override.enemyAbility(Abilities.HYDRATION); + game.override.enemyPassiveAbility(Abilities.HYDRATION); + game.override.ability(Abilities.NO_GUARD); + game.override.passiveAbility(Abilities.HUGE_POWER); + game.override.startingWave(3); + game.override.enemyMoveset([Moves.SPLASH,Moves.SPLASH,Moves.SPLASH,Moves.SPLASH]); + game.override.moveset([Moves.TOXIC_SPIKES,Moves.SPLASH, Moves.ROAR, Moves.GLACIAL_LANCE]); + }); + + it("no switches, no effect", async() => { + game.override.enemySpecies(Species.RATTATA); + // player set toxic spikes on the field and do splash for 3 turns + // opponent do splash for 4 turns + // nobody should take damage + await game.classicMode.runToSummon([ + Species.MIGHTYENA, + Species.POOCHYENA, + ]); + await game.phaseInterceptor.to(CommandPhase, true); + const initialHp = game.scene.getEnemyParty()[0].hp; + expect(game.scene.getEnemyParty()[0].hp).toBe(initialHp); + game.doAttack(0); + await game.toNextTurn(); + game.doAttack(1); + await game.toNextTurn(); + game.doAttack(1); + await game.toNextTurn(); + game.doAttack(1); + await game.toNextTurn(); + game.doAttack(1); + await game.toNextTurn(); + // Opponent should be full health and not statused + expect(game.scene.getEnemyParty()[0].hp).toBe(initialHp); + expect(!(game.scene.getEnemyParty()[0].status?.effect)); + console.log(game.textInterceptor.logs); + }, 20000); + + it("user switch, no effect", async() => { + game.override.enemySpecies(Species.RATTATA); + // player set toxic spikes on the field and switch back to back + // opponent do splash for 2 turns + // nobody should be poisoned or damaged + await game.classicMode.runToSummon([ + Species.MIGHTYENA, + Species.POOCHYENA, + ]); + await game.phaseInterceptor.to(CommandPhase, false); + + const initialHp = game.scene.getParty()[0].hp; + game.doSwitchPokemon(1); + await game.phaseInterceptor.run(CommandPhase); + await game.phaseInterceptor.to(CommandPhase, false); + + game.doSwitchPokemon(1); + await game.phaseInterceptor.run(CommandPhase); + await game.phaseInterceptor.to(CommandPhase, false); + + expect(game.scene.getParty()[0].hp).toBe(initialHp); + expect(!(game.scene.getEnemyParty()[0].status?.effect)); + }, 20000); + + it("force enemy switch - poisoned", async() => { + game.override.startingWave(5); + game.override.enemySpecies(Species.RATTATA); + // player sets 1 layer of toxic spikes + // then forces a switch with roar + // opponent should be damaged and poisoned at end of next turn + await game.classicMode.runToSummon([ + Species.MIGHTYENA, + Species.POOCHYENA, + ]); + await game.phaseInterceptor.to(CommandPhase, true); + game.doAttack(0); + await game.toNextTurn(); + game.doAttack(2); + await game.toNextTurn(); + const opponent = game.scene.currentBattle.enemyParty[0]; + expect(opponent.hp).toBeLessThan(opponent.getMaxHp()); + expect(game.scene.currentBattle.enemyParty[0].status?.effect).toBe(StatusEffect.POISON); + }, 20000); + + it("force enemy switch - toxic", async() => { + game.override.startingWave(5); + game.override.enemySpecies(Species.RATTATA); + // player sets 1 layer of toxic spikes + // then forces a switch with roar + // opponent should be damaged and poisoned at end of next turn + await game.classicMode.runToSummon([ + Species.MIGHTYENA, + Species.POOCHYENA, + ]); + await game.phaseInterceptor.to(CommandPhase, true); + game.doAttack(0); + await game.toNextTurn(); + game.doAttack(0); + await game.toNextTurn(); + game.doAttack(2); + await game.toNextTurn(); + const opponent = game.scene.currentBattle.enemyParty[0]; + expect(opponent.hp).toBeLessThan(opponent.getMaxHp()); + expect(game.scene.currentBattle.enemyParty[0].status?.effect).toBe(StatusEffect.TOXIC); + }, 20000); + + it("enemy switch - poison", async() => { + game.override.startingWave(5); + game.override.startingLevel(5000); + game.override.enemySpecies(Species.RATTATA); + // turn 1: player set toxic spikes, opponent do splash + // turn 2: player do splash, opponent switch pokemon + // opponent pokemon should trigger spikes and get poisoned + await game.classicMode.runToSummon([ + Species.MIGHTYENA, + Species.POOCHYENA, + ]); + await game.phaseInterceptor.to(CommandPhase, true); + + game.doAttack(0); + await game.toNextTurn(); + + game.forceOpponentToSwitch(); + game.doAttack(1); + await game.toNextTurn(); + const opponent = game.scene.currentBattle.enemyParty[0]; + expect(opponent.hp).toBeLessThan(opponent.getMaxHp()); + expect(opponent.status?.effect).toBe(StatusEffect.POISON); + }, 20000); + + it("enemy switch - toxic", async() => { + game.override.startingWave(5); + game.override.startingLevel(5000); + game.override.enemySpecies(Species.RATTATA); + // turn 1: player set toxic spikes, opponent do splash + // turn 2: player do splash, opponent switch pokemon + // opponent pokemon should trigger spikes and get poisoned + await game.classicMode.runToSummon([ + Species.MIGHTYENA, + Species.POOCHYENA, + ]); + await game.phaseInterceptor.to(CommandPhase, true); + + game.doAttack(0); + await game.toNextTurn(); + + game.doAttack(0); + await game.toNextTurn(); + + game.forceOpponentToSwitch(); + game.doAttack(1); + await game.toNextTurn(); + const opponent = game.scene.currentBattle.enemyParty[0]; + expect(opponent.hp).toBeLessThan(opponent.getMaxHp()); + expect(opponent.status?.effect).toBe(StatusEffect.TOXIC); + }, 20000); + + it("grounded poison switch - absorb ", async() => { + game.override.startingWave(5); + game.override.startingLevel(5000); + game.override.enemySpecies(Species.GRIMER); + // turn 1: player set toxic spikes, opponent do splash + // turn 2: player do splash, opponent switch pokemon + // opponent pokemon should trigger spikes and get poisoned + await game.classicMode.runToSummon([ + Species.MIGHTYENA, + Species.POOCHYENA, + ]); + await game.phaseInterceptor.to(CommandPhase, true); + + game.doAttack(0); + await game.toNextTurn(); + + game.doAttack(0); + await game.toNextTurn(); + + game.forceOpponentToSwitch(); + game.doAttack(1); + await game.toNextTurn(); + const opponent = game.scene.currentBattle.enemyParty[0]; + + // Enemy pokemon should be undamaged, and not poisoned + expect(opponent.hp).toBe(opponent.getMaxHp()); + expect(!opponent.status?.effect); + + // There should be no Arena Tags, as the Toxic Spikes have been absorbed + expect(game.scene.arena.tags.length === 0); + }, 20000); + + it("doubles - one stack per use ", async() => { + game.override.startingWave(5); + game.override.startingLevel(5000); + game.override.enemySpecies(Species.GRIMER); + game.override.battleType("double"); + // turn 1: player set toxic spikes, verify one layer down + // turn 2: player set toxic spikes, verify two layers down + // turn 3: player do splash, opponent switch pokemon + // incoming grimer should absorb all layers + await game.classicMode.runToSummon([ + Species.MIGHTYENA, + Species.POOCHYENA, + ]); + await game.phaseInterceptor.to(CommandPhase, true); + game.doAttack(0); + + await game.phaseInterceptor.to(CommandPhase, true); + game.doAttack(1); + + await game.toNextTurn(); + + expect(game.scene.arena.tags[0].tagType).toBe(ArenaTagType.TOXIC_SPIKES); + expect(game.scene.arena.tags[0] instanceof ArenaTrapTag); + expect((game.scene.arena.tags[0] as ArenaTrapTag).layers).toBe(1); + + game.doAttack(0); + await game.phaseInterceptor.to(CommandPhase, true); + game.doAttack(1); + await game.toNextTurn(); + expect(game.scene.arena.tags[0].tagType).toBe(ArenaTagType.TOXIC_SPIKES); + expect(game.scene.arena.tags[0] instanceof ArenaTrapTag); + expect((game.scene.arena.tags[0] as ArenaTrapTag).layers).toBe(2); + + game.forceOpponentToSwitch(); + game.doAttack(1); + await game.toNextTurn(); + + // There should ben o Arena Tags, as the Toxic Spikes have been absorbed + expect(game.scene.arena.tags.length === 0); + }, 20000); + + it("persist through reload", async() => { + game.override.startingWave(5); + game.override.startingLevel(5000); + game.override.enemySpecies(Species.RATTATA); + const scene = game.scene; + const gameData = new GameData(scene); + + + // turn 1: player set toxic spikes, opponent do splash + // export, save, and then load session Data + // tags should not change through reload. + await game.classicMode.runToSummon([ + Species.MIGHTYENA, + Species.POOCHYENA, + ]); + await game.phaseInterceptor.to(CommandPhase, true); + + game.doAttack(0); + await game.toNextTurn(); + + const sessionData : SessionSaveData = gameData["getSessionSaveData"](game.scene); + localStorage.setItem("sessionTestData", encrypt(JSON.stringify(sessionData), true)); + const recoveredData : SessionSaveData = gameData.parseSessionData(decrypt(localStorage.getItem("sessionTestData")!, true)); + gameData.loadSession(game.scene,0,recoveredData); + + + expect(sessionData.arena.tags).toEqual(recoveredData.arena.tags); + localStorage.removeItem("sessionTestData"); + }, 20000); + + + +}); From c76642798dc8273730e30c94c63fa3658685c8d9 Mon Sep 17 00:00:00 2001 From: NightKev <34855794+DayKev@users.noreply.github.com> Date: Sat, 31 Aug 2024 22:25:31 -0700 Subject: [PATCH 2/8] Update Toxic Orb test --- src/test/items/toxic_orb.test.ts | 51 ++++++++++++++------------------ 1 file changed, 22 insertions(+), 29 deletions(-) diff --git a/src/test/items/toxic_orb.test.ts b/src/test/items/toxic_orb.test.ts index 4235ad5215b..8eb178bd8b3 100644 --- a/src/test/items/toxic_orb.test.ts +++ b/src/test/items/toxic_orb.test.ts @@ -1,15 +1,14 @@ import { StatusEffect } from "#app/data/status-effect"; -import { EnemyCommandPhase } from "#app/phases/enemy-command-phase"; -import { MessagePhase } from "#app/phases/message-phase"; -import { TurnEndPhase } from "#app/phases/turn-end-phase"; import i18next, { initI18n } from "#app/plugins/i18n"; import { Abilities } from "#enums/abilities"; import { Moves } from "#enums/moves"; import { Species } from "#enums/species"; import GameManager from "#test/utils/gameManager"; +import { SPLASH_ONLY } from "#test/utils/testUtils"; import Phaser from "phaser"; import { afterEach, beforeAll, beforeEach, describe, expect, it } from "vitest"; +const TIMEOUT = 20 * 1000; describe("Items - Toxic orb", () => { let phaserGame: Phaser.Game; @@ -27,40 +26,34 @@ describe("Items - Toxic orb", () => { beforeEach(() => { game = new GameManager(phaserGame); - const moveToUse = Moves.GROWTH; - const oppMoveToUse = Moves.TACKLE; - game.override.battleType("single"); - game.override.enemySpecies(Species.RATTATA); - game.override.ability(Abilities.INSOMNIA); - game.override.enemyAbility(Abilities.INSOMNIA); - game.override.startingLevel(2000); - game.override.moveset([moveToUse]); - game.override.enemyMoveset([oppMoveToUse, oppMoveToUse, oppMoveToUse, oppMoveToUse]); - game.override.startingHeldItems([{ - name: "TOXIC_ORB", - }]); + game.override + .battleType("single") + .enemySpecies(Species.RATTATA) + .ability(Abilities.BALL_FETCH) + .enemyAbility(Abilities.BALL_FETCH) + .moveset([Moves.SPLASH]) + .enemyMoveset(SPLASH_ONLY) + .startingHeldItems([{ + name: "TOXIC_ORB", + }]); }); - it("TOXIC ORB", async () => { + it("badly poisons the holder", async () => { initI18n(); i18next.changeLanguage("en"); - const moveToUse = Moves.GROWTH; - await game.startBattle([ - Species.MIGHTYENA, - Species.MIGHTYENA, - ]); - expect(game.scene.modifiers[0].type.id).toBe("TOXIC_ORB"); + await game.classicMode.startBattle([Species.MIGHTYENA]); - game.move.select(moveToUse); + const player = game.scene.getPlayerField()[0]; - // will run the 13 phase from enemyCommandPhase to TurnEndPhase - await game.phaseInterceptor.runFrom(EnemyCommandPhase).to(TurnEndPhase); + game.move.select(Moves.SPLASH); + + await game.phaseInterceptor.to("TurnEndPhase"); // Toxic orb should trigger here - await game.phaseInterceptor.run(MessagePhase); + await game.phaseInterceptor.run("MessagePhase"); const message = game.textInterceptor.getLatestMessage(); expect(message).toContain("was badly poisoned by the Toxic Orb"); - expect(game.scene.getParty()[0].status!.effect).toBe(StatusEffect.TOXIC); + expect(player.status?.effect).toBe(StatusEffect.TOXIC); // Damage should not have ticked yet. - expect(game.scene.getParty()[0].status!.turnCount).toBe(0); - }, 2000); + expect(player.status?.turnCount).toBe(0); + }, TIMEOUT); }); From e14f9c7113ecb1b0bf531c950b29d4ea52a11f38 Mon Sep 17 00:00:00 2001 From: NightKev <34855794+DayKev@users.noreply.github.com> Date: Sat, 31 Aug 2024 23:09:13 -0700 Subject: [PATCH 3/8] Updates Toxic Spikes tests --- src/test/moves/toxic_spikes.test.ts | 317 +++++++--------------------- 1 file changed, 80 insertions(+), 237 deletions(-) diff --git a/src/test/moves/toxic_spikes.test.ts b/src/test/moves/toxic_spikes.test.ts index 2d7a10f21cd..51d26f3d080 100644 --- a/src/test/moves/toxic_spikes.test.ts +++ b/src/test/moves/toxic_spikes.test.ts @@ -1,18 +1,17 @@ -import { CommandPhase } from "#app/phases/command-phase"; -import GameManager from "#test/utils/gameManager"; +import { ArenaTagSide, ArenaTrapTag } from "#app/data/arena-tag"; +import { StatusEffect } from "#app/data/status-effect"; +import { decrypt, encrypt, GameData, SessionSaveData } from "#app/system/game-data"; import { Abilities } from "#enums/abilities"; +import { ArenaTagType } from "#enums/arena-tag-type"; 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 {StatusEffect} from "#app/data/status-effect"; -import {ArenaTagType} from "#enums/arena-tag-type"; -import {ArenaTrapTag} from "#app/data/arena-tag"; -import {decrypt, encrypt, GameData, SessionSaveData} from "#app/system/game-data"; +import { SPLASH_ONLY } from "../utils/testUtils"; +const TIMEOUT = 20 * 1000; - -// I am frankly, not that comfortable with the testing system, these could prob be more precise describe("Moves - Toxic Spikes", () => { let phaserGame: Phaser.Game; let game: GameManager; @@ -29,266 +28,110 @@ describe("Moves - Toxic Spikes", () => { beforeEach(() => { game = new GameManager(phaserGame); - game.scene.battleStyle = 1; - game.override.battleType("single"); - game.override.enemySpecies(Species.RATTATA); - game.override.enemyAbility(Abilities.HYDRATION); - game.override.enemyPassiveAbility(Abilities.HYDRATION); - game.override.ability(Abilities.NO_GUARD); - game.override.passiveAbility(Abilities.HUGE_POWER); - game.override.startingWave(3); - game.override.enemyMoveset([Moves.SPLASH,Moves.SPLASH,Moves.SPLASH,Moves.SPLASH]); - game.override.moveset([Moves.TOXIC_SPIKES,Moves.SPLASH, Moves.ROAR, Moves.GLACIAL_LANCE]); + game.override + .battleType("single") + .startingWave(5) + .enemySpecies(Species.RATTATA) + .enemyAbility(Abilities.BALL_FETCH) + .ability(Abilities.BALL_FETCH) + .enemyMoveset(SPLASH_ONLY) + .moveset([Moves.TOXIC_SPIKES, Moves.SPLASH, Moves.ROAR]); }); - it("no switches, no effect", async() => { - game.override.enemySpecies(Species.RATTATA); - // player set toxic spikes on the field and do splash for 3 turns - // opponent do splash for 4 turns - // nobody should take damage - await game.classicMode.runToSummon([ - Species.MIGHTYENA, - Species.POOCHYENA, - ]); - await game.phaseInterceptor.to(CommandPhase, true); - const initialHp = game.scene.getEnemyParty()[0].hp; - expect(game.scene.getEnemyParty()[0].hp).toBe(initialHp); - game.doAttack(0); - await game.toNextTurn(); - game.doAttack(1); - await game.toNextTurn(); - game.doAttack(1); - await game.toNextTurn(); - game.doAttack(1); - await game.toNextTurn(); - game.doAttack(1); - await game.toNextTurn(); - // Opponent should be full health and not statused - expect(game.scene.getEnemyParty()[0].hp).toBe(initialHp); - expect(!(game.scene.getEnemyParty()[0].status?.effect)); - console.log(game.textInterceptor.logs); - }, 20000); + it("should not affect the opponent if they do not switch", async() => { + await game.classicMode.runToSummon([Species.MIGHTYENA, Species.POOCHYENA]); - it("user switch, no effect", async() => { - game.override.enemySpecies(Species.RATTATA); - // player set toxic spikes on the field and switch back to back - // opponent do splash for 2 turns - // nobody should be poisoned or damaged - await game.classicMode.runToSummon([ - Species.MIGHTYENA, - Species.POOCHYENA, - ]); - await game.phaseInterceptor.to(CommandPhase, false); + const enemy = game.scene.getEnemyField()[0]; - const initialHp = game.scene.getParty()[0].hp; + game.move.select(Moves.TOXIC_SPIKES); + await game.phaseInterceptor.to("TurnEndPhase"); + game.move.select(Moves.SPLASH); + await game.phaseInterceptor.to("TurnEndPhase"); game.doSwitchPokemon(1); - await game.phaseInterceptor.run(CommandPhase); - await game.phaseInterceptor.to(CommandPhase, false); + await game.phaseInterceptor.to("TurnEndPhase"); - game.doSwitchPokemon(1); - await game.phaseInterceptor.run(CommandPhase); - await game.phaseInterceptor.to(CommandPhase, false); + expect(enemy.hp).toBe(enemy.getMaxHp()); + expect(enemy.status?.effect).toBeUndefined(); + }, TIMEOUT); - expect(game.scene.getParty()[0].hp).toBe(initialHp); - expect(!(game.scene.getEnemyParty()[0].status?.effect)); - }, 20000); + it("should poison the opponent if they switch into 1 layer", async() => { + await game.classicMode.runToSummon([Species.MIGHTYENA]); - it("force enemy switch - poisoned", async() => { - game.override.startingWave(5); - game.override.enemySpecies(Species.RATTATA); - // player sets 1 layer of toxic spikes - // then forces a switch with roar - // opponent should be damaged and poisoned at end of next turn - await game.classicMode.runToSummon([ - Species.MIGHTYENA, - Species.POOCHYENA, - ]); - await game.phaseInterceptor.to(CommandPhase, true); - game.doAttack(0); - await game.toNextTurn(); - game.doAttack(2); - await game.toNextTurn(); - const opponent = game.scene.currentBattle.enemyParty[0]; - expect(opponent.hp).toBeLessThan(opponent.getMaxHp()); - expect(game.scene.currentBattle.enemyParty[0].status?.effect).toBe(StatusEffect.POISON); - }, 20000); + game.move.select(Moves.TOXIC_SPIKES); + await game.phaseInterceptor.to("TurnEndPhase"); + game.move.select(Moves.ROAR); + await game.phaseInterceptor.to("TurnEndPhase"); - it("force enemy switch - toxic", async() => { - game.override.startingWave(5); - game.override.enemySpecies(Species.RATTATA); - // player sets 1 layer of toxic spikes - // then forces a switch with roar - // opponent should be damaged and poisoned at end of next turn - await game.classicMode.runToSummon([ - Species.MIGHTYENA, - Species.POOCHYENA, - ]); - await game.phaseInterceptor.to(CommandPhase, true); - game.doAttack(0); - await game.toNextTurn(); - game.doAttack(0); - await game.toNextTurn(); - game.doAttack(2); - await game.toNextTurn(); - const opponent = game.scene.currentBattle.enemyParty[0]; - expect(opponent.hp).toBeLessThan(opponent.getMaxHp()); - expect(game.scene.currentBattle.enemyParty[0].status?.effect).toBe(StatusEffect.TOXIC); - }, 20000); + const enemy = game.scene.getEnemyField()[0]; - it("enemy switch - poison", async() => { - game.override.startingWave(5); - game.override.startingLevel(5000); - game.override.enemySpecies(Species.RATTATA); - // turn 1: player set toxic spikes, opponent do splash - // turn 2: player do splash, opponent switch pokemon - // opponent pokemon should trigger spikes and get poisoned - await game.classicMode.runToSummon([ - Species.MIGHTYENA, - Species.POOCHYENA, - ]); - await game.phaseInterceptor.to(CommandPhase, true); + expect(enemy.hp).toBeLessThan(enemy.getMaxHp()); + expect(enemy.status?.effect).toBe(StatusEffect.POISON); + }, TIMEOUT); - game.doAttack(0); - await game.toNextTurn(); + it("should badly poison the opponent if they switch into 2 layers", async() => { + await game.classicMode.runToSummon([Species.MIGHTYENA]); - game.forceOpponentToSwitch(); - game.doAttack(1); - await game.toNextTurn(); - const opponent = game.scene.currentBattle.enemyParty[0]; - expect(opponent.hp).toBeLessThan(opponent.getMaxHp()); - expect(opponent.status?.effect).toBe(StatusEffect.POISON); - }, 20000); + game.move.select(Moves.TOXIC_SPIKES); + await game.phaseInterceptor.to("TurnEndPhase"); + game.move.select(Moves.TOXIC_SPIKES); + await game.phaseInterceptor.to("TurnEndPhase"); + game.move.select(Moves.ROAR); + await game.phaseInterceptor.to("TurnEndPhase"); - it("enemy switch - toxic", async() => { - game.override.startingWave(5); - game.override.startingLevel(5000); - game.override.enemySpecies(Species.RATTATA); - // turn 1: player set toxic spikes, opponent do splash - // turn 2: player do splash, opponent switch pokemon - // opponent pokemon should trigger spikes and get poisoned - await game.classicMode.runToSummon([ - Species.MIGHTYENA, - Species.POOCHYENA, - ]); - await game.phaseInterceptor.to(CommandPhase, true); + const enemy = game.scene.getEnemyField()[0]; + expect(enemy.hp).toBeLessThan(enemy.getMaxHp()); + expect(enemy.status?.effect).toBe(StatusEffect.TOXIC); + }, TIMEOUT); - game.doAttack(0); - await game.toNextTurn(); - - game.doAttack(0); - await game.toNextTurn(); - - game.forceOpponentToSwitch(); - game.doAttack(1); - await game.toNextTurn(); - const opponent = game.scene.currentBattle.enemyParty[0]; - expect(opponent.hp).toBeLessThan(opponent.getMaxHp()); - expect(opponent.status?.effect).toBe(StatusEffect.TOXIC); - }, 20000); - - it("grounded poison switch - absorb ", async() => { - game.override.startingWave(5); - game.override.startingLevel(5000); + it("should be removed if a grounded poison pokemon switches in", async() => { game.override.enemySpecies(Species.GRIMER); - // turn 1: player set toxic spikes, opponent do splash - // turn 2: player do splash, opponent switch pokemon - // opponent pokemon should trigger spikes and get poisoned - await game.classicMode.runToSummon([ - Species.MIGHTYENA, - Species.POOCHYENA, - ]); - await game.phaseInterceptor.to(CommandPhase, true); + await game.classicMode.runToSummon([Species.MIGHTYENA]); - game.doAttack(0); - await game.toNextTurn(); + game.move.select(Moves.TOXIC_SPIKES); + await game.phaseInterceptor.to("TurnEndPhase"); + game.move.select(Moves.TOXIC_SPIKES); + await game.phaseInterceptor.to("TurnEndPhase"); + game.move.select(Moves.ROAR); + await game.phaseInterceptor.to("TurnEndPhase"); - game.doAttack(0); - await game.toNextTurn(); + const enemy = game.scene.getEnemyField()[0]; + expect(enemy.hp).toBe(enemy.getMaxHp()); + expect(enemy.status?.effect).toBeUndefined(); - game.forceOpponentToSwitch(); - game.doAttack(1); - await game.toNextTurn(); - const opponent = game.scene.currentBattle.enemyParty[0]; + expect(game.scene.arena.tags.length).toBe(0); + }, TIMEOUT); - // Enemy pokemon should be undamaged, and not poisoned - expect(opponent.hp).toBe(opponent.getMaxHp()); - expect(!opponent.status?.effect); + it("shouldn't create multiple layers per use in doubles", async() => { + await game.classicMode.runToSummon([Species.MIGHTYENA, Species.POOCHYENA]); - // There should be no Arena Tags, as the Toxic Spikes have been absorbed - expect(game.scene.arena.tags.length === 0); - }, 20000); + game.move.select(Moves.TOXIC_SPIKES); + await game.phaseInterceptor.to("TurnEndPhase"); - it("doubles - one stack per use ", async() => { - game.override.startingWave(5); - game.override.startingLevel(5000); - game.override.enemySpecies(Species.GRIMER); - game.override.battleType("double"); - // turn 1: player set toxic spikes, verify one layer down - // turn 2: player set toxic spikes, verify two layers down - // turn 3: player do splash, opponent switch pokemon - // incoming grimer should absorb all layers - await game.classicMode.runToSummon([ - Species.MIGHTYENA, - Species.POOCHYENA, - ]); - await game.phaseInterceptor.to(CommandPhase, true); - game.doAttack(0); + const arenaTags = (game.scene.arena.getTagOnSide(ArenaTagType.TOXIC_SPIKES, ArenaTagSide.ENEMY) as ArenaTrapTag); + expect(arenaTags.tagType).toBe(ArenaTagType.TOXIC_SPIKES); + expect(arenaTags.layers).toBe(1); + }, TIMEOUT); - await game.phaseInterceptor.to(CommandPhase, true); - game.doAttack(1); - - await game.toNextTurn(); - - expect(game.scene.arena.tags[0].tagType).toBe(ArenaTagType.TOXIC_SPIKES); - expect(game.scene.arena.tags[0] instanceof ArenaTrapTag); - expect((game.scene.arena.tags[0] as ArenaTrapTag).layers).toBe(1); - - game.doAttack(0); - await game.phaseInterceptor.to(CommandPhase, true); - game.doAttack(1); - await game.toNextTurn(); - expect(game.scene.arena.tags[0].tagType).toBe(ArenaTagType.TOXIC_SPIKES); - expect(game.scene.arena.tags[0] instanceof ArenaTrapTag); - expect((game.scene.arena.tags[0] as ArenaTrapTag).layers).toBe(2); - - game.forceOpponentToSwitch(); - game.doAttack(1); - await game.toNextTurn(); - - // There should ben o Arena Tags, as the Toxic Spikes have been absorbed - expect(game.scene.arena.tags.length === 0); - }, 20000); - - it("persist through reload", async() => { - game.override.startingWave(5); - game.override.startingLevel(5000); - game.override.enemySpecies(Species.RATTATA); + it("should persist through reload", async() => { + game.override.startingWave(1); const scene = game.scene; const gameData = new GameData(scene); + await game.classicMode.runToSummon([Species.MIGHTYENA]); - // turn 1: player set toxic spikes, opponent do splash - // export, save, and then load session Data - // tags should not change through reload. - await game.classicMode.runToSummon([ - Species.MIGHTYENA, - Species.POOCHYENA, - ]); - await game.phaseInterceptor.to(CommandPhase, true); - - game.doAttack(0); - await game.toNextTurn(); + game.move.select(Moves.TOXIC_SPIKES); + await game.phaseInterceptor.to("TurnEndPhase"); + game.move.select(Moves.SPLASH); + await game.doKillOpponents(); + await game.phaseInterceptor.to("BattleEndPhase"); + await game.toNextWave(); const sessionData : SessionSaveData = gameData["getSessionSaveData"](game.scene); localStorage.setItem("sessionTestData", encrypt(JSON.stringify(sessionData), true)); const recoveredData : SessionSaveData = gameData.parseSessionData(decrypt(localStorage.getItem("sessionTestData")!, true)); - gameData.loadSession(game.scene,0,recoveredData); - + gameData.loadSession(game.scene, 0, recoveredData); expect(sessionData.arena.tags).toEqual(recoveredData.arena.tags); localStorage.removeItem("sessionTestData"); - }, 20000); - - - + }, TIMEOUT); }); From 839df96cde25a3a68d17dfae12a4c0299f5dca9d Mon Sep 17 00:00:00 2001 From: NightKev <34855794+DayKev@users.noreply.github.com> Date: Sat, 31 Aug 2024 23:12:52 -0700 Subject: [PATCH 4/8] Apply suggestions from code review --- src/phases/check-status-effect-phase.ts | 6 +++--- src/phases/move-effect-phase.ts | 2 +- src/phases/turn-start-phase.ts | 2 +- src/system/arena-data.ts | 2 +- src/system/game-data.ts | 4 ++-- 5 files changed, 8 insertions(+), 8 deletions(-) diff --git a/src/phases/check-status-effect-phase.ts b/src/phases/check-status-effect-phase.ts index 8dafdfed227..44918b54966 100644 --- a/src/phases/check-status-effect-phase.ts +++ b/src/phases/check-status-effect-phase.ts @@ -1,6 +1,6 @@ -import {PostTurnStatusEffectPhase} from "#app/phases/post-turn-status-effect-phase"; -import {Phase} from "#app/phase"; -import {BattlerIndex} from "#app/battle"; +import { PostTurnStatusEffectPhase } from "#app/phases/post-turn-status-effect-phase"; +import { Phase } from "#app/phase"; +import { BattlerIndex } from "#app/battle"; import BattleScene from "#app/battle-scene"; export class CheckStatusEffectPhase extends Phase { diff --git a/src/phases/move-effect-phase.ts b/src/phases/move-effect-phase.ts index 8bfde0e82c9..51068fa39d5 100644 --- a/src/phases/move-effect-phase.ts +++ b/src/phases/move-effect-phase.ts @@ -14,7 +14,7 @@ import { PokemonMultiHitModifier, FlinchChanceModifier, EnemyAttackStatusEffectC import i18next from "i18next"; import * as Utils from "#app/utils.js"; import { PokemonPhase } from "./pokemon-phase"; -import {Abilities} from "#enums/abilities"; +import { Abilities } from "#enums/abilities"; export class MoveEffectPhase extends PokemonPhase { public move: PokemonMove; diff --git a/src/phases/turn-start-phase.ts b/src/phases/turn-start-phase.ts index fc0bcf8485b..0fc05968bbd 100644 --- a/src/phases/turn-start-phase.ts +++ b/src/phases/turn-start-phase.ts @@ -16,7 +16,7 @@ import { MovePhase } from "./move-phase"; import { SwitchSummonPhase } from "./switch-summon-phase"; import { TurnEndPhase } from "./turn-end-phase"; import { WeatherEffectPhase } from "./weather-effect-phase"; -import {CheckStatusEffectPhase} from "#app/phases/check-status-effect-phase"; +import { CheckStatusEffectPhase } from "#app/phases/check-status-effect-phase"; export class TurnStartPhase extends FieldPhase { constructor(scene: BattleScene) { diff --git a/src/system/arena-data.ts b/src/system/arena-data.ts index 682ade58e48..d011dec4af5 100644 --- a/src/system/arena-data.ts +++ b/src/system/arena-data.ts @@ -1,5 +1,5 @@ import { Arena } from "../field/arena"; -import {ArenaTag, loadArenaTag} from "../data/arena-tag"; +import { ArenaTag, loadArenaTag } from "../data/arena-tag"; import { Biome } from "#enums/biome"; import { Weather } from "../data/weather"; import { Terrain } from "#app/data/terrain.js"; diff --git a/src/system/game-data.ts b/src/system/game-data.ts index 03f18922384..42b20b5be35 100644 --- a/src/system/game-data.ts +++ b/src/system/game-data.ts @@ -45,8 +45,8 @@ import { TerrainType } from "#app/data/terrain.js"; import { OutdatedPhase } from "#app/phases/outdated-phase.js"; import { ReloadSessionPhase } from "#app/phases/reload-session-phase.js"; import { RUN_HISTORY_LIMIT } from "#app/ui/run-history-ui-handler"; -import {TagAddedEvent} from "#app/events/arena"; -import {ArenaTrapTag} from "#app/data/arena-tag"; +import { TagAddedEvent } from "#app/events/arena"; +import { ArenaTrapTag } from "#app/data/arena-tag"; export const defaultStarterSpecies: Species[] = [ Species.BULBASAUR, Species.CHARMANDER, Species.SQUIRTLE, From e44c423aa8262122301744a3512df26b83b56b5e Mon Sep 17 00:00:00 2001 From: NightKev <34855794+DayKev@users.noreply.github.com> Date: Wed, 4 Sep 2024 19:38:24 -0700 Subject: [PATCH 5/8] Fix merge issues Replace `integer` with `number` in `arena-tag.ts` --- src/data/arena-tag.ts | 121 +++++++++++++++---------------- src/phases/turn-start-phase.ts | 2 +- src/test/moves/safeguard.test.ts | 17 +++-- 3 files changed, 67 insertions(+), 73 deletions(-) diff --git a/src/data/arena-tag.ts b/src/data/arena-tag.ts index 5570550c1b1..60eff2492b2 100644 --- a/src/data/arena-tag.ts +++ b/src/data/arena-tag.ts @@ -1,23 +1,23 @@ -import { Arena } from "../field/arena"; -import { Type } from "./type"; -import * as Utils from "../utils"; -import { MoveCategory, allMoves, MoveTarget, IncrementMovePriorityAttr, applyMoveAttrs } from "./move"; -import { getPokemonNameWithAffix } from "../messages"; -import Pokemon, { HitResult, PokemonMove } from "../field/pokemon"; -import { StatusEffect } from "./status-effect"; -import { BattlerIndex } from "../battle"; -import { BlockNonDirectDamageAbAttr, ChangeMovePriorityAbAttr, ProtectStatAbAttr, applyAbAttrs } from "./ability"; -import { Stat } from "#enums/stat"; -import { CommonAnim, CommonBattleAnim } from "./battle-anims"; -import i18next from "i18next"; -import { Abilities } from "#enums/abilities"; -import { ArenaTagType } from "#enums/arena-tag-type"; -import { BattlerTagType } from "#enums/battler-tag-type"; -import { Moves } from "#enums/moves"; +import { BattlerIndex } from "#app/battle"; +import { BlockNonDirectDamageAbAttr, ChangeMovePriorityAbAttr, ProtectStatAbAttr, applyAbAttrs } from "#app/data/ability"; +import { CommonAnim, CommonBattleAnim } from "#app/data/battle-anims"; +import { IncrementMovePriorityAttr, MoveCategory, MoveTarget, allMoves, applyMoveAttrs } from "#app/data/move"; +import { StatusEffect } from "#app/data/status-effect"; +import { Type } from "#app/data/type"; +import { Arena } from "#app/field/arena"; +import Pokemon, { HitResult, PokemonMove } from "#app/field/pokemon"; +import { getPokemonNameWithAffix } from "#app/messages"; import { MoveEffectPhase } from "#app/phases/move-effect-phase"; import { PokemonHealPhase } from "#app/phases/pokemon-heal-phase"; import { ShowAbilityPhase } from "#app/phases/show-ability-phase"; import { StatStageChangePhase } from "#app/phases/stat-stage-change-phase"; +import * as Utils from "#app/utils"; +import { Abilities } from "#enums/abilities"; +import { ArenaTagType } from "#enums/arena-tag-type"; +import { BattlerTagType } from "#enums/battler-tag-type"; +import { Moves } from "#enums/moves"; +import { Stat } from "#enums/stat"; +import i18next from "i18next"; export enum ArenaTagSide { BOTH, @@ -26,20 +26,13 @@ export enum ArenaTagSide { } export abstract class ArenaTag { - public tagType: ArenaTagType; - public turnCount: integer; - public sourceMove?: Moves; - public sourceId?: integer; - public side: ArenaTagSide; - - - constructor(tagType: ArenaTagType, turnCount: integer, sourceMove: Moves | undefined, sourceId?: integer, side: ArenaTagSide = ArenaTagSide.BOTH) { - this.tagType = tagType; - this.turnCount = turnCount; - this.sourceMove = sourceMove; - this.sourceId = sourceId; - this.side = side; - } + constructor( + public tagType: ArenaTagType, + public turnCount: number, + public sourceMove?: Moves, + public sourceId?: number, + public side: ArenaTagSide = ArenaTagSide.BOTH + ) {} apply(arena: Arena, args: any[]): boolean { return true; @@ -83,7 +76,7 @@ export abstract class ArenaTag { * Prevents Pokémon on the opposing side from lowering the stats of the Pokémon in the Mist. */ export class MistTag extends ArenaTag { - constructor(turnCount: integer, sourceId: integer, side: ArenaTagSide) { + constructor(turnCount: number, sourceId: number, side: ArenaTagSide) { super(ArenaTagType.MIST, turnCount, Moves.MIST, sourceId, side); } @@ -127,7 +120,7 @@ export class WeakenMoveScreenTag extends ArenaTag { * @param side - The side (player or enemy) the tag affects. * @param weakenedCategories - The categories of moves that are weakened by this tag. */ - constructor(tagType: ArenaTagType, turnCount: integer, sourceMove: Moves, sourceId: integer, side: ArenaTagSide, weakenedCategories: MoveCategory[]) { + constructor(tagType: ArenaTagType, turnCount: number, sourceMove: Moves, sourceId: number, side: ArenaTagSide, weakenedCategories: MoveCategory[]) { super(tagType, turnCount, sourceMove, sourceId, side); this.weakenedCategories = weakenedCategories; @@ -158,7 +151,7 @@ export class WeakenMoveScreenTag extends ArenaTag { * Used by {@linkcode Moves.REFLECT} */ class ReflectTag extends WeakenMoveScreenTag { - constructor(turnCount: integer, sourceId: integer, side: ArenaTagSide) { + constructor(turnCount: number, sourceId: number, side: ArenaTagSide) { super(ArenaTagType.REFLECT, turnCount, Moves.REFLECT, sourceId, side, [MoveCategory.PHYSICAL]); } @@ -174,7 +167,7 @@ class ReflectTag extends WeakenMoveScreenTag { * Used by {@linkcode Moves.LIGHT_SCREEN} */ class LightScreenTag extends WeakenMoveScreenTag { - constructor(turnCount: integer, sourceId: integer, side: ArenaTagSide) { + constructor(turnCount: number, sourceId: number, side: ArenaTagSide) { super(ArenaTagType.LIGHT_SCREEN, turnCount, Moves.LIGHT_SCREEN, sourceId, side, [MoveCategory.SPECIAL]); } @@ -190,7 +183,7 @@ class LightScreenTag extends WeakenMoveScreenTag { * Used by {@linkcode Moves.AURORA_VEIL} */ class AuroraVeilTag extends WeakenMoveScreenTag { - constructor(turnCount: integer, sourceId: integer, side: ArenaTagSide) { + constructor(turnCount: number, sourceId: number, side: ArenaTagSide) { super(ArenaTagType.AURORA_VEIL, turnCount, Moves.AURORA_VEIL, sourceId, side, [MoveCategory.SPECIAL, MoveCategory.PHYSICAL]); } @@ -213,7 +206,7 @@ export class ConditionalProtectTag extends ArenaTag { /** Does this apply to all moves, including those that ignore other forms of protection? */ protected ignoresBypass: boolean; - constructor(tagType: ArenaTagType, sourceMove: Moves, sourceId: integer, side: ArenaTagSide, condition: ProtectConditionFunc, ignoresBypass: boolean = false) { + constructor(tagType: ArenaTagType, sourceMove: Moves, sourceId: number, side: ArenaTagSide, condition: ProtectConditionFunc, ignoresBypass: boolean = false) { super(tagType, 1, sourceMove, sourceId, side); this.protectConditionFunc = condition; @@ -275,7 +268,7 @@ export class ConditionalProtectTag extends ArenaTag { */ const QuickGuardConditionFunc: ProtectConditionFunc = (arena, moveId) => { const move = allMoves[moveId]; - const priority = new Utils.IntegerHolder(move.priority); + const priority = new Utils.NumberHolder(move.priority); const effectPhase = arena.scene.getCurrentPhase(); if (effectPhase instanceof MoveEffectPhase) { @@ -291,7 +284,7 @@ const QuickGuardConditionFunc: ProtectConditionFunc = (arena, moveId) => { * Condition: The incoming move has increased priority. */ class QuickGuardTag extends ConditionalProtectTag { - constructor(sourceId: integer, side: ArenaTagSide) { + constructor(sourceId: number, side: ArenaTagSide) { super(ArenaTagType.QUICK_GUARD, Moves.QUICK_GUARD, sourceId, side, QuickGuardConditionFunc); } } @@ -322,7 +315,7 @@ const WideGuardConditionFunc: ProtectConditionFunc = (arena, moveId) : boolean = * can be an ally or enemy. */ class WideGuardTag extends ConditionalProtectTag { - constructor(sourceId: integer, side: ArenaTagSide) { + constructor(sourceId: number, side: ArenaTagSide) { super(ArenaTagType.WIDE_GUARD, Moves.WIDE_GUARD, sourceId, side, WideGuardConditionFunc); } } @@ -344,7 +337,7 @@ const MatBlockConditionFunc: ProtectConditionFunc = (arena, moveId) : boolean => * Condition: The incoming move is a Physical or Special attack move. */ class MatBlockTag extends ConditionalProtectTag { - constructor(sourceId: integer, side: ArenaTagSide) { + constructor(sourceId: number, side: ArenaTagSide) { super(ArenaTagType.MAT_BLOCK, Moves.MAT_BLOCK, sourceId, side, MatBlockConditionFunc); } @@ -382,7 +375,7 @@ const CraftyShieldConditionFunc: ProtectConditionFunc = (arena, moveId) => { * not target all Pokemon or sides of the field. */ class CraftyShieldTag extends ConditionalProtectTag { - constructor(sourceId: integer, side: ArenaTagSide) { + constructor(sourceId: number, side: ArenaTagSide) { super(ArenaTagType.CRAFTY_SHIELD, Moves.CRAFTY_SHIELD, sourceId, side, CraftyShieldConditionFunc, true); } } @@ -394,12 +387,12 @@ class CraftyShieldTag extends ConditionalProtectTag { export class NoCritTag extends ArenaTag { /** * Constructor method for the NoCritTag class - * @param turnCount `integer` the number of turns this effect lasts + * @param turnCount `number` the number of turns this effect lasts * @param sourceMove {@linkcode Moves} the move that created this effect - * @param sourceId `integer` the ID of the {@linkcode Pokemon} that created this effect + * @param sourceId `number` the ID of the {@linkcode Pokemon} that created this effect * @param side {@linkcode ArenaTagSide} the side to which this effect belongs */ - constructor(turnCount: integer, sourceMove: Moves, sourceId: integer, side: ArenaTagSide) { + constructor(turnCount: number, sourceMove: Moves, sourceId: number, side: ArenaTagSide) { super(ArenaTagType.NO_CRIT, turnCount, sourceMove, sourceId, side); } @@ -429,7 +422,7 @@ class WishTag extends ArenaTag { private triggerMessage: string; private healHp: number; - constructor(turnCount: integer, sourceId: integer, side: ArenaTagSide) { + constructor(turnCount: number, sourceId: number, side: ArenaTagSide) { super(ArenaTagType.WISH, turnCount, Moves.WISH, sourceId, side); } @@ -470,7 +463,7 @@ export class WeakenMoveTypeTag extends ArenaTag { * @param sourceMove - The move that created the tag. * @param sourceId - The ID of the source of the tag. */ - constructor(tagType: ArenaTagType, turnCount: integer, type: Type, sourceMove: Moves, sourceId: integer) { + constructor(tagType: ArenaTagType, turnCount: number, type: Type, sourceMove: Moves, sourceId: number) { super(tagType, turnCount, sourceMove, sourceId); this.weakenedType = type; @@ -491,7 +484,7 @@ export class WeakenMoveTypeTag extends ArenaTag { * Weakens Electric type moves for a set amount of turns, usually 5. */ class MudSportTag extends WeakenMoveTypeTag { - constructor(turnCount: integer, sourceId: integer) { + constructor(turnCount: number, sourceId: number) { super(ArenaTagType.MUD_SPORT, turnCount, Type.ELECTRIC, Moves.MUD_SPORT, sourceId); } @@ -509,7 +502,7 @@ class MudSportTag extends WeakenMoveTypeTag { * Weakens Fire type moves for a set amount of turns, usually 5. */ class WaterSportTag extends WeakenMoveTypeTag { - constructor(turnCount: integer, sourceId: integer) { + constructor(turnCount: number, sourceId: number) { super(ArenaTagType.WATER_SPORT, turnCount, Type.FIRE, Moves.WATER_SPORT, sourceId); } @@ -526,8 +519,8 @@ class WaterSportTag extends WeakenMoveTypeTag { * Abstract class to implement arena traps. */ export class ArenaTrapTag extends ArenaTag { - public layers: integer; - public maxLayers: integer; + public layers: number; + public maxLayers: number; /** * Creates a new instance of the ArenaTrapTag class. @@ -538,7 +531,7 @@ export class ArenaTrapTag extends ArenaTag { * @param side - The side (player or enemy) the tag affects. * @param maxLayers - The maximum amount of layers this tag can have. */ - constructor(tagType: ArenaTagType, sourceMove: Moves, sourceId: integer, side: ArenaTagSide, maxLayers: integer) { + constructor(tagType: ArenaTagType, sourceMove: Moves, sourceId: number, side: ArenaTagSide, maxLayers: number) { super(tagType, 0, sourceMove, sourceId, side); this.layers = 1; @@ -583,7 +576,7 @@ export class ArenaTrapTag extends ArenaTag { * in damage for 1, 2, or 3 layers of Spikes respectively if they are summoned into this trap. */ class SpikesTag extends ArenaTrapTag { - constructor(sourceId: integer, side: ArenaTagSide) { + constructor(sourceId: number, side: ArenaTagSide) { super(ArenaTagType.SPIKES, Moves.SPIKES, sourceId, side, 3); } @@ -627,7 +620,7 @@ class SpikesTag extends ArenaTrapTag { class ToxicSpikesTag extends ArenaTrapTag { private neutralized: boolean; - constructor(sourceId: integer, side: ArenaTagSide) { + constructor(sourceId: number, side: ArenaTagSide) { super(ArenaTagType.TOXIC_SPIKES, Moves.TOXIC_SPIKES, sourceId, side, 2); this.neutralized = false; } @@ -685,7 +678,7 @@ class ToxicSpikesTag extends ArenaTrapTag { class DelayedAttackTag extends ArenaTag { public targetIndex: BattlerIndex; - constructor(tagType: ArenaTagType, sourceMove: Moves | undefined, sourceId: integer, targetIndex: BattlerIndex) { + constructor(tagType: ArenaTagType, sourceMove: Moves | undefined, sourceId: number, targetIndex: BattlerIndex) { super(tagType, 3, sourceMove, sourceId); this.targetIndex = targetIndex; @@ -710,7 +703,7 @@ class DelayedAttackTag extends ArenaTag { * who is summoned into the trap, based on the Rock type's type effectiveness. */ class StealthRockTag extends ArenaTrapTag { - constructor(sourceId: integer, side: ArenaTagSide) { + constructor(sourceId: number, side: ArenaTagSide) { super(ArenaTagType.STEALTH_ROCK, Moves.STEALTH_ROCK, sourceId, side, 1); } @@ -786,7 +779,7 @@ class StealthRockTag extends ArenaTrapTag { * to any Pokémon who is summoned into this trap. */ class StickyWebTag extends ArenaTrapTag { - constructor(sourceId: integer, side: ArenaTagSide) { + constructor(sourceId: number, side: ArenaTagSide) { super(ArenaTagType.STICKY_WEB, Moves.STICKY_WEB, sourceId, side, 1); } @@ -820,7 +813,7 @@ class StickyWebTag extends ArenaTrapTag { * also reversing the turn order for all Pokémon on the field as well. */ export class TrickRoomTag extends ArenaTag { - constructor(turnCount: integer, sourceId: integer) { + constructor(turnCount: number, sourceId: number) { super(ArenaTagType.TRICK_ROOM, turnCount, Moves.TRICK_ROOM, sourceId); } @@ -848,7 +841,7 @@ export class TrickRoomTag extends ArenaTag { * {@linkcode Abilities.LEVITATE} for the duration of the arena tag, usually 5 turns. */ export class GravityTag extends ArenaTag { - constructor(turnCount: integer) { + constructor(turnCount: number) { super(ArenaTagType.GRAVITY, turnCount, Moves.GRAVITY); } @@ -872,7 +865,7 @@ export class GravityTag extends ArenaTag { * Applies this arena tag for 4 turns (including the turn the move was used). */ class TailwindTag extends ArenaTag { - constructor(turnCount: integer, sourceId: integer, side: ArenaTagSide) { + constructor(turnCount: number, sourceId: number, side: ArenaTagSide) { super(ArenaTagType.TAILWIND, turnCount, Moves.TAILWIND, sourceId, side); } @@ -910,7 +903,7 @@ class TailwindTag extends ArenaTag { * Doubles the prize money from trainers and money moves like {@linkcode Moves.PAY_DAY} and {@linkcode Moves.MAKE_IT_RAIN}. */ class HappyHourTag extends ArenaTag { - constructor(turnCount: integer, sourceId: integer, side: ArenaTagSide) { + constructor(turnCount: number, sourceId: number, side: ArenaTagSide) { super(ArenaTagType.HAPPY_HOUR, turnCount, Moves.HAPPY_HOUR, sourceId, side); } @@ -924,7 +917,7 @@ class HappyHourTag extends ArenaTag { } class SafeguardTag extends ArenaTag { - constructor(turnCount: integer, sourceId: integer, side: ArenaTagSide) { + constructor(turnCount: number, sourceId: number, side: ArenaTagSide) { super(ArenaTagType.SAFEGUARD, turnCount, Moves.SAFEGUARD, sourceId, side); } @@ -937,14 +930,14 @@ class SafeguardTag extends ArenaTag { } } - class NoneTag extends ArenaTag { constructor() { - super(ArenaTagType.NONE, 0, undefined, undefined); + super(ArenaTagType.NONE, 0); } } -export function getArenaTag(tagType: ArenaTagType, turnCount: integer, sourceMove: Moves | undefined, sourceId: integer, targetIndex?: BattlerIndex, side: ArenaTagSide = ArenaTagSide.BOTH): ArenaTag | null { +// TODO: swap `sourceMove` and `sourceId` and make `sourceMove` an optional parameter +export function getArenaTag(tagType: ArenaTagType, turnCount: number, sourceMove: Moves | undefined, sourceId: number, targetIndex?: BattlerIndex, side: ArenaTagSide = ArenaTagSide.BOTH): ArenaTag | null { switch (tagType) { case ArenaTagType.MIST: return new MistTag(turnCount, sourceId, side); diff --git a/src/phases/turn-start-phase.ts b/src/phases/turn-start-phase.ts index df851d857d1..40b5b57aac5 100644 --- a/src/phases/turn-start-phase.ts +++ b/src/phases/turn-start-phase.ts @@ -208,7 +208,7 @@ export class TurnStartPhase extends FieldPhase { this.scene.pushPhase(new WeatherEffectPhase(this.scene)); /** Add a new phase to check who should be taking status damage */ - this.scene.pushPhase(new CheckStatusEffectPhase(this.scene, order)); + this.scene.pushPhase(new CheckStatusEffectPhase(this.scene, moveOrder)); this.scene.pushPhase(new BerryPhase(this.scene)); this.scene.pushPhase(new TurnEndPhase(this.scene)); diff --git a/src/test/moves/safeguard.test.ts b/src/test/moves/safeguard.test.ts index 94a7aa6031e..27e764f75cf 100644 --- a/src/test/moves/safeguard.test.ts +++ b/src/test/moves/safeguard.test.ts @@ -38,7 +38,7 @@ describe("Moves - Safeguard", () => { }); it("protects from damaging moves with additional effects", async () => { - await game.startBattle(); + await game.classicMode.startBattle(); const enemy = game.scene.getEnemyPokemon()!; game.move.select(Moves.NUZZLE); @@ -49,7 +49,7 @@ describe("Moves - Safeguard", () => { }, TIMEOUT); it("protects from status moves", async () => { - await game.startBattle(); + await game.classicMode.startBattle(); const enemyPokemon = game.scene.getEnemyPokemon()!; game.move.select(Moves.SPORE); @@ -61,7 +61,7 @@ describe("Moves - Safeguard", () => { it("protects from confusion", async () => { game.override.moveset([Moves.CONFUSE_RAY]); - await game.startBattle(); + await game.classicMode.startBattle(); const enemyPokemon = game.scene.getEnemyPokemon()!; game.move.select(Moves.CONFUSE_RAY); @@ -74,7 +74,7 @@ describe("Moves - Safeguard", () => { it("protects ally from status", async () => { game.override.battleType("double"); - await game.startBattle(); + await game.classicMode.startBattle(); game.move.select(Moves.SPORE, 0, BattlerIndex.ENEMY_2); game.move.select(Moves.NUZZLE, 1, BattlerIndex.ENEMY_2); @@ -90,7 +90,7 @@ describe("Moves - Safeguard", () => { }, TIMEOUT); it("protects from Yawn", async () => { - await game.startBattle(); + await game.classicMode.startBattle(); const enemyPokemon = game.scene.getEnemyPokemon()!; game.move.select(Moves.YAWN); @@ -101,7 +101,7 @@ describe("Moves - Safeguard", () => { }, TIMEOUT); it("doesn't protect from already existing Yawn", async () => { - await game.startBattle(); + await game.classicMode.startBattle(); const enemyPokemon = game.scene.getEnemyPokemon()!; game.move.select(Moves.YAWN); @@ -116,10 +116,11 @@ describe("Moves - Safeguard", () => { it("doesn't protect from self-inflicted via Rest or Flame Orb", async () => { game.override.enemyHeldItems([{name: "FLAME_ORB"}]); - await game.startBattle(); + await game.classicMode.startBattle(); const enemyPokemon = game.scene.getEnemyPokemon()!; game.move.select(Moves.SPLASH); + enemyPokemon.damageAndUpdate(1); await game.setTurnOrder([BattlerIndex.ENEMY, BattlerIndex.PLAYER]); await game.toNextTurn(); @@ -135,7 +136,7 @@ describe("Moves - Safeguard", () => { it("protects from ability-inflicted status", async () => { game.override.ability(Abilities.STATIC); vi.spyOn(allAbilities[Abilities.STATIC].getAttrs(PostDefendContactApplyStatusEffectAbAttr)[0], "chance", "get").mockReturnValue(100); - await game.startBattle(); + await game.classicMode.startBattle(); const enemyPokemon = game.scene.getEnemyPokemon()!; game.move.select(Moves.SPLASH); From 89b5893f82dfa97ef061e21fed46b8d40a700eb5 Mon Sep 17 00:00:00 2001 From: NightKev <34855794+DayKev@users.noreply.github.com> Date: Wed, 4 Sep 2024 19:46:34 -0700 Subject: [PATCH 6/8] Remove partial Magic Bounce implementation --- src/phases/move-effect-phase.ts | 16 ---------------- 1 file changed, 16 deletions(-) diff --git a/src/phases/move-effect-phase.ts b/src/phases/move-effect-phase.ts index 51068fa39d5..1670e1e192c 100644 --- a/src/phases/move-effect-phase.ts +++ b/src/phases/move-effect-phase.ts @@ -14,7 +14,6 @@ import { PokemonMultiHitModifier, FlinchChanceModifier, EnemyAttackStatusEffectC import i18next from "i18next"; import * as Utils from "#app/utils.js"; import { PokemonPhase } from "./pokemon-phase"; -import { Abilities } from "#enums/abilities"; export class MoveEffectPhase extends PokemonPhase { public move: PokemonMove; @@ -125,10 +124,6 @@ export class MoveEffectPhase extends PokemonPhase { /** Has the move successfully hit a target (for damage) yet? */ let hasHit: boolean = false; - /** Hazards bounce if any opposing Pokémon has the ability **/ - /** TODO: Update to support future Magic Coat / Magic Bounce implementation **/ - let hazardBounce: boolean = false; - for (const target of targets) { /** * If the move missed a target, stop all future hits against that target @@ -163,17 +158,6 @@ export class MoveEffectPhase extends PokemonPhase { /** Does this phase represent the invoked move's first strike? */ const firstHit = (user.turnData.hitsLeft === user.turnData.hitCount); - /** Check if current target is affected by Magic Bounce or used Magic Coat - * If they did, then field hazards should be bounced back in their entirety - */ - const bounceStatus = target.hasAbility(Abilities.MAGIC_BOUNCE) || (this.scene.currentBattle.turnCommands[target.getBattlerIndex()]?.move?.move === Moves.MAGIC_COAT); - hazardBounce = hazardBounce && bounceStatus; - - // Prevent ENEMY_SIDE targeted moves from occurring twice in double battles - if (move.moveTarget === MoveTarget.ENEMY_SIDE && target !== targets[targets.length-1]) { - continue; - } - // Only log the move's result on the first strike if (firstHit) { user.pushMoveHistory(moveHistoryEntry); From 50fec80db5c1ec79b448abf1ef32e050343b2a0b Mon Sep 17 00:00:00 2001 From: NightKev <34855794+DayKev@users.noreply.github.com> Date: Wed, 4 Sep 2024 19:49:16 -0700 Subject: [PATCH 7/8] Remove stray newline --- src/phases/move-effect-phase.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/src/phases/move-effect-phase.ts b/src/phases/move-effect-phase.ts index 1670e1e192c..f100a763219 100644 --- a/src/phases/move-effect-phase.ts +++ b/src/phases/move-effect-phase.ts @@ -123,7 +123,6 @@ export class MoveEffectPhase extends PokemonPhase { new MoveAnim(move.id as Moves, user, this.getTarget()?.getBattlerIndex()!).play(this.scene, () => { // TODO: is the bang correct here? /** Has the move successfully hit a target (for damage) yet? */ let hasHit: boolean = false; - for (const target of targets) { /** * If the move missed a target, stop all future hits against that target From b4ef315e5ec3a89ba4586dd32e18d62d54fb228f Mon Sep 17 00:00:00 2001 From: NightKev <34855794+DayKev@users.noreply.github.com> Date: Wed, 11 Sep 2024 15:01:30 -0700 Subject: [PATCH 8/8] Remove extra change in safeguard test --- src/test/moves/safeguard.test.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/src/test/moves/safeguard.test.ts b/src/test/moves/safeguard.test.ts index 5e0faf8bfe9..2caf698a73a 100644 --- a/src/test/moves/safeguard.test.ts +++ b/src/test/moves/safeguard.test.ts @@ -120,7 +120,6 @@ describe("Moves - Safeguard", () => { const enemyPokemon = game.scene.getEnemyPokemon()!; game.move.select(Moves.SPLASH); - enemyPokemon.damageAndUpdate(1); await game.setTurnOrder([BattlerIndex.ENEMY, BattlerIndex.PLAYER]); await game.toNextTurn(); enemyPokemon.damageAndUpdate(1);