mirror of
https://github.com/pagefaultgames/pokerogue.git
synced 2025-05-12 19:34:47 +01:00
[Bug] Fix Reviver Seed and endure triggering on indirect damage (#5182)
* Create new turnData field for tracking damageResults, check for HitResult in Reviver Seed modifier * Optional chaining for cases like stealth rock * Adds HitResult.SELF for confusion to distinguish from indirect damage * Adds HitResult.SELF to damage sound effect switch * Cover edge case of salt cure, insert HitResult for ALL damage regardless of optional variable * Change Liquid Ooze HitResult to OTHER from HEAL * Adjust OHKO moves to not bypass endure or RSeed * Add tests for reviver seed * Fixes endure to no longer block indirect damage, updates weather damage to be HitResult.OTHER, adds/fixes unit test * Change destiny bond to HitResult.OTHER so it doesn't trigger rseed * Adds destiny bond unit test * Creates additional unit tests for endure * Rename SELF hitresult to CONFUSION * Update CONFUSION enum * Refactors implementation per Wlowscha's suggestions: removes damageSources array and preventEndure variable * Rename HitResult.OTHER to INDIRECT, create INDIRECT_KO for PSong/DBond, add functionality for INDIRECT_KO to damageanim/number handler * Fixes hit result for stealth rock * Removes unnecessary check, makes DamageResult default to EFFECTIVE, updates remaining damageAndUpdate calls to use INDIRECT * Refactors damageAndUpdate to replace optional parameters with object parameter * Fixes based on Kev's suggestions * Updates tsdocs for damageAndUpdate * Fix merge conflict --------- Co-authored-by: Wlowscha <54003515+Wlowscha@users.noreply.github.com>
This commit is contained in:
parent
817095d895
commit
40e1e7fd4e
@ -1 +1 @@
|
||||
Subproject commit 6b3f37cb351552721232f4dabefa17bddb5b9004
|
||||
Subproject commit 0e5c6096ba26f6b87aed1aab3fe9b0b23f6cbb7b
|
@ -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 });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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);
|
||||
|
@ -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) {
|
||||
|
@ -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),
|
||||
|
@ -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;
|
||||
|
@ -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 {
|
||||
|
@ -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";
|
||||
|
@ -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,
|
||||
|
@ -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;
|
||||
|
@ -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)
|
||||
|
@ -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) => {
|
||||
|
159
test/items/reviver_seed.test.ts
Normal file
159
test/items/reviver_seed.test.ts
Normal file
@ -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();
|
||||
});
|
||||
});
|
@ -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();
|
||||
});
|
||||
});
|
||||
|
Loading…
x
Reference in New Issue
Block a user