From c364348a16b604ccc5890a884170d8b3242f94db Mon Sep 17 00:00:00 2001 From: Benjamin Date: Thu, 24 Oct 2024 06:15:28 -0400 Subject: [PATCH 1/3] check last source of damage instead of last successful attacker --- src/data/ability.ts | 1 + src/data/move.ts | 1 + src/field/pokemon.ts | 2 ++ src/phases/faint-phase.ts | 5 +++-- src/phases/post-turn-status-effect-phase.ts | 1 + src/phases/weather-effect-phase.ts | 1 + 6 files changed, 9 insertions(+), 2 deletions(-) diff --git a/src/data/ability.ts b/src/data/ability.ts index ebdd5105bb4..76b39e07b04 100644 --- a/src/data/ability.ts +++ b/src/data/ability.ts @@ -3343,6 +3343,7 @@ export class PostWeatherLapseDamageAbAttr extends PostWeatherLapseAbAttr { const abilityName = (!passive ? pokemon.getAbility() : pokemon.getPassiveAbility()).name; scene.queueMessage(i18next.t("abilityTriggers:postWeatherLapseDamage", { pokemonNameWithAffix: getPokemonNameWithAffix(pokemon), abilityName })); pokemon.damageAndUpdate(Utils.toDmgValue(pokemon.getMaxHp() / (16 / this.damageFactor)), HitResult.OTHER); + pokemon.turnData.lastDmgSrc = 0; } return true; diff --git a/src/data/move.ts b/src/data/move.ts index ec25844909e..bcfc7b6e381 100644 --- a/src/data/move.ts +++ b/src/data/move.ts @@ -1352,6 +1352,7 @@ export class RecoilAttr extends MoveEffectAttr { user.damageAndUpdate(recoilDamage, HitResult.OTHER, false, true, true); user.scene.queueMessage(i18next.t("moveTriggers:hitWithRecoil", { pokemonName: getPokemonNameWithAffix(user) })); user.turnData.damageTaken += recoilDamage; + user.turnData.lastDmgSrc = user.id; return true; } diff --git a/src/field/pokemon.ts b/src/field/pokemon.ts index a3d7429ed9b..90dfb8dda4c 100644 --- a/src/field/pokemon.ts +++ b/src/field/pokemon.ts @@ -2796,6 +2796,7 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container { this.battleData.hitCount++; const attackResult = { move: move.id, result: result as DamageResult, damage: damage, critical: isCritical, sourceId: source.id, sourceBattlerIndex: source.getBattlerIndex() }; this.turnData.attacksReceived.unshift(attackResult); + this.turnData.lastDmgSrc = source.id; if (source.isPlayer() && !this.isPlayer()) { this.scene.applyModifiers(DamageMoneyRewardModifier, true, source, new Utils.NumberHolder(damage)); } @@ -5125,6 +5126,7 @@ export class PokemonTurnData { public statStagesDecreased: boolean = false; public moveEffectiveness: TypeDamageMultiplier | null = null; public combiningPledge?: Moves; + public lastDmgSrc: integer; } export enum AiType { diff --git a/src/phases/faint-phase.ts b/src/phases/faint-phase.ts index eee1fd52938..976b62cefb8 100644 --- a/src/phases/faint-phase.ts +++ b/src/phases/faint-phase.ts @@ -96,12 +96,13 @@ export class FaintPhase extends PokemonPhase { const alivePlayField = this.scene.getField(true); alivePlayField.forEach(p => applyPostKnockOutAbAttrs(PostKnockOutAbAttr, p, pokemon)); if (pokemon.turnData?.attacksReceived?.length) { - const defeatSource = this.scene.getPokemonById(pokemon.turnData.attacksReceived[0].sourceId); + const defeatSource = this.scene.getPokemonById(pokemon.turnData?.lastDmgSrc); + const selfKO = pokemon === defeatSource; if (defeatSource?.isOnField()) { applyPostVictoryAbAttrs(PostVictoryAbAttr, defeatSource); const pvmove = allMoves[pokemon.turnData.attacksReceived[0].move]; const pvattrs = pvmove.getAttrs(PostVictoryStatStageChangeAttr); - if (pvattrs.length) { + if (pvattrs.length && !selfKO) { for (const pvattr of pvattrs) { pvattr.applyPostVictory(defeatSource, defeatSource, pvmove); } diff --git a/src/phases/post-turn-status-effect-phase.ts b/src/phases/post-turn-status-effect-phase.ts index 2efd992a2b5..6635507bee4 100644 --- a/src/phases/post-turn-status-effect-phase.ts +++ b/src/phases/post-turn-status-effect-phase.ts @@ -25,6 +25,7 @@ export class PostTurnStatusEffectPhase extends PokemonPhase { if (!cancelled.value) { this.scene.queueMessage(getStatusEffectActivationText(pokemon.status.effect, getPokemonNameWithAffix(pokemon))); const damage = new Utils.NumberHolder(0); + pokemon.turnData.lastDmgSrc = 0; switch (pokemon.status.effect) { case StatusEffect.POISON: damage.value = Math.max(pokemon.getMaxHp() >> 3, 1); diff --git a/src/phases/weather-effect-phase.ts b/src/phases/weather-effect-phase.ts index b48ee342780..83858a38505 100644 --- a/src/phases/weather-effect-phase.ts +++ b/src/phases/weather-effect-phase.ts @@ -54,6 +54,7 @@ export class WeatherEffectPhase extends CommonAnimPhase { const immune = !pokemon || !!pokemon.getTypes(true, true).filter(t => this.weather?.isTypeDamageImmune(t)).length; if (!immune) { inflictDamage(pokemon); + pokemon.turnData.lastDmgSrc = 0; } }); } From 2a325df3c6ab8c63fb141d9f0d60bcfa8b538b12 Mon Sep 17 00:00:00 2001 From: Benjamin Date: Fri, 25 Oct 2024 01:45:34 -0400 Subject: [PATCH 2/3] clean up code and made more extensible for future use --- src/data/ability.ts | 2 +- src/field/pokemon.ts | 4 ++-- src/phases/faint-phase.ts | 8 ++++---- src/phases/post-turn-status-effect-phase.ts | 4 +++- src/phases/weather-effect-phase.ts | 4 +++- 5 files changed, 13 insertions(+), 9 deletions(-) diff --git a/src/data/ability.ts b/src/data/ability.ts index 76b39e07b04..64dd4b6b976 100644 --- a/src/data/ability.ts +++ b/src/data/ability.ts @@ -3343,7 +3343,7 @@ export class PostWeatherLapseDamageAbAttr extends PostWeatherLapseAbAttr { const abilityName = (!passive ? pokemon.getAbility() : pokemon.getPassiveAbility()).name; scene.queueMessage(i18next.t("abilityTriggers:postWeatherLapseDamage", { pokemonNameWithAffix: getPokemonNameWithAffix(pokemon), abilityName })); pokemon.damageAndUpdate(Utils.toDmgValue(pokemon.getMaxHp() / (16 / this.damageFactor)), HitResult.OTHER); - pokemon.turnData.lastDmgSrc = 0; + pokemon.turnData.lastDmgSrc = WeatherType.HARSH_SUN; } return true; diff --git a/src/field/pokemon.ts b/src/field/pokemon.ts index 90dfb8dda4c..e46bddddda5 100644 --- a/src/field/pokemon.ts +++ b/src/field/pokemon.ts @@ -2796,7 +2796,7 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container { this.battleData.hitCount++; const attackResult = { move: move.id, result: result as DamageResult, damage: damage, critical: isCritical, sourceId: source.id, sourceBattlerIndex: source.getBattlerIndex() }; this.turnData.attacksReceived.unshift(attackResult); - this.turnData.lastDmgSrc = source.id; + this.turnData.lastDmgSrc = source; if (source.isPlayer() && !this.isPlayer()) { this.scene.applyModifiers(DamageMoneyRewardModifier, true, source, new Utils.NumberHolder(damage)); } @@ -5126,7 +5126,7 @@ export class PokemonTurnData { public statStagesDecreased: boolean = false; public moveEffectiveness: TypeDamageMultiplier | null = null; public combiningPledge?: Moves; - public lastDmgSrc: integer; + public lastDmgSrc: Pokemon | WeatherType | StatusEffect; } export enum AiType { diff --git a/src/phases/faint-phase.ts b/src/phases/faint-phase.ts index 976b62cefb8..fb19ca7179f 100644 --- a/src/phases/faint-phase.ts +++ b/src/phases/faint-phase.ts @@ -96,13 +96,13 @@ export class FaintPhase extends PokemonPhase { const alivePlayField = this.scene.getField(true); alivePlayField.forEach(p => applyPostKnockOutAbAttrs(PostKnockOutAbAttr, p, pokemon)); if (pokemon.turnData?.attacksReceived?.length) { - const defeatSource = this.scene.getPokemonById(pokemon.turnData?.lastDmgSrc); - const selfKO = pokemon === defeatSource; - if (defeatSource?.isOnField()) { + const defeatSource = pokemon.turnData.lastDmgSrc; + + if (defeatSource instanceof Pokemon && defeatSource?.isOnField()) { applyPostVictoryAbAttrs(PostVictoryAbAttr, defeatSource); const pvmove = allMoves[pokemon.turnData.attacksReceived[0].move]; const pvattrs = pvmove.getAttrs(PostVictoryStatStageChangeAttr); - if (pvattrs.length && !selfKO) { + if (pvattrs.length) { for (const pvattr of pvattrs) { pvattr.applyPostVictory(defeatSource, defeatSource, pvmove); } diff --git a/src/phases/post-turn-status-effect-phase.ts b/src/phases/post-turn-status-effect-phase.ts index 6635507bee4..efbe019e3de 100644 --- a/src/phases/post-turn-status-effect-phase.ts +++ b/src/phases/post-turn-status-effect-phase.ts @@ -25,17 +25,19 @@ export class PostTurnStatusEffectPhase extends PokemonPhase { if (!cancelled.value) { this.scene.queueMessage(getStatusEffectActivationText(pokemon.status.effect, getPokemonNameWithAffix(pokemon))); const damage = new Utils.NumberHolder(0); - pokemon.turnData.lastDmgSrc = 0; switch (pokemon.status.effect) { case StatusEffect.POISON: damage.value = Math.max(pokemon.getMaxHp() >> 3, 1); + pokemon.turnData.lastDmgSrc = StatusEffect.POISON; break; case StatusEffect.TOXIC: damage.value = Math.max(Math.floor((pokemon.getMaxHp() / 16) * pokemon.status.toxicTurnCount), 1); + pokemon.turnData.lastDmgSrc = StatusEffect.TOXIC; break; case StatusEffect.BURN: damage.value = Math.max(pokemon.getMaxHp() >> 4, 1); applyAbAttrs(ReduceBurnDamageAbAttr, pokemon, null, false, damage); + pokemon.turnData.lastDmgSrc = StatusEffect.BURN; break; } if (damage.value) { diff --git a/src/phases/weather-effect-phase.ts b/src/phases/weather-effect-phase.ts index 83858a38505..1a5865faf55 100644 --- a/src/phases/weather-effect-phase.ts +++ b/src/phases/weather-effect-phase.ts @@ -54,7 +54,9 @@ export class WeatherEffectPhase extends CommonAnimPhase { const immune = !pokemon || !!pokemon.getTypes(true, true).filter(t => this.weather?.isTypeDamageImmune(t)).length; if (!immune) { inflictDamage(pokemon); - pokemon.turnData.lastDmgSrc = 0; + if (this.weather?.weatherType) { + pokemon.turnData.lastDmgSrc = this.weather.weatherType; + } } }); } From ce5739603bb25d881fd2ce0f3abf99626881d348 Mon Sep 17 00:00:00 2001 From: Benjamin Date: Fri, 25 Oct 2024 04:08:52 -0400 Subject: [PATCH 3/3] implement unit tests for Fell Stinger --- src/test/moves/fell_stinger.test.ts | 121 ++++++++++++++++++++++++++++ 1 file changed, 121 insertions(+) create mode 100644 src/test/moves/fell_stinger.test.ts diff --git a/src/test/moves/fell_stinger.test.ts b/src/test/moves/fell_stinger.test.ts new file mode 100644 index 00000000000..596a4aa8db0 --- /dev/null +++ b/src/test/moves/fell_stinger.test.ts @@ -0,0 +1,121 @@ +import Phaser from "phaser"; +import { afterEach, beforeAll, beforeEach, describe, expect, it } from "vitest"; +import GameManager from "../utils/gameManager"; +import { Species } from "#enums/species"; +import { Abilities } from "#enums/abilities"; +import { Moves } from "#enums/moves"; +import { Stat } from "#enums/stat"; +import { StatusEffect } from "#app/enums/status-effect"; +import { WeatherType } from "#app/enums/weather-type"; + + +describe("Moves - Fell Stinger", () => { + let phaserGame: Phaser.Game; + let game: GameManager; + + beforeAll(() => { + phaserGame = new Phaser.Game({ + type: Phaser.HEADLESS, + }); + }); + + afterEach(() => { + game.phaseInterceptor.restoreOg(); + }); + + beforeEach(() => { + game = new GameManager(phaserGame); + + game.override.battleType("single") + .moveset([ Moves.FELL_STINGER ]) + .startingLevel(50); + + game.override.enemyAbility(Abilities.STURDY) + .enemySpecies(Species.HYPNO) + .enemyLevel(5) + .enemyHeldItems([]); + + game.override.weather(WeatherType.NONE); + }); + + it("should not grant stat boost when opponent gets KO'd by recoil", async () => { + game.override.enemyMoveset([ Moves.DOUBLE_EDGE ]); + await game.classicMode.startBattle([ Species.LEAVANNY ]); + + const leadPokemon = game.scene.getPlayerPokemon()!; + game.move.select(Moves.FELL_STINGER); + + await game.phaseInterceptor.to("VictoryPhase"); + + expect(leadPokemon.getStatStage(Stat.ATK) === 0); // Attack stage should still be at 0 + }); + + it("should not grant stat boost when enemy is KO'd by status effect", async () => { + game.override + .enemyMoveset(Moves.SPLASH) + .enemyStatusEffect(StatusEffect.BURN); + await game.classicMode.startBattle([ Species.LEAVANNY ]); + + const leadPokemon = game.scene.getPlayerPokemon()!; + game.move.select(Moves.FELL_STINGER); + + await game.phaseInterceptor.to("VictoryPhase"); + + expect(leadPokemon.getStatStage(Stat.ATK) === 0); // Attack stage should still be at 0 + }); + + it("should not grant stat boost when enemy is KO'd by damaging weather", async () => { + game.override.weather(WeatherType.HAIL); + await game.classicMode.startBattle([ Species.LEAVANNY ]); + + const leadPokemon = game.scene.getPlayerPokemon()!; + + game.move.select(Moves.FELL_STINGER); + + await game.phaseInterceptor.to("VictoryPhase"); + + expect(leadPokemon.getStatStage(Stat.ATK) === 0); // Attack stage should still be at 0 + }); + + it("should not grant stat boost when enemy is KO'd by Dry Skin + Harsh Sunlight", async () => { + game.override + .enemyPassiveAbility(Abilities.STURDY) + .enemyAbility(Abilities.DRY_SKIN) + .weather(WeatherType.HARSH_SUN); + await game.challengeMode.startBattle([ Species.LEAVANNY ]); + + const leadPokemon = game.scene.getPlayerPokemon()!; + + game.move.select(Moves.FELL_STINGER); + + await game.phaseInterceptor.to("VictoryPhase"); + + expect(leadPokemon.getStatStage(Stat.ATK) === 0); // Attack stage should still be at 0 + }); + + it("should not grant stat boost if enemy is saved by Reviver Seed", async () => { + game.override + .enemyAbility(Abilities.KLUTZ) + .enemyHeldItems([{ name: "REVIVER_SEED" }]); + + await game.classicMode.startBattle([ Species.LEAVANNY ]); + + const leadPokemon = game.scene.getPlayerPokemon()!; + game.move.select(Moves.FELL_STINGER); + + await game.phaseInterceptor.to("TurnEndPhase"); + expect(leadPokemon.getStatStage(Stat.ATK) === 0); // Attack stage should still be at 0 + }); + + it("should grant stat boost when enemy dies directly to hit", async () => { + game.override.enemyAbility(Abilities.KLUTZ); + await game.challengeMode.startBattle([ Species.LEAVANNY ]); + + const leadPokemon = game.scene.getPlayerPokemon()!; + game.move.select(Moves.FELL_STINGER); + + await game.phaseInterceptor.to("VictoryPhase"); + + expect(leadPokemon.getStatStage(Stat.ATK) === 3); // Attack stage should have risen to 3 + }); +});