From 39e7591d3bb335cee09e6b3364fb8161496e16ca Mon Sep 17 00:00:00 2001 From: NightKev <34855794+DayKev@users.noreply.github.com> Date: Tue, 13 Aug 2024 14:00:19 -0700 Subject: [PATCH] [Ability] Heatproof now reduces burn damage by half (#3524) * Heatproof now reduces burn damage by half * Add tests * Add comment to test --- src/data/ability.ts | 25 +++++++++ src/phases.ts | 15 +++--- src/test/abilities/heatproof.test.ts | 77 ++++++++++++++++++++++++++++ 3 files changed, 110 insertions(+), 7 deletions(-) create mode 100644 src/test/abilities/heatproof.test.ts diff --git a/src/data/ability.ts b/src/data/ability.ts index 7e270f4d3f26..054e32ad0ff4 100644 --- a/src/data/ability.ts +++ b/src/data/ability.ts @@ -3428,6 +3428,30 @@ export class BypassBurnDamageReductionAbAttr extends AbAttr { } } +/** + * Causes Pokemon to take reduced damage from the {@linkcode StatusEffect.BURN | Burn} status + * @param multiplier Multiplied with the damage taken +*/ +export class ReduceBurnDamageAbAttr extends AbAttr { + constructor(protected multiplier: number) { + super(false); + } + + /** + * Applies the damage reduction + * @param pokemon N/A + * @param passive N/A + * @param cancelled N/A + * @param args `[0]` {@linkcode Utils.NumberHolder} The damage value being modified + * @returns `true` + */ + apply(pokemon: Pokemon, passive: boolean, cancelled: Utils.BooleanHolder, args: any[]): boolean { + (args[0] as Utils.NumberHolder).value = Math.max(Math.floor((args[0] as Utils.NumberHolder).value * this.multiplier), 1); + + return true; + } +} + export class DoubleBerryEffectAbAttr extends AbAttr { apply(pokemon: Pokemon, passive: boolean, cancelled: Utils.BooleanHolder, args: any[]): boolean { (args[0] as Utils.NumberHolder).value *= 2; @@ -4613,6 +4637,7 @@ export function initAbilities() { .unimplemented(), new Ability(Abilities.HEATPROOF, 4) .attr(ReceivedTypeDamageMultiplierAbAttr, Type.FIRE, 0.5) + .attr(ReduceBurnDamageAbAttr, 0.5) .ignorable(), new Ability(Abilities.SIMPLE, 4) .attr(StatChangeMultiplierAbAttr, 2) diff --git a/src/phases.ts b/src/phases.ts index adfcdeca64df..1d1a5e2473ab 100644 --- a/src/phases.ts +++ b/src/phases.ts @@ -25,7 +25,7 @@ import { Starter } from "./ui/starter-select-ui-handler"; import { Gender } from "./data/gender"; import { Weather, WeatherType, getRandomWeatherType, getTerrainBlockMessage, getWeatherDamageMessage, getWeatherLapseMessage } from "./data/weather"; import { ArenaTagSide, ArenaTrapTag, ConditionalProtectTag, MistTag, TrickRoomTag } from "./data/arena-tag"; -import { CheckTrappedAbAttr, PostAttackAbAttr, PostBattleAbAttr, PostDefendAbAttr, PostSummonAbAttr, PostTurnAbAttr, PostWeatherLapseAbAttr, PreSwitchOutAbAttr, PreWeatherDamageAbAttr, ProtectStatAbAttr, RedirectMoveAbAttr, BlockRedirectAbAttr, RunSuccessAbAttr, StatChangeMultiplierAbAttr, SuppressWeatherEffectAbAttr, SyncEncounterNatureAbAttr, applyAbAttrs, applyCheckTrappedAbAttrs, applyPostAttackAbAttrs, applyPostBattleAbAttrs, applyPostDefendAbAttrs, applyPostSummonAbAttrs, applyPostTurnAbAttrs, applyPostWeatherLapseAbAttrs, applyPreStatChangeAbAttrs, applyPreSwitchOutAbAttrs, applyPreWeatherEffectAbAttrs, ChangeMovePriorityAbAttr, applyPostVictoryAbAttrs, PostVictoryAbAttr, BlockNonDirectDamageAbAttr as BlockNonDirectDamageAbAttr, applyPostKnockOutAbAttrs, PostKnockOutAbAttr, PostBiomeChangeAbAttr, PreventBypassSpeedChanceAbAttr, applyPostFaintAbAttrs, PostFaintAbAttr, IncreasePpAbAttr, PostStatChangeAbAttr, applyPostStatChangeAbAttrs, AlwaysHitAbAttr, PreventBerryUseAbAttr, StatChangeCopyAbAttr, PokemonTypeChangeAbAttr, applyPreAttackAbAttrs, applyPostMoveUsedAbAttrs, PostMoveUsedAbAttr, MaxMultiHitAbAttr, HealFromBerryUseAbAttr, IgnoreMoveEffectsAbAttr, BlockStatusDamageAbAttr, BypassSpeedChanceAbAttr, AddSecondStrikeAbAttr } from "./data/ability"; +import { CheckTrappedAbAttr, PostAttackAbAttr, PostBattleAbAttr, PostDefendAbAttr, PostSummonAbAttr, PostTurnAbAttr, PostWeatherLapseAbAttr, PreSwitchOutAbAttr, PreWeatherDamageAbAttr, ProtectStatAbAttr, RedirectMoveAbAttr, BlockRedirectAbAttr, RunSuccessAbAttr, StatChangeMultiplierAbAttr, SuppressWeatherEffectAbAttr, SyncEncounterNatureAbAttr, applyAbAttrs, applyCheckTrappedAbAttrs, applyPostAttackAbAttrs, applyPostBattleAbAttrs, applyPostDefendAbAttrs, applyPostSummonAbAttrs, applyPostTurnAbAttrs, applyPostWeatherLapseAbAttrs, applyPreStatChangeAbAttrs, applyPreSwitchOutAbAttrs, applyPreWeatherEffectAbAttrs, ChangeMovePriorityAbAttr, applyPostVictoryAbAttrs, PostVictoryAbAttr, BlockNonDirectDamageAbAttr as BlockNonDirectDamageAbAttr, applyPostKnockOutAbAttrs, PostKnockOutAbAttr, PostBiomeChangeAbAttr, PreventBypassSpeedChanceAbAttr, applyPostFaintAbAttrs, PostFaintAbAttr, IncreasePpAbAttr, PostStatChangeAbAttr, applyPostStatChangeAbAttrs, AlwaysHitAbAttr, PreventBerryUseAbAttr, StatChangeCopyAbAttr, PokemonTypeChangeAbAttr, applyPreAttackAbAttrs, applyPostMoveUsedAbAttrs, PostMoveUsedAbAttr, MaxMultiHitAbAttr, HealFromBerryUseAbAttr, IgnoreMoveEffectsAbAttr, BlockStatusDamageAbAttr, BypassSpeedChanceAbAttr, AddSecondStrikeAbAttr, ReduceBurnDamageAbAttr } from "./data/ability"; import { Unlockables, getUnlockableName } from "./system/unlockables"; import { getBiomeKey } from "./field/arena"; import { BattleType, BattlerIndex, TurnCommand } from "./battle"; @@ -3815,21 +3815,22 @@ export class PostTurnStatusEffectPhase extends PokemonPhase { if (!cancelled.value) { this.scene.queueMessage(getStatusEffectActivationText(pokemon.status.effect, getPokemonNameWithAffix(pokemon))); - let damage: integer = 0; + const damage = new Utils.NumberHolder(0); switch (pokemon.status.effect) { case StatusEffect.POISON: - damage = Math.max(pokemon.getMaxHp() >> 3, 1); + damage.value = Math.max(pokemon.getMaxHp() >> 3, 1); break; case StatusEffect.TOXIC: - damage = Math.max(Math.floor((pokemon.getMaxHp() / 16) * pokemon.status.turnCount), 1); + damage.value = Math.max(Math.floor((pokemon.getMaxHp() / 16) * pokemon.status.turnCount), 1); break; case StatusEffect.BURN: - damage = Math.max(pokemon.getMaxHp() >> 4, 1); + damage.value = Math.max(pokemon.getMaxHp() >> 4, 1); + applyAbAttrs(ReduceBurnDamageAbAttr, pokemon, null, damage); break; } - if (damage) { + if (damage.value) { // Set preventEndure flag to avoid pokemon surviving thanks to focus band, sturdy, endure ... - this.scene.damageNumberHandler.add(this.getPokemon(), pokemon.damage(damage, false, true)); + this.scene.damageNumberHandler.add(this.getPokemon(), pokemon.damage(damage.value, false, true)); pokemon.updateInfo(); } new CommonBattleAnim(CommonAnim.POISON + (pokemon.status.effect - 1), pokemon).play(this.scene, () => this.end()); diff --git a/src/test/abilities/heatproof.test.ts b/src/test/abilities/heatproof.test.ts new file mode 100644 index 000000000000..8249ba6996fa --- /dev/null +++ b/src/test/abilities/heatproof.test.ts @@ -0,0 +1,77 @@ +import { Species } from "#app/enums/species.js"; +import { TurnEndPhase } from "#app/phases"; +import GameManager from "#test/utils/gameManager"; +import { getMovePosition } from "#test/utils/gameManagerUtils"; +import { Abilities } from "#enums/abilities"; +import { Moves } from "#enums/moves"; +import Phaser from "phaser"; +import { afterEach, beforeAll, beforeEach, describe, expect, it } from "vitest"; +import { SPLASH_ONLY } from "#test/utils/testUtils"; +import { StatusEffect } from "#app/enums/status-effect.js"; + +describe("Abilities - Heatproof", () => { + 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") + .disableCrits() + .enemySpecies(Species.CHARMANDER) + .enemyAbility(Abilities.HEATPROOF) + .enemyMoveset(SPLASH_ONLY) + .enemyLevel(100) + .starterSpecies(Species.CHANDELURE) + .ability(Abilities.BALL_FETCH) + .moveset([Moves.FLAMETHROWER, Moves.SPLASH]) + .startingLevel(100); + }); + + it("reduces Fire type damage by half", async () => { + await game.startBattle(); + + const enemy = game.scene.getEnemyPokemon()!; + const initialHP = 1000; + enemy.hp = initialHP; + + game.doAttack(getMovePosition(game.scene, 0, Moves.FLAMETHROWER)); + await game.phaseInterceptor.to(TurnEndPhase); + const heatproofDamage = initialHP - enemy.hp; + + enemy.hp = initialHP; + game.override.enemyAbility(Abilities.BALL_FETCH); + + game.doAttack(getMovePosition(game.scene, 0, Moves.FLAMETHROWER)); + await game.phaseInterceptor.to(TurnEndPhase); + const regularDamage = initialHP - enemy.hp; + + expect(heatproofDamage).toBeLessThanOrEqual((regularDamage / 2) + 1); + expect(heatproofDamage).toBeGreaterThanOrEqual((regularDamage / 2) - 1); + }); + + it("reduces Burn damage by half", async () => { + game.override + .enemyStatusEffect(StatusEffect.BURN) + .enemySpecies(Species.ABRA); + await game.startBattle(); + + const enemy = game.scene.getEnemyPokemon()!; + + game.doAttack(getMovePosition(game.scene, 0, Moves.SPLASH)); + await game.toNextTurn(); + + // Normal burn damage is /16 + expect(enemy.hp).toBe(enemy.getMaxHp() - Math.floor(enemy.getMaxHp() / 32)); + }); +});