diff --git a/public/locales b/public/locales index 6b3f37cb351..0e5c6096ba2 160000 --- a/public/locales +++ b/public/locales @@ -1 +1 @@ -Subproject commit 6b3f37cb351552721232f4dabefa17bddb5b9004 +Subproject commit 0e5c6096ba26f6b87aed1aab3fe9b0b23f6cbb7b diff --git a/src/data/ability.ts b/src/data/ability.ts index d9e29c5506c..3a14ad1456d 100644 --- a/src/data/ability.ts +++ b/src/data/ability.ts @@ -1064,7 +1064,7 @@ export class PostDefendContactDamageAbAttr extends PostDefendAbAttr { } override applyPostDefend(pokemon: Pokemon, _passive: boolean, simulated: boolean, attacker: Pokemon, move: Move, _hitResult: HitResult, _args: any[]): void { - attacker.damageAndUpdate(Utils.toDmgValue(attacker.getMaxHp() * (1 / this.damageRatio)), HitResult.OTHER); + attacker.damageAndUpdate(Utils.toDmgValue(attacker.getMaxHp() * (1 / this.damageRatio)), { result: HitResult.INDIRECT }); attacker.turnData.damageTaken += Utils.toDmgValue(attacker.getMaxHp() * (1 / this.damageRatio)); } @@ -3792,7 +3792,7 @@ export class PostWeatherLapseDamageAbAttr extends PostWeatherLapseAbAttr { if (!simulated) { const abilityName = (!passive ? pokemon.getAbility() : pokemon.getPassiveAbility()).name; globalScene.queueMessage(i18next.t("abilityTriggers:postWeatherLapseDamage", { pokemonNameWithAffix: getPokemonNameWithAffix(pokemon), abilityName })); - pokemon.damageAndUpdate(Utils.toDmgValue(pokemon.getMaxHp() / (16 / this.damageFactor)), HitResult.OTHER); + pokemon.damageAndUpdate(Utils.toDmgValue(pokemon.getMaxHp() / (16 / this.damageFactor)), { result: HitResult.INDIRECT }); } } } @@ -4084,7 +4084,7 @@ export class PostTurnHurtIfSleepingAbAttr extends PostTurnAbAttr { for (const opp of pokemon.getOpponents()) { if ((opp.status?.effect === StatusEffect.SLEEP || opp.hasAbility(Abilities.COMATOSE)) && !opp.hasAbilityWithAttr(BlockNonDirectDamageAbAttr) && !opp.switchOutStatus) { if (!simulated) { - opp.damageAndUpdate(Utils.toDmgValue(opp.getMaxHp() / 8), HitResult.OTHER); + opp.damageAndUpdate(Utils.toDmgValue(opp.getMaxHp() / 8), { result: HitResult.INDIRECT }); globalScene.queueMessage(i18next.t("abilityTriggers:badDreams", { pokemonName: getPokemonNameWithAffix(opp) })); } } @@ -4567,7 +4567,7 @@ export class PostFaintContactDamageAbAttr extends PostFaintAbAttr { override applyPostFaint(pokemon: Pokemon, passive: boolean, simulated: boolean, attacker?: Pokemon, move?: Move, hitResult?: HitResult, ...args: any[]): void { if (!simulated) { - attacker!.damageAndUpdate(Utils.toDmgValue(attacker!.getMaxHp() * (1 / this.damageRatio)), HitResult.OTHER); + attacker!.damageAndUpdate(Utils.toDmgValue(attacker!.getMaxHp() * (1 / this.damageRatio)), { result: HitResult.INDIRECT }); attacker!.turnData.damageTaken += Utils.toDmgValue(attacker!.getMaxHp() * (1 / this.damageRatio)); } } @@ -4588,7 +4588,7 @@ export class PostFaintHPDamageAbAttr extends PostFaintAbAttr { override applyPostFaint(pokemon: Pokemon, passive: boolean, simulated: boolean, attacker?: Pokemon, move?: Move, hitResult?: HitResult, ...args: any[]): void { if (move !== undefined && attacker !== undefined && !simulated) { //If the mon didn't die to indirect damage const damage = pokemon.turnData.attacksReceived[0].damage; - attacker.damageAndUpdate((damage), HitResult.OTHER); + attacker.damageAndUpdate((damage), { result: HitResult.INDIRECT }); attacker.turnData.damageTaken += damage; } } @@ -4989,7 +4989,7 @@ export class FormBlockDamageAbAttr extends ReceivedMoveDamageMultiplierAbAttr { (args[0] as Utils.NumberHolder).value = this.multiplier; pokemon.removeTag(this.tagType); if (this.recoilDamageFunc) { - pokemon.damageAndUpdate(this.recoilDamageFunc(pokemon), HitResult.OTHER, false, false, true, true); + pokemon.damageAndUpdate(this.recoilDamageFunc(pokemon), { result: HitResult.INDIRECT, ignoreSegments: true, ignoreFaintPhase: true }); } } } diff --git a/src/data/arena-tag.ts b/src/data/arena-tag.ts index 6e8339dd54c..8f1d6b09a73 100644 --- a/src/data/arena-tag.ts +++ b/src/data/arena-tag.ts @@ -788,7 +788,7 @@ class SpikesTag extends ArenaTrapTag { pokemonNameWithAffix: getPokemonNameWithAffix(pokemon), }), ); - pokemon.damageAndUpdate(damage, HitResult.OTHER); + pokemon.damageAndUpdate(damage, { result: HitResult.INDIRECT }); if (pokemon.turnData) { pokemon.turnData.damageTaken += damage; } @@ -982,7 +982,7 @@ class StealthRockTag extends ArenaTrapTag { pokemonNameWithAffix: getPokemonNameWithAffix(pokemon), }), ); - pokemon.damageAndUpdate(damage, HitResult.OTHER); + pokemon.damageAndUpdate(damage, { result: HitResult.INDIRECT }); if (pokemon.turnData) { pokemon.turnData.damageTaken += damage; } @@ -1327,7 +1327,7 @@ class FireGrassPledgeTag extends ArenaTag { globalScene.unshiftPhase( new CommonAnimPhase(pokemon.getBattlerIndex(), pokemon.getBattlerIndex(), CommonAnim.MAGMA_STORM), ); - pokemon.damageAndUpdate(toDmgValue(pokemon.getMaxHp() / 8)); + pokemon.damageAndUpdate(toDmgValue(pokemon.getMaxHp() / 8), { result: HitResult.INDIRECT }); }); return super.lapse(arena); diff --git a/src/data/battler-tags.ts b/src/data/battler-tags.ts index 7ff74d893b6..4952488b48e 100644 --- a/src/data/battler-tags.ts +++ b/src/data/battler-tags.ts @@ -757,7 +757,7 @@ export class ConfusedTag extends BattlerTag { ((((2 * pokemon.level) / 5 + 2) * 40 * atk) / def / 50 + 2) * (pokemon.randSeedIntRange(85, 100) / 100), ); globalScene.queueMessage(i18next.t("battlerTags:confusedLapseHurtItself")); - pokemon.damageAndUpdate(damage); + pokemon.damageAndUpdate(damage, { result: HitResult.CONFUSION }); pokemon.battleData.hitCount++; (globalScene.getCurrentPhase() as MovePhase).cancel(); } @@ -818,7 +818,7 @@ export class DestinyBondTag extends BattlerTag { pokemonNameWithAffix2: getPokemonNameWithAffix(pokemon), }), ); - pokemon.damageAndUpdate(pokemon.hp, HitResult.ONE_HIT_KO, false, false, true); + pokemon.damageAndUpdate(pokemon.hp, { result: HitResult.INDIRECT_KO, ignoreSegments: true }); return false; } } @@ -952,7 +952,7 @@ export class SeedTag extends BattlerTag { new CommonAnimPhase(source.getBattlerIndex(), pokemon.getBattlerIndex(), CommonAnim.LEECH_SEED), ); - const damage = pokemon.damageAndUpdate(toDmgValue(pokemon.getMaxHp() / 8)); + const damage = pokemon.damageAndUpdate(toDmgValue(pokemon.getMaxHp() / 8), { result: HitResult.INDIRECT }); const reverseDrain = pokemon.hasAbilityWithAttr(ReverseDrainAbAttr, false); globalScene.unshiftPhase( new PokemonHealPhase( @@ -1029,7 +1029,7 @@ export class PowderTag extends BattlerTag { const cancelDamage = new BooleanHolder(false); applyAbAttrs(BlockNonDirectDamageAbAttr, pokemon, cancelDamage); if (!cancelDamage.value) { - pokemon.damageAndUpdate(Math.floor(pokemon.getMaxHp() / 4), HitResult.OTHER); + pokemon.damageAndUpdate(Math.floor(pokemon.getMaxHp() / 4), { result: HitResult.INDIRECT }); } // "When the flame touched the powder\non the Pokémon, it exploded!" @@ -1082,7 +1082,7 @@ export class NightmareTag extends BattlerTag { applyAbAttrs(BlockNonDirectDamageAbAttr, pokemon, cancelled); if (!cancelled.value) { - pokemon.damageAndUpdate(toDmgValue(pokemon.getMaxHp() / 4)); + pokemon.damageAndUpdate(toDmgValue(pokemon.getMaxHp() / 4), { result: HitResult.INDIRECT }); } } @@ -1440,7 +1440,7 @@ export abstract class DamagingTrapTag extends TrappedTag { applyAbAttrs(BlockNonDirectDamageAbAttr, pokemon, cancelled); if (!cancelled.value) { - pokemon.damageAndUpdate(toDmgValue(pokemon.getMaxHp() / 8)); + pokemon.damageAndUpdate(toDmgValue(pokemon.getMaxHp() / 8), { result: HitResult.INDIRECT }); } } @@ -1644,7 +1644,7 @@ export class ContactDamageProtectedTag extends ProtectedTag { if (effectPhase instanceof MoveEffectPhase && effectPhase.move.getMove().hasFlag(MoveFlags.MAKES_CONTACT)) { const attacker = effectPhase.getPokemon(); if (!attacker.hasAbilityWithAttr(BlockNonDirectDamageAbAttr)) { - attacker.damageAndUpdate(toDmgValue(attacker.getMaxHp() * (1 / this.damageRatio)), HitResult.OTHER); + attacker.damageAndUpdate(toDmgValue(attacker.getMaxHp() * (1 / this.damageRatio)), { result: HitResult.INDIRECT }); } } } @@ -1810,7 +1810,7 @@ export class PerishSongTag extends BattlerTag { }), ); } else { - pokemon.damageAndUpdate(pokemon.hp, HitResult.ONE_HIT_KO, false, true, true); + pokemon.damageAndUpdate(pokemon.hp, { result: HitResult.INDIRECT_KO, ignoreSegments: true }); } return ret; @@ -2240,7 +2240,7 @@ export class SaltCuredTag extends BattlerTag { if (!cancelled.value) { const pokemonSteelOrWater = pokemon.isOfType(PokemonType.STEEL) || pokemon.isOfType(PokemonType.WATER); - pokemon.damageAndUpdate(toDmgValue(pokemonSteelOrWater ? pokemon.getMaxHp() / 4 : pokemon.getMaxHp() / 8)); + pokemon.damageAndUpdate(toDmgValue(pokemonSteelOrWater ? pokemon.getMaxHp() / 4 : pokemon.getMaxHp() / 8), { result: HitResult.INDIRECT }); globalScene.queueMessage( i18next.t("battlerTags:saltCuredLapse", { @@ -2288,7 +2288,7 @@ export class CursedTag extends BattlerTag { applyAbAttrs(BlockNonDirectDamageAbAttr, pokemon, cancelled); if (!cancelled.value) { - pokemon.damageAndUpdate(toDmgValue(pokemon.getMaxHp() / 4)); + pokemon.damageAndUpdate(toDmgValue(pokemon.getMaxHp() / 4), { result: HitResult.INDIRECT }); globalScene.queueMessage( i18next.t("battlerTags:cursedLapse", { pokemonNameWithAffix: getPokemonNameWithAffix(pokemon), @@ -2611,7 +2611,7 @@ export class GulpMissileTag extends BattlerTag { applyAbAttrs(BlockNonDirectDamageAbAttr, attacker, cancelled); if (!cancelled.value) { - attacker.damageAndUpdate(Math.max(1, Math.floor(attacker.getMaxHp() / 4)), HitResult.OTHER); + attacker.damageAndUpdate(Math.max(1, Math.floor(attacker.getMaxHp() / 4)), { result: HitResult.INDIRECT }); } if (this.tagType === BattlerTagType.GULP_MISSILE_ARROKUDA) { diff --git a/src/data/moves/move.ts b/src/data/moves/move.ts index 9d470b86186..1555967789c 100644 --- a/src/data/moves/move.ts +++ b/src/data/moves/move.ts @@ -1647,7 +1647,7 @@ export class RecoilAttr extends MoveEffectAttr { return false; } - user.damageAndUpdate(recoilDamage, HitResult.OTHER, false, true, true); + user.damageAndUpdate(recoilDamage, { result: HitResult.INDIRECT, ignoreSegments: true }); globalScene.queueMessage(i18next.t("moveTriggers:hitWithRecoil", { pokemonName: getPokemonNameWithAffix(user) })); user.turnData.damageTaken += recoilDamage; @@ -1679,7 +1679,7 @@ export class SacrificialAttr extends MoveEffectAttr { * @returns true if the function succeeds **/ apply(user: Pokemon, target: Pokemon, move: Move, args: any[]): boolean { - user.damageAndUpdate(user.hp, HitResult.OTHER, false, true, true); + user.damageAndUpdate(user.hp, { result: HitResult.INDIRECT, ignoreSegments: true }); user.turnData.damageTaken += user.hp; return true; @@ -1717,7 +1717,7 @@ export class SacrificialAttrOnHit extends MoveEffectAttr { return false; } - user.damageAndUpdate(user.hp, HitResult.OTHER, false, true, true); + user.damageAndUpdate(user.hp, { result: HitResult.INDIRECT, ignoreSegments: true }); user.turnData.damageTaken += user.hp; return true; @@ -1759,7 +1759,7 @@ export class HalfSacrificialAttr extends MoveEffectAttr { // Check to see if the Pokemon has an ability that blocks non-direct damage applyAbAttrs(BlockNonDirectDamageAbAttr, user, cancelled); if (!cancelled.value) { - user.damageAndUpdate(Utils.toDmgValue(user.getMaxHp() / 2), HitResult.OTHER, false, true, true); + user.damageAndUpdate(Utils.toDmgValue(user.getMaxHp() / 2), { result: HitResult.INDIRECT, ignoreSegments: true }); globalScene.queueMessage(i18next.t("moveTriggers:cutHpPowerUpMove", { pokemonName: getPokemonNameWithAffix(user) })); // Queue recoil message } return true; @@ -1806,7 +1806,7 @@ export class AddSubstituteAttr extends MoveEffectAttr { } const damageTaken = this.roundUp ? Math.ceil(user.getMaxHp() * this.hpCost) : Math.floor(user.getMaxHp() * this.hpCost); - user.damageAndUpdate(damageTaken, HitResult.OTHER, false, true, true); + user.damageAndUpdate(damageTaken, { result: HitResult.INDIRECT, ignoreSegments: true, ignoreFaintPhase: true }); user.addTag(BattlerTagType.SUBSTITUTE, 0, move.id, user.id); return true; } @@ -1956,7 +1956,7 @@ export class FlameBurstAttr extends MoveEffectAttr { return false; } - targetAlly.damageAndUpdate(Math.max(1, Math.floor(1 / 16 * targetAlly.getMaxHp())), HitResult.OTHER); + targetAlly.damageAndUpdate(Math.max(1, Math.floor(1 / 16 * targetAlly.getMaxHp())), { result: HitResult.INDIRECT }); return true; } @@ -3435,9 +3435,8 @@ export class CutHpStatStageBoostAttr extends StatStageChangeAttr { this.cutRatio = cutRatio; this.messageCallback = messageCallback; } - override apply(user: Pokemon, target: Pokemon, move: Move, args: any[]): boolean { - user.damageAndUpdate(Utils.toDmgValue(user.getMaxHp() / this.cutRatio), HitResult.OTHER, false, true); + user.damageAndUpdate(Utils.toDmgValue(user.getMaxHp() / this.cutRatio), { result: HitResult.INDIRECT }); user.updateInfo(); const ret = super.apply(user, target, move, args); if (this.messageCallback) { @@ -5329,7 +5328,7 @@ const crashDamageFunc = (user: Pokemon, move: Move) => { return false; } - user.damageAndUpdate(Utils.toDmgValue(user.getMaxHp() / 2), HitResult.OTHER, false, true); + user.damageAndUpdate(Utils.toDmgValue(user.getMaxHp() / 2), { result: HitResult.INDIRECT }); globalScene.queueMessage(i18next.t("moveTriggers:keptGoingAndCrashed", { pokemonName: getPokemonNameWithAffix(user) })); user.turnData.damageTaken += Utils.toDmgValue(user.getMaxHp() / 2); @@ -5650,7 +5649,7 @@ export class CurseAttr extends MoveEffectAttr { return false; } const curseRecoilDamage = Math.max(1, Math.floor(user.getMaxHp() / 2)); - user.damageAndUpdate(curseRecoilDamage, HitResult.OTHER, false, true, true); + user.damageAndUpdate(curseRecoilDamage, { result: HitResult.INDIRECT, ignoreSegments: true }); globalScene.queueMessage( i18next.t("battlerTags:cursedOnAdd", { pokemonNameWithAffix: getPokemonNameWithAffix(user), diff --git a/src/field/damage-number-handler.ts b/src/field/damage-number-handler.ts index 63da641a114..9e0010a0c10 100644 --- a/src/field/damage-number-handler.ts +++ b/src/field/damage-number-handler.ts @@ -46,6 +46,7 @@ export default class DamageNumberHandler { case HitResult.NOT_VERY_EFFECTIVE: [textColor, shadowColor] = ["#f08030", "#c03028"]; break; + case HitResult.INDIRECT_KO: case HitResult.ONE_HIT_KO: [textColor, shadowColor] = ["#a040a0", "#483850"]; break; diff --git a/src/field/pokemon.ts b/src/field/pokemon.ts index 42fe1dc1010..43bab0f049d 100644 --- a/src/field/pokemon.ts +++ b/src/field/pokemon.ts @@ -4465,11 +4465,9 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container { return result; } - // In case of fatal damage, this tag would have gotten cleared before we could lapse it. - const destinyTag = this.getTag(BattlerTagType.DESTINY_BOND); - const grudgeTag = this.getTag(BattlerTagType.GRUDGE); - - const isOneHitKo = result === HitResult.ONE_HIT_KO; + // In case of fatal damage, this tag would have gotten cleared before we could lapse it. + const destinyTag = this.getTag(BattlerTagType.DESTINY_BOND); + const grudgeTag = this.getTag(BattlerTagType.GRUDGE); if (dmg) { this.lapseTags(BattlerTagLapseType.HIT); @@ -4484,19 +4482,17 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container { globalScene.applyModifiers(EnemyEndureChanceModifier, false, this); } - /** - * We explicitly require to ignore the faint phase here, as we want to show the messages - * about the critical hit and the super effective/not very effective messages before the faint phase. - */ - const damage = this.damageAndUpdate( - isBlockedBySubstitute ? 0 : dmg, - result as DamageResult, - isCritical, - isOneHitKo, - isOneHitKo, - true, - source, - ); + /** + * We explicitly require to ignore the faint phase here, as we want to show the messages + * about the critical hit and the super effective/not very effective messages before the faint phase. + */ + const damage = this.damageAndUpdate(isBlockedBySubstitute ? 0 : dmg, + { + result: result as DamageResult, + isCritical, + ignoreFaintPhase: true, + source + }); if (damage > 0) { if (source.isPlayer()) { @@ -4557,7 +4553,7 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container { globalScene.unshiftPhase( new FaintPhase( this.getBattlerIndex(), - isOneHitKo, + false, destinyTag, grudgeTag, source, @@ -4635,28 +4631,37 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container { /** * Called by apply(), given the damage, adds a new DamagePhase and actually updates HP values, etc. + * Checks for 'Indirect' HitResults to account for Endure/Reviver Seed applying correctly * @param damage integer - passed to damage() * @param result an enum if it's super effective, not very, etc. - * @param critical boolean if move is a critical hit + * @param isCritical boolean if move is a critical hit * @param ignoreSegments boolean, passed to damage() and not used currently * @param preventEndure boolean, ignore endure properties of pokemon, passed to damage() * @param ignoreFaintPhase boolean to ignore adding a FaintPhase, passsed to damage() * @returns integer of damage done */ - damageAndUpdate( - damage: number, - result?: DamageResult, - critical = false, - ignoreSegments = false, - preventEndure = false, - ignoreFaintPhase = false, - source?: Pokemon, + damageAndUpdate(damage: number, + { + result = HitResult.EFFECTIVE, + isCritical = false, + ignoreSegments = false, + ignoreFaintPhase = false, + source = undefined, + }: + { + result?: DamageResult, + isCritical?: boolean, + ignoreSegments?: boolean, + ignoreFaintPhase?: boolean, + source?: Pokemon, + } = {} ): number { + const isIndirectDamage = [ HitResult.INDIRECT, HitResult.INDIRECT_KO ].includes(result); const damagePhase = new DamageAnimPhase( - this.getBattlerIndex(), - damage, - result as DamageResult, - critical, + this.getBattlerIndex(), + damage, + result as DamageResult, + isCritical ); globalScene.unshiftPhase(damagePhase); if (this.switchOutStatus && source) { @@ -4665,7 +4670,7 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container { damage = this.damage( damage, ignoreSegments, - preventEndure, + isIndirectDamage, ignoreFaintPhase, ); // Damage amount may have changed, but needed to be queued before calling damage function @@ -7711,8 +7716,10 @@ export enum HitResult { HEAL, FAIL, MISS, - OTHER, + INDIRECT, IMMUNE, + CONFUSION, + INDIRECT_KO, } export type DamageResult = @@ -7720,7 +7727,9 @@ export type DamageResult = | HitResult.SUPER_EFFECTIVE | HitResult.NOT_VERY_EFFECTIVE | HitResult.ONE_HIT_KO - | HitResult.OTHER; + | HitResult.CONFUSION + | HitResult.INDIRECT_KO + | HitResult.INDIRECT; /** Interface containing the results of a damage calculation for a given move */ export interface DamageCalculationResult { diff --git a/src/modifier/modifier.ts b/src/modifier/modifier.ts index a084474ddac..4c59bfe1ef1 100644 --- a/src/modifier/modifier.ts +++ b/src/modifier/modifier.ts @@ -5,8 +5,7 @@ import { allMoves } from "#app/data/moves/move"; import { MAX_PER_TYPE_POKEBALLS } from "#app/data/pokeball"; import { type FormChangeItem, SpeciesFormChangeItemTrigger } from "#app/data/pokemon-forms"; import { getStatusEffectHealText } from "#app/data/status-effect"; -import type { PlayerPokemon } from "#app/field/pokemon"; -import Pokemon from "#app/field/pokemon"; +import Pokemon, { type PlayerPokemon } from "#app/field/pokemon"; import { getPokemonNameWithAffix } from "#app/messages"; import Overrides from "#app/overrides"; import { EvolutionPhase } from "#app/phases/evolution-phase"; diff --git a/src/phases/damage-anim-phase.ts b/src/phases/damage-anim-phase.ts index e31fe8c9475..703cd3d160e 100644 --- a/src/phases/damage-anim-phase.ts +++ b/src/phases/damage-anim-phase.ts @@ -21,7 +21,7 @@ export class DamageAnimPhase extends PokemonPhase { start() { super.start(); - if (this.damageResult === HitResult.ONE_HIT_KO) { + if (this.damageResult === HitResult.ONE_HIT_KO || this.damageResult === HitResult.INDIRECT_KO) { if (globalScene.moveAnimations) { globalScene.toggleInvert(true); } @@ -42,9 +42,11 @@ export class DamageAnimPhase extends PokemonPhase { applyDamage() { switch (this.damageResult) { case HitResult.EFFECTIVE: + case HitResult.CONFUSION: globalScene.playSound("se/hit"); break; case HitResult.SUPER_EFFECTIVE: + case HitResult.INDIRECT_KO: case HitResult.ONE_HIT_KO: globalScene.playSound("se/hit_strong"); break; @@ -57,7 +59,7 @@ export class DamageAnimPhase extends PokemonPhase { globalScene.damageNumberHandler.add(this.getPokemon(), this.amount, this.damageResult, this.critical); } - if (this.damageResult !== HitResult.OTHER && this.amount > 0) { + if (this.damageResult !== HitResult.INDIRECT && this.amount > 0) { const flashTimer = globalScene.time.addEvent({ delay: 100, repeat: 5, diff --git a/src/phases/faint-phase.ts b/src/phases/faint-phase.ts index 7fc7a517853..dfc0e0653a5 100644 --- a/src/phases/faint-phase.ts +++ b/src/phases/faint-phase.ts @@ -258,7 +258,7 @@ export class FaintPhase extends PokemonPhase { } else { // Final boss' HP threshold has been bypassed; cancel faint and force check for 2nd phase enemy.hp++; - globalScene.unshiftPhase(new DamageAnimPhase(enemy.getBattlerIndex(), 0, HitResult.OTHER)); + globalScene.unshiftPhase(new DamageAnimPhase(enemy.getBattlerIndex(), 0, HitResult.INDIRECT)); this.end(); } return true; diff --git a/src/phases/pokemon-heal-phase.ts b/src/phases/pokemon-heal-phase.ts index ab49def5a3d..ecfe99389eb 100644 --- a/src/phases/pokemon-heal-phase.ts +++ b/src/phases/pokemon-heal-phase.ts @@ -3,7 +3,6 @@ import type { BattlerIndex } from "#app/battle"; import { CommonAnim } from "#app/data/battle-anims"; import { getStatusEffectHealText } from "#app/data/status-effect"; import { StatusEffect } from "#app/enums/status-effect"; -import type { DamageResult } from "#app/field/pokemon"; import { HitResult } from "#app/field/pokemon"; import { getPokemonNameWithAffix } from "#app/messages"; import { HealingBoosterModifier } from "#app/modifier/modifier"; @@ -79,7 +78,7 @@ export class PokemonHealPhase extends CommonAnimPhase { } const healAmount = new Utils.NumberHolder(Math.floor(this.hpHealed * hpRestoreMultiplier.value)); if (healAmount.value < 0) { - pokemon.damageAndUpdate(healAmount.value * -1, HitResult.HEAL as DamageResult); + pokemon.damageAndUpdate(healAmount.value * -1, { result: HitResult.INDIRECT }); healAmount.value = 0; } // Prevent healing to full if specified (in case of healing tokens so Sturdy doesn't cause a softlock) diff --git a/src/phases/weather-effect-phase.ts b/src/phases/weather-effect-phase.ts index 9199b7996bc..d7a1f193029 100644 --- a/src/phases/weather-effect-phase.ts +++ b/src/phases/weather-effect-phase.ts @@ -66,7 +66,7 @@ export class WeatherEffectPhase extends CommonAnimPhase { const damage = Utils.toDmgValue(pokemon.getMaxHp() / 16); globalScene.queueMessage(getWeatherDamageMessage(this.weather?.weatherType!, pokemon)!); // TODO: are those bangs correct? - pokemon.damageAndUpdate(damage, HitResult.EFFECTIVE, false, false, true); + pokemon.damageAndUpdate(damage, { result: HitResult.INDIRECT, ignoreSegments: true }); }; this.executeForAll((pokemon: Pokemon) => { diff --git a/test/items/reviver_seed.test.ts b/test/items/reviver_seed.test.ts new file mode 100644 index 00000000000..ab249d48a23 --- /dev/null +++ b/test/items/reviver_seed.test.ts @@ -0,0 +1,159 @@ +import { BattlerIndex } from "#app/battle"; +import { allMoves } from "#app/data/moves/move"; +import { BattlerTagType } from "#app/enums/battler-tag-type"; +import type { PokemonInstantReviveModifier } from "#app/modifier/modifier"; +import { Abilities } from "#enums/abilities"; +import { Moves } from "#enums/moves"; +import { Species } from "#enums/species"; +import GameManager from "#test/testUtils/gameManager"; +import Phaser from "phaser"; +import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest"; + +describe("Items - Reviver Seed", () => { + 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 + .moveset([ Moves.SPLASH, Moves.TACKLE, Moves.ENDURE ]) + .ability(Abilities.BALL_FETCH) + .battleType("single") + .disableCrits() + .enemySpecies(Species.MAGIKARP) + .enemyAbility(Abilities.BALL_FETCH) + .startingHeldItems([{ name: "REVIVER_SEED" }]) + .enemyHeldItems([{ name: "REVIVER_SEED" }]) + .enemyMoveset(Moves.SPLASH); + vi.spyOn(allMoves[Moves.SHEER_COLD], "accuracy", "get").mockReturnValue(100); + vi.spyOn(allMoves[Moves.LEECH_SEED], "accuracy", "get").mockReturnValue(100); + vi.spyOn(allMoves[Moves.WHIRLPOOL], "accuracy", "get").mockReturnValue(100); + vi.spyOn(allMoves[Moves.WILL_O_WISP], "accuracy", "get").mockReturnValue(100); + }); + + it.each([ + { moveType: "Special Move", move: Moves.WATER_GUN }, + { moveType: "Physical Move", move: Moves.TACKLE }, + { moveType: "Fixed Damage Move", move: Moves.SEISMIC_TOSS }, + { moveType: "Final Gambit", move: Moves.FINAL_GAMBIT }, + { moveType: "Counter", move: Moves.COUNTER }, + { moveType: "OHKO", move: Moves.SHEER_COLD } + ])("should activate the holder's reviver seed from a $moveType", async ({ move }) => { + game.override + .enemyLevel(100) + .startingLevel(1) + .enemyMoveset(move); + await game.classicMode.startBattle([ Species.MAGIKARP, Species.FEEBAS ]); + const player = game.scene.getPlayerPokemon()!; + player.damageAndUpdate(player.hp - 1); + + const reviverSeed = player.getHeldItems()[0] as PokemonInstantReviveModifier; + vi.spyOn(reviverSeed, "apply"); + + game.move.select(Moves.TACKLE); + await game.phaseInterceptor.to("BerryPhase"); + + expect(player.isFainted()).toBeFalsy(); + }); + + it("should activate the holder's reviver seed from confusion self-hit", async () => { + game.override + .enemyLevel(1) + .startingLevel(100) + .enemyMoveset(Moves.SPLASH); + await game.classicMode.startBattle([ Species.MAGIKARP, Species.FEEBAS ]); + const player = game.scene.getPlayerPokemon()!; + player.damageAndUpdate(player.hp - 1); + player.addTag(BattlerTagType.CONFUSED, 3); + + const reviverSeed = player.getHeldItems()[0] as PokemonInstantReviveModifier; + vi.spyOn(reviverSeed, "apply"); + + vi.spyOn(player, "randSeedInt").mockReturnValue(0); // Force confusion self-hit + game.move.select(Moves.TACKLE); + await game.phaseInterceptor.to("BerryPhase"); + + expect(player.isFainted()).toBeFalsy(); + }); + + // Damaging opponents tests + it.each([ + { moveType: "Damaging Move Chip Damage", move: Moves.SALT_CURE }, + { moveType: "Chip Damage", move: Moves.LEECH_SEED }, + { moveType: "Trapping Chip Damage", move: Moves.WHIRLPOOL }, + { moveType: "Status Effect Damage", move: Moves.WILL_O_WISP }, + { moveType: "Weather", move: Moves.SANDSTORM }, + ])("should not activate the holder's reviver seed from $moveType", async ({ move }) => { + game.override + .enemyLevel(1) + .startingLevel(100) + .enemySpecies(Species.MAGIKARP) + .moveset(move) + .enemyMoveset(Moves.ENDURE); + await game.classicMode.startBattle([ Species.MAGIKARP, Species.FEEBAS ]); + const enemy = game.scene.getEnemyPokemon()!; + enemy.damageAndUpdate(enemy.hp - 1); + + game.move.select(move); + await game.phaseInterceptor.to("TurnEndPhase"); + + expect(enemy.isFainted()).toBeTruthy(); + }); + + // Self-damage tests + it.each([ + { moveType: "Recoil", move: Moves.DOUBLE_EDGE }, + { moveType: "Self-KO", move: Moves.EXPLOSION }, + { moveType: "Self-Deduction", move: Moves.CURSE }, + { moveType: "Liquid Ooze", move: Moves.GIGA_DRAIN }, + ])("should not activate the holder's reviver seed from $moveType", async ({ move }) => { + game.override + .enemyLevel(100) + .startingLevel(1) + .enemySpecies(Species.MAGIKARP) + .moveset(move) + .enemyAbility(Abilities.LIQUID_OOZE) + .enemyMoveset(Moves.SPLASH); + await game.classicMode.startBattle([ Species.GASTLY, Species.FEEBAS ]); + const player = game.scene.getPlayerPokemon()!; + player.damageAndUpdate(player.hp - 1); + + const playerSeed = player.getHeldItems()[0] as PokemonInstantReviveModifier; + vi.spyOn(playerSeed, "apply"); + + game.move.select(move); + await game.phaseInterceptor.to("TurnEndPhase"); + + expect(player.isFainted()).toBeTruthy(); + }); + + it("should not activate the holder's reviver seed from Destiny Bond fainting", async () => { + game.override + .enemyLevel(100) + .startingLevel(1) + .enemySpecies(Species.MAGIKARP) + .moveset(Moves.DESTINY_BOND) + .startingHeldItems([]) // reset held items to nothing so user doesn't revive and not trigger Destiny Bond + .enemyMoveset(Moves.TACKLE); + await game.classicMode.startBattle([ Species.MAGIKARP, Species.FEEBAS ]); + const player = game.scene.getPlayerPokemon()!; + player.damageAndUpdate(player.hp - 1); + const enemy = game.scene.getEnemyPokemon()!; + + game.move.select(Moves.DESTINY_BOND); + await game.setTurnOrder([ BattlerIndex.PLAYER, BattlerIndex.ENEMY ]); + await game.phaseInterceptor.to("TurnEndPhase"); + + expect(enemy.isFainted()).toBeTruthy(); + }); +}); diff --git a/test/moves/endure.test.ts b/test/moves/endure.test.ts index 8151128479d..d706d5d9581 100644 --- a/test/moves/endure.test.ts +++ b/test/moves/endure.test.ts @@ -22,7 +22,7 @@ describe("Moves - Endure", () => { beforeEach(() => { game = new GameManager(phaserGame); game.override - .moveset([Moves.THUNDER, Moves.BULLET_SEED, Moves.TOXIC]) + .moveset([ Moves.THUNDER, Moves.BULLET_SEED, Moves.TOXIC, Moves.SHEER_COLD ]) .ability(Abilities.SKILL_LINK) .startingLevel(100) .battleType("single") @@ -50,16 +50,37 @@ describe("Moves - Endure", () => { expect(game.scene.getEnemyPokemon()!.hp).toBe(1); }); - it("shouldn't prevent fainting from indirect damage", async () => { - game.override.enemyLevel(100); - await game.classicMode.startBattle([Species.ARCEUS]); - + it("should let the pokemon survive against OHKO moves", async () => { + await game.classicMode.startBattle([ Species.MAGIKARP ]); const enemy = game.scene.getEnemyPokemon()!; - enemy.hp = 2; - game.move.select(Moves.TOXIC); - await game.phaseInterceptor.to("VictoryPhase"); + game.move.select(Moves.SHEER_COLD); + await game.phaseInterceptor.to("TurnEndPhase"); - expect(enemy.isFainted()).toBe(true); + expect(enemy.isFainted()).toBeFalsy(); + }); + + // comprehensive indirect damage test copied from Reviver Seed test + it.each([ + { moveType: "Damaging Move Chip Damage", move: Moves.SALT_CURE }, + { moveType: "Chip Damage", move: Moves.LEECH_SEED }, + { moveType: "Trapping Chip Damage", move: Moves.WHIRLPOOL }, + { moveType: "Status Effect Damage", move: Moves.TOXIC }, + { moveType: "Weather", move: Moves.SANDSTORM }, + ])("should not prevent fainting from $moveType", async ({ move }) => { + game.override + .enemyLevel(1) + .startingLevel(100) + .enemySpecies(Species.MAGIKARP) + .moveset(move) + .enemyMoveset(Moves.ENDURE); + await game.classicMode.startBattle([ Species.MAGIKARP, Species.FEEBAS ]); + const enemy = game.scene.getEnemyPokemon()!; + enemy.damageAndUpdate(enemy.hp - 1); + + game.move.select(move); + await game.phaseInterceptor.to("TurnEndPhase"); + + expect(enemy.isFainted()).toBeTruthy(); }); });