Rewrite move phase

Co-authored-by: innerthunder <brandonerickson98@gmail.com>
This commit is contained in:
Sirz Benjie 2025-04-12 14:50:08 -05:00
parent 607dc70b9b
commit f85e008202
No known key found for this signature in database
GPG Key ID: 4A524B4D196C759E
9 changed files with 797 additions and 750 deletions

View File

@ -7,7 +7,7 @@ import { MoveTarget } from "#enums/MoveTarget";
import { MoveCategory } from "#enums/MoveCategory"; import { MoveCategory } from "#enums/MoveCategory";
import { getPokemonNameWithAffix } from "#app/messages"; import { getPokemonNameWithAffix } from "#app/messages";
import type Pokemon from "#app/field/pokemon"; import type Pokemon from "#app/field/pokemon";
import { HitResult, PokemonMove } from "#app/field/pokemon"; import { HitResult } from "#app/field/pokemon";
import { StatusEffect } from "#enums/status-effect"; import { StatusEffect } from "#enums/status-effect";
import type { BattlerIndex } from "#app/battle"; import type { BattlerIndex } from "#app/battle";
import { import {
@ -335,7 +335,7 @@ export class ConditionalProtectTag extends ArenaTag {
* @param arena the {@linkcode Arena} containing this tag * @param arena the {@linkcode Arena} containing this tag
* @param simulated `true` if the tag is applied quietly; `false` otherwise. * @param simulated `true` if the tag is applied quietly; `false` otherwise.
* @param isProtected a {@linkcode BooleanHolder} used to flag if the move is protected against * @param isProtected a {@linkcode BooleanHolder} used to flag if the move is protected against
* @param attacker the attacking {@linkcode Pokemon} * @param _attacker the attacking {@linkcode Pokemon}
* @param defender the defending {@linkcode Pokemon} * @param defender the defending {@linkcode Pokemon}
* @param moveId the {@linkcode Moves | identifier} for the move being used * @param moveId the {@linkcode Moves | identifier} for the move being used
* @param ignoresProtectBypass a {@linkcode BooleanHolder} used to flag if a protection effect supercedes effects that ignore protection * @param ignoresProtectBypass a {@linkcode BooleanHolder} used to flag if a protection effect supercedes effects that ignore protection
@ -345,7 +345,7 @@ export class ConditionalProtectTag extends ArenaTag {
arena: Arena, arena: Arena,
simulated: boolean, simulated: boolean,
isProtected: BooleanHolder, isProtected: BooleanHolder,
attacker: Pokemon, _attacker: Pokemon,
defender: Pokemon, defender: Pokemon,
moveId: Moves, moveId: Moves,
ignoresProtectBypass: BooleanHolder, ignoresProtectBypass: BooleanHolder,
@ -354,8 +354,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(); new CommonBattleAnim(CommonAnim.PROTECT, defender).play();
globalScene.queueMessage( globalScene.queueMessage(
i18next.t("arenaTag:conditionalProtectApply", { i18next.t("arenaTag:conditionalProtectApply", {
@ -899,7 +897,7 @@ export class DelayedAttackTag extends ArenaTag {
if (!ret) { if (!ret) {
globalScene.unshiftPhase( globalScene.unshiftPhase(
new MoveEffectPhase(this.sourceId!, [this.targetIndex], new PokemonMove(this.sourceMove!, 0, 0, true)), new MoveEffectPhase(this.sourceId!, [this.targetIndex], allMoves[this.sourceMove!], false, true),
); // TODO: are those bangs correct? ); // TODO: are those bangs correct?
} }

View File

@ -2637,7 +2637,7 @@ export class GulpMissileTag extends BattlerTag {
return false; return false;
} }
if (moveEffectPhase.move.getMove().hitsSubstitute(attacker, pokemon)) { if (moveEffectPhase.move.hitsSubstitute(attacker, pokemon)) {
return true; return true;
} }
@ -2993,7 +2993,7 @@ export class SubstituteTag extends BattlerTag {
if (!attacker) { if (!attacker) {
return; return;
} }
const move = moveEffectPhase.move.getMove(); const move = moveEffectPhase.move;
const firstHit = attacker.turnData.hitCount === attacker.turnData.hitsLeft; const firstHit = attacker.turnData.hitCount === attacker.turnData.hitsLeft;
if (firstHit && move.hitsSubstitute(attacker, pokemon)) { if (firstHit && move.hitsSubstitute(attacker, pokemon)) {
@ -3681,7 +3681,7 @@ function getMoveEffectPhaseData(_pokemon: Pokemon): { phase: MoveEffectPhase; at
return { return {
phase: phase, phase: phase,
attacker: phase.getPokemon(), attacker: phase.getPokemon(),
move: phase.move.getMove(), move: phase.move,
}; };
} }
return null; return null;

View File

@ -0,0 +1,20 @@
import { MoveTarget } from "#enums/MoveTarget";
import type Move from "./move";
/**
* Return whether the move targets the field
*
* Examples include
* - Hazard moves like spikes
* - Weather moves like rain dance
* - User side moves like reflect and safeguard
*/
export function isFieldTargeted(move: Move): boolean {
switch (move.moveTarget) {
case MoveTarget.BOTH_SIDES:
case MoveTarget.USER_SIDE:
case MoveTarget.ENEMY_SIDE:
return true;
}
return false;
}

View File

@ -60,6 +60,7 @@ import {
MoveTypeChangeAbAttr, MoveTypeChangeAbAttr,
PostDamageForceSwitchAbAttr, PostDamageForceSwitchAbAttr,
PostItemLostAbAttr, PostItemLostAbAttr,
ReflectStatusMoveAbAttr,
ReverseDrainAbAttr, ReverseDrainAbAttr,
UserFieldMoveTypePowerBoostAbAttr, UserFieldMoveTypePowerBoostAbAttr,
VariableMovePowerAbAttr, VariableMovePowerAbAttr,
@ -665,6 +666,17 @@ export default class Move implements Localizable {
return true; return true;
} }
break; break;
case MoveFlags.REFLECTABLE:
// If the target is not semi-invulnerable and either has magic coat active or an unignored magic bounce ability
if (
target?.getTag(SemiInvulnerableTag) &&
(target.getTag(BattlerTagType.MAGIC_COAT) ||
(!this.doesFlagEffectApply({ flag: MoveFlags.IGNORE_ABILITIES, user, target }) &&
target.hasAbilityWithAttr(ReflectStatusMoveAbAttr)))
) {
return false;
}
break;
} }
return !!(this.flags & flag); return !!(this.flags & flag);
@ -1716,7 +1728,7 @@ export class SacrificialAttr extends MoveEffectAttr {
**/ **/
export class SacrificialAttrOnHit extends MoveEffectAttr { export class SacrificialAttrOnHit extends MoveEffectAttr {
constructor() { constructor() {
super(true, { trigger: MoveEffectTrigger.HIT }); super(true);
} }
/** /**
@ -1955,6 +1967,14 @@ export class PartyStatusCureAttr extends MoveEffectAttr {
* @extends MoveEffectAttr * @extends MoveEffectAttr
*/ */
export class FlameBurstAttr extends MoveEffectAttr { export class FlameBurstAttr extends MoveEffectAttr {
constructor() {
/**
* This is self-targeted to bypass immunity to target-facing secondary
* effects when the target has an active Substitute doll.
* TODO: Find a more intuitive way to implement Substitute bypassing.
*/
super(true);
}
/** /**
* @param user - n/a * @param user - n/a
* @param target - The target Pokémon. * @param target - The target Pokémon.
@ -2177,7 +2197,7 @@ export class HitHealAttr extends MoveEffectAttr {
private healStat: EffectiveStat | null; private healStat: EffectiveStat | null;
constructor(healRatio?: number | null, healStat?: EffectiveStat) { constructor(healRatio?: number | null, healStat?: EffectiveStat) {
super(true, { trigger: MoveEffectTrigger.HIT }); super(true);
this.healRatio = healRatio ?? 0.5; this.healRatio = healRatio ?? 0.5;
this.healStat = healStat ?? null; this.healStat = healStat ?? null;
@ -2426,7 +2446,7 @@ export class StatusEffectAttr extends MoveEffectAttr {
public overrideStatus: boolean = false; public overrideStatus: boolean = false;
constructor(effect: StatusEffect, selfTarget?: boolean, turnsRemaining?: number, overrideStatus: boolean = false) { constructor(effect: StatusEffect, selfTarget?: boolean, turnsRemaining?: number, overrideStatus: boolean = false) {
super(selfTarget, { trigger: MoveEffectTrigger.HIT }); super(selfTarget);
this.effect = effect; this.effect = effect;
this.turnsRemaining = turnsRemaining; this.turnsRemaining = turnsRemaining;
@ -2434,10 +2454,6 @@ export class StatusEffectAttr extends MoveEffectAttr {
} }
apply(user: Pokemon, target: Pokemon, move: Move, args: any[]): boolean { apply(user: Pokemon, target: Pokemon, move: Move, args: any[]): boolean {
if (!this.selfTarget && move.hitsSubstitute(user, target)) {
return false;
}
const moveChance = this.getMoveChance(user, target, move, this.selfTarget, true); const moveChance = this.getMoveChance(user, target, move, this.selfTarget, true);
const statusCheck = moveChance < 0 || moveChance === 100 || user.randSeedInt(100) < moveChance; const statusCheck = moveChance < 0 || moveChance === 100 || user.randSeedInt(100) < moveChance;
if (statusCheck) { if (statusCheck) {
@ -2495,7 +2511,7 @@ export class MultiStatusEffectAttr extends StatusEffectAttr {
export class PsychoShiftEffectAttr extends MoveEffectAttr { export class PsychoShiftEffectAttr extends MoveEffectAttr {
constructor() { constructor() {
super(false, { trigger: MoveEffectTrigger.HIT }); super(false);
} }
/** /**
@ -2534,15 +2550,11 @@ export class StealHeldItemChanceAttr extends MoveEffectAttr {
private chance: number; private chance: number;
constructor(chance: number) { constructor(chance: number) {
super(false, { trigger: MoveEffectTrigger.HIT }); super(false);
this.chance = chance; this.chance = chance;
} }
apply(user: Pokemon, target: Pokemon, move: Move, args: any[]): boolean { apply(user: Pokemon, target: Pokemon, move: Move, args: any[]): boolean {
if (move.hitsSubstitute(user, target)) {
return false;
}
const rand = Phaser.Math.RND.realInRange(0, 1); const rand = Phaser.Math.RND.realInRange(0, 1);
if (rand >= this.chance) { if (rand >= this.chance) {
return false; return false;
@ -2590,7 +2602,7 @@ export class RemoveHeldItemAttr extends MoveEffectAttr {
private berriesOnly: boolean; private berriesOnly: boolean;
constructor(berriesOnly: boolean) { constructor(berriesOnly: boolean) {
super(false, { trigger: MoveEffectTrigger.HIT }); super(false);
this.berriesOnly = berriesOnly; this.berriesOnly = berriesOnly;
} }
@ -2600,17 +2612,13 @@ export class RemoveHeldItemAttr extends MoveEffectAttr {
* @param target Target {@linkcode Pokemon} that the moves applies to * @param target Target {@linkcode Pokemon} that the moves applies to
* @param move {@linkcode Move} that is used * @param move {@linkcode Move} that is used
* @param args N/A * @param args N/A
* @returns {boolean} True if an item was removed * @returns True if an item was removed
*/ */
apply(user: Pokemon, target: Pokemon, move: Move, args: any[]): boolean { apply(user: Pokemon, target: Pokemon, move: Move, args: any[]): boolean {
if (!this.berriesOnly && target.isPlayer()) { // "Wild Pokemon cannot knock off Player Pokemon's held items" (See Bulbapedia) if (!this.berriesOnly && target.isPlayer()) { // "Wild Pokemon cannot knock off Player Pokemon's held items" (See Bulbapedia)
return false; return false;
} }
if (move.hitsSubstitute(user, target)) {
return false;
}
const cancelled = new BooleanHolder(false); const cancelled = new BooleanHolder(false);
applyAbAttrs(BlockItemTheftAbAttr, target, cancelled); // Check for abilities that block item theft applyAbAttrs(BlockItemTheftAbAttr, target, cancelled); // Check for abilities that block item theft
@ -2664,8 +2672,8 @@ export class RemoveHeldItemAttr extends MoveEffectAttr {
*/ */
export class EatBerryAttr extends MoveEffectAttr { export class EatBerryAttr extends MoveEffectAttr {
protected chosenBerry: BerryModifier | undefined; protected chosenBerry: BerryModifier | undefined;
constructor() { constructor(selfTarget: boolean) {
super(true, { trigger: MoveEffectTrigger.HIT }); super(selfTarget);
} }
/** /**
* Causes the target to eat a berry. * Causes the target to eat a berry.
@ -2680,17 +2688,20 @@ export class EatBerryAttr extends MoveEffectAttr {
return false; return false;
} }
const heldBerries = this.getTargetHeldBerries(target); const pokemon = this.selfTarget ? user : target;
const heldBerries = this.getTargetHeldBerries(pokemon);
if (heldBerries.length <= 0) { if (heldBerries.length <= 0) {
return false; return false;
} }
this.chosenBerry = heldBerries[user.randSeedInt(heldBerries.length)]; this.chosenBerry = heldBerries[user.randSeedInt(heldBerries.length)];
const preserve = new BooleanHolder(false); const preserve = new BooleanHolder(false);
globalScene.applyModifiers(PreserveBerryModifier, target.isPlayer(), target, preserve); // check for berry pouch preservation // check for berry pouch preservation
globalScene.applyModifiers(PreserveBerryModifier, pokemon.isPlayer(), pokemon, preserve);
if (!preserve.value) { if (!preserve.value) {
this.reduceBerryModifier(target); this.reduceBerryModifier(pokemon);
} }
this.eatBerry(target); this.eatBerry(pokemon);
return true; return true;
} }
@ -2718,7 +2729,7 @@ export class EatBerryAttr extends MoveEffectAttr {
*/ */
export class StealEatBerryAttr extends EatBerryAttr { export class StealEatBerryAttr extends EatBerryAttr {
constructor() { constructor() {
super(); super(true);
} }
/** /**
* User steals a random berry from the target and then eats it. * User steals a random berry from the target and then eats it.
@ -2729,9 +2740,6 @@ export class StealEatBerryAttr extends EatBerryAttr {
* @returns {boolean} true if the function succeeds * @returns {boolean} true if the function succeeds
*/ */
apply(user: Pokemon, target: Pokemon, move: Move, args: any[]): boolean { apply(user: Pokemon, target: Pokemon, move: Move, args: any[]): boolean {
if (move.hitsSubstitute(user, target)) {
return false;
}
const cancelled = new BooleanHolder(false); const cancelled = new BooleanHolder(false);
applyAbAttrs(BlockItemTheftAbAttr, target, cancelled); // check for abilities that block item theft applyAbAttrs(BlockItemTheftAbAttr, target, cancelled); // check for abilities that block item theft
if (cancelled.value === true) { if (cancelled.value === true) {
@ -2782,10 +2790,6 @@ export class HealStatusEffectAttr extends MoveEffectAttr {
return false; return false;
} }
if (!this.selfTarget && move.hitsSubstitute(user, target)) {
return false;
}
// Special edge case for shield dust blocking Sparkling Aria curing burn // Special edge case for shield dust blocking Sparkling Aria curing burn
const moveTargets = getMoveTargets(user, move.id); const moveTargets = getMoveTargets(user, move.id);
if (target.hasAbilityWithAttr(IgnoreMoveEffectsAbAttr) && move.id === Moves.SPARKLING_ARIA && moveTargets.targets.length === 1) { if (target.hasAbilityWithAttr(IgnoreMoveEffectsAbAttr) && move.id === Moves.SPARKLING_ARIA && moveTargets.targets.length === 1) {
@ -3162,15 +3166,7 @@ export class StatStageChangeAttr extends MoveEffectAttr {
private get showMessage () { private get showMessage () {
return this.options?.showMessage ?? true; return this.options?.showMessage ?? true;
} }
/**
* Indicates when the stat change should trigger
* @default MoveEffectTrigger.HIT
*/
public override get trigger () {
return this.options?.trigger ?? MoveEffectTrigger.HIT;
}
/** /**
* Attempts to change stats of the user or target (depending on value of selfTarget) if conditions are met * Attempts to change stats of the user or target (depending on value of selfTarget) if conditions are met
* @param user {@linkcode Pokemon} the user of the move * @param user {@linkcode Pokemon} the user of the move
@ -3184,10 +3180,6 @@ export class StatStageChangeAttr extends MoveEffectAttr {
return false; return false;
} }
if (!this.selfTarget && move.hitsSubstitute(user, target)) {
return false;
}
const moveChance = this.getMoveChance(user, target, move, this.selfTarget, true); const moveChance = this.getMoveChance(user, target, move, this.selfTarget, true);
if (moveChance < 0 || moveChance === 100 || user.randSeedInt(100) < moveChance) { if (moveChance < 0 || moveChance === 100 || user.randSeedInt(100) < moveChance) {
const stages = this.getLevels(user); const stages = this.getLevels(user);
@ -3471,7 +3463,7 @@ export class CutHpStatStageBoostAttr extends StatStageChangeAttr {
*/ */
export class OrderUpStatBoostAttr extends MoveEffectAttr { export class OrderUpStatBoostAttr extends MoveEffectAttr {
constructor() { constructor() {
super(true, { trigger: MoveEffectTrigger.HIT }); super(true);
} }
override apply(user: Pokemon, target: Pokemon, move: Move, args?: any[]): boolean { override apply(user: Pokemon, target: Pokemon, move: Move, args?: any[]): boolean {
@ -3548,17 +3540,15 @@ export class ResetStatsAttr extends MoveEffectAttr {
this.targetAllPokemon = targetAllPokemon; this.targetAllPokemon = targetAllPokemon;
} }
override apply(user: Pokemon, target: Pokemon, move: Move, args: any[]): boolean { override apply(_user: Pokemon, target: Pokemon, _move: Move, _args: any[]): boolean {
if (this.targetAllPokemon) { if (this.targetAllPokemon) {
// Target all pokemon on the field when Freezy Frost or Haze are used // Target all pokemon on the field when Freezy Frost or Haze are used
const activePokemon = globalScene.getField(true); const activePokemon = globalScene.getField(true);
activePokemon.forEach((p) => this.resetStats(p)); activePokemon.forEach((p) => this.resetStats(p));
globalScene.queueMessage(i18next.t("moveTriggers:statEliminated")); globalScene.queueMessage(i18next.t("moveTriggers:statEliminated"));
} else { // Affects only the single target when Clear Smog is used } else { // Affects only the single target when Clear Smog is used
if (!move.hitsSubstitute(user, target)) { this.resetStats(target);
this.resetStats(target); globalScene.queueMessage(i18next.t("moveTriggers:resetStats", { pokemonName: getPokemonNameWithAffix(target) }));
globalScene.queueMessage(i18next.t("moveTriggers:resetStats", { pokemonName: getPokemonNameWithAffix(target) }));
}
} }
return true; return true;
} }
@ -4217,7 +4207,8 @@ export class PresentPowerAttr extends VariablePowerAttr {
(args[0] as NumberHolder).value = 120; (args[0] as 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 // If this move is multi-hit, disable all other hits
user.stopMultiHit(); user.turnData.hitCount = 1;
user.turnData.hitsLeft = 1;
globalScene.unshiftPhase(new PokemonHealPhase(target.getBattlerIndex(), globalScene.unshiftPhase(new PokemonHealPhase(target.getBattlerIndex(),
toDmgValue(target.getMaxHp() / 4), i18next.t("moveTriggers:regainedHealth", { pokemonName: getPokemonNameWithAffix(target) }), true)); toDmgValue(target.getMaxHp() / 4), i18next.t("moveTriggers:regainedHealth", { pokemonName: getPokemonNameWithAffix(target) }), true));
} }
@ -5371,7 +5362,7 @@ export class BypassRedirectAttr extends MoveAttr {
export class FrenzyAttr extends MoveEffectAttr { export class FrenzyAttr extends MoveEffectAttr {
constructor() { constructor() {
super(true, { trigger: MoveEffectTrigger.HIT, lastHitOnly: true }); super(true, { lastHitOnly: true });
} }
canApply(user: Pokemon, target: Pokemon, move: Move, args: any[]) { canApply(user: Pokemon, target: Pokemon, move: Move, args: any[]) {
@ -5443,22 +5434,20 @@ export class AddBattlerTagAttr extends MoveEffectAttr {
protected cancelOnFail: boolean; protected cancelOnFail: boolean;
private failOnOverlap: boolean; private failOnOverlap: boolean;
constructor(tagType: BattlerTagType, selfTarget: boolean = false, failOnOverlap: boolean = false, turnCountMin: number = 0, turnCountMax?: number, lastHitOnly: boolean = false, cancelOnFail: boolean = false) { constructor(tagType: BattlerTagType, selfTarget: boolean = false, failOnOverlap: boolean = false, turnCountMin: number = 0, turnCountMax?: number, lastHitOnly: boolean = false) {
super(selfTarget, { lastHitOnly: lastHitOnly }); super(selfTarget, { lastHitOnly: lastHitOnly });
this.tagType = tagType; this.tagType = tagType;
this.turnCountMin = turnCountMin; this.turnCountMin = turnCountMin;
this.turnCountMax = turnCountMax !== undefined ? turnCountMax : turnCountMin; this.turnCountMax = turnCountMax !== undefined ? turnCountMax : turnCountMin;
this.failOnOverlap = !!failOnOverlap; this.failOnOverlap = !!failOnOverlap;
this.cancelOnFail = cancelOnFail;
} }
canApply(user: Pokemon, target: Pokemon, move: Move, args: any[]): boolean { canApply(user: Pokemon, target: Pokemon, move: Move, args: any[]): boolean {
if (!super.canApply(user, target, move, args) || (this.cancelOnFail === true && user.getLastXMoves(1)[0]?.result === MoveResult.FAIL)) { if (!super.canApply(user, target, move, args)) {
return false; return false;
} else {
return true;
} }
return true;
} }
apply(user: Pokemon, target: Pokemon, move: Move, args: any[]): boolean { apply(user: Pokemon, target: Pokemon, move: Move, args: any[]): boolean {
@ -5549,19 +5538,6 @@ export class LeechSeedAttr extends AddBattlerTagAttr {
constructor() { constructor() {
super(BattlerTagType.SEEDED); super(BattlerTagType.SEEDED);
} }
/**
* Adds a Seeding effect to the target if the target does not have an active Substitute.
* @param user the {@linkcode Pokemon} using the move
* @param target the {@linkcode Pokemon} targeted by the move
* @param move the {@linkcode Move} invoking this effect
* @param args n/a
* @returns `true` if the effect successfully applies; `false` otherwise
*/
apply(user: Pokemon, target: Pokemon, move: Move, args: any[]): boolean {
return !move.hitsSubstitute(user, target)
&& super.apply(user, target, move, args);
}
} }
/** /**
@ -5737,13 +5713,6 @@ export class FlinchAttr extends AddBattlerTagAttr {
constructor() { constructor() {
super(BattlerTagType.FLINCHED, false); super(BattlerTagType.FLINCHED, false);
} }
apply(user: Pokemon, target: Pokemon, move: Move, args: any[]): boolean {
if (!move.hitsSubstitute(user, target)) {
return super.apply(user, target, move, args);
}
return false;
}
} }
export class ConfuseAttr extends AddBattlerTagAttr { export class ConfuseAttr extends AddBattlerTagAttr {
@ -5759,16 +5728,13 @@ export class ConfuseAttr extends AddBattlerTagAttr {
return false; return false;
} }
if (!move.hitsSubstitute(user, target)) { return super.apply(user, target, move, args);
return super.apply(user, target, move, args);
}
return false;
} }
} }
export class RechargeAttr extends AddBattlerTagAttr { export class RechargeAttr extends AddBattlerTagAttr {
constructor() { constructor() {
super(BattlerTagType.RECHARGING, true, false, 1, 1, true, true); super(BattlerTagType.RECHARGING, true, false, 1, 1, true);
} }
} }
@ -6151,7 +6117,7 @@ export class AddPledgeEffectAttr extends AddArenaTagAttr {
* @see {@linkcode apply} * @see {@linkcode apply}
*/ */
export class RevivalBlessingAttr extends MoveEffectAttr { export class RevivalBlessingAttr extends MoveEffectAttr {
constructor(user?: boolean) { constructor() {
super(true); super(true);
} }
@ -6392,10 +6358,6 @@ export class ForceSwitchOutAttr extends MoveEffectAttr {
const player = switchOutTarget instanceof PlayerPokemon; const player = switchOutTarget instanceof PlayerPokemon;
if (!this.selfSwitch) { if (!this.selfSwitch) {
if (move.hitsSubstitute(user, target)) {
return false;
}
// Dondozo with an allied Tatsugiri in its mouth cannot be forced out // Dondozo with an allied Tatsugiri in its mouth cannot be forced out
const commandedTag = switchOutTarget.getTag(BattlerTagType.COMMANDED); const commandedTag = switchOutTarget.getTag(BattlerTagType.COMMANDED);
if (commandedTag?.getSourcePokemon()?.isActive(true)) { if (commandedTag?.getSourcePokemon()?.isActive(true)) {
@ -6650,7 +6612,7 @@ export class ChangeTypeAttr extends MoveEffectAttr {
private type: PokemonType; private type: PokemonType;
constructor(type: PokemonType) { constructor(type: PokemonType) {
super(false, { trigger: MoveEffectTrigger.HIT }); super(false);
this.type = type; this.type = type;
} }
@ -6673,7 +6635,7 @@ export class AddTypeAttr extends MoveEffectAttr {
private type: PokemonType; private type: PokemonType;
constructor(type: PokemonType) { constructor(type: PokemonType) {
super(false, { trigger: MoveEffectTrigger.HIT }); super(false);
this.type = type; this.type = type;
} }
@ -7369,7 +7331,7 @@ export class AbilityChangeAttr extends MoveEffectAttr {
public ability: Abilities; public ability: Abilities;
constructor(ability: Abilities, selfTarget?: boolean) { constructor(ability: Abilities, selfTarget?: boolean) {
super(selfTarget, { trigger: MoveEffectTrigger.HIT }); super(selfTarget);
this.ability = ability; this.ability = ability;
} }
@ -7400,7 +7362,7 @@ export class AbilityCopyAttr extends MoveEffectAttr {
public copyToPartner: boolean; public copyToPartner: boolean;
constructor(copyToPartner: boolean = false) { constructor(copyToPartner: boolean = false) {
super(false, { trigger: MoveEffectTrigger.HIT }); super(false);
this.copyToPartner = copyToPartner; this.copyToPartner = copyToPartner;
} }
@ -7441,7 +7403,7 @@ export class AbilityGiveAttr extends MoveEffectAttr {
public copyToPartner: boolean; public copyToPartner: boolean;
constructor() { constructor() {
super(false, { trigger: MoveEffectTrigger.HIT }); super(false);
} }
apply(user: Pokemon, target: Pokemon, move: Move, args: any[]): boolean { apply(user: Pokemon, target: Pokemon, move: Move, args: any[]): boolean {
@ -7720,7 +7682,7 @@ export class DiscourageFrequentUseAttr extends MoveAttr {
export class MoneyAttr extends MoveEffectAttr { export class MoneyAttr extends MoveEffectAttr {
constructor() { constructor() {
super(true, { trigger: MoveEffectTrigger.HIT, firstHitOnly: true }); super(true, {firstHitOnly: true });
} }
apply(user: Pokemon, target: Pokemon, move: Move): boolean { apply(user: Pokemon, target: Pokemon, move: Move): boolean {
@ -7787,7 +7749,7 @@ export class StatusIfBoostedAttr extends MoveEffectAttr {
public effect: StatusEffect; public effect: StatusEffect;
constructor(effect: StatusEffect) { constructor(effect: StatusEffect) {
super(true, { trigger: MoveEffectTrigger.HIT }); super(true);
this.effect = effect; this.effect = effect;
} }
@ -10566,7 +10528,7 @@ export function initMoves() {
.attr(JawLockAttr) .attr(JawLockAttr)
.bitingMove(), .bitingMove(),
new SelfStatusMove(Moves.STUFF_CHEEKS, PokemonType.NORMAL, -1, 10, -1, 0, 8) new SelfStatusMove(Moves.STUFF_CHEEKS, PokemonType.NORMAL, -1, 10, -1, 0, 8)
.attr(EatBerryAttr) .attr(EatBerryAttr, true)
.attr(StatStageChangeAttr, [ Stat.DEF ], 2, true) .attr(StatStageChangeAttr, [ Stat.DEF ], 2, true)
.condition((user) => { .condition((user) => {
const userBerries = globalScene.findModifiers(m => m instanceof BerryModifier, user.isPlayer()); const userBerries = globalScene.findModifiers(m => m instanceof BerryModifier, user.isPlayer());
@ -10590,7 +10552,7 @@ export function initMoves() {
.makesContact(false) .makesContact(false)
.partial(), // smart targetting is unimplemented .partial(), // smart targetting is unimplemented
new StatusMove(Moves.TEATIME, PokemonType.NORMAL, -1, 10, -1, 0, 8) new StatusMove(Moves.TEATIME, PokemonType.NORMAL, -1, 10, -1, 0, 8)
.attr(EatBerryAttr) .attr(EatBerryAttr, false)
.target(MoveTarget.ALL), .target(MoveTarget.ALL),
new StatusMove(Moves.OCTOLOCK, PokemonType.FIGHTING, 100, 15, -1, 0, 8) new StatusMove(Moves.OCTOLOCK, PokemonType.FIGHTING, 100, 15, -1, 0, 8)
.condition(failIfGhostTypeCondition) .condition(failIfGhostTypeCondition)

View File

@ -1,4 +1,4 @@
/** Represent the result of a hit check against a target. */ /** The result of a hit check calculation */
export const HitCheckResult = { export const HitCheckResult = {
/** Hit checks haven't been evaluated yet in this pass */ /** Hit checks haven't been evaluated yet in this pass */
PENDING: 0, PENDING: 0,
@ -6,14 +6,18 @@ export const HitCheckResult = {
HIT: 1, HIT: 1,
/** The move has no effect on the target */ /** The move has no effect on the target */
NO_EFFECT: 2, NO_EFFECT: 2,
/** The move has no effect on the target, but doesn't proc the default "no effect" message. */ /** The move has no effect on the target, but doesn't proc the default "no effect" message */
NO_EFFECT_NO_MESSAGE: 3, NO_EFFECT_NO_MESSAGE: 3,
/** The target protected itself against the move */ /** The target protected itself against the move */
PROTECTED: 4, PROTECTED: 4,
/** The move missed the target */ /** The move missed the target */
MISS: 5, MISS: 5,
/** The move is reflected by magic coat or magic bounce */
REFLECTED: 6,
/** The target is no longer on the field */
TARGET_NOT_ON_FIELD: 7,
/** The move failed unexpectedly */ /** The move failed unexpectedly */
ERROR: 6, ERROR: 8,
} as const; } as const;
export type HitCheckResult = typeof HitCheckResult[keyof typeof HitCheckResult]; export type HitCheckResult = typeof HitCheckResult[keyof typeof HitCheckResult];

View File

@ -4600,212 +4600,36 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container {
}; };
} }
/** /** Calculate whether the given move critically hits this pokemon
* Applies the results of a move to 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 * @param simulated - If `true`, suppresses changes to game state during calculation (defaults to `true`)
* @returns The {@linkcode HitResult} of the attack * @returns whether the move critically hits the pokemon
*/ */
apply(source: Pokemon, move: Move): HitResult { getCriticalHitResult(source: Pokemon, move: Move, simulated: boolean = true): boolean {
const defendingSide = this.isPlayer() const defendingSide = this.isPlayer() ? ArenaTagSide.PLAYER : ArenaTagSide.ENEMY;
? ArenaTagSide.PLAYER const noCritTag = globalScene.arena.getTagOnSide(NoCritTag, defendingSide);
: ArenaTagSide.ENEMY; if (noCritTag || Overrides.NEVER_CRIT_OVERRIDE || move.hasAttr(FixedDamageAttr)) {
const moveCategory = new NumberHolder(move.category); return false;
applyMoveAttrs(VariableMoveCategoryAttr, source, this, move, moveCategory);
if (moveCategory.value === MoveCategory.STATUS) {
const cancelled = new BooleanHolder(false);
const typeMultiplier = this.getMoveEffectiveness(
source,
move,
false,
false,
cancelled,
);
if (!cancelled.value && typeMultiplier === 0) {
globalScene.queueMessage(
i18next.t("battle:hitResultNoEffect", {
pokemonName: getPokemonNameWithAffix(this),
}),
);
}
return typeMultiplier === 0 ? HitResult.NO_EFFECT : HitResult.STATUS;
} }
/** Determines whether the attack critically hits */ const isCritical = new BooleanHolder(false);
let isCritical: boolean;
const critOnly = new BooleanHolder(false); if (source.getTag(BattlerTagType.ALWAYS_CRIT)) {
const critAlways = source.getTag(BattlerTagType.ALWAYS_CRIT); isCritical.value = true;
applyMoveAttrs(CritOnlyAttr, source, this, move, critOnly); }
applyAbAttrs( applyMoveAttrs(CritOnlyAttr, source, this, move, isCritical);
ConditionalCritAbAttr, applyAbAttrs(ConditionalCritAbAttr, source, null, simulated, isCritical, this, move);
source, if (!isCritical.value) {
null,
false,
critOnly,
this,
move,
);
if (critOnly.value || critAlways) {
isCritical = true;
} else {
const critChance = [24, 8, 2, 1][ const critChance = [24, 8, 2, 1][
Math.max(0, Math.min(this.getCritStage(source, move), 3)) Math.max(0, Math.min(this.getCritStage(source, move), 3))
]; ];
isCritical = isCritical.value = critChance === 1 || !globalScene.randBattleSeedInt(critChance);
critChance === 1 || !globalScene.randBattleSeedInt(critChance);
} }
const noCritTag = globalScene.arena.getTagOnSide(NoCritTag, defendingSide); applyAbAttrs(BlockCritAbAttr, this, null, simulated, isCritical);
const blockCrit = new BooleanHolder(false);
applyAbAttrs(BlockCritAbAttr, this, null, false, blockCrit);
if (noCritTag || blockCrit.value || Overrides.NEVER_CRIT_OVERRIDE) {
isCritical = false;
}
/** return isCritical.value;
* Applies stat changes from {@linkcode move} and gives it to {@linkcode source}
* before damage calculation
*/
applyMoveAttrs(StatChangeBeforeDmgCalcAttr, source, this, move);
const {
cancelled,
result,
damage: dmg,
} = this.getAttackDamage(
{source, move, isCritical, simulated: 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) {
globalScene.queueMessage(
i18next.t("battle:hitResultImmune", {
pokemonName: getPokemonNameWithAffix(this),
}),
);
} else {
globalScene.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);
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) {
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: result as DamageResult,
isCritical,
ignoreFaintPhase: true,
source
});
if (damage > 0) {
if (source.isPlayer()) {
globalScene.validateAchvs(DamageAchv, new NumberHolder(damage));
if (damage > globalScene.gameData.gameStats.highestDamage) {
globalScene.gameData.gameStats.highestDamage = damage;
}
}
source.turnData.totalDamageDealt += damage;
source.turnData.singleHitDamageDealt = damage;
this.turnData.damageTaken += damage;
this.battleData.hitCount++;
const attackResult = {
move: move.id,
result: result as DamageResult,
damage: damage,
critical: isCritical,
sourceId: source.id,
sourceBattlerIndex: source.getBattlerIndex(),
};
this.turnData.attacksReceived.unshift(attackResult);
if (source.isPlayer() && !this.isPlayer()) {
globalScene.applyModifiers(
DamageMoneyRewardModifier,
true,
source,
new NumberHolder(damage),
);
}
}
}
if (isCritical) {
globalScene.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:
globalScene.queueMessage(i18next.t("battle:hitResultSuperEffective"));
break;
case HitResult.NOT_VERY_EFFECTIVE:
globalScene.queueMessage(
i18next.t("battle:hitResultNotVeryEffective"),
);
break;
case HitResult.ONE_HIT_KO:
globalScene.queueMessage(i18next.t("battle:hitResultOneHitKO"));
break;
}
}
if (this.isFainted()) {
// set splice index here, so future scene queues happen before FaintedPhase
globalScene.setPhaseQueueSplice();
globalScene.unshiftPhase(
new FaintPhase(
this.getBattlerIndex(),
false,
source,
),
);
this.destroySubstitute();
this.lapseTag(BattlerTagType.COMMANDED);
}
return result;
} }
/** /**
@ -4869,7 +4693,8 @@ 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. * Given the damage, adds a new DamagePhase and update HP values, etc.
*
* Checks for 'Indirect' HitResults to account for Endure/Reviver Seed applying correctly * Checks for 'Indirect' HitResults to account for Endure/Reviver Seed applying correctly
* @param damage integer - passed to damage() * @param damage integer - passed to damage()
* @param result an enum if it's super effective, not very, etc. * @param result an enum if it's super effective, not very, etc.
@ -5172,8 +4997,8 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container {
/** /**
* Gets whether the given move is currently disabled for this Pokemon. * Gets whether the given move is currently disabled for this Pokemon.
* *
* @param {Moves} moveId {@linkcode Moves} ID of the move to check * @param moveId - The {@linkcode Moves} ID of the move to check
* @returns {boolean} `true` if the move is disabled for this Pokemon, otherwise `false` * @returns `true` if the move is disabled for this Pokemon, otherwise `false`
* *
* @see {@linkcode MoveRestrictionBattlerTag} * @see {@linkcode MoveRestrictionBattlerTag}
*/ */
@ -5184,9 +5009,9 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container {
/** /**
* Gets whether the given move is currently disabled for the user based on the player's target selection * Gets whether the given move is currently disabled for the user based on the player's target selection
* *
* @param {Moves} moveId {@linkcode Moves} ID of the move to check * @param moveId - The {@linkcode Moves} ID of the move to check
* @param {Pokemon} user {@linkcode Pokemon} the move user * @param user - The move user
* @param {Pokemon} target {@linkcode Pokemon} the target of the move * @param target - The target of the move
* *
* @returns {boolean} `true` if the move is disabled for this Pokemon due to the player's target selection * @returns {boolean} `true` if the move is disabled for this Pokemon due to the player's target selection
* *
@ -5216,10 +5041,10 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container {
/** /**
* Gets the {@link MoveRestrictionBattlerTag} that is restricting a move, if it exists. * Gets the {@link MoveRestrictionBattlerTag} that is restricting a move, if it exists.
* *
* @param {Moves} moveId {@linkcode Moves} ID of the move to check * @param moveId - {@linkcode Moves} ID of the move to check
* @param {Pokemon} user {@linkcode Pokemon} the move user, optional and used when the target is a factor in the move's restricted status * @param user - {@linkcode Pokemon} the move user, optional and used when the target is a factor in the move's restricted status
* @param {Pokemon} target {@linkcode Pokemon} the target of the move, optional and used when the target is a factor in the move's restricted status * @param target - {@linkcode Pokemon} the target of the move, optional and used when the target is a factor in the move's restricted status
* @returns {MoveRestrictionBattlerTag | null} the first tag on this Pokemon that restricts the move, or `null` if the move is not restricted. * @returns The first tag on this Pokemon that restricts the move, or `null` if the move is not restricted.
*/ */
getRestrictingTag( getRestrictingTag(
moveId: Moves, moveId: Moves,
@ -5281,20 +5106,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 = globalScene.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.formIndex = Math.max(
@ -5712,7 +5523,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 = globalScene.getCurrentPhase();
if (currentPhase instanceof MoveEffectPhase && currentPhase.getUserPokemon() === this) {
this.turnData.hitCount = 1;
this.turnData.hitsLeft = 1;
}
} }
if (asPhase) { if (asPhase) {

File diff suppressed because it is too large Load Diff

View File

@ -404,9 +404,10 @@ export class MovePhase extends BattlePhase {
* if the move fails. * if the move fails.
*/ */
if (success) { if (success) {
applyPreAttackAbAttrs(PokemonTypeChangeAbAttr, this.pokemon, null, this.move.getMove()); const move = this.move.getMove();
applyPreAttackAbAttrs(PokemonTypeChangeAbAttr, this.pokemon, null, move);
globalScene.unshiftPhase( globalScene.unshiftPhase(
new MoveEffectPhase(this.pokemon.getBattlerIndex(), this.targets, this.move, this.reflected), new MoveEffectPhase(this.pokemon.getBattlerIndex(), this.targets, move, this.reflected, this.move.virtual),
); );
} else { } else {
if ([Moves.ROAR, Moves.WHIRLWIND, Moves.TRICK_OR_TREAT, Moves.FORESTS_CURSE].includes(this.move.moveId)) { if ([Moves.ROAR, Moves.WHIRLWIND, Moves.TRICK_OR_TREAT, Moves.FORESTS_CURSE].includes(this.move.moveId)) {

View File

@ -50,7 +50,7 @@ describe("Moves - Dynamax Cannon", () => {
game.move.select(dynamaxCannon.id); game.move.select(dynamaxCannon.id);
await game.phaseInterceptor.to(MoveEffectPhase, false); await game.phaseInterceptor.to(MoveEffectPhase, false);
expect((game.scene.getCurrentPhase() as MoveEffectPhase).move.moveId).toBe(dynamaxCannon.id); expect((game.scene.getCurrentPhase() as MoveEffectPhase).move.id).toBe(dynamaxCannon.id);
await game.phaseInterceptor.to(DamageAnimPhase, false); await game.phaseInterceptor.to(DamageAnimPhase, false);
expect(dynamaxCannon.calculateBattlePower).toHaveLastReturnedWith(100); expect(dynamaxCannon.calculateBattlePower).toHaveLastReturnedWith(100);
}, 20000); }, 20000);
@ -62,7 +62,7 @@ describe("Moves - Dynamax Cannon", () => {
game.move.select(dynamaxCannon.id); game.move.select(dynamaxCannon.id);
await game.phaseInterceptor.to(MoveEffectPhase, false); await game.phaseInterceptor.to(MoveEffectPhase, false);
expect((game.scene.getCurrentPhase() as MoveEffectPhase).move.moveId).toBe(dynamaxCannon.id); expect((game.scene.getCurrentPhase() as MoveEffectPhase).move.id).toBe(dynamaxCannon.id);
await game.phaseInterceptor.to(DamageAnimPhase, false); await game.phaseInterceptor.to(DamageAnimPhase, false);
expect(dynamaxCannon.calculateBattlePower).toHaveLastReturnedWith(100); expect(dynamaxCannon.calculateBattlePower).toHaveLastReturnedWith(100);
}, 20000); }, 20000);
@ -75,7 +75,7 @@ describe("Moves - Dynamax Cannon", () => {
await game.phaseInterceptor.to(MoveEffectPhase, false); await game.phaseInterceptor.to(MoveEffectPhase, false);
const phase = game.scene.getCurrentPhase() as MoveEffectPhase; const phase = game.scene.getCurrentPhase() as MoveEffectPhase;
expect(phase.move.moveId).toBe(dynamaxCannon.id); expect(phase.move.id).toBe(dynamaxCannon.id);
// Force level cap to be 100 // Force level cap to be 100
vi.spyOn(game.scene, "getMaxExpLevel").mockReturnValue(100); vi.spyOn(game.scene, "getMaxExpLevel").mockReturnValue(100);
await game.phaseInterceptor.to(DamageAnimPhase, false); await game.phaseInterceptor.to(DamageAnimPhase, false);
@ -90,7 +90,7 @@ describe("Moves - Dynamax Cannon", () => {
await game.phaseInterceptor.to(MoveEffectPhase, false); await game.phaseInterceptor.to(MoveEffectPhase, false);
const phase = game.scene.getCurrentPhase() as MoveEffectPhase; const phase = game.scene.getCurrentPhase() as MoveEffectPhase;
expect(phase.move.moveId).toBe(dynamaxCannon.id); expect(phase.move.id).toBe(dynamaxCannon.id);
// Force level cap to be 100 // Force level cap to be 100
vi.spyOn(game.scene, "getMaxExpLevel").mockReturnValue(100); vi.spyOn(game.scene, "getMaxExpLevel").mockReturnValue(100);
await game.phaseInterceptor.to(DamageAnimPhase, false); await game.phaseInterceptor.to(DamageAnimPhase, false);
@ -105,7 +105,7 @@ describe("Moves - Dynamax Cannon", () => {
await game.phaseInterceptor.to(MoveEffectPhase, false); await game.phaseInterceptor.to(MoveEffectPhase, false);
const phase = game.scene.getCurrentPhase() as MoveEffectPhase; const phase = game.scene.getCurrentPhase() as MoveEffectPhase;
expect(phase.move.moveId).toBe(dynamaxCannon.id); expect(phase.move.id).toBe(dynamaxCannon.id);
// Force level cap to be 100 // Force level cap to be 100
vi.spyOn(game.scene, "getMaxExpLevel").mockReturnValue(100); vi.spyOn(game.scene, "getMaxExpLevel").mockReturnValue(100);
await game.phaseInterceptor.to(DamageAnimPhase, false); await game.phaseInterceptor.to(DamageAnimPhase, false);
@ -120,7 +120,7 @@ describe("Moves - Dynamax Cannon", () => {
await game.phaseInterceptor.to(MoveEffectPhase, false); await game.phaseInterceptor.to(MoveEffectPhase, false);
const phase = game.scene.getCurrentPhase() as MoveEffectPhase; const phase = game.scene.getCurrentPhase() as MoveEffectPhase;
expect(phase.move.moveId).toBe(dynamaxCannon.id); expect(phase.move.id).toBe(dynamaxCannon.id);
// Force level cap to be 100 // Force level cap to be 100
vi.spyOn(game.scene, "getMaxExpLevel").mockReturnValue(100); vi.spyOn(game.scene, "getMaxExpLevel").mockReturnValue(100);
await game.phaseInterceptor.to(DamageAnimPhase, false); await game.phaseInterceptor.to(DamageAnimPhase, false);
@ -135,7 +135,7 @@ describe("Moves - Dynamax Cannon", () => {
await game.phaseInterceptor.to(MoveEffectPhase, false); await game.phaseInterceptor.to(MoveEffectPhase, false);
const phase = game.scene.getCurrentPhase() as MoveEffectPhase; const phase = game.scene.getCurrentPhase() as MoveEffectPhase;
expect(phase.move.moveId).toBe(dynamaxCannon.id); expect(phase.move.id).toBe(dynamaxCannon.id);
// Force level cap to be 100 // Force level cap to be 100
vi.spyOn(game.scene, "getMaxExpLevel").mockReturnValue(100); vi.spyOn(game.scene, "getMaxExpLevel").mockReturnValue(100);
await game.phaseInterceptor.to(DamageAnimPhase, false); await game.phaseInterceptor.to(DamageAnimPhase, false);
@ -150,7 +150,7 @@ describe("Moves - Dynamax Cannon", () => {
await game.setTurnOrder([BattlerIndex.PLAYER, BattlerIndex.ENEMY]); await game.setTurnOrder([BattlerIndex.PLAYER, BattlerIndex.ENEMY]);
await game.phaseInterceptor.to(MoveEffectPhase, false); await game.phaseInterceptor.to(MoveEffectPhase, false);
expect((game.scene.getCurrentPhase() as MoveEffectPhase).move.moveId).toBe(dynamaxCannon.id); expect((game.scene.getCurrentPhase() as MoveEffectPhase).move.id).toBe(dynamaxCannon.id);
await game.phaseInterceptor.to(DamageAnimPhase, false); await game.phaseInterceptor.to(DamageAnimPhase, false);
expect(dynamaxCannon.calculateBattlePower).toHaveLastReturnedWith(200); expect(dynamaxCannon.calculateBattlePower).toHaveLastReturnedWith(200);
}, 20000); }, 20000);