From 751d824af8bd40c678bb7b28084e096324635065 Mon Sep 17 00:00:00 2001 From: Bertie690 Date: Tue, 5 Aug 2025 17:17:05 -0400 Subject: [PATCH] Cleaned up Stockpile tests --- src/data/battler-tags.ts | 45 ++++---- src/data/moves/move.ts | 7 +- test/moves/stockpile.test.ts | 169 +++++++++++++++-------------- test/moves/swallow-spit-up.test.ts | 65 +++++------ 4 files changed, 142 insertions(+), 144 deletions(-) diff --git a/src/data/battler-tags.ts b/src/data/battler-tags.ts index 2c9193b3647..9e921e0f93b 100644 --- a/src/data/battler-tags.ts +++ b/src/data/battler-tags.ts @@ -2698,29 +2698,30 @@ export class StockpilingTag extends SerializableBattlerTag { * For each stat, an internal counter is incremented (by 1) if the stat was successfully changed. */ onAdd(pokemon: Pokemon): void { - if (this.stockpiledCount < 3) { - this.stockpiledCount++; - - globalScene.phaseManager.queueMessage( - i18next.t("battlerTags:stockpilingOnAdd", { - pokemonNameWithAffix: getPokemonNameWithAffix(pokemon), - stockpiledCount: this.stockpiledCount, - }), - ); - - // Attempt to increase DEF and SPDEF by one stage, keeping track of successful changes. - globalScene.phaseManager.unshiftNew( - "StatStageChangePhase", - pokemon.getBattlerIndex(), - true, - [Stat.SPDEF, Stat.DEF], - 1, - true, - false, - true, - this.onStatStagesChanged, - ); + if (this.stockpiledCount >= 3) { + return; } + this.stockpiledCount++; + + globalScene.phaseManager.queueMessage( + i18next.t("battlerTags:stockpilingOnAdd", { + pokemonNameWithAffix: getPokemonNameWithAffix(pokemon), + stockpiledCount: this.stockpiledCount, + }), + ); + + // Attempt to increase DEF and SPDEF by one stage, keeping track of successful changes. + globalScene.phaseManager.unshiftNew( + "StatStageChangePhase", + pokemon.getBattlerIndex(), + true, + [Stat.SPDEF, Stat.DEF], + 1, + true, + false, + true, + this.onStatStagesChanged, + ); } onOverlap(pokemon: Pokemon): void { diff --git a/src/data/moves/move.ts b/src/data/moves/move.ts index ab284ba792f..7ef924d902f 100644 --- a/src/data/moves/move.ts +++ b/src/data/moves/move.ts @@ -2038,8 +2038,9 @@ export class VariableHealAttr extends HealAttr { private healFunc: (user: Pokemon, target: Pokemon, move: Move) => number, showAnim = false, selfTarget = true, + failOnFullHp = true, ) { - super(1, showAnim, selfTarget); + super(1, showAnim, selfTarget, selfTarget); this.healFunc = healFunc; } @@ -9293,14 +9294,14 @@ export function initMoves() { .partial(), // Does not lock the user, does not stop Pokemon from sleeping // Likely can make use of FrenzyAttr and an ArenaTag (just without the FrenzyMissFunc) new SelfStatusMove(MoveId.STOCKPILE, PokemonType.NORMAL, -1, 20, -1, 0, 3) - .condition(user => (user.getTag(StockpilingTag)?.stockpiledCount ?? 0) < 3) + .condition(user => (user.getTag(BattlerTagType.STOCKPILING)?.stockpiledCount ?? 0) < 3) .attr(AddBattlerTagAttr, BattlerTagType.STOCKPILING, true), new AttackMove(MoveId.SPIT_UP, PokemonType.NORMAL, MoveCategory.SPECIAL, -1, 100, 10, -1, 0, 3) .attr(SpitUpPowerAttr, 100) .condition(hasStockpileStacksCondition) .attr(RemoveBattlerTagAttr, [ BattlerTagType.STOCKPILING ], true), new SelfStatusMove(MoveId.SWALLOW, PokemonType.NORMAL, -1, 10, -1, 0, 3) - .attr(VariableHealAttr, swallowHealFunc) + .attr(VariableHealAttr, swallowHealFunc, false, true, true) .condition(hasStockpileStacksCondition) .attr(RemoveBattlerTagAttr, [ BattlerTagType.STOCKPILING ], true) .triageMove(), diff --git a/test/moves/stockpile.test.ts b/test/moves/stockpile.test.ts index 2da1285ace2..2cae948a6c5 100644 --- a/test/moves/stockpile.test.ts +++ b/test/moves/stockpile.test.ts @@ -1,109 +1,110 @@ -import { StockpilingTag } from "#data/battler-tags"; import { AbilityId } from "#enums/ability-id"; +import { BattlerTagType } from "#enums/battler-tag-type"; import { MoveId } from "#enums/move-id"; import { MoveResult } from "#enums/move-result"; import { SpeciesId } from "#enums/species-id"; import { Stat } from "#enums/stat"; -import { TurnInitPhase } from "#phases/turn-init-phase"; import { GameManager } from "#test/test-utils/game-manager"; import Phaser from "phaser"; import { afterEach, beforeAll, beforeEach, describe, expect, it } from "vitest"; describe("Moves - Stockpile", () => { - describe("integration tests", () => { - let phaserGame: Phaser.Game; - let game: GameManager; + let phaserGame: Phaser.Game; + let game: GameManager; - beforeAll(() => { - phaserGame = new Phaser.Game({ type: Phaser.HEADLESS }); - }); + beforeAll(() => { + phaserGame = new Phaser.Game({ type: Phaser.HEADLESS }); + }); - afterEach(() => { - game.phaseInterceptor.restoreOg(); - }); + afterEach(() => { + game.phaseInterceptor.restoreOg(); + }); - beforeEach(() => { - game = new GameManager(phaserGame); + beforeEach(() => { + game = new GameManager(phaserGame); - game.override - .battleStyle("single") - .enemySpecies(SpeciesId.RATTATA) - .enemyMoveset(MoveId.SPLASH) - .enemyAbility(AbilityId.NONE) - .startingLevel(2000) - .moveset([MoveId.STOCKPILE, MoveId.SPLASH]) - .ability(AbilityId.NONE); - }); + game.override + .battleStyle("single") + .enemySpecies(SpeciesId.RATTATA) + .enemyMoveset(MoveId.SPLASH) + .enemyAbility(AbilityId.BALL_FETCH) + .startingLevel(2000) + .ability(AbilityId.BALL_FETCH); + }); - it("gains a stockpile stack and raises user's DEF and SPDEF stat stages by 1 on each use, fails at max stacks (3)", async () => { - await game.classicMode.startBattle([SpeciesId.ABOMASNOW]); + it("should gain a stockpile stack and raise DEF and SPDEF when used, up to 3 times", async () => { + await game.classicMode.startBattle([SpeciesId.ABOMASNOW]); - const user = game.field.getPlayerPokemon(); + const user = game.field.getPlayerPokemon(); - // Unfortunately, Stockpile stacks are not directly queryable (i.e. there is no pokemon.getStockpileStacks()), - // we just have to know that they're implemented as a BattlerTag. + expect(user).toHaveStatStage(Stat.DEF, 0); + expect(user).toHaveStatStage(Stat.SPDEF, 0); - expect(user.getTag(StockpilingTag)).toBeUndefined(); - expect(user.getStatStage(Stat.DEF)).toBe(0); - expect(user.getStatStage(Stat.SPDEF)).toBe(0); + // use Stockpile thrice + for (let i = 0; i < 3; i++) { + game.move.use(MoveId.STOCKPILE); + await game.toNextTurn(); - // use Stockpile four times - for (let i = 0; i < 4; i++) { - game.move.select(MoveId.STOCKPILE); - await game.toNextTurn(); - - const stockpilingTag = user.getTag(StockpilingTag)!; - - if (i < 3) { - // first three uses should behave normally - expect(user.getStatStage(Stat.DEF)).toBe(i + 1); - expect(user.getStatStage(Stat.SPDEF)).toBe(i + 1); - expect(stockpilingTag).toBeDefined(); - expect(stockpilingTag.stockpiledCount).toBe(i + 1); - } else { - // fourth should have failed - expect(user.getStatStage(Stat.DEF)).toBe(3); - expect(user.getStatStage(Stat.SPDEF)).toBe(3); - expect(stockpilingTag).toBeDefined(); - expect(stockpilingTag.stockpiledCount).toBe(3); - expect(user.getMoveHistory().at(-1)).toMatchObject({ - result: MoveResult.FAIL, - move: MoveId.STOCKPILE, - targets: [user.getBattlerIndex()], - }); - } - } - }); - - it("gains a stockpile stack even if user's DEF and SPDEF stat stages are at +6", async () => { - await game.classicMode.startBattle([SpeciesId.ABOMASNOW]); - - const user = game.field.getPlayerPokemon(); - - user.setStatStage(Stat.DEF, 6); - user.setStatStage(Stat.SPDEF, 6); - - expect(user.getTag(StockpilingTag)).toBeUndefined(); - expect(user.getStatStage(Stat.DEF)).toBe(6); - expect(user.getStatStage(Stat.SPDEF)).toBe(6); - - game.move.select(MoveId.STOCKPILE); - await game.phaseInterceptor.to(TurnInitPhase); - - const stockpilingTag = user.getTag(StockpilingTag)!; + const stockpilingTag = user.getTag(BattlerTagType.STOCKPILING)!; expect(stockpilingTag).toBeDefined(); - expect(stockpilingTag.stockpiledCount).toBe(1); - expect(user.getStatStage(Stat.DEF)).toBe(6); - expect(user.getStatStage(Stat.SPDEF)).toBe(6); + expect(stockpilingTag.stockpiledCount).toBe(i + 1); + expect(user).toHaveStatStage(Stat.DEF, i + 1); + expect(user).toHaveStatStage(Stat.SPDEF, i + 1); + } + }); - game.move.select(MoveId.STOCKPILE); - await game.phaseInterceptor.to(TurnInitPhase); + it("should fail when used at max stacks", async () => { + await game.classicMode.startBattle([SpeciesId.ABOMASNOW]); - const stockpilingTagAgain = user.getTag(StockpilingTag)!; - expect(stockpilingTagAgain).toBeDefined(); - expect(stockpilingTagAgain.stockpiledCount).toBe(2); - expect(user.getStatStage(Stat.DEF)).toBe(6); - expect(user.getStatStage(Stat.SPDEF)).toBe(6); + const user = game.field.getPlayerPokemon(); + + user.addTag(BattlerTagType.STOCKPILING); + user.addTag(BattlerTagType.STOCKPILING); + user.addTag(BattlerTagType.STOCKPILING); + + const stockpilingTag = user.getTag(BattlerTagType.STOCKPILING)!; + expect(stockpilingTag).toBeDefined(); + expect(stockpilingTag.stockpiledCount).toBe(3); + + game.move.use(MoveId.STOCKPILE); + await game.toNextTurn(); + + // should have failed + expect(user).toHaveStatStage(Stat.DEF, 3); + expect(user).toHaveStatStage(Stat.SPDEF, 3); + expect(stockpilingTag.stockpiledCount).toBe(3); + expect(user).toHaveUsedMove({ + move: MoveId.STOCKPILE, + result: MoveResult.FAIL, }); }); + + it("gains a stockpile stack even if user's DEF and SPDEF stat stages are at +6", async () => { + await game.classicMode.startBattle([SpeciesId.ABOMASNOW]); + + const user = game.field.getPlayerPokemon(); + + user.setStatStage(Stat.DEF, 6); + user.setStatStage(Stat.SPDEF, 6); + + expect(user).not.toHaveBattlerTag(BattlerTagType.STOCKPILING); + + game.move.use(MoveId.STOCKPILE); + await game.toNextTurn(); + + const stockpilingTag = user.getTag(BattlerTagType.STOCKPILING)!; + expect(stockpilingTag).toBeDefined(); + expect(stockpilingTag.stockpiledCount).toBe(1); + expect(user).toHaveStatStage(Stat.DEF, 6); + expect(user).toHaveStatStage(Stat.SPDEF, 6); + + game.move.use(MoveId.STOCKPILE); + await game.toNextTurn(); + + const stockpilingTagAgain = user.getTag(BattlerTagType.STOCKPILING)!; + expect(stockpilingTagAgain).toBeDefined(); + expect(stockpilingTagAgain.stockpiledCount).toBe(2); + expect(user).toHaveStatStage(Stat.DEF, 6); + expect(user).toHaveStatStage(Stat.SPDEF, 6); + }); }); diff --git a/test/moves/swallow-spit-up.test.ts b/test/moves/swallow-spit-up.test.ts index 6784d73c9e8..0324c13aa3e 100644 --- a/test/moves/swallow-spit-up.test.ts +++ b/test/moves/swallow-spit-up.test.ts @@ -63,8 +63,8 @@ describe("Moves - Swallow & Spit Up - ", () => { game.move.use(MoveId.SWALLOW); await game.toEndOfTurn(); - expect(swalot.getHpRatio()).toBeCloseTo(healPercent / 100, 1); - expect(swalot.getTag(StockpilingTag)).toBeUndefined(); + expect(swalot).toHaveHp((swalot.getMaxHp() * healPercent) / 100 + 1); + expect(swalot).not.toHaveBattlerTag(BattlerTagType.STOCKPILING); }, ); @@ -74,40 +74,38 @@ describe("Moves - Swallow & Spit Up - ", () => { const player = game.field.getPlayerPokemon(); player.hp = 1; - const stockpilingTag = player.getTag(StockpilingTag)!; - expect(stockpilingTag).toBeUndefined(); + expect(player).not.toHaveBattlerTag(BattlerTagType.STOCKPILING); game.move.use(MoveId.SWALLOW); await game.toEndOfTurn(); - expect(player.getLastXMoves()[0]).toMatchObject({ + expect(player).toHaveUsedMove({ move: MoveId.SWALLOW, result: MoveResult.FAIL, }); }); - // TODO: Does this consume stacks or not? - it.todo("should fail and display message at full HP, consuming stacks", async () => { + it("should count as a success and consume stacks despite displaying message at full HP", async () => { await game.classicMode.startBattle([SpeciesId.SWALOT]); const swalot = game.field.getPlayerPokemon(); swalot.addTag(BattlerTagType.STOCKPILING); - const stockpilingTag = swalot.getTag(StockpilingTag)!; - expect(stockpilingTag).toBeDefined(); + expect(swalot).toHaveBattlerTag(BattlerTagType.STOCKPILING); game.move.use(MoveId.SWALLOW); await game.toEndOfTurn(); - expect(swalot.getLastXMoves()[0]).toMatchObject({ + // Swallow counted as a "success" as its other effect (removing Stockpile) _did_ work + expect(swalot).toHaveUsedMove({ move: MoveId.SWALLOW, - result: MoveResult.FAIL, + result: MoveResult.SUCCESS, }); expect(game.textInterceptor.logs).toContain( i18next.t("battle:hpIsFull", { pokemonName: getPokemonNameWithAffix(swalot), }), ); - expect(stockpilingTag).toBeDefined(); + expect(swalot).not.toHaveBattlerTag(BattlerTagType.STOCKPILING); }); }); @@ -147,13 +145,12 @@ describe("Moves - Swallow & Spit Up - ", () => { const player = game.field.getPlayerPokemon(); - const stockpilingTag = player.getTag(StockpilingTag)!; - expect(stockpilingTag).toBeUndefined(); + expect(player).not.toHaveBattlerTag(BattlerTagType.STOCKPILING); game.move.use(MoveId.SPIT_UP); await game.toEndOfTurn(); - expect(player.getLastXMoves()[0]).toMatchObject({ + expect(player).toHaveUsedMove({ move: MoveId.SPIT_UP, result: MoveResult.FAIL, }); @@ -162,28 +159,27 @@ describe("Moves - Swallow & Spit Up - ", () => { describe("Stockpile stack removal", () => { it("should undo stat boosts when losing stacks", async () => { - await game.classicMode.startBattle([SpeciesId.ABOMASNOW]); + await game.classicMode.startBattle([SpeciesId.SWALOT]); const player = game.field.getPlayerPokemon(); - player.hp = 1; game.move.use(MoveId.STOCKPILE); await game.toNextTurn(); - const stockpilingTag = player.getTag(StockpilingTag)!; - expect(stockpilingTag).toBeDefined(); - expect(player.getStatStage(Stat.DEF)).toBe(1); - expect(player.getStatStage(Stat.SPDEF)).toBe(1); + expect(player).toHaveBattlerTag(BattlerTagType.STOCKPILING); + expect(player).toHaveStatStage(Stat.DEF, 1); + expect(player).toHaveStatStage(Stat.SPDEF, 1); - // remove the prior stat boosts from the log + // remove the prior stat boost phases from the log game.phaseInterceptor.clearLogs(); + game.move.use(MoveId.SWALLOW); await game.move.forceEnemyMove(MoveId.ACID_SPRAY); await game.setTurnOrder([BattlerIndex.ENEMY, BattlerIndex.PLAYER]); await game.toEndOfTurn(); - expect(player.getStatStage(Stat.DEF)).toBe(0); - expect(player.getStatStage(Stat.SPDEF)).toBe(-2); // +1 --> -1 --> -2 + expect(player).toHaveStatStage(Stat.DEF, 0); + expect(player).toHaveStatStage(Stat.SPDEF, -2); // +1 --> -1 --> -2 expect(game.phaseInterceptor.log.filter(l => l === "StatStageChangePhase")).toHaveLength(3); }); @@ -197,21 +193,20 @@ describe("Moves - Swallow & Spit Up - ", () => { await game.setTurnOrder([BattlerIndex.PLAYER, BattlerIndex.ENEMY]); await game.toNextTurn(); - expect(player.getStatStage(Stat.DEF)).toBe(1); - expect(player.getStatStage(Stat.SPDEF)).toBe(1); + expect(player).toHaveStatStage(Stat.DEF, 1); + expect(player).toHaveStatStage(Stat.SPDEF, 1); expect(player.hasAbility(AbilityId.SIMPLE)).toBe(true); - game.move.use(MoveId.SPIT_UP); + game.move.use(MoveId.SWALLOW); await game.move.forceEnemyMove(MoveId.SPLASH); await game.toEndOfTurn(); // should have fallen by 2 stages from Simple - expect(player.getStatStage(Stat.DEF)).toBe(-1); - expect(player.getStatStage(Stat.SPDEF)).toBe(-1); + expect(player).toHaveStatStage(Stat.DEF, -1); + expect(player).toHaveStatStage(Stat.SPDEF, -1); }); it("should invert stat drops when gaining Contrary", async () => { - game.override.enemyAbility(AbilityId.CONTRARY); await game.classicMode.startBattle([SpeciesId.ABOMASNOW]); const player = game.field.getPlayerPokemon(); @@ -221,17 +216,17 @@ describe("Moves - Swallow & Spit Up - ", () => { await game.setTurnOrder([BattlerIndex.PLAYER, BattlerIndex.ENEMY]); await game.toEndOfTurn(); - expect(player.getStatStage(Stat.DEF)).toBe(1); - expect(player.getStatStage(Stat.SPDEF)).toBe(1); - expect(player.hasAbility(AbilityId.CONTRARY)).toBe(true); + expect(player).toHaveStatStage(Stat.DEF, 1); + expect(player).toHaveStatStage(Stat.SPDEF, 1); + expect(player).toHaveAbilityApplied(AbilityId.CONTRARY); game.move.use(MoveId.SPIT_UP); await game.move.forceEnemyMove(MoveId.SPLASH); await game.toEndOfTurn(); // should have risen 1 stage from Contrary - expect(player.getStatStage(Stat.DEF)).toBe(2); - expect(player.getStatStage(Stat.SPDEF)).toBe(2); + expect(player).toHaveStatStage(Stat.DEF, 2); + expect(player).toHaveStatStage(Stat.SPDEF, 2); }); }); });