diff --git a/src/data/move.ts b/src/data/move.ts index 14ffbc12013..8c97e305829 100644 --- a/src/data/move.ts +++ b/src/data/move.ts @@ -7753,9 +7753,19 @@ export function initMoves() { .ignoresVirtual(), /* End Unused */ new AttackMove(Moves.DYNAMAX_CANNON, Type.DRAGON, MoveCategory.SPECIAL, 100, 100, 5, -1, 0, 8) - .attr(MovePowerMultiplierAttr, (user, target, move) => target.level > 200 ? 2 : 1) + .attr(MovePowerMultiplierAttr, (user, target, move) => { + // Move is only stronger against overleveled foes. + if (target.level > target.scene.getMaxExpLevel()) { + const dynamaxCannonPercentMarginBeforeFullDamage = 0.05; // How much % above MaxExpLevel of wave will the target need to be to take full damage. + // The move's power scales as the margin is approached, reaching double power when it does or goes over it. + return 1 + Math.min(1, (target.level - target.scene.getMaxExpLevel()) / (target.scene.getMaxExpLevel() * dynamaxCannonPercentMarginBeforeFullDamage)); + } else { + return 1; + } + }) .attr(DiscourageFrequentUseAttr) .ignoresVirtual(), + new AttackMove(Moves.SNIPE_SHOT, Type.WATER, MoveCategory.SPECIAL, 80, 100, 15, -1, 0, 8) .attr(HighCritAttr) .attr(BypassRedirectAttr), diff --git a/src/locales/de/move.ts b/src/locales/de/move.ts index 430b4f85ec3..f8e5f7c6ff5 100644 --- a/src/locales/de/move.ts +++ b/src/locales/de/move.ts @@ -2975,7 +2975,7 @@ export const move: MoveTranslationEntries = { }, "dynamaxCannon": { name: "Dynamax-Kanone", - effect: "Der Anwender schießt einen Strahl aus seinem Kern ab. Dynamaximierte Ziele erleiden doppelten Schaden." + effect: "Der Anwender schießt einen Strahl aus seinem Kern ab. Verursacht bis zu doppelt so viel Schaden, wenn das Level des Gegners höher als die Levelgrenze ist." }, "snipeShot": { name: "Präzisionsschuss", diff --git a/src/locales/en/move.ts b/src/locales/en/move.ts index b9a8836dfec..cec7c93ede5 100644 --- a/src/locales/en/move.ts +++ b/src/locales/en/move.ts @@ -2975,7 +2975,7 @@ export const move: MoveTranslationEntries = { }, "dynamaxCannon": { name: "Dynamax Cannon", - effect: "The user unleashes a strong beam from its core. This move deals twice the damage if the target is over level 200." + effect: "The user unleashes a strong beam from its core. Deals up to twice the damage if the target is overly leveled." }, "snipeShot": { name: "Snipe Shot", diff --git a/src/locales/es/move.ts b/src/locales/es/move.ts index 3693e008574..873e7e2da94 100644 --- a/src/locales/es/move.ts +++ b/src/locales/es/move.ts @@ -2975,7 +2975,7 @@ export const move: MoveTranslationEntries = { }, dynamaxCannon: { name: "Cañón Dinamax", - effect: "El usuario ataca emitiendo un rayo desde su núcleo. El daño infligido se duplica si el objetivo supera el nivel 200.", + effect: "El usuario ataca emitiendo un rayo desde su núcleo. Inflinge hasta el doble de daño si el objetivo tiene más niveles de lo normal." }, snipeShot: { name: "Disparo Certero", diff --git a/src/locales/fr/move.ts b/src/locales/fr/move.ts index 3fea8995694..425d9226d57 100644 --- a/src/locales/fr/move.ts +++ b/src/locales/fr/move.ts @@ -2975,7 +2975,7 @@ export const move: MoveTranslationEntries = { }, "dynamaxCannon": { name: "Canon Dynamax", - effect: "Le lanceur attaque en émettant un laser depuis son noyau. Cette capacité inflige deux fois plus de dégâts si l’adversaire est level 200." + effect: "Le lanceur attaque en libérant l’énergie concentrée dans son noyau. Inflige jusqu’à deux fois plus de dégâts si l’adversaire a un niveau très élevé." }, "snipeShot": { name: "Tir de Précision", diff --git a/src/locales/it/move.ts b/src/locales/it/move.ts index a7ebd605f18..a2c99218cec 100644 --- a/src/locales/it/move.ts +++ b/src/locales/it/move.ts @@ -2975,7 +2975,7 @@ export const move: MoveTranslationEntries = { }, dynamaxCannon: { name: "Cannone Dynamax", - effect: "Il Pokémon attacca emettendo dal suo nucleo l'energia concentrata nel corpo.", + effect: "Il Pokémon attacca emettendo dal suo nucleo l'energia concentrata nel corpo. Se il bersaglio è overlivellato, i danni inflitti aumentano.", }, snipeShot: { name: "Tiromirato", diff --git a/src/locales/ko/move.ts b/src/locales/ko/move.ts index 3781725bc6f..bb9d4515ba8 100644 --- a/src/locales/ko/move.ts +++ b/src/locales/ko/move.ts @@ -2981,8 +2981,7 @@ export const move: MoveTranslationEntries = { }, dynamaxCannon: { name: "다이맥스포", - /* 다이맥스에서 200레벨로 조건 변경 */ - effect: "코어에서 빔을 발사해서 공격한다. 상대의 레벨이 200보다 크면 데미지가 2배가 된다." + effect: "코어에서 빔을 발사해서 공격한다. 상대가 웨이브 레벨 최대치를 초과했다면, 초과한 정도에 비례하여 데미지가 최대 2배가 된다." }, snipeShot: { name: "노려맞히기", diff --git a/src/locales/pt_BR/move.ts b/src/locales/pt_BR/move.ts index 0008cc55416..97e14cb81c2 100644 --- a/src/locales/pt_BR/move.ts +++ b/src/locales/pt_BR/move.ts @@ -2975,7 +2975,7 @@ export const move: MoveTranslationEntries = { }, dynamaxCannon: { name: "Dynamax Cannon", - effect: "O usuário libera um forte feixe de seu núcleo. Este movimento causa o dobro do dano se o alvo estiver acima do nível 200." + effect: "O usuário libera um forte feixe de seu núcleo. Este movimento causa até o dobro do dano se o alvo estiver com seu nível acima do limite." }, snipeShot: { name: "Snipe Shot", diff --git a/src/locales/zh_CN/move.ts b/src/locales/zh_CN/move.ts index 6513f3bcfc7..0c022559329 100644 --- a/src/locales/zh_CN/move.ts +++ b/src/locales/zh_CN/move.ts @@ -2975,7 +2975,7 @@ export const move: MoveTranslationEntries = { }, "dynamaxCannon": { name: "极巨炮", - effect: "将凝缩在体内的能量从核心放出进行攻击", + effect: "将凝缩在体内的能量从核心放出进行攻击,\n对手等级比当前波次的等级上限越高,造成的伤害越高,最多两倍。", }, "snipeShot": { name: "狙击", diff --git a/src/locales/zh_TW/move.ts b/src/locales/zh_TW/move.ts index 0a6321850db..92b70429cfd 100644 --- a/src/locales/zh_TW/move.ts +++ b/src/locales/zh_TW/move.ts @@ -2861,7 +2861,7 @@ export const move: MoveTranslationEntries = { }, dynamaxCannon: { name: "極巨炮", - effect: "將凝縮在體內的能量從核心\n放出進行攻擊", + effect: "將凝縮在體內的能量從核心放出進行攻擊,\n對手等級比當前波次的等級上限越高,造成的傷害越高,最多兩倍。", }, snipeShot: { name: "狙擊", diff --git a/src/test/moves/dynamax_cannon.test.ts b/src/test/moves/dynamax_cannon.test.ts new file mode 100644 index 00000000000..f2223fc6d45 --- /dev/null +++ b/src/test/moves/dynamax_cannon.test.ts @@ -0,0 +1,201 @@ +import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest"; +import Phaser from "phaser"; +import GameManager from "#app/test/utils/gameManager"; +import * as overrides from "#app/overrides"; +import { MoveEffectPhase } from "#app/phases"; +import { getMovePosition } from "#app/test/utils/gameManagerUtils"; +import { Stat } from "#app/data/pokemon-stat"; +import { applyMoveAttrs, VariablePowerAttr } from "#app/data/move"; +import * as Utils from "#app/utils"; +import { Moves } from "#enums/moves"; +import { Species } from "#enums/species"; + +describe("Moves - Dynamax Cannon", () => { + let phaserGame: Phaser.Game; + let game: GameManager; + + /** + * Checks the base power of the {@linkcode intendedMove} before and after any + * {@linkcode VariablePowerAttr}s have been applied. + * @param phase current {@linkcode MoveEffectPhase} + * @param intendedMove Expected move during this {@linkcode phase} + * @param before Expected base power before any base power changes + * @param after Expected base power after any base power changes + */ + const checkBasePowerChanges = (phase: MoveEffectPhase, intendedMove: Moves, before: number, after: number) => { + // Double check if the intended move was used and verify its initial base power + const move = phase.move.getMove(); + expect(move.id).toBe(intendedMove); + expect(move.power).toBe(before); + + /** Mocking application of {@linkcode VariablePowerAttr} */ + const power = new Utils.IntegerHolder(move.power); + applyMoveAttrs(VariablePowerAttr, phase.getUserPokemon(), phase.getTarget(), move, power); + expect(power.value).toBe(after); + }; + + beforeAll(() => { + phaserGame = new Phaser.Game({ + type: Phaser.HEADLESS, + }); + }); + + afterEach(() => { + game.phaseInterceptor.restoreOg(); + }); + + beforeEach(() => { + game = new GameManager(phaserGame); + const moveToUse = Moves.DYNAMAX_CANNON; + + // Note that, for Waves 1-10, the level cap is 10 + vi.spyOn(overrides, "STARTING_WAVE_OVERRIDE", "get").mockReturnValue(1); + vi.spyOn(overrides, "SINGLE_BATTLE_OVERRIDE", "get").mockReturnValue(true); + vi.spyOn(overrides, "OPP_SPECIES_OVERRIDE", "get").mockReturnValue(Species.MAGIKARP); + vi.spyOn(overrides, "STARTING_LEVEL_OVERRIDE", "get").mockReturnValue(200); + vi.spyOn(overrides, "MOVESET_OVERRIDE", "get").mockReturnValue([ moveToUse ]); + vi.spyOn(overrides, "OPP_MOVESET_OVERRIDE", "get").mockReturnValue([ Moves.SPLASH, Moves.SPLASH, Moves.SPLASH, Moves.SPLASH ]); + vi.spyOn(overrides, "NEVER_CRIT_OVERRIDE", "get").mockReturnValue(true); + }); + + it("DYNAMAX CANNON against enemy below level cap", async() => { + vi.spyOn(overrides, "OPP_LEVEL_OVERRIDE", "get").mockReturnValue(1); + const moveToUse = Moves.DYNAMAX_CANNON; + await game.startBattle([ + Species.ETERNATUS, + ]); + + game.doAttack(getMovePosition(game.scene, 0, moveToUse)); + + await game.phaseInterceptor.to(MoveEffectPhase, false); + + checkBasePowerChanges(game.scene.getCurrentPhase() as MoveEffectPhase, moveToUse, 100, 100); + }, 20000); + + it("DYNAMAX CANNON against enemy exactly at level cap", async() => { + vi.spyOn(overrides, "OPP_LEVEL_OVERRIDE", "get").mockReturnValue(10); + const moveToUse = Moves.DYNAMAX_CANNON; + await game.startBattle([ + Species.ETERNATUS, + ]); + + game.doAttack(getMovePosition(game.scene, 0, moveToUse)); + + await game.phaseInterceptor.to(MoveEffectPhase, false); + + checkBasePowerChanges(game.scene.getCurrentPhase() as MoveEffectPhase, moveToUse, 100, 100); + }, 20000); + + it("DYNAMAX CANNON against enemy exactly at 1% above level cap", async() => { + vi.spyOn(overrides, "OPP_LEVEL_OVERRIDE", "get").mockReturnValue(101); + const moveToUse = Moves.DYNAMAX_CANNON; + await game.startBattle([ + Species.ETERNATUS, + ]); + + game.doAttack(getMovePosition(game.scene, 0, moveToUse)); + + await game.phaseInterceptor.to(MoveEffectPhase, false); + + const phase = game.scene.getCurrentPhase() as MoveEffectPhase; + + const target = phase.getTarget(); + // Force level cap to be 100; that is, level cap is no longer 10 + target.scene.getMaxExpLevel = vi.fn().mockReturnValue(100); + + checkBasePowerChanges(phase, moveToUse, 100, 120); + }, 20000); + + it("DYNAMAX CANNON against enemy exactly at 2% above level cap", async() => { + vi.spyOn(overrides, "OPP_LEVEL_OVERRIDE", "get").mockReturnValue(102); + const moveToUse = Moves.DYNAMAX_CANNON; + await game.startBattle([ + Species.ETERNATUS, + ]); + + game.doAttack(getMovePosition(game.scene, 0, moveToUse)); + + await game.phaseInterceptor.to(MoveEffectPhase, false); + + const phase = game.scene.getCurrentPhase() as MoveEffectPhase; + + const target = phase.getTarget(); + target.scene.getMaxExpLevel = vi.fn().mockReturnValue(100); + + checkBasePowerChanges(phase, moveToUse, 100, 140); + }, 20000); + + it("DYNAMAX CANNON against enemy exactly at 3% above level cap", async() => { + vi.spyOn(overrides, "OPP_LEVEL_OVERRIDE", "get").mockReturnValue(103); + const moveToUse = Moves.DYNAMAX_CANNON; + await game.startBattle([ + Species.ETERNATUS, + ]); + + game.doAttack(getMovePosition(game.scene, 0, moveToUse)); + + await game.phaseInterceptor.to(MoveEffectPhase, false); + + const phase = game.scene.getCurrentPhase() as MoveEffectPhase; + + const target = phase.getTarget(); + target.scene.getMaxExpLevel = vi.fn().mockReturnValue(100); + + checkBasePowerChanges(phase, moveToUse, 100, 160); + }, 20000); + + it("DYNAMAX CANNON against enemy exactly at 4% above level cap", async() => { + vi.spyOn(overrides, "OPP_LEVEL_OVERRIDE", "get").mockReturnValue(104); + const moveToUse = Moves.DYNAMAX_CANNON; + await game.startBattle([ + Species.ETERNATUS, + ]); + + game.doAttack(getMovePosition(game.scene, 0, moveToUse)); + + await game.phaseInterceptor.to(MoveEffectPhase, false); + + const phase = game.scene.getCurrentPhase() as MoveEffectPhase; + + const target = phase.getTarget(); + target.scene.getMaxExpLevel = vi.fn().mockReturnValue(100); + + checkBasePowerChanges(phase, moveToUse, 100, 180); + }, 20000); + + it("DYNAMAX CANNON against enemy exactly at 5% above level cap", async() => { + vi.spyOn(overrides, "OPP_LEVEL_OVERRIDE", "get").mockReturnValue(105); + const moveToUse = Moves.DYNAMAX_CANNON; + await game.startBattle([ + Species.ETERNATUS, + ]); + + game.doAttack(getMovePosition(game.scene, 0, moveToUse)); + + await game.phaseInterceptor.to(MoveEffectPhase, false); + + const phase = game.scene.getCurrentPhase() as MoveEffectPhase; + + const target = phase.getTarget(); + target.scene.getMaxExpLevel = vi.fn().mockReturnValue(100); + + checkBasePowerChanges(phase, moveToUse, 100, 200); + }, 20000); + + it("DYNAMAX CANNON against enemy way above level cap", async() => { + vi.spyOn(overrides, "OPP_LEVEL_OVERRIDE", "get").mockReturnValue(999); + const moveToUse = Moves.DYNAMAX_CANNON; + await game.startBattle([ + Species.ETERNATUS, + ]); + + // Force enemy to go last + game.scene.getEnemyParty()[0].stats[Stat.SPD] = 1; + + game.doAttack(getMovePosition(game.scene, 0, moveToUse)); + + await game.phaseInterceptor.to(MoveEffectPhase, false); + + checkBasePowerChanges(game.scene.getCurrentPhase() as MoveEffectPhase, moveToUse, 100, 200); + }, 20000); +});