mirror of
https://github.com/pagefaultgames/pokerogue.git
synced 2025-08-20 06:19:29 +02:00
Rewrite Move Effect Phase
This commit is contained in:
parent
360a897ed2
commit
2f26afd6fd
@ -2943,8 +2943,13 @@ export class UserFieldBattlerTagImmunityAbAttr extends PreApplyBattlerTagImmunit
|
|||||||
|
|
||||||
export class BlockCritAbAttr extends AbAttr {
|
export class BlockCritAbAttr extends AbAttr {
|
||||||
apply(pokemon: Pokemon, passive: boolean, simulated: boolean, cancelled: Utils.BooleanHolder, args: any[]): boolean {
|
apply(pokemon: Pokemon, passive: boolean, simulated: boolean, cancelled: Utils.BooleanHolder, args: any[]): boolean {
|
||||||
(args[0] as Utils.BooleanHolder).value = true;
|
const isCritical = args[0] as Utils.BooleanHolder;
|
||||||
return true;
|
|
||||||
|
if (isCritical.value) {
|
||||||
|
isCritical.value = false;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -294,8 +294,6 @@ export class ConditionalProtectTag extends ArenaTag {
|
|||||||
if (!isProtected.value) {
|
if (!isProtected.value) {
|
||||||
isProtected.value = true;
|
isProtected.value = true;
|
||||||
if (!simulated) {
|
if (!simulated) {
|
||||||
attacker.stopMultiHit(defender);
|
|
||||||
|
|
||||||
new CommonBattleAnim(CommonAnim.PROTECT, defender).play(arena.scene);
|
new CommonBattleAnim(CommonAnim.PROTECT, defender).play(arena.scene);
|
||||||
arena.scene.queueMessage(i18next.t("arenaTag:conditionalProtectApply", { moveName: super.getMoveName(), pokemonNameWithAffix: getPokemonNameWithAffix(defender) }));
|
arena.scene.queueMessage(i18next.t("arenaTag:conditionalProtectApply", { moveName: super.getMoveName(), pokemonNameWithAffix: getPokemonNameWithAffix(defender) }));
|
||||||
}
|
}
|
||||||
|
@ -1363,17 +1363,15 @@ export class ProtectedTag extends BattlerTag {
|
|||||||
if (lapseType === BattlerTagLapseType.CUSTOM) {
|
if (lapseType === BattlerTagLapseType.CUSTOM) {
|
||||||
new CommonBattleAnim(CommonAnim.PROTECT, pokemon).play(pokemon.scene);
|
new CommonBattleAnim(CommonAnim.PROTECT, pokemon).play(pokemon.scene);
|
||||||
pokemon.scene.queueMessage(i18next.t("battlerTags:protectedLapse", { pokemonNameWithAffix: getPokemonNameWithAffix(pokemon) }));
|
pokemon.scene.queueMessage(i18next.t("battlerTags:protectedLapse", { pokemonNameWithAffix: getPokemonNameWithAffix(pokemon) }));
|
||||||
|
|
||||||
// Stop multi-hit moves early
|
|
||||||
const effectPhase = pokemon.scene.getCurrentPhase();
|
|
||||||
if (effectPhase instanceof MoveEffectPhase) {
|
|
||||||
effectPhase.stopMultiHit(pokemon);
|
|
||||||
}
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
return super.lapse(pokemon, lapseType);
|
return super.lapse(pokemon, lapseType);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
apply(pokemon: Pokemon, hitCheckResult: NumberHolder) {
|
||||||
|
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Base class for `BattlerTag`s that block damaging moves but not status moves */
|
/** Base class for `BattlerTag`s that block damaging moves but not status moves */
|
||||||
|
@ -307,6 +307,16 @@ export default class Move implements Localizable {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
isFieldTarget(): boolean {
|
||||||
|
switch (this.moveTarget) {
|
||||||
|
case MoveTarget.BOTH_SIDES:
|
||||||
|
case MoveTarget.USER_SIDE:
|
||||||
|
case MoveTarget.ENEMY_SIDE:
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
isChargingMove(): this is ChargingMove {
|
isChargingMove(): this is ChargingMove {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@ -4037,8 +4047,8 @@ export class PresentPowerAttr extends VariablePowerAttr {
|
|||||||
} else if (70 < powerSeed && powerSeed <= 80) {
|
} else if (70 < powerSeed && powerSeed <= 80) {
|
||||||
(args[0] as Utils.NumberHolder).value = 120;
|
(args[0] as Utils.NumberHolder).value = 120;
|
||||||
} else if (80 < powerSeed && powerSeed <= 100) {
|
} else if (80 < powerSeed && powerSeed <= 100) {
|
||||||
// If this move is multi-hit, disable all other hits
|
user.turnData.hitCount -= user.turnData.hitsLeft - 1;
|
||||||
user.stopMultiHit();
|
user.turnData.hitsLeft = 1;
|
||||||
target.scene.unshiftPhase(new PokemonHealPhase(target.scene, target.getBattlerIndex(),
|
target.scene.unshiftPhase(new PokemonHealPhase(target.scene, target.getBattlerIndex(),
|
||||||
Utils.toDmgValue(target.getMaxHp() / 4), i18next.t("moveTriggers:regainedHealth", { pokemonName: getPokemonNameWithAffix(target) }), true));
|
Utils.toDmgValue(target.getMaxHp() / 4), i18next.t("moveTriggers:regainedHealth", { pokemonName: getPokemonNameWithAffix(target) }), true));
|
||||||
}
|
}
|
||||||
|
@ -13,24 +13,24 @@ import { TypeDamageMultiplier, getTypeDamageMultiplier, getTypeRgb } from "#app/
|
|||||||
import { Type } from "#enums/type";
|
import { Type } from "#enums/type";
|
||||||
import { getLevelTotalExp } from "#app/data/exp";
|
import { getLevelTotalExp } from "#app/data/exp";
|
||||||
import { Stat, type PermanentStat, type BattleStat, type EffectiveStat, PERMANENT_STATS, BATTLE_STATS, EFFECTIVE_STATS } from "#enums/stat";
|
import { Stat, type PermanentStat, type BattleStat, type EffectiveStat, PERMANENT_STATS, BATTLE_STATS, EFFECTIVE_STATS } from "#enums/stat";
|
||||||
import { DamageMoneyRewardModifier, EnemyDamageBoosterModifier, EnemyDamageReducerModifier, EnemyEndureChanceModifier, EnemyFusionChanceModifier, HiddenAbilityRateBoosterModifier, BaseStatModifier, PokemonFriendshipBoosterModifier, PokemonHeldItemModifier, PokemonNatureWeightModifier, ShinyRateBoosterModifier, SurviveDamageModifier, TempStatStageBoosterModifier, TempCritBoosterModifier, StatBoosterModifier, CritBoosterModifier, TerastallizeModifier, PokemonBaseStatFlatModifier, PokemonBaseStatTotalModifier, PokemonIncrementingStatModifier, EvoTrackerModifier, PokemonMultiHitModifier } from "#app/modifier/modifier";
|
import { EnemyDamageBoosterModifier, EnemyFusionChanceModifier, HiddenAbilityRateBoosterModifier, BaseStatModifier, PokemonFriendshipBoosterModifier, PokemonHeldItemModifier, PokemonNatureWeightModifier, ShinyRateBoosterModifier, SurviveDamageModifier, TempStatStageBoosterModifier, TempCritBoosterModifier, StatBoosterModifier, CritBoosterModifier, TerastallizeModifier, PokemonBaseStatFlatModifier, PokemonBaseStatTotalModifier, PokemonIncrementingStatModifier, EvoTrackerModifier, PokemonMultiHitModifier, EnemyDamageReducerModifier } from "#app/modifier/modifier";
|
||||||
import { PokeballType } from "#enums/pokeball";
|
import { PokeballType } from "#enums/pokeball";
|
||||||
import { Gender } from "#app/data/gender";
|
import { Gender } from "#app/data/gender";
|
||||||
import { initMoveAnim, loadMoveAnimAssets } from "#app/data/battle-anims";
|
import { initMoveAnim, loadMoveAnimAssets } from "#app/data/battle-anims";
|
||||||
import { Status, getRandomStatus } from "#app/data/status-effect";
|
import { Status, getRandomStatus } from "#app/data/status-effect";
|
||||||
import { pokemonEvolutions, pokemonPrevolutions, SpeciesFormEvolution, SpeciesEvolutionCondition, FusionSpeciesFormEvolution } from "#app/data/balance/pokemon-evolutions";
|
import { pokemonEvolutions, pokemonPrevolutions, SpeciesFormEvolution, SpeciesEvolutionCondition, FusionSpeciesFormEvolution } from "#app/data/balance/pokemon-evolutions";
|
||||||
import { reverseCompatibleTms, tmSpecies, tmPoolTiers } from "#app/data/balance/tms";
|
import { reverseCompatibleTms, tmSpecies, tmPoolTiers } from "#app/data/balance/tms";
|
||||||
import { BattlerTag, BattlerTagLapseType, EncoreTag, GroundedTag, HighestStatBoostTag, SubstituteTag, TypeImmuneTag, getBattlerTag, SemiInvulnerableTag, TypeBoostTag, MoveRestrictionBattlerTag, ExposedTag, DragonCheerTag, CritBoostTag, TrappedTag, TarShotTag, AutotomizedTag, PowerTrickTag } from "../data/battler-tags";
|
import { BattlerTag, BattlerTagLapseType, EncoreTag, GroundedTag, HighestStatBoostTag, SubstituteTag, TypeImmuneTag, getBattlerTag, SemiInvulnerableTag, MoveRestrictionBattlerTag, ExposedTag, DragonCheerTag, CritBoostTag, TrappedTag, TarShotTag, AutotomizedTag, PowerTrickTag } from "../data/battler-tags";
|
||||||
import { WeatherType } from "#enums/weather-type";
|
import { WeatherType } from "#enums/weather-type";
|
||||||
import { ArenaTagSide, NoCritTag, WeakenMoveScreenTag } from "#app/data/arena-tag";
|
import { ArenaTagSide, NoCritTag, WeakenMoveScreenTag } from "#app/data/arena-tag";
|
||||||
import { Ability, AbAttr, StatMultiplierAbAttr, BlockCritAbAttr, BonusCritAbAttr, BypassBurnDamageReductionAbAttr, FieldPriorityMoveImmunityAbAttr, IgnoreOpponentStatStagesAbAttr, MoveImmunityAbAttr, PreDefendFullHpEndureAbAttr, ReceivedMoveDamageMultiplierAbAttr, StabBoostAbAttr, StatusEffectImmunityAbAttr, TypeImmunityAbAttr, WeightMultiplierAbAttr, allAbilities, applyAbAttrs, applyStatMultiplierAbAttrs, applyPreApplyBattlerTagAbAttrs, applyPreAttackAbAttrs, applyPreDefendAbAttrs, applyPreSetStatusAbAttrs, UnsuppressableAbilityAbAttr, SuppressFieldAbilitiesAbAttr, NoFusionAbilityAbAttr, MultCritAbAttr, IgnoreTypeImmunityAbAttr, DamageBoostAbAttr, IgnoreTypeStatusEffectImmunityAbAttr, ConditionalCritAbAttr, applyFieldStatMultiplierAbAttrs, FieldMultiplyStatAbAttr, AddSecondStrikeAbAttr, UserFieldStatusEffectImmunityAbAttr, UserFieldBattlerTagImmunityAbAttr, BattlerTagImmunityAbAttr, MoveTypeChangeAbAttr, FullHpResistTypeAbAttr, applyCheckTrappedAbAttrs, CheckTrappedAbAttr, PostSetStatusAbAttr, applyPostSetStatusAbAttrs, InfiltratorAbAttr, AlliedFieldDamageReductionAbAttr, PostDamageAbAttr, applyPostDamageAbAttrs, PostDamageForceSwitchAbAttr, CommanderAbAttr, applyPostItemLostAbAttrs, PostItemLostAbAttr } from "#app/data/ability";
|
import { Ability, AbAttr, StatMultiplierAbAttr, BlockCritAbAttr, BonusCritAbAttr, BypassBurnDamageReductionAbAttr, FieldPriorityMoveImmunityAbAttr, IgnoreOpponentStatStagesAbAttr, MoveImmunityAbAttr, PreDefendFullHpEndureAbAttr, ReceivedMoveDamageMultiplierAbAttr, StabBoostAbAttr, StatusEffectImmunityAbAttr, TypeImmunityAbAttr, WeightMultiplierAbAttr, allAbilities, applyAbAttrs, applyStatMultiplierAbAttrs, applyPreApplyBattlerTagAbAttrs, applyPreAttackAbAttrs, applyPreDefendAbAttrs, applyPreSetStatusAbAttrs, UnsuppressableAbilityAbAttr, SuppressFieldAbilitiesAbAttr, NoFusionAbilityAbAttr, MultCritAbAttr, IgnoreTypeImmunityAbAttr, DamageBoostAbAttr, IgnoreTypeStatusEffectImmunityAbAttr, ConditionalCritAbAttr, applyFieldStatMultiplierAbAttrs, FieldMultiplyStatAbAttr, AddSecondStrikeAbAttr, UserFieldStatusEffectImmunityAbAttr, UserFieldBattlerTagImmunityAbAttr, BattlerTagImmunityAbAttr, MoveTypeChangeAbAttr, FullHpResistTypeAbAttr, applyCheckTrappedAbAttrs, CheckTrappedAbAttr, PostSetStatusAbAttr, applyPostSetStatusAbAttrs, InfiltratorAbAttr, AlliedFieldDamageReductionAbAttr, PostDamageAbAttr, applyPostDamageAbAttrs, CommanderAbAttr, applyPostItemLostAbAttrs, PostItemLostAbAttr } from "#app/data/ability";
|
||||||
import PokemonData from "#app/system/pokemon-data";
|
import PokemonData from "#app/system/pokemon-data";
|
||||||
import { BattlerIndex } from "#app/battle";
|
import { BattlerIndex } from "#app/battle";
|
||||||
import { Mode } from "#app/ui/ui";
|
import { Mode } from "#app/ui/ui";
|
||||||
import PartyUiHandler, { PartyOption, PartyUiMode } from "#app/ui/party-ui-handler";
|
import PartyUiHandler, { PartyOption, PartyUiMode } from "#app/ui/party-ui-handler";
|
||||||
import SoundFade from "phaser3-rex-plugins/plugins/soundfade";
|
import SoundFade from "phaser3-rex-plugins/plugins/soundfade";
|
||||||
import { LevelMoves } from "#app/data/balance/pokemon-level-moves";
|
import { LevelMoves } from "#app/data/balance/pokemon-level-moves";
|
||||||
import { DamageAchv, achvs } from "#app/system/achv";
|
import { achvs } from "#app/system/achv";
|
||||||
import { DexAttr, StarterDataEntry, StarterMoveset } from "#app/system/game-data";
|
import { DexAttr, StarterDataEntry, StarterMoveset } from "#app/system/game-data";
|
||||||
import { QuantizerCelebi, argbFromRgba, rgbaFromArgb } from "@material/material-color-utilities";
|
import { QuantizerCelebi, argbFromRgba, rgbaFromArgb } from "@material/material-color-utilities";
|
||||||
import { getNatureStatMultiplier } from "#app/data/nature";
|
import { getNatureStatMultiplier } from "#app/data/nature";
|
||||||
@ -50,7 +50,6 @@ import { BerryType } from "#enums/berry-type";
|
|||||||
import { Biome } from "#enums/biome";
|
import { Biome } from "#enums/biome";
|
||||||
import { Moves } from "#enums/moves";
|
import { Moves } from "#enums/moves";
|
||||||
import { Species } from "#enums/species";
|
import { Species } from "#enums/species";
|
||||||
import { getPokemonNameWithAffix } from "#app/messages";
|
|
||||||
import { DamagePhase } from "#app/phases/damage-phase";
|
import { DamagePhase } from "#app/phases/damage-phase";
|
||||||
import { FaintPhase } from "#app/phases/faint-phase";
|
import { FaintPhase } from "#app/phases/faint-phase";
|
||||||
import { LearnMovePhase } from "#app/phases/learn-move-phase";
|
import { LearnMovePhase } from "#app/phases/learn-move-phase";
|
||||||
@ -896,19 +895,18 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container {
|
|||||||
* who used it.
|
* who used it.
|
||||||
* @param source the {@linkcode Pokemon} who using the move
|
* @param source the {@linkcode Pokemon} who using the move
|
||||||
* @param move the {@linkcode Move} being used
|
* @param move the {@linkcode Move} being used
|
||||||
|
* @param simulated if `true`, obtains the critical-hit stage quietly
|
||||||
* @returns the final critical-hit stage value
|
* @returns the final critical-hit stage value
|
||||||
*/
|
*/
|
||||||
getCritStage(source: Pokemon, move: Move): number {
|
getCritStage(source: Pokemon, move: Move, simulated: boolean = true): number {
|
||||||
const critStage = new Utils.IntegerHolder(0);
|
const critStage = new Utils.NumberHolder(0);
|
||||||
applyMoveAttrs(HighCritAttr, source, this, move, critStage);
|
applyMoveAttrs(HighCritAttr, source, this, move, critStage);
|
||||||
this.scene.applyModifiers(CritBoosterModifier, source.isPlayer(), source, critStage);
|
this.scene.applyModifiers(CritBoosterModifier, source.isPlayer(), source, critStage);
|
||||||
this.scene.applyModifiers(TempCritBoosterModifier, source.isPlayer(), critStage);
|
this.scene.applyModifiers(TempCritBoosterModifier, source.isPlayer(), critStage);
|
||||||
const bonusCrit = new Utils.BooleanHolder(false);
|
const bonusCrit = new Utils.BooleanHolder(false);
|
||||||
//@ts-ignore
|
applyAbAttrs(BonusCritAbAttr, source, null, simulated, bonusCrit);
|
||||||
if (applyAbAttrs(BonusCritAbAttr, source, null, false, bonusCrit)) { // TODO: resolve ts-ignore. This is a promise. Checking a promise is bogus.
|
if (bonusCrit.value) {
|
||||||
if (bonusCrit.value) {
|
critStage.value += 1;
|
||||||
critStage.value += 1;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
const critBoostTag = source.getTag(CritBoostTag);
|
const critBoostTag = source.getTag(CritBoostTag);
|
||||||
if (critBoostTag) {
|
if (critBoostTag) {
|
||||||
@ -1591,8 +1589,8 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container {
|
|||||||
/**
|
/**
|
||||||
* Calculates the type of a move when used by this Pokemon after
|
* Calculates the type of a move when used by this Pokemon after
|
||||||
* type-changing move and ability attributes have applied.
|
* type-changing move and ability attributes have applied.
|
||||||
* @param move - {@linkcode Move} The move being used.
|
* @param move The {@linkcode Move} being used.
|
||||||
* @param simulated - If `true`, prevents showing abilities applied in this calculation.
|
* @param simulated If `true`, prevents showing abilities applied in this calculation.
|
||||||
* @returns The {@linkcode Type} of the move after attributes are applied
|
* @returns The {@linkcode Type} of the move after attributes are applied
|
||||||
*/
|
*/
|
||||||
public getMoveType(move: Move, simulated: boolean = true): Type {
|
public getMoveType(move: Move, simulated: boolean = true): Type {
|
||||||
@ -1609,6 +1607,19 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container {
|
|||||||
return moveTypeHolder.value as Type;
|
return moveTypeHolder.value as Type;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Calculates the category of a move when used by this Pokemon after
|
||||||
|
* category-changing move effects are applied.
|
||||||
|
* @param target The {@linkcode Pokemon} targeted by the move
|
||||||
|
* @param move The {@linkcode Move} being used
|
||||||
|
* @returns The given move's final category when used against the target
|
||||||
|
*/
|
||||||
|
public getMoveCategory(target: Pokemon, move: Move): MoveCategory {
|
||||||
|
const moveCategory = new Utils.NumberHolder(move.category);
|
||||||
|
applyMoveAttrs(VariableMoveCategoryAttr, this, target, move, moveCategory);
|
||||||
|
return moveCategory.value;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Calculates the effectiveness of a move against the Pokémon.
|
* Calculates the effectiveness of a move against the Pokémon.
|
||||||
@ -2569,12 +2580,14 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container {
|
|||||||
* @param ignoreSourceAbility If `true`, ignores the attacking Pokemon's ability effects
|
* @param ignoreSourceAbility If `true`, ignores the attacking Pokemon's ability effects
|
||||||
* @param isCritical If `true`, calculates damage for a critical hit.
|
* @param isCritical If `true`, calculates damage for a critical hit.
|
||||||
* @param simulated If `true`, suppresses changes to game state during the calculation.
|
* @param simulated If `true`, suppresses changes to game state during the calculation.
|
||||||
|
* @param effectiveness If defined, this is used in place of calculated effectiveness values.
|
||||||
* @returns a {@linkcode DamageCalculationResult} object with three fields:
|
* @returns a {@linkcode DamageCalculationResult} object with three fields:
|
||||||
* - `cancelled`: `true` if the move was cancelled by another effect.
|
* - `cancelled`: `true` if the move was cancelled by another effect.
|
||||||
* - `result`: {@linkcode HitResult} indicates the attack's type effectiveness.
|
* - `result`: {@linkcode HitResult} indicates the attack's type effectiveness.
|
||||||
* - `damage`: `number` the attack's final damage output.
|
* - `damage`: `number` the attack's final damage output.
|
||||||
*/
|
*/
|
||||||
getAttackDamage(source: Pokemon, move: Move, ignoreAbility: boolean = false, ignoreSourceAbility: boolean = false, isCritical: boolean = false, simulated: boolean = true): DamageCalculationResult {
|
getAttackDamage(source: Pokemon, move: Move, ignoreAbility: boolean = false, ignoreSourceAbility: boolean = false,
|
||||||
|
isCritical: boolean = false, simulated: boolean = true, effectiveness?: TypeDamageMultiplier): DamageCalculationResult {
|
||||||
const damage = new Utils.NumberHolder(0);
|
const damage = new Utils.NumberHolder(0);
|
||||||
const defendingSide = this.isPlayer() ? ArenaTagSide.PLAYER : ArenaTagSide.ENEMY;
|
const defendingSide = this.isPlayer() ? ArenaTagSide.PLAYER : ArenaTagSide.ENEMY;
|
||||||
|
|
||||||
@ -2595,7 +2608,7 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container {
|
|||||||
*
|
*
|
||||||
* Note that the source's abilities are not ignored here
|
* Note that the source's abilities are not ignored here
|
||||||
*/
|
*/
|
||||||
const typeMultiplier = this.getMoveEffectiveness(source, move, ignoreAbility, simulated, cancelled);
|
const typeMultiplier = effectiveness ?? this.getMoveEffectiveness(source, move, ignoreAbility, simulated, cancelled);
|
||||||
|
|
||||||
const isPhysical = moveCategory === MoveCategory.PHYSICAL;
|
const isPhysical = moveCategory === MoveCategory.PHYSICAL;
|
||||||
|
|
||||||
@ -2793,147 +2806,32 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Applies the results of a move to this pokemon
|
* Calculates whether the given move critically hits against this Pokemon
|
||||||
* @param source The {@linkcode Pokemon} using the move
|
* @param source the {@linkcode Pokemon} using the move
|
||||||
* @param move The {@linkcode Move} being used
|
* @param move the {@linkcode Move} being used
|
||||||
* @returns The {@linkcode HitResult} of the attack
|
* @param simulated if `true`, the calculation is resolved quietly (e.g. without Ability pop-ups)
|
||||||
|
* @returns `true` if the move critically hits; `false` otherwise
|
||||||
*/
|
*/
|
||||||
apply(source: Pokemon, move: Move): HitResult {
|
getCriticalHitResult(source: Pokemon, move: Move, simulated: boolean = true): boolean {
|
||||||
const defendingSide = this.isPlayer() ? ArenaTagSide.PLAYER : ArenaTagSide.ENEMY;
|
const defendingSide = this.isPlayer() ? ArenaTagSide.PLAYER : ArenaTagSide.ENEMY;
|
||||||
const moveCategory = new Utils.NumberHolder(move.category);
|
const noCritTag = this.scene.arena.getTagOnSide(NoCritTag, defendingSide);
|
||||||
applyMoveAttrs(VariableMoveCategoryAttr, source, this, move, moveCategory);
|
if (noCritTag || Overrides.NEVER_CRIT_OVERRIDE || move.hasAttr(FixedDamageAttr)) {
|
||||||
if (moveCategory.value === MoveCategory.STATUS) {
|
return false;
|
||||||
const cancelled = new Utils.BooleanHolder(false);
|
|
||||||
const typeMultiplier = this.getMoveEffectiveness(source, move, false, false, cancelled);
|
|
||||||
|
|
||||||
if (!cancelled.value && typeMultiplier === 0) {
|
|
||||||
this.scene.queueMessage(i18next.t("battle:hitResultNoEffect", { pokemonName: getPokemonNameWithAffix(this) }));
|
|
||||||
}
|
|
||||||
return (typeMultiplier === 0) ? HitResult.NO_EFFECT : HitResult.STATUS;
|
|
||||||
} else {
|
|
||||||
/** Determines whether the attack critically hits */
|
|
||||||
let isCritical: boolean;
|
|
||||||
const critOnly = new Utils.BooleanHolder(false);
|
|
||||||
const critAlways = source.getTag(BattlerTagType.ALWAYS_CRIT);
|
|
||||||
applyMoveAttrs(CritOnlyAttr, source, this, move, critOnly);
|
|
||||||
applyAbAttrs(ConditionalCritAbAttr, source, null, false, critOnly, this, move);
|
|
||||||
if (critOnly.value || critAlways) {
|
|
||||||
isCritical = true;
|
|
||||||
} else {
|
|
||||||
const critChance = [ 24, 8, 2, 1 ][Math.max(0, Math.min(this.getCritStage(source, move), 3))];
|
|
||||||
isCritical = critChance === 1 || !this.scene.randBattleSeedInt(critChance);
|
|
||||||
}
|
|
||||||
|
|
||||||
const noCritTag = this.scene.arena.getTagOnSide(NoCritTag, defendingSide);
|
|
||||||
const blockCrit = new Utils.BooleanHolder(false);
|
|
||||||
applyAbAttrs(BlockCritAbAttr, this, null, false, blockCrit);
|
|
||||||
if (noCritTag || blockCrit.value || Overrides.NEVER_CRIT_OVERRIDE) {
|
|
||||||
isCritical = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
const { cancelled, result, damage: dmg } = this.getAttackDamage(source, move, false, false, isCritical, false);
|
|
||||||
|
|
||||||
const typeBoost = source.findTag(t => t instanceof TypeBoostTag && t.boostedType === source.getMoveType(move)) as TypeBoostTag;
|
|
||||||
if (typeBoost?.oneUse) {
|
|
||||||
source.removeTag(typeBoost.tagType);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (cancelled || result === HitResult.IMMUNE || result === HitResult.NO_EFFECT) {
|
|
||||||
source.stopMultiHit(this);
|
|
||||||
|
|
||||||
if (!cancelled) {
|
|
||||||
if (result === HitResult.IMMUNE) {
|
|
||||||
this.scene.queueMessage(i18next.t("battle:hitResultImmune", { pokemonName: getPokemonNameWithAffix(this) }));
|
|
||||||
} else {
|
|
||||||
this.scene.queueMessage(i18next.t("battle:hitResultNoEffect", { pokemonName: getPokemonNameWithAffix(this) }));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
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;
|
|
||||||
|
|
||||||
if (dmg) {
|
|
||||||
this.lapseTags(BattlerTagLapseType.HIT);
|
|
||||||
|
|
||||||
const substitute = this.getTag(SubstituteTag);
|
|
||||||
const isBlockedBySubstitute = !!substitute && move.hitsSubstitute(source, this);
|
|
||||||
if (isBlockedBySubstitute) {
|
|
||||||
substitute.hp -= dmg;
|
|
||||||
}
|
|
||||||
if (!this.isPlayer() && dmg >= this.hp) {
|
|
||||||
this.scene.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);
|
|
||||||
|
|
||||||
if (damage > 0) {
|
|
||||||
if (source.isPlayer()) {
|
|
||||||
this.scene.validateAchvs(DamageAchv, new Utils.NumberHolder(damage));
|
|
||||||
if (damage > this.scene.gameData.gameStats.highestDamage) {
|
|
||||||
this.scene.gameData.gameStats.highestDamage = damage;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
source.turnData.totalDamageDealt += damage;
|
|
||||||
source.turnData.singleHitDamageDealt = damage;
|
|
||||||
this.turnData.damageTaken += damage;
|
|
||||||
this.battleData.hitCount++;
|
|
||||||
|
|
||||||
// Multi-Lens and Parental Bond check for Wimp Out/Emergency Exit
|
|
||||||
if (this.hasAbilityWithAttr(PostDamageForceSwitchAbAttr)) {
|
|
||||||
const multiHitModifier = source.getHeldItems().find(m => m instanceof PokemonMultiHitModifier);
|
|
||||||
if (multiHitModifier || source.hasAbilityWithAttr(AddSecondStrikeAbAttr)) {
|
|
||||||
applyPostDamageAbAttrs(PostDamageAbAttr, this, damage, this.hasPassive(), false, [], source);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const attackResult = { move: move.id, result: result as DamageResult, damage: damage, critical: isCritical, sourceId: source.id, sourceBattlerIndex: source.getBattlerIndex() };
|
|
||||||
this.turnData.attacksReceived.unshift(attackResult);
|
|
||||||
if (source.isPlayer() && !this.isPlayer()) {
|
|
||||||
this.scene.applyModifiers(DamageMoneyRewardModifier, true, source, new Utils.NumberHolder(damage));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (isCritical) {
|
|
||||||
this.scene.queueMessage(i18next.t("battle:hitResultCriticalHit"));
|
|
||||||
}
|
|
||||||
|
|
||||||
// want to include is.Fainted() in case multi hit move ends early, still want to render message
|
|
||||||
if (source.turnData.hitsLeft === 1 || this.isFainted()) {
|
|
||||||
switch (result) {
|
|
||||||
case HitResult.SUPER_EFFECTIVE:
|
|
||||||
this.scene.queueMessage(i18next.t("battle:hitResultSuperEffective"));
|
|
||||||
break;
|
|
||||||
case HitResult.NOT_VERY_EFFECTIVE:
|
|
||||||
this.scene.queueMessage(i18next.t("battle:hitResultNotVeryEffective"));
|
|
||||||
break;
|
|
||||||
case HitResult.ONE_HIT_KO:
|
|
||||||
this.scene.queueMessage(i18next.t("battle:hitResultOneHitKO"));
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this.isFainted()) {
|
|
||||||
// set splice index here, so future scene queues happen before FaintedPhase
|
|
||||||
this.scene.setPhaseQueueSplice();
|
|
||||||
this.scene.unshiftPhase(new FaintPhase(this.scene, this.getBattlerIndex(), isOneHitKo, destinyTag, grudgeTag, source));
|
|
||||||
|
|
||||||
this.destroySubstitute();
|
|
||||||
this.lapseTag(BattlerTagType.COMMANDED);
|
|
||||||
this.resetSummonData();
|
|
||||||
}
|
|
||||||
|
|
||||||
return result;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const isCritical = new Utils.BooleanHolder(false);
|
||||||
|
if (source.getTag(BattlerTagType.ALWAYS_CRIT)) {
|
||||||
|
isCritical.value = true;
|
||||||
|
}
|
||||||
|
applyMoveAttrs(CritOnlyAttr, source, this, move, isCritical);
|
||||||
|
applyAbAttrs(ConditionalCritAbAttr, source, null, simulated, isCritical, this, move);
|
||||||
|
if (!isCritical.value) {
|
||||||
|
const critChance = [ 24, 8, 2, 1 ][Math.max(0, Math.min(this.getCritStage(source, move, false), 3))];
|
||||||
|
isCritical.value = critChance === 1 || !this.scene.randBattleSeedInt(critChance);
|
||||||
|
}
|
||||||
|
applyAbAttrs(BlockCritAbAttr, this, null, simulated, isCritical);
|
||||||
|
|
||||||
|
return isCritical.value;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -3258,17 +3156,6 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container {
|
|||||||
return this.summonData.moveQueue;
|
return this.summonData.moveQueue;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* If this Pokemon is using a multi-hit move, cancels all subsequent strikes
|
|
||||||
* @param {Pokemon} target If specified, this only cancels subsequent strikes against the given target
|
|
||||||
*/
|
|
||||||
stopMultiHit(target?: Pokemon): void {
|
|
||||||
const effectPhase = this.scene.getCurrentPhase();
|
|
||||||
if (effectPhase instanceof MoveEffectPhase && effectPhase.getUserPokemon() === this) {
|
|
||||||
effectPhase.stopMultiHit(target);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
changeForm(formChange: SpeciesFormChange): Promise<void> {
|
changeForm(formChange: SpeciesFormChange): Promise<void> {
|
||||||
return new Promise(resolve => {
|
return new Promise(resolve => {
|
||||||
this.formIndex = Math.max(this.species.forms.findIndex(f => f.formKey === formChange.formKey), 0);
|
this.formIndex = Math.max(this.species.forms.findIndex(f => f.formKey === formChange.formKey), 0);
|
||||||
@ -3558,7 +3445,11 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container {
|
|||||||
* cancel the attack's subsequent hits.
|
* cancel the attack's subsequent hits.
|
||||||
*/
|
*/
|
||||||
if (effect === StatusEffect.SLEEP || effect === StatusEffect.FREEZE) {
|
if (effect === StatusEffect.SLEEP || effect === StatusEffect.FREEZE) {
|
||||||
this.stopMultiHit();
|
const currentPhase = this.scene.getCurrentPhase();
|
||||||
|
if (currentPhase instanceof MoveEffectPhase && currentPhase.getUserPokemon() === this) {
|
||||||
|
this.turnData.hitCount -= this.turnData.hitsLeft - 1;
|
||||||
|
this.turnData.hitsLeft = 1;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (asPhase) {
|
if (asPhase) {
|
||||||
|
@ -4,13 +4,15 @@ import {
|
|||||||
AddSecondStrikeAbAttr,
|
AddSecondStrikeAbAttr,
|
||||||
AlwaysHitAbAttr,
|
AlwaysHitAbAttr,
|
||||||
applyPostAttackAbAttrs,
|
applyPostAttackAbAttrs,
|
||||||
|
applyPostDamageAbAttrs,
|
||||||
applyPostDefendAbAttrs,
|
applyPostDefendAbAttrs,
|
||||||
applyPreAttackAbAttrs,
|
applyPreAttackAbAttrs,
|
||||||
IgnoreMoveEffectsAbAttr,
|
IgnoreMoveEffectsAbAttr,
|
||||||
MaxMultiHitAbAttr,
|
MaxMultiHitAbAttr,
|
||||||
PostAttackAbAttr,
|
PostAttackAbAttr,
|
||||||
|
PostDamageAbAttr,
|
||||||
|
PostDamageForceSwitchAbAttr,
|
||||||
PostDefendAbAttr,
|
PostDefendAbAttr,
|
||||||
TypeImmunityAbAttr,
|
|
||||||
} from "#app/data/ability";
|
} from "#app/data/ability";
|
||||||
import { ArenaTagSide, ConditionalProtectTag } from "#app/data/arena-tag";
|
import { ArenaTagSide, ConditionalProtectTag } from "#app/data/arena-tag";
|
||||||
import { MoveAnim } from "#app/data/battle-anims";
|
import { MoveAnim } from "#app/data/battle-anims";
|
||||||
@ -20,6 +22,7 @@ import {
|
|||||||
ProtectedTag,
|
ProtectedTag,
|
||||||
SemiInvulnerableTag,
|
SemiInvulnerableTag,
|
||||||
SubstituteTag,
|
SubstituteTag,
|
||||||
|
TypeBoostTag,
|
||||||
} from "#app/data/battler-tags";
|
} from "#app/data/battler-tags";
|
||||||
import {
|
import {
|
||||||
applyFilteredMoveAttrs,
|
applyFilteredMoveAttrs,
|
||||||
@ -40,29 +43,47 @@ import {
|
|||||||
OneHitKOAttr,
|
OneHitKOAttr,
|
||||||
OverrideMoveEffectAttr,
|
OverrideMoveEffectAttr,
|
||||||
ToxicAccuracyAttr,
|
ToxicAccuracyAttr,
|
||||||
VariableTargetAttr,
|
|
||||||
} from "#app/data/move";
|
} from "#app/data/move";
|
||||||
import { SpeciesFormChangePostMoveTrigger } from "#app/data/pokemon-forms";
|
import { SpeciesFormChangePostMoveTrigger } from "#app/data/pokemon-forms";
|
||||||
import { Type } from "#enums/type";
|
import { Type } from "#enums/type";
|
||||||
import Pokemon, { HitResult, MoveResult, PokemonMove } from "#app/field/pokemon";
|
import Pokemon, { DamageResult, HitResult, MoveResult, PokemonMove, TurnMove } from "#app/field/pokemon";
|
||||||
import { getPokemonNameWithAffix } from "#app/messages";
|
import { getPokemonNameWithAffix } from "#app/messages";
|
||||||
import {
|
import {
|
||||||
ContactHeldItemTransferChanceModifier,
|
ContactHeldItemTransferChanceModifier,
|
||||||
|
DamageMoneyRewardModifier,
|
||||||
EnemyAttackStatusEffectChanceModifier,
|
EnemyAttackStatusEffectChanceModifier,
|
||||||
|
EnemyEndureChanceModifier,
|
||||||
FlinchChanceModifier,
|
FlinchChanceModifier,
|
||||||
HitHealModifier,
|
HitHealModifier,
|
||||||
PokemonMultiHitModifier,
|
PokemonMultiHitModifier,
|
||||||
} from "#app/modifier/modifier";
|
} from "#app/modifier/modifier";
|
||||||
import { PokemonPhase } from "#app/phases/pokemon-phase";
|
import { PokemonPhase } from "#app/phases/pokemon-phase";
|
||||||
import { BooleanHolder, executeIf, NumberHolder } from "#app/utils";
|
import { BooleanHolder, executeIf, isNullOrUndefined, NumberHolder } from "#app/utils";
|
||||||
import { BattlerTagType } from "#enums/battler-tag-type";
|
import { BattlerTagType } from "#enums/battler-tag-type";
|
||||||
import { Moves } from "#enums/moves";
|
import { Moves } from "#enums/moves";
|
||||||
import i18next from "i18next";
|
import i18next from "i18next";
|
||||||
|
import { TypeDamageMultiplier } from "#app/data/type";
|
||||||
|
import { DamageAchv } from "#app/system/achv";
|
||||||
|
import { FaintPhase } from "./faint-phase";
|
||||||
|
|
||||||
|
type HitCheckEntry = [ HitCheckResult, TypeDamageMultiplier ];
|
||||||
|
|
||||||
export class MoveEffectPhase extends PokemonPhase {
|
export class MoveEffectPhase extends PokemonPhase {
|
||||||
public move: PokemonMove;
|
public move: PokemonMove;
|
||||||
protected targets: BattlerIndex[];
|
protected targets: BattlerIndex[];
|
||||||
|
|
||||||
|
private hitChecks: HitCheckEntry[];
|
||||||
|
private moveHistoryEntry: TurnMove;
|
||||||
|
|
||||||
|
/** MOVE EFFECT TRIGGER CONDITIONS */
|
||||||
|
|
||||||
|
/** Is this the first strike of a move? */
|
||||||
|
private firstHit: boolean;
|
||||||
|
/** Is this the last strike of a move? */
|
||||||
|
private lastHit: boolean;
|
||||||
|
/** Is this the first target to be hit by this strike? */
|
||||||
|
private firstTarget: boolean = true;
|
||||||
|
|
||||||
constructor(scene: BattleScene, battlerIndex: BattlerIndex, targets: BattlerIndex[], move: PokemonMove) {
|
constructor(scene: BattleScene, battlerIndex: BattlerIndex, targets: BattlerIndex[], move: PokemonMove) {
|
||||||
super(scene, battlerIndex);
|
super(scene, battlerIndex);
|
||||||
this.move = move;
|
this.move = move;
|
||||||
@ -76,9 +97,11 @@ export class MoveEffectPhase extends PokemonPhase {
|
|||||||
targets.splice(i, i + 1);
|
targets.splice(i, i + 1);
|
||||||
}
|
}
|
||||||
this.targets = targets;
|
this.targets = targets;
|
||||||
|
|
||||||
|
this.hitChecks = Array(this.targets.length).fill([ HitCheckResult.PENDING, 0 ]);
|
||||||
}
|
}
|
||||||
|
|
||||||
public override start(): void {
|
public override start() {
|
||||||
super.start();
|
super.start();
|
||||||
|
|
||||||
/** The Pokemon using this phase's invoked move */
|
/** The Pokemon using this phase's invoked move */
|
||||||
@ -86,7 +109,7 @@ export class MoveEffectPhase extends PokemonPhase {
|
|||||||
/** All Pokemon targeted by this phase's invoked move */
|
/** All Pokemon targeted by this phase's invoked move */
|
||||||
const targets = this.getTargets();
|
const targets = this.getTargets();
|
||||||
|
|
||||||
if (!user) {
|
if (isNullOrUndefined(user)) {
|
||||||
return super.end();
|
return super.end();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -98,265 +121,153 @@ export class MoveEffectPhase extends PokemonPhase {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Does an effect from this move override other effects on this turn?
|
* Does an effect from this move override other effects on this turn?
|
||||||
* e.g. Charging moves (Fly, etc.) on their first turn of use.
|
* e.g. Metronome/Nature Power/etc. when queueing a generated move.
|
||||||
*/
|
*/
|
||||||
const overridden = new BooleanHolder(false);
|
const overridden = new BooleanHolder(false);
|
||||||
/** The {@linkcode Move} object from {@linkcode allMoves} invoked by this phase */
|
/** The {@linkcode Move} object from {@linkcode allMoves} invoked by this phase */
|
||||||
const move = this.move.getMove();
|
const move = this.move.getMove();
|
||||||
|
|
||||||
// Assume single target for override
|
// This assumes single target for override
|
||||||
applyMoveAttrs(OverrideMoveEffectAttr, user, this.getFirstTarget() ?? null, move, overridden, this.move.virtual).then(() => {
|
applyMoveAttrs(OverrideMoveEffectAttr, user, this.getFirstTarget() ?? null, move, overridden, this.move.virtual);
|
||||||
// If other effects were overriden, stop this phase before they can be applied
|
// If other effects were overridden, stop this phase before they can be applied
|
||||||
if (overridden.value) {
|
if (overridden.value) {
|
||||||
return this.end();
|
return this.end();
|
||||||
}
|
}
|
||||||
|
|
||||||
user.lapseTags(BattlerTagLapseType.MOVE_EFFECT);
|
user.lapseTags(BattlerTagLapseType.MOVE_EFFECT);
|
||||||
|
|
||||||
/**
|
this.moveHistoryEntry = { move: move.id, targets: this.targets, result: MoveResult.PENDING, virtual: this.move.virtual };
|
||||||
* If this phase is for the first hit of the invoked move,
|
|
||||||
* resolve the move's total hit count. This block combines the
|
|
||||||
* effects of the move itself, Parental Bond, and Multi-Lens to do so.
|
|
||||||
*/
|
|
||||||
if (user.turnData.hitsLeft === -1) {
|
|
||||||
const hitCount = new NumberHolder(1);
|
|
||||||
// Assume single target for multi hit
|
|
||||||
applyMoveAttrs(MultiHitAttr, user, this.getFirstTarget() ?? null, move, hitCount);
|
|
||||||
// If Parental Bond is applicable, add another hit
|
|
||||||
applyPreAttackAbAttrs(AddSecondStrikeAbAttr, user, null, move, false, hitCount, null);
|
|
||||||
// If Multi-Lens is applicable, add hits equal to the number of held Multi-Lenses
|
|
||||||
this.scene.applyModifiers(PokemonMultiHitModifier, user.isPlayer(), user, move.id, hitCount);
|
|
||||||
// Set the user's relevant turnData fields to reflect the final hit count
|
|
||||||
user.turnData.hitCount = hitCount.value;
|
|
||||||
user.turnData.hitsLeft = hitCount.value;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
targets.forEach((t, i) => this.hitChecks[i] = this.hitCheck(t));
|
||||||
* Log to be entered into the user's move history once the move result is resolved.
|
|
||||||
* Note that `result` (a {@linkcode MoveResult}) logs whether the move was successfully
|
|
||||||
* used in the sense of "Does it have an effect on the user?".
|
|
||||||
*/
|
|
||||||
const moveHistoryEntry = { move: this.move.moveId, targets: this.targets, result: MoveResult.PENDING, virtual: this.move.virtual };
|
|
||||||
|
|
||||||
/**
|
if (!targets.some(t => t.isActive(true))) {
|
||||||
* Stores results of hit checks of the invoked move against all targets, organized by battler index.
|
this.scene.queueMessage(i18next.t("battle:attackFailed"));
|
||||||
* @see {@linkcode hitCheck}
|
this.moveHistoryEntry.result = MoveResult.FAIL;
|
||||||
*/
|
}
|
||||||
const targetHitChecks = Object.fromEntries(targets.map(p => [ p.getBattlerIndex(), this.hitCheck(p) ]));
|
|
||||||
const hasActiveTargets = targets.some(t => t.isActive(true));
|
|
||||||
|
|
||||||
/** Check if the target is immune via ability to the attacking move, and NOT in semi invulnerable state */
|
if (this.hitChecks.some(hc => hc[0] === HitCheckResult.HIT)) {
|
||||||
const isImmune = targets[0]?.hasAbilityWithAttr(TypeImmunityAbAttr)
|
this.moveHistoryEntry.result = MoveResult.SUCCESS;
|
||||||
&& (targets[0]?.getAbility()?.getAttrs(TypeImmunityAbAttr)?.[0]?.getImmuneType() === user.getMoveType(move))
|
} else if (this.hitChecks.every(hc => hc[0] === HitCheckResult.MISS)) {
|
||||||
&& !targets[0]?.getTag(SemiInvulnerableTag);
|
this.moveHistoryEntry.result = MoveResult.MISS;
|
||||||
|
} else {
|
||||||
/**
|
this.moveHistoryEntry.result = MoveResult.FAIL;
|
||||||
* If no targets are left for the move to hit (FAIL), or the invoked move is single-target
|
}
|
||||||
* (and not random target) and failed the hit check against its target (MISS), log the move
|
|
||||||
* as FAILed or MISSed (depending on the conditions above) and end this phase.
|
|
||||||
*/
|
|
||||||
if (!hasActiveTargets || (!move.hasAttr(VariableTargetAttr) && !move.isMultiTarget() && !targetHitChecks[this.targets[0]] && !targets[0].getTag(ProtectedTag) && !isImmune)) {
|
|
||||||
this.stopMultiHit();
|
|
||||||
if (hasActiveTargets) {
|
|
||||||
this.scene.queueMessage(i18next.t("battle:attackMissed", { pokemonNameWithAffix: this.getFirstTarget() ? getPokemonNameWithAffix(this.getFirstTarget()!) : "" }));
|
|
||||||
moveHistoryEntry.result = MoveResult.MISS;
|
|
||||||
applyMoveAttrs(MissEffectAttr, user, null, this.move.getMove());
|
|
||||||
} else {
|
|
||||||
this.scene.queueMessage(i18next.t("battle:attackFailed"));
|
|
||||||
moveHistoryEntry.result = MoveResult.FAIL;
|
|
||||||
}
|
|
||||||
user.pushMoveHistory(moveHistoryEntry);
|
|
||||||
return this.end();
|
|
||||||
}
|
|
||||||
|
|
||||||
/** All move effect attributes are chained together in this array to be applied asynchronously. */
|
|
||||||
const applyAttrs: Promise<void>[] = [];
|
|
||||||
|
|
||||||
|
// If the move has a post-target effect (e.g. Explosion), but doesn't
|
||||||
|
// successfully hit a target, play the move's animation and return
|
||||||
|
if (move.getAttrs(MoveEffectAttr).some(attr => attr.trigger === MoveEffectTrigger.POST_TARGET)
|
||||||
|
&& this.hitChecks.every(hc => hc[1] === 0, this)) {
|
||||||
const playOnEmptyField = this.scene.currentBattle?.mysteryEncounter?.hasBattleAnimationsWithoutTargets ?? false;
|
const playOnEmptyField = this.scene.currentBattle?.mysteryEncounter?.hasBattleAnimationsWithoutTargets ?? false;
|
||||||
// Move animation only needs one target
|
return new MoveAnim(move.id, user, this.getFirstTarget()!.getBattlerIndex(), playOnEmptyField).play(this.scene, false, () =>
|
||||||
new MoveAnim(move.id as Moves, user, this.getFirstTarget()!.getBattlerIndex()!, playOnEmptyField).play(this.scene, move.hitsSubstitute(user, this.getFirstTarget()!), () => {
|
this.triggerMoveEffects(MoveEffectTrigger.POST_TARGET, user, null).then(() => this.end())
|
||||||
/** Has the move successfully hit a target (for damage) yet? */
|
);
|
||||||
let hasHit: boolean = false;
|
}
|
||||||
for (const target of targets) {
|
|
||||||
// Prevent ENEMY_SIDE targeted moves from occurring twice in double battles
|
// If this phase represents the first strike of the given move,
|
||||||
if (move.moveTarget === MoveTarget.ENEMY_SIDE && target !== targets[targets.length - 1]) {
|
// log the move in the user's move history.
|
||||||
continue;
|
if (user.turnData.hitsLeft === -1) {
|
||||||
|
user.pushMoveHistory(this.moveHistoryEntry);
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(this.hitChecks);
|
||||||
|
|
||||||
|
targets.forEach((target, i) => {
|
||||||
|
const [ hitCheckResult, effectiveness ] = this.hitChecks[i];
|
||||||
|
|
||||||
|
switch (hitCheckResult) {
|
||||||
|
case HitCheckResult.HIT:
|
||||||
|
this.applyMoveEffects(target, effectiveness);
|
||||||
|
this.firstTarget = false;
|
||||||
|
break;
|
||||||
|
case HitCheckResult.NO_EFFECT:
|
||||||
|
if (move.id === Moves.SHEER_COLD) {
|
||||||
|
this.scene.queueMessage(i18next.t("battle:hitResultImmune", { pokemonName: getPokemonNameWithAffix(target) }));
|
||||||
|
} else {
|
||||||
|
this.scene.queueMessage(i18next.t("battle:hitResultNoEffect", { pokemonName: getPokemonNameWithAffix(target) }));
|
||||||
}
|
}
|
||||||
|
case HitCheckResult.PROTECTED:
|
||||||
|
case HitCheckResult.NO_EFFECT_NO_MESSAGE:
|
||||||
|
applyMoveAttrs(NoEffectAttr, user, target, move);
|
||||||
|
break;
|
||||||
|
case HitCheckResult.MISS:
|
||||||
|
this.scene.queueMessage(i18next.t("battle:attackMissed", { pokemonNameWithAffix: this.getFirstTarget() ? getPokemonNameWithAffix(this.getFirstTarget()!) : "" }));
|
||||||
|
applyMoveAttrs(MissEffectAttr, user, target, move);
|
||||||
|
break;
|
||||||
|
case HitCheckResult.PENDING:
|
||||||
|
case HitCheckResult.ERROR:
|
||||||
|
console.log(`Unexpected hit check result ${HitCheckResult[hitCheckResult]}. Aborting phase.`);
|
||||||
|
return this.end();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
/** The {@linkcode ArenaTagSide} to which the target belongs */
|
const doPostTarget = this.lastHit ? this.triggerMoveEffects(MoveEffectTrigger.POST_TARGET, user, null) : Promise.resolve();
|
||||||
const targetSide = target.isPlayer() ? ArenaTagSide.PLAYER : ArenaTagSide.ENEMY;
|
doPostTarget.then(() => {
|
||||||
/** Has the invoked move been cancelled by conditional protection (e.g Quick Guard)? */
|
this.updateSubstitutes();
|
||||||
const hasConditionalProtectApplied = new BooleanHolder(false);
|
this.end();
|
||||||
/** Does the applied conditional protection bypass Protect-ignoring effects? */
|
});
|
||||||
const bypassIgnoreProtect = new BooleanHolder(false);
|
}
|
||||||
/** If the move is not targeting a Pokemon on the user's side, try to apply conditional protection effects */
|
|
||||||
if (!this.move.getMove().isAllyTarget()) {
|
|
||||||
this.scene.arena.applyTagsForSide(ConditionalProtectTag, targetSide, false, hasConditionalProtectApplied, user, target, move.id, bypassIgnoreProtect);
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Is the target protected by Protect, etc. or a relevant conditional protection effect? */
|
protected applyMoveEffects(target: Pokemon, effectiveness: TypeDamageMultiplier): void {
|
||||||
const isProtected = (
|
const user = this.getUserPokemon();
|
||||||
bypassIgnoreProtect.value
|
const move = this.move.getMove();
|
||||||
|| !this.move.getMove().checkFlag(MoveFlags.IGNORE_PROTECT, user, target))
|
|
||||||
&& (hasConditionalProtectApplied.value
|
|
||||||
|| (!target.findTags(t => t instanceof DamageProtectedTag).length
|
|
||||||
&& target.findTags(t => t instanceof ProtectedTag).find(t => target.lapseTag(t.tagType)))
|
|
||||||
|| (this.move.getMove().category !== MoveCategory.STATUS
|
|
||||||
&& target.findTags(t => t instanceof DamageProtectedTag).find(t => target.lapseTag(t.tagType))));
|
|
||||||
|
|
||||||
/** Is the pokemon immune due to an ablility, and also not in a semi invulnerable state? */
|
if (isNullOrUndefined(user)) {
|
||||||
const isImmune = target.hasAbilityWithAttr(TypeImmunityAbAttr)
|
return;
|
||||||
&& (target.getAbility()?.getAttrs(TypeImmunityAbAttr)?.[0]?.getImmuneType() === user.getMoveType(move))
|
}
|
||||||
&& !target.getTag(SemiInvulnerableTag);
|
|
||||||
|
|
||||||
/** Is the target hidden by the effects of its Commander ability? */
|
// prevent field-targeted moves from activating multiple times
|
||||||
const isCommanding = this.scene.currentBattle.double && target.getAlly()?.getTag(BattlerTagType.COMMANDED)?.getSourcePokemon(this.scene) === target;
|
if (move.isFieldTarget() && target !== this.getTargets()[this.targets.length - 1]) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* If the move missed a target, stop all future hits against that target
|
* If this phase is for the first hit of the invoked move,
|
||||||
* and move on to the next target (if there is one).
|
* resolve the move's total hit count. This block combines the
|
||||||
*/
|
* effects of the move itself, Parental Bond, and Multi-Lens to do so.
|
||||||
if (isCommanding || (!isImmune && !isProtected && !targetHitChecks[target.getBattlerIndex()])) {
|
*/
|
||||||
this.stopMultiHit(target);
|
if (user.turnData.hitsLeft === -1) {
|
||||||
this.scene.queueMessage(i18next.t("battle:attackMissed", { pokemonNameWithAffix: getPokemonNameWithAffix(target) }));
|
const hitCount = new NumberHolder(1);
|
||||||
if (moveHistoryEntry.result === MoveResult.PENDING) {
|
// Assume single target for multi hit
|
||||||
moveHistoryEntry.result = MoveResult.MISS;
|
applyMoveAttrs(MultiHitAttr, user, this.getFirstTarget() ?? null, move, hitCount);
|
||||||
}
|
// If Parental Bond is applicable, add another hit
|
||||||
user.pushMoveHistory(moveHistoryEntry);
|
applyPreAttackAbAttrs(AddSecondStrikeAbAttr, user, null, move, false, hitCount, null);
|
||||||
applyMoveAttrs(MissEffectAttr, user, null, move);
|
// If Multi-Lens is applicable, add hits equal to the number of held Multi-Lenses
|
||||||
continue;
|
this.scene.applyModifiers(PokemonMultiHitModifier, user.isPlayer(), user, move.id, hitCount);
|
||||||
}
|
// Set the user's relevant turnData fields to reflect the final hit count
|
||||||
|
user.turnData.hitCount = hitCount.value;
|
||||||
|
user.turnData.hitsLeft = hitCount.value;
|
||||||
|
}
|
||||||
|
|
||||||
/** Does this phase represent the invoked move's first strike? */
|
this.firstHit = user.turnData.hitsLeft === user.turnData.hitCount;
|
||||||
const firstHit = (user.turnData.hitsLeft === user.turnData.hitCount);
|
this.lastHit = user.turnData.hitsLeft === 1 || !this.getTargets().some(t => t.isActive(true));
|
||||||
|
|
||||||
// Only log the move's result on the first strike
|
const playOnEmptyField = this.scene.currentBattle?.mysteryEncounter?.hasBattleAnimationsWithoutTargets ?? false;
|
||||||
if (firstHit) {
|
return new MoveAnim(move.id as Moves, user, this.getFirstTarget()!.getBattlerIndex()!, playOnEmptyField).play(this.scene, move.hitsSubstitute(user, this.getFirstTarget()!), async () => {
|
||||||
user.pushMoveHistory(moveHistoryEntry);
|
await this.triggerMoveEffects(MoveEffectTrigger.PRE_APPLY, user, target);
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
const hitResult = this.applyMove(target, effectiveness);
|
||||||
* Since all fail/miss checks have applied, the move is considered successfully applied.
|
|
||||||
* It's worth noting that if the move has no effect or is protected against, this assignment
|
|
||||||
* is overwritten and the move is logged as a FAIL.
|
|
||||||
*/
|
|
||||||
moveHistoryEntry.result = MoveResult.SUCCESS;
|
|
||||||
|
|
||||||
/**
|
/** Does {@linkcode hitResult} indicate that damage was dealt to the target? */
|
||||||
* Stores the result of applying the invoked move to the target.
|
const dealsDamage = [
|
||||||
* If the target is protected, the result is always `NO_EFFECT`.
|
HitResult.EFFECTIVE,
|
||||||
* Otherwise, the hit result is based on type effectiveness, immunities,
|
HitResult.SUPER_EFFECTIVE,
|
||||||
* and other factors that may negate the attack or status application.
|
HitResult.NOT_VERY_EFFECTIVE,
|
||||||
*
|
HitResult.ONE_HIT_KO
|
||||||
* Internally, the call to {@linkcode Pokemon.apply} is where damage is calculated
|
].includes(hitResult);
|
||||||
* (for attack moves) and the target's HP is updated. However, this isn't
|
|
||||||
* made visible to the user until the resulting {@linkcode DamagePhase}
|
|
||||||
* is invoked.
|
|
||||||
*/
|
|
||||||
const hitResult = !isProtected ? target.apply(user, move) : HitResult.NO_EFFECT;
|
|
||||||
|
|
||||||
/** Does {@linkcode hitResult} indicate that damage was dealt to the target? */
|
await this.triggerMoveEffects(MoveEffectTrigger.POST_APPLY, user, target);
|
||||||
const dealsDamage = [
|
this.applyHeldItemFlinchCheck(user, target, dealsDamage);
|
||||||
HitResult.EFFECTIVE,
|
await this.triggerMoveEffects(MoveEffectTrigger.HIT, user, target);
|
||||||
HitResult.SUPER_EFFECTIVE,
|
await this.applyOnGetHitAbEffects(user, target, hitResult);
|
||||||
HitResult.NOT_VERY_EFFECTIVE,
|
await applyPostAttackAbAttrs(PostAttackAbAttr, user, target, move, hitResult);
|
||||||
HitResult.ONE_HIT_KO
|
|
||||||
].includes(hitResult);
|
|
||||||
|
|
||||||
/** Is this target the first one hit by the move on its current strike? */
|
if (move instanceof AttackMove) {
|
||||||
const firstTarget = dealsDamage && !hasHit;
|
this.scene.applyModifiers(ContactHeldItemTransferChanceModifier, this.player, user, target);
|
||||||
if (firstTarget) {
|
}
|
||||||
hasHit = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
if (this.lastHit) {
|
||||||
* If the move has no effect on the target (i.e. the target is protected or immune),
|
this.scene.triggerPokemonFormChange(user, SpeciesFormChangePostMoveTrigger);
|
||||||
* change the logged move result to FAIL.
|
}
|
||||||
*/
|
|
||||||
if (hitResult === HitResult.NO_EFFECT) {
|
|
||||||
moveHistoryEntry.result = MoveResult.FAIL;
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Does this phase represent the invoked move's last strike? */
|
|
||||||
const lastHit = (user.turnData.hitsLeft === 1 || !this.getFirstTarget()?.isActive());
|
|
||||||
|
|
||||||
/**
|
|
||||||
* If the user can change forms by using the invoked move,
|
|
||||||
* it only changes forms after the move's last hit
|
|
||||||
* (see Relic Song's interaction with Parental Bond when used by Meloetta).
|
|
||||||
*/
|
|
||||||
if (lastHit) {
|
|
||||||
this.scene.triggerPokemonFormChange(user, SpeciesFormChangePostMoveTrigger);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Create a Promise that applys *all* effects from the invoked move's MoveEffectAttrs.
|
|
||||||
* These are ordered by trigger type (see {@linkcode MoveEffectTrigger}), and each trigger
|
|
||||||
* type requires different conditions to be met with respect to the move's hit result.
|
|
||||||
*/
|
|
||||||
const k = new Promise<void>((resolve) => {
|
|
||||||
//Start promise chain and apply PRE_APPLY move attributes
|
|
||||||
let promiseChain: Promise<void | null> = applyFilteredMoveAttrs((attr: MoveAttr) =>
|
|
||||||
attr instanceof MoveEffectAttr
|
|
||||||
&& attr.trigger === MoveEffectTrigger.PRE_APPLY
|
|
||||||
&& (!attr.firstHitOnly || firstHit)
|
|
||||||
&& (!attr.lastHitOnly || lastHit)
|
|
||||||
&& hitResult !== HitResult.NO_EFFECT, user, target, move);
|
|
||||||
|
|
||||||
/** Don't complete if the move failed */
|
|
||||||
if (hitResult === HitResult.FAIL) {
|
|
||||||
return resolve();
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Apply Move/Ability Effects in correct order */
|
|
||||||
promiseChain = promiseChain
|
|
||||||
.then(this.applySelfTargetEffects(user, target, firstHit, lastHit));
|
|
||||||
|
|
||||||
if (hitResult !== HitResult.NO_EFFECT) {
|
|
||||||
promiseChain
|
|
||||||
.then(this.applyPostApplyEffects(user, target, firstHit, lastHit))
|
|
||||||
.then(this.applyHeldItemFlinchCheck(user, target, dealsDamage))
|
|
||||||
.then(this.applySuccessfulAttackEffects(user, target, firstHit, lastHit, !!isProtected, hitResult, firstTarget))
|
|
||||||
.then(() => resolve());
|
|
||||||
} else {
|
|
||||||
promiseChain
|
|
||||||
.then(() => applyMoveAttrs(NoEffectAttr, user, null, move))
|
|
||||||
.then(resolve);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
applyAttrs.push(k);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Apply the move's POST_TARGET effects on the move's last hit, after all targeted effects have resolved
|
|
||||||
const postTarget = (user.turnData.hitsLeft === 1 || !this.getFirstTarget()?.isActive()) ?
|
|
||||||
applyFilteredMoveAttrs((attr: MoveAttr) => attr instanceof MoveEffectAttr && attr.trigger === MoveEffectTrigger.POST_TARGET, user, null, move) :
|
|
||||||
null;
|
|
||||||
|
|
||||||
if (postTarget) {
|
|
||||||
if (applyAttrs.length) { // If there is a pending asynchronous move effect, do this after
|
|
||||||
applyAttrs[applyAttrs.length - 1].then(() => postTarget);
|
|
||||||
} else { // Otherwise, push a new asynchronous move effect
|
|
||||||
applyAttrs.push(postTarget);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Wait for all move effects to finish applying, then end this phase
|
|
||||||
Promise.allSettled(applyAttrs).then(() => {
|
|
||||||
/**
|
|
||||||
* Remove the target's substitute (if it exists and has expired)
|
|
||||||
* after all targeted effects have applied.
|
|
||||||
* This prevents blocked effects from applying until after this hit resolves.
|
|
||||||
*/
|
|
||||||
targets.forEach(target => {
|
|
||||||
const substitute = target.getTag(SubstituteTag);
|
|
||||||
if (substitute && substitute.hp <= 0) {
|
|
||||||
target.lapseTag(BattlerTagType.SUBSTITUTE);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
this.end();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -371,6 +282,8 @@ export class MoveEffectPhase extends PokemonPhase {
|
|||||||
*/
|
*/
|
||||||
if (user) {
|
if (user) {
|
||||||
if (user.turnData.hitsLeft && --user.turnData.hitsLeft >= 1 && this.getFirstTarget()?.isActive()) {
|
if (user.turnData.hitsLeft && --user.turnData.hitsLeft >= 1 && this.getFirstTarget()?.isActive()) {
|
||||||
|
// Only apply the next phase to previously hit targets
|
||||||
|
this.targets = this.targets.filter((_, i) => this.hitChecks[i][0] === HitCheckResult.HIT);
|
||||||
this.scene.unshiftPhase(this.getNewHitPhase());
|
this.scene.unshiftPhase(this.getNewHitPhase());
|
||||||
} else {
|
} else {
|
||||||
// Queue message for number of hits made by multi-move
|
// Queue message for number of hits made by multi-move
|
||||||
@ -390,60 +303,135 @@ export class MoveEffectPhase extends PokemonPhase {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Apply self-targeted effects that trigger `POST_APPLY`
|
* Triggers move effects of the given move effect trigger.
|
||||||
*
|
* @param triggerType The {@linkcode MoveEffectTrigger} being applied
|
||||||
* @param user - The {@linkcode Pokemon} using this phase's invoked move
|
* @param user The {@linkcode Pokemon} using the move
|
||||||
* @param target - {@linkcode Pokemon} the current target of this phase's invoked move
|
* @param target The {@linkcode Pokemon} targeted by the move
|
||||||
* @param firstHit - `true` if this is the first hit in a multi-hit attack
|
* @param selfTarget If defined, limits the effects triggered to either self-targeted
|
||||||
* @param lastHit - `true` if this is the last hit in a multi-hit attack
|
* effects (if set to `true`) or targeted effects (if set to `false`).
|
||||||
* @returns a function intended to be passed into a `then()` call.
|
* @returns a `Promise` applying the relevant move effects.
|
||||||
*/
|
*/
|
||||||
protected applySelfTargetEffects(user: Pokemon, target: Pokemon, firstHit: boolean, lastHit: boolean): () => Promise<void | null> {
|
protected triggerMoveEffects(triggerType: MoveEffectTrigger, user: Pokemon, target: Pokemon | null, selfTarget?: boolean): Promise<void> {
|
||||||
return () => applyFilteredMoveAttrs((attr: MoveAttr) =>
|
|
||||||
attr instanceof MoveEffectAttr
|
|
||||||
&& attr.trigger === MoveEffectTrigger.POST_APPLY
|
|
||||||
&& attr.selfTarget
|
|
||||||
&& (!attr.firstHitOnly || firstHit)
|
|
||||||
&& (!attr.lastHitOnly || lastHit), user, target, this.move.getMove());
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Applies non-self-targeted effects that trigger `POST_APPLY`
|
|
||||||
* (i.e. Smelling Salts curing Paralysis, and the forced switch from U-Turn, Dragon Tail, etc)
|
|
||||||
* @param user - The {@linkcode Pokemon} using this phase's invoked move
|
|
||||||
* @param target - {@linkcode Pokemon} the current target of this phase's invoked move
|
|
||||||
* @param firstHit - `true` if this is the first hit in a multi-hit attack
|
|
||||||
* @param lastHit - `true` if this is the last hit in a multi-hit attack
|
|
||||||
* @returns a function intended to be passed into a `then()` call.
|
|
||||||
*/
|
|
||||||
protected applyPostApplyEffects(user: Pokemon, target: Pokemon, firstHit: boolean, lastHit: boolean): () => Promise<void | null> {
|
|
||||||
return () => applyFilteredMoveAttrs((attr: MoveAttr) =>
|
|
||||||
attr instanceof MoveEffectAttr
|
|
||||||
&& attr.trigger === MoveEffectTrigger.POST_APPLY
|
|
||||||
&& !attr.selfTarget
|
|
||||||
&& (!attr.firstHitOnly || firstHit)
|
|
||||||
&& (!attr.lastHitOnly || lastHit), user, target, this.move.getMove());
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Applies effects that trigger on HIT
|
|
||||||
* (i.e. Final Gambit, Power-Up Punch, Drain Punch)
|
|
||||||
* @param user - The {@linkcode Pokemon} using this phase's invoked move
|
|
||||||
* @param target - {@linkcode Pokemon} the current target of this phase's invoked move
|
|
||||||
* @param firstHit - `true` if this is the first hit in a multi-hit attack
|
|
||||||
* @param lastHit - `true` if this is the last hit in a multi-hit attack
|
|
||||||
* @param firstTarget - `true` if {@linkcode target} is the first target hit by this strike of {@linkcode move}
|
|
||||||
* @returns a function intended to be passed into a `then()` call.
|
|
||||||
*/
|
|
||||||
protected applyOnHitEffects(user: Pokemon, target: Pokemon, firstHit : boolean, lastHit: boolean, firstTarget: boolean): Promise<void> {
|
|
||||||
return applyFilteredMoveAttrs((attr: MoveAttr) =>
|
return applyFilteredMoveAttrs((attr: MoveAttr) =>
|
||||||
attr instanceof MoveEffectAttr
|
attr instanceof MoveEffectAttr
|
||||||
&& attr.trigger === MoveEffectTrigger.HIT
|
&& attr.trigger === triggerType
|
||||||
&& (!attr.firstHitOnly || firstHit)
|
&& (isNullOrUndefined(selfTarget) || (attr.selfTarget === selfTarget))
|
||||||
&& (!attr.lastHitOnly || lastHit)
|
&& (!attr.firstHitOnly || this.firstHit)
|
||||||
&& (!attr.firstTargetOnly || firstTarget), user, target, this.move.getMove());
|
&& (!attr.lastHitOnly || this.lastHit)
|
||||||
|
&& (!attr.firstTargetOnly || this.firstTarget),
|
||||||
|
user, target, this.move.getMove());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Apply the results of this phase's move to the given target
|
||||||
|
* @param target The {@linkcode Pokemon} struck by the move
|
||||||
|
*/
|
||||||
|
protected applyMove(target: Pokemon, effectiveness: TypeDamageMultiplier): HitResult {
|
||||||
|
/** The {@linkcode Pokemon} using the move */
|
||||||
|
const user = this.getUserPokemon()!;
|
||||||
|
|
||||||
|
/** The {@linkcode Move} being used */
|
||||||
|
const move = this.move.getMove();
|
||||||
|
const moveCategory = user.getMoveCategory(target, move);
|
||||||
|
|
||||||
|
if (moveCategory === MoveCategory.STATUS) {
|
||||||
|
return HitResult.STATUS;
|
||||||
|
}
|
||||||
|
|
||||||
|
const isCritical = target.getCriticalHitResult(user, move, false);
|
||||||
|
|
||||||
|
const { result: result, damage: dmg } = target.getAttackDamage(user, move, false, false, isCritical, false, effectiveness);
|
||||||
|
|
||||||
|
const typeBoost = user.findTag(t => t instanceof TypeBoostTag && t.boostedType === user.getMoveType(move)) as TypeBoostTag;
|
||||||
|
if (typeBoost?.oneUse) {
|
||||||
|
user.removeTag(typeBoost.tagType);
|
||||||
|
}
|
||||||
|
|
||||||
|
// In case of fatal damage, this tag would have gotten cleared before we could lapse it.
|
||||||
|
const destinyTag = target.getTag(BattlerTagType.DESTINY_BOND);
|
||||||
|
const grudgeTag = target.getTag(BattlerTagType.GRUDGE);
|
||||||
|
|
||||||
|
const isOneHitKo = result === HitResult.ONE_HIT_KO;
|
||||||
|
|
||||||
|
if (dmg) {
|
||||||
|
target.lapseTags(BattlerTagLapseType.HIT);
|
||||||
|
|
||||||
|
const substitute = target.getTag(SubstituteTag);
|
||||||
|
const isBlockedBySubstitute = !!substitute && move.hitsSubstitute(user, target);
|
||||||
|
if (isBlockedBySubstitute) {
|
||||||
|
substitute.hp -= dmg;
|
||||||
|
}
|
||||||
|
if (!target.isPlayer() && dmg >= target.hp) {
|
||||||
|
this.scene.applyModifiers(EnemyEndureChanceModifier, false, target);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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 = target.damageAndUpdate(isBlockedBySubstitute ? 0 : dmg, result as DamageResult, isCritical, isOneHitKo, isOneHitKo, true, user);
|
||||||
|
|
||||||
|
if (damage > 0) {
|
||||||
|
if (user.isPlayer()) {
|
||||||
|
this.scene.validateAchvs(DamageAchv, new NumberHolder(damage));
|
||||||
|
if (damage > this.scene.gameData.gameStats.highestDamage) {
|
||||||
|
this.scene.gameData.gameStats.highestDamage = damage;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
user.turnData.totalDamageDealt += damage;
|
||||||
|
user.turnData.singleHitDamageDealt = damage;
|
||||||
|
target.turnData.damageTaken += damage;
|
||||||
|
target.battleData.hitCount++;
|
||||||
|
|
||||||
|
// Multi-Lens and Parental Bond check for Wimp Out/Emergency Exit
|
||||||
|
if (target.hasAbilityWithAttr(PostDamageForceSwitchAbAttr)) {
|
||||||
|
const multiHitModifier = user.getHeldItems().find(m => m instanceof PokemonMultiHitModifier);
|
||||||
|
if (multiHitModifier || user.hasAbilityWithAttr(AddSecondStrikeAbAttr)) {
|
||||||
|
applyPostDamageAbAttrs(PostDamageAbAttr, target, damage, target.hasPassive(), false, [], user);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const attackResult = { move: move.id, result: result as DamageResult, damage: damage, critical: isCritical, sourceId: user.id, sourceBattlerIndex: user.getBattlerIndex() };
|
||||||
|
target.turnData.attacksReceived.unshift(attackResult);
|
||||||
|
if (user.isPlayer() && !target.isPlayer()) {
|
||||||
|
this.scene.applyModifiers(DamageMoneyRewardModifier, true, user, new NumberHolder(damage));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isCritical) {
|
||||||
|
this.scene.queueMessage(i18next.t("battle:hitResultCriticalHit"));
|
||||||
|
}
|
||||||
|
|
||||||
|
// want to include is.Fainted() in case multi hit move ends early, still want to render message
|
||||||
|
if (user.turnData.hitsLeft === 1 || target.isFainted()) {
|
||||||
|
switch (result) {
|
||||||
|
case HitResult.SUPER_EFFECTIVE:
|
||||||
|
this.scene.queueMessage(i18next.t("battle:hitResultSuperEffective"));
|
||||||
|
break;
|
||||||
|
case HitResult.NOT_VERY_EFFECTIVE:
|
||||||
|
this.scene.queueMessage(i18next.t("battle:hitResultNotVeryEffective"));
|
||||||
|
break;
|
||||||
|
case HitResult.ONE_HIT_KO:
|
||||||
|
this.scene.queueMessage(i18next.t("battle:hitResultOneHitKO"));
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (target.isFainted()) {
|
||||||
|
// set splice index here, so future scene queues happen before FaintedPhase
|
||||||
|
this.scene.setPhaseQueueSplice();
|
||||||
|
this.scene.unshiftPhase(new FaintPhase(this.scene, target.getBattlerIndex(), isOneHitKo, destinyTag, grudgeTag, user));
|
||||||
|
|
||||||
|
target.destroySubstitute();
|
||||||
|
target.lapseTag(BattlerTagType.COMMANDED);
|
||||||
|
target.resetSummonData();
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Applies reactive effects that occur when a Pokémon is hit.
|
* Applies reactive effects that occur when a Pokémon is hit.
|
||||||
* (i.e. Effect Spore, Disguise, Liquid Ooze, Beak Blast)
|
* (i.e. Effect Spore, Disguise, Liquid Ooze, Beak Blast)
|
||||||
@ -470,114 +458,141 @@ export class MoveEffectPhase extends PokemonPhase {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Applies all effects and attributes that require a move to connect with a target,
|
* Handles checking for and applying flinches from held items (i.e. King's Rock)
|
||||||
* namely reactive effects like Weak Armor, on-hit effects like that of Power-Up Punch, and item stealing effects
|
|
||||||
* @param user - The {@linkcode Pokemon} using this phase's invoked move
|
|
||||||
* @param target - {@linkcode Pokemon} the current target of this phase's invoked move
|
|
||||||
* @param firstHit - `true` if this is the first hit in a multi-hit attack
|
|
||||||
* @param lastHit - `true` if this is the last hit in a multi-hit attack
|
|
||||||
* @param isProtected - `true` if the target is protected by effects such as Protect
|
|
||||||
* @param hitResult - The {@linkcode HitResult} of the attempted move
|
|
||||||
* @param firstTarget - `true` if {@linkcode target} is the first target hit by this strike of {@linkcode move}
|
|
||||||
* @returns a function intended to be passed into a `then()` call.
|
|
||||||
*/
|
|
||||||
protected applySuccessfulAttackEffects(user: Pokemon, target: Pokemon, firstHit : boolean, lastHit: boolean, isProtected : boolean, hitResult: HitResult, firstTarget: boolean) : () => Promise<void | null> {
|
|
||||||
return () => executeIf(!isProtected, () =>
|
|
||||||
this.applyOnHitEffects(user, target, firstHit, lastHit, firstTarget).then(() =>
|
|
||||||
this.applyOnGetHitAbEffects(user, target, hitResult)).then(() =>
|
|
||||||
applyPostAttackAbAttrs(PostAttackAbAttr, user, target, this.move.getMove(), hitResult)).then(() => { // Item Stealing Effects
|
|
||||||
|
|
||||||
if (this.move.getMove() instanceof AttackMove) {
|
|
||||||
this.scene.applyModifiers(ContactHeldItemTransferChanceModifier, this.player, user, target);
|
|
||||||
}
|
|
||||||
})
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Handles checking for and applying Flinches
|
|
||||||
* @param user - The {@linkcode Pokemon} using this phase's invoked move
|
* @param user - The {@linkcode Pokemon} using this phase's invoked move
|
||||||
* @param target - {@linkcode Pokemon} the current target of this phase's invoked move
|
* @param target - {@linkcode Pokemon} the current target of this phase's invoked move
|
||||||
* @param dealsDamage - `true` if the attempted move successfully dealt damage
|
* @param dealsDamage - `true` if the attempted move successfully dealt damage
|
||||||
* @returns a function intended to be passed into a `then()` call.
|
* @returns a function intended to be passed into a `then()` call.
|
||||||
*/
|
*/
|
||||||
protected applyHeldItemFlinchCheck(user: Pokemon, target: Pokemon, dealsDamage: boolean) : () => void {
|
protected applyHeldItemFlinchCheck(user: Pokemon, target: Pokemon, dealsDamage: boolean) : void {
|
||||||
return () => {
|
if (this.move.getMove().hasAttr(FlinchAttr)) {
|
||||||
if (this.move.getMove().hasAttr(FlinchAttr)) {
|
return;
|
||||||
return;
|
}
|
||||||
}
|
|
||||||
|
|
||||||
if (dealsDamage && !target.hasAbilityWithAttr(IgnoreMoveEffectsAbAttr) && !this.move.getMove().hitsSubstitute(user, target)) {
|
if (dealsDamage && !target.hasAbilityWithAttr(IgnoreMoveEffectsAbAttr) && !this.move.getMove().hitsSubstitute(user, target)) {
|
||||||
const flinched = new BooleanHolder(false);
|
const flinched = new BooleanHolder(false);
|
||||||
user.scene.applyModifiers(FlinchChanceModifier, user.isPlayer(), user, flinched);
|
user.scene.applyModifiers(FlinchChanceModifier, user.isPlayer(), user, flinched);
|
||||||
if (flinched.value) {
|
if (flinched.value) {
|
||||||
target.addTag(BattlerTagType.FLINCHED, undefined, this.move.moveId, user.id);
|
target.addTag(BattlerTagType.FLINCHED, undefined, this.move.moveId, user.id);
|
||||||
}
|
|
||||||
}
|
}
|
||||||
};
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Resolves whether this phase's invoked move hits the given target
|
* Resolves whether this phase's invoked move hits the given target
|
||||||
* @param target - The {@linkcode Pokemon} targeted by the invoked move
|
* @param target - The {@linkcode Pokemon} targeted by the invoked move
|
||||||
* @returns `true` if the move hits the target
|
* @returns A {@linkcode HitCheckEntry} which specifies the move's outcome and
|
||||||
|
* effectiveness (if applicable)
|
||||||
*/
|
*/
|
||||||
public hitCheck(target: Pokemon): boolean {
|
public hitCheck(target: Pokemon): HitCheckEntry {
|
||||||
// Moves targeting the user and entry hazards can't miss
|
|
||||||
if ([ MoveTarget.USER, MoveTarget.ENEMY_SIDE ].includes(this.move.getMove().moveTarget)) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
const user = this.getUserPokemon();
|
const user = this.getUserPokemon();
|
||||||
|
const move = this.move.getMove();
|
||||||
|
|
||||||
if (!user) {
|
if (isNullOrUndefined(user)) {
|
||||||
return false;
|
return [ HitCheckResult.ERROR, 0 ];
|
||||||
}
|
}
|
||||||
|
|
||||||
// Hit check only calculated on first hit for multi-hit moves unless flag is set to check all hits.
|
// Moves targeting the user or field bypass accuracy and effectiveness checks
|
||||||
// However, if an ability with the MaxMultiHitAbAttr, namely Skill Link, is present, act as a normal
|
if (move.moveTarget === MoveTarget.USER || move.isFieldTarget()) {
|
||||||
// multi-hit move and proceed with all hits
|
return [ HitCheckResult.HIT, 1 ];
|
||||||
|
}
|
||||||
|
|
||||||
|
// If the target is somehow not on the field, cancel the hit check silently
|
||||||
|
if (!target.isActive(true)) {
|
||||||
|
return [ HitCheckResult.NO_EFFECT_NO_MESSAGE, 0 ];
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Is the target hidden by the effects of its Commander ability? */
|
||||||
|
const isCommanding = this.scene.currentBattle.double && target.getAlly()?.getTag(BattlerTagType.COMMANDED)?.getSourcePokemon(this.scene) === target;
|
||||||
|
if (isCommanding) {
|
||||||
|
return [ HitCheckResult.MISS, 0 ];
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Is there an effect that causes the move to bypass accuracy checks, including semi-invulnerability? */
|
||||||
|
const alwaysHit = [ user, target ].some(p => p.hasAbilityWithAttr(AlwaysHitAbAttr))
|
||||||
|
|| (user.getTag(BattlerTagType.IGNORE_ACCURACY) && (user.getLastXMoves().find(() => true)?.targets || []).indexOf(target.getBattlerIndex()) !== -1)
|
||||||
|
|| !!target.getTag(BattlerTagType.ALWAYS_GET_HIT);
|
||||||
|
|
||||||
|
const semiInvulnerableTag = target.getTag(SemiInvulnerableTag);
|
||||||
|
/** Should the move miss due to the target's semi-invulnerability? */
|
||||||
|
const targetIsSemiInvulnerable = !!semiInvulnerableTag
|
||||||
|
&& !this.move.getMove().getAttrs(HitsTagAttr).some(hta => hta.tagType === semiInvulnerableTag.tagType)
|
||||||
|
&& !(this.move.getMove().hasAttr(ToxicAccuracyAttr) && user.isOfType(Type.POISON));
|
||||||
|
|
||||||
|
if (targetIsSemiInvulnerable && !alwaysHit) {
|
||||||
|
return [ HitCheckResult.MISS, 0 ];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if the target is protected by any effect
|
||||||
|
/** The {@linkcode ArenaTagSide} to which the target belongs */
|
||||||
|
const targetSide = target.isPlayer() ? ArenaTagSide.PLAYER : ArenaTagSide.ENEMY;
|
||||||
|
/** Has the invoked move been cancelled by conditional protection (e.g Quick Guard)? */
|
||||||
|
const hasConditionalProtectApplied = new BooleanHolder(false);
|
||||||
|
/** Does the applied conditional protection bypass Protect-ignoring effects? */
|
||||||
|
const bypassIgnoreProtect = new BooleanHolder(false);
|
||||||
|
/** If the move is not targeting a Pokemon on the user's side, try to apply conditional protection effects */
|
||||||
|
if (!this.move.getMove().isAllyTarget()) {
|
||||||
|
this.scene.arena.applyTagsForSide(ConditionalProtectTag, targetSide, false, hasConditionalProtectApplied, user, target, move.id, bypassIgnoreProtect);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Is the target protected by Protect, etc. or a relevant conditional protection effect? */
|
||||||
|
const isProtected = (
|
||||||
|
bypassIgnoreProtect.value
|
||||||
|
|| !this.move.getMove().checkFlag(MoveFlags.IGNORE_PROTECT, user, target))
|
||||||
|
&& (hasConditionalProtectApplied.value
|
||||||
|
|| (!target.findTags(t => t instanceof DamageProtectedTag).length
|
||||||
|
&& target.findTags(t => t instanceof ProtectedTag).find(t => target.lapseTag(t.tagType)))
|
||||||
|
|| (this.move.getMove().category !== MoveCategory.STATUS
|
||||||
|
&& target.findTags(t => t instanceof DamageProtectedTag).find(t => target.lapseTag(t.tagType))));
|
||||||
|
|
||||||
|
if (isProtected) {
|
||||||
|
return [ HitCheckResult.PROTECTED, 0 ];
|
||||||
|
}
|
||||||
|
|
||||||
|
const cancelNoEffectMessage = new BooleanHolder(false);
|
||||||
|
const effectiveness = target.getMoveEffectiveness(user, move, false, false, cancelNoEffectMessage);
|
||||||
|
if (effectiveness === 0) {
|
||||||
|
return cancelNoEffectMessage.value
|
||||||
|
? [ HitCheckResult.NO_EFFECT_NO_MESSAGE, effectiveness ]
|
||||||
|
: [ HitCheckResult.NO_EFFECT, effectiveness ];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Strikes after the first in a multi-strike move are guaranteed to hit,
|
||||||
|
// unless the move is flagged to check all hits and the user does not have Skill Link.
|
||||||
if (user.turnData.hitsLeft < user.turnData.hitCount) {
|
if (user.turnData.hitsLeft < user.turnData.hitCount) {
|
||||||
if (!this.move.getMove().hasFlag(MoveFlags.CHECK_ALL_HITS) || user.hasAbilityWithAttr(MaxMultiHitAbAttr)) {
|
if (!move.hasFlag(MoveFlags.CHECK_ALL_HITS) || user.hasAbilityWithAttr(MaxMultiHitAbAttr)) {
|
||||||
return true;
|
return [ HitCheckResult.HIT, effectiveness ];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (user.hasAbilityWithAttr(AlwaysHitAbAttr) || target.hasAbilityWithAttr(AlwaysHitAbAttr)) {
|
if (alwaysHit || target.getTag(BattlerTagType.TELEKINESIS) && !move.hasAttr(OneHitKOAttr)) {
|
||||||
return true;
|
return [ HitCheckResult.HIT, effectiveness ];
|
||||||
}
|
}
|
||||||
|
|
||||||
// If the user should ignore accuracy on a target, check who the user targeted last turn and see if they match
|
const moveAccuracy = move.calculateBattleAccuracy(user, target);
|
||||||
if (user.getTag(BattlerTagType.IGNORE_ACCURACY) && (user.getLastXMoves().find(() => true)?.targets || []).indexOf(target.getBattlerIndex()) !== -1) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (target.getTag(BattlerTagType.ALWAYS_GET_HIT)) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (target.getTag(BattlerTagType.TELEKINESIS) && !target.getTag(SemiInvulnerableTag) && !this.move.getMove().hasAttr(OneHitKOAttr)) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
const semiInvulnerableTag = target.getTag(SemiInvulnerableTag);
|
|
||||||
if (semiInvulnerableTag
|
|
||||||
&& !this.move.getMove().getAttrs(HitsTagAttr).some(hta => hta.tagType === semiInvulnerableTag.tagType)
|
|
||||||
&& !(this.move.getMove().hasAttr(ToxicAccuracyAttr) && user.isOfType(Type.POISON))
|
|
||||||
) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
const moveAccuracy = this.move.getMove().calculateBattleAccuracy(user, target);
|
|
||||||
|
|
||||||
if (moveAccuracy === -1) {
|
if (moveAccuracy === -1) {
|
||||||
return true;
|
return [ HitCheckResult.HIT, effectiveness ];
|
||||||
}
|
}
|
||||||
|
|
||||||
const accuracyMultiplier = user.getAccuracyMultiplier(target, this.move.getMove());
|
const accuracyMultiplier = user.getAccuracyMultiplier(target, move);
|
||||||
const rand = user.randSeedInt(100);
|
const rand = user.randSeedInt(100);
|
||||||
|
|
||||||
return rand < (moveAccuracy * accuracyMultiplier);
|
if (rand < (moveAccuracy * accuracyMultiplier)) {
|
||||||
|
return [ HitCheckResult.HIT, effectiveness ];
|
||||||
|
} else {
|
||||||
|
return [ HitCheckResult.MISS, 0 ];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected updateSubstitutes(): void {
|
||||||
|
const targets = this.getTargets();
|
||||||
|
targets.forEach(target => {
|
||||||
|
const substitute = target.getTag(SubstituteTag);
|
||||||
|
if (substitute && substitute.hp <= 0) {
|
||||||
|
target.lapseTag(BattlerTagType.SUBSTITUTE);
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/** @returns The {@linkcode Pokemon} using this phase's invoked move */
|
/** @returns The {@linkcode Pokemon} using this phase's invoked move */
|
||||||
@ -598,40 +613,26 @@ export class MoveEffectPhase extends PokemonPhase {
|
|||||||
return this.getTargets()[0];
|
return this.getTargets()[0];
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Removes the given {@linkcode Pokemon} from this phase's target list
|
|
||||||
* @param target - The {@linkcode Pokemon} to be removed
|
|
||||||
*/
|
|
||||||
protected removeTarget(target: Pokemon): void {
|
|
||||||
const targetIndex = this.targets.findIndex(ind => ind === target.getBattlerIndex());
|
|
||||||
if (targetIndex !== -1) {
|
|
||||||
this.targets.splice(this.targets.findIndex(ind => ind === target.getBattlerIndex()), 1);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Prevents subsequent strikes of this phase's invoked move from occurring
|
|
||||||
* @param target - If defined, only stop subsequent strikes against this {@linkcode Pokemon}
|
|
||||||
*/
|
|
||||||
public stopMultiHit(target?: Pokemon): void {
|
|
||||||
// If given a specific target, remove the target from subsequent strikes
|
|
||||||
if (target) {
|
|
||||||
this.removeTarget(target);
|
|
||||||
}
|
|
||||||
const user = this.getUserPokemon();
|
|
||||||
if (!user) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
// If no target specified, or the specified target was the last of this move's
|
|
||||||
// targets, completely cancel all subsequent strikes.
|
|
||||||
if (!target || this.targets.length === 0 ) {
|
|
||||||
user.turnData.hitCount = 1;
|
|
||||||
user.turnData.hitsLeft = 1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/** @returns A new `MoveEffectPhase` with the same properties as this phase */
|
/** @returns A new `MoveEffectPhase` with the same properties as this phase */
|
||||||
protected getNewHitPhase(): MoveEffectPhase {
|
protected getNewHitPhase(): MoveEffectPhase {
|
||||||
return new MoveEffectPhase(this.scene, this.battlerIndex, this.targets, this.move);
|
return new MoveEffectPhase(this.scene, this.battlerIndex, this.targets, this.move);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Descriptor */
|
||||||
|
export enum HitCheckResult {
|
||||||
|
/** Hit checks haven't been evaluated yet in this pass */
|
||||||
|
PENDING,
|
||||||
|
/** The move hits the target successfully */
|
||||||
|
HIT,
|
||||||
|
/** The move has no effect on the target */
|
||||||
|
NO_EFFECT,
|
||||||
|
/** The move has no effect on the target, but doesn't proc the default "no effect" message. */
|
||||||
|
NO_EFFECT_NO_MESSAGE,
|
||||||
|
/** The target protected itself against the move */
|
||||||
|
PROTECTED,
|
||||||
|
/** The move missed the target */
|
||||||
|
MISS,
|
||||||
|
/** The move failed unexpectedly */
|
||||||
|
ERROR
|
||||||
|
}
|
||||||
|
@ -10,6 +10,7 @@ import GameManager from "#test/utils/gameManager";
|
|||||||
import Phaser from "phaser";
|
import Phaser from "phaser";
|
||||||
import { afterEach, beforeAll, beforeEach, describe, expect, it } from "vitest";
|
import { afterEach, beforeAll, beforeEach, describe, expect, it } from "vitest";
|
||||||
import { allMoves } from "#app/data/move";
|
import { allMoves } from "#app/data/move";
|
||||||
|
import { HitResult } from "#app/field/pokemon";
|
||||||
|
|
||||||
describe("Abilities - Sheer Force", () => {
|
describe("Abilities - Sheer Force", () => {
|
||||||
let phaserGame: Phaser.Game;
|
let phaserGame: Phaser.Game;
|
||||||
@ -156,7 +157,7 @@ describe("Abilities - Sheer Force", () => {
|
|||||||
|
|
||||||
applyAbAttrs(MoveEffectChanceMultiplierAbAttr, user, null, false, chance, move, target, false);
|
applyAbAttrs(MoveEffectChanceMultiplierAbAttr, user, null, false, chance, move, target, false);
|
||||||
applyPreAttackAbAttrs(MovePowerBoostAbAttr, user, target, move, false, power);
|
applyPreAttackAbAttrs(MovePowerBoostAbAttr, user, target, move, false, power);
|
||||||
applyPostDefendAbAttrs(PostDefendTypeChangeAbAttr, target, user, move, target.apply(user, move));
|
applyPostDefendAbAttrs(PostDefendTypeChangeAbAttr, target, user, move, HitResult.EFFECTIVE);
|
||||||
|
|
||||||
expect(chance.value).toBe(0);
|
expect(chance.value).toBe(0);
|
||||||
expect(power.value).toBe(move.power * 5461 / 4096);
|
expect(power.value).toBe(move.power * 5461 / 4096);
|
||||||
|
@ -20,7 +20,8 @@ export class MoveHelper extends GameManagerHelper {
|
|||||||
*/
|
*/
|
||||||
public async forceHit(): Promise<void> {
|
public async forceHit(): Promise<void> {
|
||||||
await this.game.phaseInterceptor.to(MoveEffectPhase, false);
|
await this.game.phaseInterceptor.to(MoveEffectPhase, false);
|
||||||
vi.spyOn(this.game.scene.getCurrentPhase() as MoveEffectPhase, "hitCheck").mockReturnValue(true);
|
const moveEffectPhase = this.game.scene.getCurrentPhase() as MoveEffectPhase;
|
||||||
|
vi.spyOn(moveEffectPhase.move.getMove(), "accuracy", "get").mockReturnValue(-1);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -31,12 +32,13 @@ export class MoveHelper extends GameManagerHelper {
|
|||||||
*/
|
*/
|
||||||
public async forceMiss(firstTargetOnly: boolean = false): Promise<void> {
|
public async forceMiss(firstTargetOnly: boolean = false): Promise<void> {
|
||||||
await this.game.phaseInterceptor.to(MoveEffectPhase, false);
|
await this.game.phaseInterceptor.to(MoveEffectPhase, false);
|
||||||
const hitCheck = vi.spyOn(this.game.scene.getCurrentPhase() as MoveEffectPhase, "hitCheck");
|
const moveEffectPhase = this.game.scene.getCurrentPhase() as MoveEffectPhase;
|
||||||
|
const accuracy = vi.spyOn(moveEffectPhase.move.getMove(), "accuracy", "get");
|
||||||
|
|
||||||
if (firstTargetOnly) {
|
if (firstTargetOnly) {
|
||||||
hitCheck.mockReturnValueOnce(false);
|
accuracy.mockReturnValueOnce(0);
|
||||||
} else {
|
} else {
|
||||||
hitCheck.mockReturnValue(false);
|
accuracy.mockReturnValue(0);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user