From 3fa2088f5beca417c1a6445185c88714851d343e Mon Sep 17 00:00:00 2001 From: Mumble Date: Sat, 3 Aug 2024 12:20:19 -0700 Subject: [PATCH] [Refactor] Merged implementation of CutHpStatBoostAttr moves (#3255) * Merged implementation of CutHpStatBoostAttr moves * Fixed failure check * Fixed Belly Drum details * Moved comment * Tests for involved moves * Fixed belly drum reference * Added localization * Manual merge * Fixed issues discovered by Temp * Updated moveset overrides to match new format * Implementing Torranx's fixes * Localized Belly Drum message to Belly Drum's initialization * Post Caffeine Activation * Actual Caffeine Fix-TypeDoc Test --------- Co-authored-by: Frutescens --- src/data/move.ts | 36 ++----- src/test/moves/belly_drum.test.ts | 115 +++++++++++++++++++++ src/test/moves/clangorous_soul.test.ts | 136 +++++++++++++++++++++++++ src/test/moves/fillet_away.test.ts | 124 ++++++++++++++++++++++ 4 files changed, 385 insertions(+), 26 deletions(-) create mode 100644 src/test/moves/belly_drum.test.ts create mode 100644 src/test/moves/clangorous_soul.test.ts create mode 100644 src/test/moves/fillet_away.test.ts diff --git a/src/data/move.ts b/src/data/move.ts index 6e0be52047c..b8593ede356 100644 --- a/src/data/move.ts +++ b/src/data/move.ts @@ -2627,36 +2627,15 @@ export class GrowthStatChangeAttr extends StatChangeAttr { } } -export class HalfHpStatMaxAttr extends StatChangeAttr { - constructor(stat: BattleStat) { - super(stat, 12, true, null, false); - } - - apply(user: Pokemon, target: Pokemon, move: Move, args: any[]): Promise { - return new Promise(resolve => { - user.damageAndUpdate(Math.floor(user.getMaxHp() / 2), HitResult.OTHER, false, true); - user.updateInfo().then(() => { - const ret = super.apply(user, target, move, args); - user.scene.queueMessage(i18next.t("moveTriggers:cutOwnHpAndMaximizedStat", {pokemonName: getPokemonNameWithAffix(user), statName: getBattleStatName(this.stats[BattleStat.ATK])})); - resolve(ret); - }); - }); - } - - getCondition(): MoveConditionFunc { - return (user, target, move) => user.getHpRatio() > 0.5 && user.summonData.battleStats[this.stats[BattleStat.ATK]] < 6; - } - - // TODO: Add benefit score that considers HP cut -} - export class CutHpStatBoostAttr extends StatChangeAttr { private cutRatio: integer; + private messageCallback: ((user: Pokemon) => void) | undefined; - constructor(stat: BattleStat | BattleStat[], levels: integer, cutRatio: integer) { + constructor(stat: BattleStat | BattleStat[], levels: integer, cutRatio: integer, messageCallback?: ((user: Pokemon) => void) | undefined) { super(stat, levels, true, null, true); this.cutRatio = cutRatio; + this.messageCallback = messageCallback; } apply(user: Pokemon, target: Pokemon, move: Move, args: any[]): Promise { @@ -2664,13 +2643,16 @@ export class CutHpStatBoostAttr extends StatChangeAttr { user.damageAndUpdate(Math.floor(user.getMaxHp() / this.cutRatio), HitResult.OTHER, false, true); user.updateInfo().then(() => { const ret = super.apply(user, target, move, args); + if (this.messageCallback) { + this.messageCallback(user); + } resolve(ret); }); }); } getCondition(): MoveConditionFunc { - return (user, target, move) => user.getHpRatio() > 1 / this.cutRatio; + return (user, target, move) => user.getHpRatio() > 1 / this.cutRatio && this.stats.some(s => user.summonData.battleStats[s] < 6); } } @@ -6477,7 +6459,9 @@ export function initMoves() { new StatusMove(Moves.SWEET_KISS, Type.FAIRY, 75, 10, -1, 0, 2) .attr(ConfuseAttr), new SelfStatusMove(Moves.BELLY_DRUM, Type.NORMAL, -1, 10, -1, 0, 2) - .attr(HalfHpStatMaxAttr, BattleStat.ATK), + .attr(CutHpStatBoostAttr, [BattleStat.ATK], 12, 2, (user) => { + user.scene.queueMessage(i18next.t("moveTriggers:cutOwnHpAndMaximizedStat", {pokemonName: getPokemonNameWithAffix(user), statName: getBattleStatName(BattleStat.ATK)})); + }), new AttackMove(Moves.SLUDGE_BOMB, Type.POISON, MoveCategory.SPECIAL, 90, 100, 10, 30, 0, 2) .attr(StatusEffectAttr, StatusEffect.POISON) .ballBombMove(), diff --git a/src/test/moves/belly_drum.test.ts b/src/test/moves/belly_drum.test.ts new file mode 100644 index 00000000000..5a9ddd41f0f --- /dev/null +++ b/src/test/moves/belly_drum.test.ts @@ -0,0 +1,115 @@ +import {afterEach, beforeAll, beforeEach, describe, expect, test, vi} from "vitest"; +import Phaser from "phaser"; +import GameManager from "#app/test/utils/gameManager"; +import overrides from "#app/overrides"; +import { + TurnEndPhase, +} from "#app/phases"; +import {getMovePosition} from "#app/test/utils/gameManagerUtils"; +import { Moves } from "#enums/moves"; +import { Species } from "#enums/species"; +import { BattleStat } from "#app/data/battle-stat"; + +const TIMEOUT = 20 * 1000; +// RATIO : HP Cost of Move +const RATIO = 2; +// PREDAMAGE : Amount of extra HP lost +const PREDAMAGE = 15; + +describe("Moves - BELLY DRUM", () => { + let phaserGame: Phaser.Game; + let game: GameManager; + + beforeAll(() => { + phaserGame = new Phaser.Game({ + type: Phaser.HEADLESS, + }); + }); + + afterEach(() => { + game.phaseInterceptor.restoreOg(); + }); + + beforeEach(() => { + game = new GameManager(phaserGame); + vi.spyOn(overrides, "STARTER_SPECIES_OVERRIDE", "get").mockReturnValue(Species.MAGIKARP); + vi.spyOn(overrides, "OPP_SPECIES_OVERRIDE", "get").mockReturnValue(Species.SNORLAX); + vi.spyOn(overrides, "STARTING_LEVEL_OVERRIDE", "get").mockReturnValue(100); + vi.spyOn(overrides, "OPP_LEVEL_OVERRIDE", "get").mockReturnValue(100); + game.override.moveset([Moves.BELLY_DRUM]); + game.override.enemyMoveset([Moves.SPLASH]); + }); + + // Bulbapedia Reference: https://bulbapedia.bulbagarden.net/wiki/Belly_Drum_(move) + + test("Belly Drum raises the user's Attack to its max, at the cost of 1/2 of its maximum HP", + async() => { + await game.startBattle([Species.MAGIKARP]); + + const leadPokemon = game.scene.getPlayerPokemon(); + expect(leadPokemon).toBeDefined(); + const hpLost = Math.floor(leadPokemon.getMaxHp() / RATIO); + + game.doAttack(getMovePosition(game.scene, 0, Moves.BELLY_DRUM)); + await game.phaseInterceptor.to(TurnEndPhase); + + expect(leadPokemon.hp).toBe(leadPokemon.getMaxHp() - hpLost); + expect(leadPokemon.summonData.battleStats[BattleStat.ATK]).toBe(6); + }, TIMEOUT + ); + + test("Belly Drum will still take effect if an uninvolved stat is at max", + async() => { + await game.startBattle([Species.MAGIKARP]); + + const leadPokemon = game.scene.getPlayerPokemon(); + expect(leadPokemon).toBeDefined(); + const hpLost = Math.floor(leadPokemon.getMaxHp() / RATIO); + + // Here - BattleStat.ATK -> -3 and BattleStat.SPATK -> 6 + leadPokemon.summonData.battleStats[BattleStat.ATK] = -3; + leadPokemon.summonData.battleStats[BattleStat.SPATK] = 6; + + game.doAttack(getMovePosition(game.scene, 0, Moves.BELLY_DRUM)); + await game.phaseInterceptor.to(TurnEndPhase); + + expect(leadPokemon.hp).toBe(leadPokemon.getMaxHp() - hpLost); + expect(leadPokemon.summonData.battleStats[BattleStat.ATK]).toBe(6); + expect(leadPokemon.summonData.battleStats[BattleStat.SPATK]).toBe(6); + }, TIMEOUT + ); + + test("Belly Drum fails if the pokemon's attack stat is at its maximum", + async() => { + await game.startBattle([Species.MAGIKARP]); + + const leadPokemon = game.scene.getPlayerPokemon(); + expect(leadPokemon).toBeDefined(); + + leadPokemon.summonData.battleStats[BattleStat.ATK] = 6; + + game.doAttack(getMovePosition(game.scene, 0, Moves.BELLY_DRUM)); + await game.phaseInterceptor.to(TurnEndPhase); + + expect(leadPokemon.hp).toBe(leadPokemon.getMaxHp()); + expect(leadPokemon.summonData.battleStats[BattleStat.ATK]).toBe(6); + }, TIMEOUT + ); + + test("Belly Drum fails if the user's health is less than 1/2", + async() => { + await game.startBattle([Species.MAGIKARP]); + + const leadPokemon = game.scene.getPlayerPokemon(); + expect(leadPokemon).toBeDefined(); + const hpLost = Math.floor(leadPokemon.getMaxHp() / RATIO); + leadPokemon.hp = hpLost - PREDAMAGE; + + game.doAttack(getMovePosition(game.scene, 0, Moves.BELLY_DRUM)); + await game.phaseInterceptor.to(TurnEndPhase); + + expect(leadPokemon.hp).toBe(hpLost - PREDAMAGE); + expect(leadPokemon.summonData.battleStats[BattleStat.ATK]).toBe(0); + }, TIMEOUT + ); +}); diff --git a/src/test/moves/clangorous_soul.test.ts b/src/test/moves/clangorous_soul.test.ts new file mode 100644 index 00000000000..1b3d16f402f --- /dev/null +++ b/src/test/moves/clangorous_soul.test.ts @@ -0,0 +1,136 @@ +import {afterEach, beforeAll, beforeEach, describe, expect, test, vi} from "vitest"; +import Phaser from "phaser"; +import GameManager from "#app/test/utils/gameManager"; +import overrides from "#app/overrides"; +import { + TurnEndPhase, +} from "#app/phases"; +import {getMovePosition} from "#app/test/utils/gameManagerUtils"; +import { Moves } from "#enums/moves"; +import { Species } from "#enums/species"; +import { BattleStat } from "#app/data/battle-stat"; + +const TIMEOUT = 20 * 1000; +// RATIO : HP Cost of Move +const RATIO = 3; +// PREDAMAGE : Amount of extra HP lost +const PREDAMAGE = 15; + +describe("Moves - CLANGOROUS_SOUL", () => { + let phaserGame: Phaser.Game; + let game: GameManager; + + beforeAll(() => { + phaserGame = new Phaser.Game({ + type: Phaser.HEADLESS, + }); + }); + + afterEach(() => { + game.phaseInterceptor.restoreOg(); + }); + + beforeEach(() => { + game = new GameManager(phaserGame); + vi.spyOn(overrides, "STARTER_SPECIES_OVERRIDE", "get").mockReturnValue(Species.MAGIKARP); + vi.spyOn(overrides, "OPP_SPECIES_OVERRIDE", "get").mockReturnValue(Species.SNORLAX); + vi.spyOn(overrides, "STARTING_LEVEL_OVERRIDE", "get").mockReturnValue(100); + vi.spyOn(overrides, "OPP_LEVEL_OVERRIDE", "get").mockReturnValue(100); + game.override.moveset([Moves.CLANGOROUS_SOUL]); + game.override.enemyMoveset([Moves.SPLASH]); + }); + + //Bulbapedia Reference: https://bulbapedia.bulbagarden.net/wiki/Clangorous_Soul_(move) + + test("Clangorous Soul raises the user's Attack, Defense, Special Attack, Special Defense and Speed by one stage each, at the cost of 1/3 of its maximum HP", + async() => { + await game.startBattle([Species.MAGIKARP]); + + const leadPokemon = game.scene.getPlayerPokemon(); + expect(leadPokemon).toBeDefined(); + const hpLost = Math.floor(leadPokemon.getMaxHp() / RATIO); + + game.doAttack(getMovePosition(game.scene, 0, Moves.CLANGOROUS_SOUL)); + await game.phaseInterceptor.to(TurnEndPhase); + + expect(leadPokemon.hp).toBe(leadPokemon.getMaxHp() - hpLost); + expect(leadPokemon.summonData.battleStats[BattleStat.ATK]).toBe(1); + expect(leadPokemon.summonData.battleStats[BattleStat.DEF]).toBe(1); + expect(leadPokemon.summonData.battleStats[BattleStat.SPATK]).toBe(1); + expect(leadPokemon.summonData.battleStats[BattleStat.SPDEF]).toBe(1); + expect(leadPokemon.summonData.battleStats[BattleStat.SPD]).toBe(1); + }, TIMEOUT + ); + + test("Clangorous Soul will still take effect if one or more of the involved stats are not at max", + async() => { + await game.startBattle([Species.MAGIKARP]); + + const leadPokemon = game.scene.getPlayerPokemon(); + expect(leadPokemon).toBeDefined(); + const hpLost = Math.floor(leadPokemon.getMaxHp() / RATIO); + + //Here - BattleStat.SPD -> 0 and BattleStat.SPDEF -> 4 + leadPokemon.summonData.battleStats[BattleStat.ATK] = 6; + leadPokemon.summonData.battleStats[BattleStat.DEF] = 6; + leadPokemon.summonData.battleStats[BattleStat.SPATK] = 6; + leadPokemon.summonData.battleStats[BattleStat.SPDEF] = 4; + + game.doAttack(getMovePosition(game.scene, 0, Moves.CLANGOROUS_SOUL)); + await game.phaseInterceptor.to(TurnEndPhase); + + expect(leadPokemon.hp).toBe(leadPokemon.getMaxHp() - hpLost); + expect(leadPokemon.summonData.battleStats[BattleStat.ATK]).toBe(6); + expect(leadPokemon.summonData.battleStats[BattleStat.DEF]).toBe(6); + expect(leadPokemon.summonData.battleStats[BattleStat.SPATK]).toBe(6); + expect(leadPokemon.summonData.battleStats[BattleStat.SPDEF]).toBe(5); + expect(leadPokemon.summonData.battleStats[BattleStat.SPD]).toBe(1); + }, TIMEOUT + ); + + test("Clangorous Soul fails if all stats involved are at max", + async() => { + await game.startBattle([Species.MAGIKARP]); + + const leadPokemon = game.scene.getPlayerPokemon(); + expect(leadPokemon).toBeDefined(); + + leadPokemon.summonData.battleStats[BattleStat.ATK] = 6; + leadPokemon.summonData.battleStats[BattleStat.DEF] = 6; + leadPokemon.summonData.battleStats[BattleStat.SPATK] = 6; + leadPokemon.summonData.battleStats[BattleStat.SPDEF] = 6; + leadPokemon.summonData.battleStats[BattleStat.SPD] = 6; + + game.doAttack(getMovePosition(game.scene, 0, Moves.CLANGOROUS_SOUL)); + await game.phaseInterceptor.to(TurnEndPhase); + + expect(leadPokemon.hp).toBe(leadPokemon.getMaxHp()); + expect(leadPokemon.summonData.battleStats[BattleStat.ATK]).toBe(6); + expect(leadPokemon.summonData.battleStats[BattleStat.DEF]).toBe(6); + expect(leadPokemon.summonData.battleStats[BattleStat.SPATK]).toBe(6); + expect(leadPokemon.summonData.battleStats[BattleStat.SPDEF]).toBe(6); + expect(leadPokemon.summonData.battleStats[BattleStat.SPD]).toBe(6); + }, TIMEOUT + ); + + test("Clangorous Soul fails if the user's health is less than 1/3", + async() => { + await game.startBattle([Species.MAGIKARP]); + + const leadPokemon = game.scene.getPlayerPokemon(); + expect(leadPokemon).toBeDefined(); + const hpLost = Math.floor(leadPokemon.getMaxHp() / RATIO); + leadPokemon.hp = hpLost - PREDAMAGE; + + game.doAttack(getMovePosition(game.scene, 0, Moves.CLANGOROUS_SOUL)); + await game.phaseInterceptor.to(TurnEndPhase); + + expect(leadPokemon.hp).toBe(hpLost - PREDAMAGE); + expect(leadPokemon.summonData.battleStats[BattleStat.ATK]).toBe(0); + expect(leadPokemon.summonData.battleStats[BattleStat.DEF]).toBe(0); + expect(leadPokemon.summonData.battleStats[BattleStat.SPATK]).toBe(0); + expect(leadPokemon.summonData.battleStats[BattleStat.SPDEF]).toBe(0); + expect(leadPokemon.summonData.battleStats[BattleStat.SPD]).toBe(0); + }, TIMEOUT + ); +}); diff --git a/src/test/moves/fillet_away.test.ts b/src/test/moves/fillet_away.test.ts new file mode 100644 index 00000000000..161bba2c284 --- /dev/null +++ b/src/test/moves/fillet_away.test.ts @@ -0,0 +1,124 @@ +import {afterEach, beforeAll, beforeEach, describe, expect, test, vi} from "vitest"; +import Phaser from "phaser"; +import GameManager from "#app/test/utils/gameManager"; +import overrides from "#app/overrides"; +import { + TurnEndPhase, +} from "#app/phases"; +import {getMovePosition} from "#app/test/utils/gameManagerUtils"; +import { Moves } from "#enums/moves"; +import { Species } from "#enums/species"; +import { BattleStat } from "#app/data/battle-stat"; + +const TIMEOUT = 20 * 1000; +// RATIO : HP Cost of Move +const RATIO = 2; +// PREDAMAGE : Amount of extra HP lost +const PREDAMAGE = 15; + +describe("Moves - FILLET AWAY", () => { + let phaserGame: Phaser.Game; + let game: GameManager; + + beforeAll(() => { + phaserGame = new Phaser.Game({ + type: Phaser.HEADLESS, + }); + }); + + afterEach(() => { + game.phaseInterceptor.restoreOg(); + }); + + beforeEach(() => { + game = new GameManager(phaserGame); + vi.spyOn(overrides, "STARTER_SPECIES_OVERRIDE", "get").mockReturnValue(Species.MAGIKARP); + vi.spyOn(overrides, "OPP_SPECIES_OVERRIDE", "get").mockReturnValue(Species.SNORLAX); + vi.spyOn(overrides, "STARTING_LEVEL_OVERRIDE", "get").mockReturnValue(100); + vi.spyOn(overrides, "OPP_LEVEL_OVERRIDE", "get").mockReturnValue(100); + game.override.moveset([Moves.FILLET_AWAY]); + game.override.enemyMoveset([Moves.SPLASH]); + }); + + //Bulbapedia Reference: https://bulbapedia.bulbagarden.net/wiki/fillet_away_(move) + + test("Fillet Away raises the user's Attack, Special Attack, and Speed by two stages each, at the cost of 1/2 of its maximum HP", + async() => { + await game.startBattle([Species.MAGIKARP]); + + const leadPokemon = game.scene.getPlayerPokemon(); + expect(leadPokemon).toBeDefined(); + const hpLost = Math.floor(leadPokemon.getMaxHp() / RATIO); + + game.doAttack(getMovePosition(game.scene, 0, Moves.FILLET_AWAY)); + await game.phaseInterceptor.to(TurnEndPhase); + + expect(leadPokemon.hp).toBe(leadPokemon.getMaxHp() - hpLost); + expect(leadPokemon.summonData.battleStats[BattleStat.ATK]).toBe(2); + expect(leadPokemon.summonData.battleStats[BattleStat.SPATK]).toBe(2); + expect(leadPokemon.summonData.battleStats[BattleStat.SPD]).toBe(2); + }, TIMEOUT + ); + + test("Fillet Away will still take effect if one or more of the involved stats are not at max", + async() => { + await game.startBattle([Species.MAGIKARP]); + + const leadPokemon = game.scene.getPlayerPokemon(); + expect(leadPokemon).toBeDefined(); + const hpLost = Math.floor(leadPokemon.getMaxHp() / RATIO); + + //Here - BattleStat.SPD -> 0 and BattleStat.SPATK -> 3 + leadPokemon.summonData.battleStats[BattleStat.ATK] = 6; + leadPokemon.summonData.battleStats[BattleStat.SPATK] = 3; + + game.doAttack(getMovePosition(game.scene, 0, Moves.FILLET_AWAY)); + await game.phaseInterceptor.to(TurnEndPhase); + + expect(leadPokemon.hp).toBe(leadPokemon.getMaxHp() - hpLost); + expect(leadPokemon.summonData.battleStats[BattleStat.ATK]).toBe(6); + expect(leadPokemon.summonData.battleStats[BattleStat.SPATK]).toBe(5); + expect(leadPokemon.summonData.battleStats[BattleStat.SPD]).toBe(2); + }, TIMEOUT + ); + + test("Fillet Away fails if all stats involved are at max", + async() => { + await game.startBattle([Species.MAGIKARP]); + + const leadPokemon = game.scene.getPlayerPokemon(); + expect(leadPokemon).toBeDefined(); + + leadPokemon.summonData.battleStats[BattleStat.ATK] = 6; + leadPokemon.summonData.battleStats[BattleStat.SPATK] = 6; + leadPokemon.summonData.battleStats[BattleStat.SPD] = 6; + + game.doAttack(getMovePosition(game.scene, 0, Moves.FILLET_AWAY)); + await game.phaseInterceptor.to(TurnEndPhase); + + expect(leadPokemon.hp).toBe(leadPokemon.getMaxHp()); + expect(leadPokemon.summonData.battleStats[BattleStat.ATK]).toBe(6); + expect(leadPokemon.summonData.battleStats[BattleStat.SPATK]).toBe(6); + expect(leadPokemon.summonData.battleStats[BattleStat.SPD]).toBe(6); + }, TIMEOUT + ); + + test("Fillet Away fails if the user's health is less than 1/2", + async() => { + await game.startBattle([Species.MAGIKARP]); + + const leadPokemon = game.scene.getPlayerPokemon(); + expect(leadPokemon).toBeDefined(); + const hpLost = Math.floor(leadPokemon.getMaxHp() / RATIO); + leadPokemon.hp = hpLost - PREDAMAGE; + + game.doAttack(getMovePosition(game.scene, 0, Moves.FILLET_AWAY)); + await game.phaseInterceptor.to(TurnEndPhase); + + expect(leadPokemon.hp).toBe(hpLost - PREDAMAGE); + expect(leadPokemon.summonData.battleStats[BattleStat.ATK]).toBe(0); + expect(leadPokemon.summonData.battleStats[BattleStat.SPATK]).toBe(0); + expect(leadPokemon.summonData.battleStats[BattleStat.SPD]).toBe(0); + }, TIMEOUT + ); +});