mirror of
https://github.com/pagefaultgames/pokerogue.git
synced 2025-06-20 16:42:45 +02:00
Dancer Fixes
This commit is contained in:
parent
5efdb0dc0b
commit
c2ad1b8e89
@ -835,7 +835,7 @@ export default class BattleScene extends SceneBase {
|
||||
/**
|
||||
* Returns an array of Pokemon on both sides of the battle - player first, then enemy.
|
||||
* Does not actually check if the pokemon are on the field or not, and always has length 4 regardless of battle type.
|
||||
* @param activeOnly - Whether to consider only active pokemon; default `false`
|
||||
* @param activeOnly - Whether to consider only active pokemon (see {@linkcode Pokemon.isActive()} for more info); default `false`
|
||||
* @returns An array of {@linkcode Pokemon}, as described above.
|
||||
*/
|
||||
public getField(activeOnly = false): Pokemon[] {
|
||||
|
@ -11,7 +11,7 @@ import {
|
||||
coerceArray,
|
||||
} from "#app/utils/common";
|
||||
import { getPokemonNameWithAffix } from "#app/messages";
|
||||
import { GroundedTag } from "#app/data/battler-tags";
|
||||
import { GroundedTag, SemiInvulnerableTag } from "#app/data/battler-tags";
|
||||
import { BattlerTagLapseType } from "#enums/battler-tag-lapse-type";
|
||||
import {
|
||||
getNonVolatileStatusEffects,
|
||||
@ -63,7 +63,7 @@ import type { StatStageChangePhase } from "#app/phases/stat-stage-change-phase";
|
||||
import type { BattleStat, EffectiveStat } from "#enums/stat";
|
||||
import type { BerryType } from "#enums/berry-type";
|
||||
import type { EnemyPokemon } from "#app/field/pokemon";
|
||||
import type { PokemonMove } from "../moves/pokemon-move";
|
||||
import { PokemonMove } from "../moves/pokemon-move";
|
||||
import type Pokemon from "#app/field/pokemon";
|
||||
import type { Weather } from "#app/data/weather";
|
||||
import type { BattlerTag } from "#app/data/battler-tags";
|
||||
@ -78,7 +78,10 @@ import type {
|
||||
import type { BattlerIndex } from "#enums/battler-index";
|
||||
import type Move from "#app/data/moves/move";
|
||||
import type { ArenaTrapTag, SuppressAbilitiesTag } from "#app/data/arena-tag";
|
||||
import type { HitCheckEntry } from "#app/phases/move-effect-phase";
|
||||
import { HitCheckResult } from "#enums/hit-check-result";
|
||||
import type { Constructor } from "#app/utils/common";
|
||||
import { getMoveTargets } from "#app/data/moves/move-utils";
|
||||
import type { Localizable } from "#app/@types/locales";
|
||||
import { applyAbAttrs } from "./apply-ab-attrs";
|
||||
|
||||
@ -3285,11 +3288,11 @@ export class PostIntimidateStatStageChangeAbAttr extends AbAttr {
|
||||
private stages: number;
|
||||
private overwrites: boolean;
|
||||
|
||||
constructor(stats: BattleStat[], stages: number, overwrites?: boolean) {
|
||||
constructor(stats: BattleStat[], stages: number, overwrites = false) {
|
||||
super(true);
|
||||
this.stats = stats;
|
||||
this.stages = stages;
|
||||
this.overwrites = !!overwrites;
|
||||
this.overwrites = overwrites;
|
||||
}
|
||||
|
||||
override apply(
|
||||
@ -3384,11 +3387,17 @@ export class PostSummonRemoveArenaTagAbAttr extends PostSummonAbAttr {
|
||||
export class PostSummonAddArenaTagAbAttr extends PostSummonAbAttr {
|
||||
private readonly tagType: ArenaTagType;
|
||||
private readonly turnCount: number;
|
||||
private readonly side?: ArenaTagSide;
|
||||
private readonly quiet?: boolean;
|
||||
private readonly side: ArenaTagSide;
|
||||
private readonly quiet: boolean;
|
||||
private sourceId: number;
|
||||
|
||||
constructor(showAbility: boolean, tagType: ArenaTagType, turnCount: number, side?: ArenaTagSide, quiet?: boolean) {
|
||||
constructor(
|
||||
showAbility: boolean,
|
||||
tagType: ArenaTagType,
|
||||
turnCount: number,
|
||||
side: ArenaTagSide = ArenaTagSide.BOTH,
|
||||
quiet = false,
|
||||
) {
|
||||
super(showAbility);
|
||||
this.tagType = tagType;
|
||||
this.turnCount = turnCount;
|
||||
@ -3441,7 +3450,7 @@ export class PostSummonAddBattlerTagAbAttr extends PostSummonAbAttr {
|
||||
private tagType: BattlerTagType;
|
||||
private turnCount: number;
|
||||
|
||||
constructor(tagType: BattlerTagType, turnCount: number, showAbility?: boolean) {
|
||||
constructor(tagType: BattlerTagType, turnCount: number, showAbility = true) {
|
||||
super(showAbility);
|
||||
|
||||
this.tagType = tagType;
|
||||
@ -3490,13 +3499,13 @@ export class PostSummonStatStageChangeAbAttr extends PostSummonAbAttr {
|
||||
private selfTarget: boolean;
|
||||
private intimidate: boolean;
|
||||
|
||||
constructor(stats: BattleStat[], stages: number, selfTarget?: boolean, intimidate?: boolean) {
|
||||
constructor(stats: BattleStat[], stages: number, selfTarget = false, intimidate = false) {
|
||||
super(true);
|
||||
|
||||
this.stats = stats;
|
||||
this.stages = stages;
|
||||
this.selfTarget = !!selfTarget;
|
||||
this.intimidate = !!intimidate;
|
||||
this.selfTarget = selfTarget;
|
||||
this.intimidate = intimidate;
|
||||
}
|
||||
|
||||
override applyPostSummon(pokemon: Pokemon, _passive: boolean, simulated: boolean, _args: any[]): void {
|
||||
@ -3539,6 +3548,10 @@ export class PostSummonStatStageChangeAbAttr extends PostSummonAbAttr {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Attribute to heal the user's allies upon entering battle.
|
||||
* Used by {@linkcode AbilityId.HOSPITALITY}.
|
||||
*/
|
||||
export class PostSummonAllyHealAbAttr extends PostSummonAbAttr {
|
||||
private healRatio: number;
|
||||
private showAnim: boolean;
|
||||
@ -4851,7 +4864,7 @@ export class MultCritAbAttr extends AbAttr {
|
||||
export class ConditionalCritAbAttr extends AbAttr {
|
||||
private condition: PokemonAttackCondition;
|
||||
|
||||
constructor(condition: PokemonAttackCondition, _checkUser?: boolean) {
|
||||
constructor(condition: PokemonAttackCondition) {
|
||||
super(false);
|
||||
|
||||
this.condition = condition;
|
||||
@ -5040,10 +5053,10 @@ export class BlockWeatherDamageAttr extends PreWeatherDamageAbAttr {
|
||||
export class SuppressWeatherEffectAbAttr extends PreWeatherEffectAbAttr {
|
||||
public affectsImmutable: boolean;
|
||||
|
||||
constructor(affectsImmutable?: boolean) {
|
||||
constructor(affectsImmutable = false) {
|
||||
super(true);
|
||||
|
||||
this.affectsImmutable = !!affectsImmutable;
|
||||
this.affectsImmutable = affectsImmutable;
|
||||
}
|
||||
|
||||
override canApplyPreWeatherEffect(
|
||||
@ -5885,6 +5898,7 @@ export class PostTurnHurtIfSleepingAbAttr extends PostTurnAbAttr {
|
||||
!opp.switchOutStatus,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Deals damage to all sleeping opponents equal to 1/8 of their max hp (min 1)
|
||||
* @param pokemon {@linkcode Pokemon} with this ability
|
||||
@ -5993,106 +6007,152 @@ export class PostBiomeChangeTerrainChangeAbAttr extends PostBiomeChangeAbAttr {
|
||||
}
|
||||
|
||||
/**
|
||||
* Triggers just after a move is used either by the opponent or the player
|
||||
* @extends AbAttr
|
||||
* Attribute to trigger effects after a move is used by either side of the field.
|
||||
*/
|
||||
export class PostMoveUsedAbAttr extends AbAttr {
|
||||
/**
|
||||
* Check whether this ability can be applied after a move is used.
|
||||
* @param pokemon - The {@linkcode Pokemon} with the ability
|
||||
* @param move - The {@linkcode Move} having been been used
|
||||
* @param source - The {@linkcode Pokemon} who used the move
|
||||
* @param targets - Array of {@linkcode BattlerIndex}es containing Pokemon targeted by move.
|
||||
* @param hitChecks - Array of {@linkcode HitCheckEntry | HitCheckEntries} containing results of move usage
|
||||
* @param simulated - Whether the ability call is simulated
|
||||
* @param args - Extra arguments passed to the function. Handled by child classes.
|
||||
* @returns Whether the ability can be successfully applied.
|
||||
* By default, it requires:
|
||||
* * The pokemon with the ability is not semi-invulnerable
|
||||
* * The move was used by another pokemon
|
||||
* * The move's effect was successfully applied against at least 1 target
|
||||
*/
|
||||
canApplyPostMoveUsed(
|
||||
_pokemon: Pokemon,
|
||||
_move: PokemonMove,
|
||||
_source: Pokemon,
|
||||
_targets: BattlerIndex[],
|
||||
pokemon: Pokemon,
|
||||
_move: Move,
|
||||
source: Pokemon,
|
||||
targets: BattlerIndex[],
|
||||
hitChecks: HitCheckEntry[],
|
||||
_simulated: boolean,
|
||||
_args: any[],
|
||||
): boolean {
|
||||
return true;
|
||||
return (
|
||||
!pokemon.getTag(SemiInvulnerableTag) &&
|
||||
source.getBattlerIndex() !== pokemon.getBattlerIndex() &&
|
||||
targets.length > 0 &&
|
||||
hitChecks.some(hr => hr[0] === HitCheckResult.HIT)
|
||||
);
|
||||
}
|
||||
|
||||
applyPostMoveUsed(
|
||||
_pokemon: Pokemon,
|
||||
_move: PokemonMove,
|
||||
_move: Move,
|
||||
_source: Pokemon,
|
||||
_targets: BattlerIndex[],
|
||||
_hitChecks: HitCheckEntry[],
|
||||
_simulated: boolean,
|
||||
_args: any[],
|
||||
): void {}
|
||||
}
|
||||
|
||||
/**
|
||||
* Triggers after a dance move is used either by the opponent or the player
|
||||
* @extends PostMoveUsedAbAttr
|
||||
* Triggers after a dance move is used either by the opponent or the player.
|
||||
*/
|
||||
export class PostDancingMoveAbAttr extends PostMoveUsedAbAttr {
|
||||
override canApplyPostMoveUsed(
|
||||
/**
|
||||
* Check whether this ability can be applied after a move is used.
|
||||
* @param dancer - The {@linkcode Pokemon} with the ability
|
||||
* @param move - The {@linkcode Move} having been used
|
||||
* @param source - The {@linkcode Pokemon} who used the move
|
||||
* @param targets - Array of {@linkcode BattlerIndex}es containing Pokemon targeted by move.
|
||||
* @param hitResults - Array of {@linkcode HitCheckEntry | HitCheckEntries} containing results of move usage
|
||||
* @param simulated - N/A
|
||||
* @param args - N/A
|
||||
* @returns `true` if the ability can be applied, `false` otherwise
|
||||
* @see {@linkcode applyPostMoveUsed}
|
||||
*/
|
||||
canApplyPostMoveUsed(
|
||||
dancer: Pokemon,
|
||||
_move: PokemonMove,
|
||||
move: Move,
|
||||
source: Pokemon,
|
||||
_targets: BattlerIndex[],
|
||||
_simulated: boolean,
|
||||
_args: any[],
|
||||
targets: BattlerIndex[],
|
||||
hitResults: HitCheckEntry[],
|
||||
simulated: boolean,
|
||||
args: any[],
|
||||
): boolean {
|
||||
// List of tags that prevent the Dancer from replicating the move
|
||||
const forbiddenTags = [
|
||||
BattlerTagType.FLYING,
|
||||
BattlerTagType.UNDERWATER,
|
||||
BattlerTagType.UNDERGROUND,
|
||||
BattlerTagType.HIDDEN,
|
||||
];
|
||||
// The move to replicate cannot come from the Dancer
|
||||
return (
|
||||
source.getBattlerIndex() !== dancer.getBattlerIndex() &&
|
||||
!dancer.summonData.tags.some(tag => forbiddenTags.includes(tag.tagType))
|
||||
super.canApplyPostMoveUsed(dancer, move, source, targets, hitResults, simulated, args) &&
|
||||
move.hasFlag(MoveFlags.DANCE_MOVE)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolves the Dancer ability by replicating the move used by the source of the dance
|
||||
* either on the source itself or on the target of the dance
|
||||
* @param dancer {@linkcode Pokemon} with Dancer ability
|
||||
* @param move {@linkcode PokemonMove} Dancing move used by the source
|
||||
* @param source {@linkcode Pokemon} that used the dancing move
|
||||
* @param targets {@linkcode BattlerIndex}Targets of the dancing move
|
||||
* @param _args N/A
|
||||
* Resolves the {@linkcode AbilityId.DANCER | Dancer} ability by replicating other Pokemon's dance moves
|
||||
* on either this Pokemon or the user of the move.
|
||||
* @param dancer - The {@linkcode Pokemon} with Dancer
|
||||
* @param move - The {@linkcode Move} having been used
|
||||
* @param source - The {@linkcode Pokemon} who used the move
|
||||
* @param targets - Array of {@linkcode BattlerIndex}es containing Pokemon targeted by original move
|
||||
* @param _hitResults - N/A
|
||||
* @param _simulated - Whether the ability call is simulated
|
||||
* @param _args - N/A
|
||||
*/
|
||||
override applyPostMoveUsed(
|
||||
dancer: Pokemon,
|
||||
move: PokemonMove,
|
||||
move: Move,
|
||||
source: Pokemon,
|
||||
targets: BattlerIndex[],
|
||||
simulated: boolean,
|
||||
_hitResults: HitCheckEntry[],
|
||||
_simulated: boolean,
|
||||
_args: any[],
|
||||
): void {
|
||||
if (!simulated) {
|
||||
dancer.turnData.extraTurns++;
|
||||
// If the move is an AttackMove or a StatusMove the Dancer must replicate the move on the source of the Dance
|
||||
if (move.getMove().is("AttackMove") || move.getMove().is("StatusMove")) {
|
||||
const target = this.getTarget(dancer, source, targets);
|
||||
globalScene.phaseManager.unshiftNew("MovePhase", dancer, target, move, MoveUseMode.INDIRECT);
|
||||
} else if (move.getMove().is("SelfStatusMove")) {
|
||||
// If the move is a SelfStatusMove (ie. Swords Dance) the Dancer should replicate it on itself
|
||||
globalScene.phaseManager.unshiftNew(
|
||||
"MovePhase",
|
||||
dancer,
|
||||
[dancer.getBattlerIndex()],
|
||||
move,
|
||||
MoveUseMode.INDIRECT,
|
||||
);
|
||||
}
|
||||
}
|
||||
// increment extra turns var to ensure subsequent multi-hit moves don't bork
|
||||
dancer.turnData.extraTurns++;
|
||||
|
||||
globalScene.phaseManager.unshiftNew(
|
||||
"MovePhase",
|
||||
dancer,
|
||||
this.getMoveTargets(dancer, source, move, targets),
|
||||
new PokemonMove(move.id),
|
||||
MoveUseMode.INDIRECT,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the correct targets of Dancer ability
|
||||
* Get the correct targets of Dancer ability.
|
||||
*
|
||||
* @param dancer {@linkcode Pokemon} Pokemon with Dancer ability
|
||||
* @param source {@linkcode Pokemon} Source of the dancing move
|
||||
* @param targets {@linkcode BattlerIndex} Targets of the dancing move
|
||||
* @param dancer - The {@linkcode Pokemon} with Dancer
|
||||
* @param source - The {@linkcode Pokemon} that used the dancing move
|
||||
* @param move - The {@linkcode Move} being used
|
||||
* @param targets - An array of {@linkcode BattlerIndex}es containing original targets of copied move
|
||||
*/
|
||||
getTarget(dancer: Pokemon, source: Pokemon, targets: BattlerIndex[]): BattlerIndex[] {
|
||||
if (dancer.isPlayer()) {
|
||||
return source.isPlayer() ? targets : [source.getBattlerIndex()];
|
||||
private getMoveTargets(dancer: Pokemon, source: Pokemon, move: Move, targets: BattlerIndex[]): BattlerIndex[] {
|
||||
if (move.isMultiTarget()) {
|
||||
return getMoveTargets(dancer, move.id).targets;
|
||||
}
|
||||
return source.isPlayer() ? [source.getBattlerIndex()] : targets;
|
||||
|
||||
// Self-targeted status moves (Swords Dance & co.) are always replicated on the user.
|
||||
if (move.is("SelfStatusMove")) {
|
||||
return [dancer.getBattlerIndex()];
|
||||
}
|
||||
|
||||
// Attack moves are unleashed on the source of the dance UNLESS they are an ally attacking an enemy
|
||||
// (in which case we retain the prior move's targets)
|
||||
if (dancer.isPlayer() !== source.isPlayer() || targets.includes(dancer.getBattlerIndex())) {
|
||||
targets = [source.getBattlerIndex()];
|
||||
}
|
||||
|
||||
// Attempt to redirect to the prior target's partner if fainted and not our own ally.
|
||||
const firstTarget = globalScene.getField()[targets[0]];
|
||||
const ally = firstTarget.getAlly();
|
||||
if (
|
||||
globalScene.currentBattle.double &&
|
||||
firstTarget.isFainted() &&
|
||||
firstTarget.isPlayer() !== dancer.isPlayer() &&
|
||||
ally?.isActive()
|
||||
) {
|
||||
return [ally.getBattlerIndex()];
|
||||
}
|
||||
|
||||
return targets;
|
||||
}
|
||||
}
|
||||
|
||||
@ -6194,7 +6254,7 @@ export class BypassBurnDamageReductionAbAttr extends AbAttr {
|
||||
|
||||
/**
|
||||
* Causes Pokemon to take reduced damage from the {@linkcode StatusEffect.BURN | Burn} status
|
||||
* @param multiplier Multiplied with the damage taken
|
||||
* @param multiplier - Multiplier for burn damage taken
|
||||
*/
|
||||
export class ReduceBurnDamageAbAttr extends AbAttr {
|
||||
constructor(protected multiplier: number) {
|
||||
@ -6202,7 +6262,7 @@ export class ReduceBurnDamageAbAttr extends AbAttr {
|
||||
}
|
||||
|
||||
/**
|
||||
* Applies the damage reduction
|
||||
* Applies the burn damage reduction
|
||||
* @param _pokemon N/A
|
||||
* @param _passive N/A
|
||||
* @param _cancelled N/A
|
||||
@ -6651,6 +6711,7 @@ export class RedirectTypeMoveAbAttr extends RedirectMoveAbAttr {
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: Rework this - currently it's just an empty class used as a marker
|
||||
export class BlockRedirectAbAttr extends AbAttr {}
|
||||
|
||||
/**
|
||||
@ -6658,6 +6719,7 @@ export class BlockRedirectAbAttr extends AbAttr {}
|
||||
* @param statusEffect - The {@linkcode StatusEffect} to check for
|
||||
* @see {@linkcode apply}
|
||||
*/
|
||||
// TODO: Make this take no args and affect the status (once public accessors are added)
|
||||
export class ReduceStatusEffectDurationAbAttr extends AbAttr {
|
||||
private statusEffect: StatusEffect;
|
||||
|
||||
@ -6724,6 +6786,7 @@ export class FlinchStatStageChangeAbAttr extends FlinchEffectAbAttr {
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: Move PP increasing property from `move-phase` into the ability class
|
||||
export class IncreasePpAbAttr extends AbAttr {}
|
||||
|
||||
export class ForceSwitchOutImmunityAbAttr extends AbAttr {
|
||||
@ -8665,8 +8728,8 @@ export function initAbilities() {
|
||||
.attr(PostDancingMoveAbAttr)
|
||||
/* Incorrect interations with:
|
||||
* Petal Dance (should not lock in or count down timer; currently does both)
|
||||
* Flinches (due to tag being removed earlier)
|
||||
* Failed/protected moves (should not trigger if original move is protected against)
|
||||
* All status moves whose `apply` function unshifts a phase with eligibility checks
|
||||
* (this includes stat stage moves as well as Teeter Dance and co.) due to moves being still considered "successful"
|
||||
*/
|
||||
.edgeCase(),
|
||||
new Ability(AbilityId.BATTERY, 7)
|
||||
|
@ -1,6 +1,7 @@
|
||||
import type { AbAttrApplyFunc, AbAttrMap, AbAttrString, AbAttrSuccessFunc } from "#app/@types/ability-types";
|
||||
import type Pokemon from "#app/field/pokemon";
|
||||
import { globalScene } from "#app/global-scene";
|
||||
import type { HitCheckEntry } from "#app/phases/move-effect-phase";
|
||||
import type { BooleanHolder, NumberHolder } from "#app/utils/common";
|
||||
import type { BattlerIndex } from "#enums/battler-index";
|
||||
import type { HitResult } from "#enums/hit-result";
|
||||
@ -9,7 +10,6 @@ import type { StatusEffect } from "#enums/status-effect";
|
||||
import type { WeatherType } from "#enums/weather-type";
|
||||
import type { BattlerTag } from "../battler-tags";
|
||||
import type Move from "../moves/move";
|
||||
import type { PokemonMove } from "../moves/pokemon-move";
|
||||
import type { TerrainType } from "../terrain";
|
||||
import type { Weather } from "../weather";
|
||||
import type {
|
||||
@ -79,8 +79,6 @@ function applySingleAbAttrs<T extends AbAttrString>(
|
||||
continue;
|
||||
}
|
||||
|
||||
globalScene.phaseManager.setPhaseQueueSplice();
|
||||
|
||||
if (attr.showAbility && !simulated) {
|
||||
globalScene.phaseManager.queueAbilityDisplay(pokemon, passive, true);
|
||||
abShown = true;
|
||||
@ -120,7 +118,6 @@ function applyAbAttrsInternal<T extends AbAttrString>(
|
||||
for (const passive of [false, true]) {
|
||||
if (pokemon) {
|
||||
applySingleAbAttrs(pokemon, passive, attrType, applyFunc, successFunc, args, gainedMidTurn, simulated, messages);
|
||||
globalScene.phaseManager.clearPhaseQueueSplice();
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -206,18 +203,20 @@ export function applyPostDefendAbAttrs<K extends AbAttrString>(
|
||||
export function applyPostMoveUsedAbAttrs<K extends AbAttrString>(
|
||||
attrType: AbAttrMap[K] extends PostMoveUsedAbAttr ? K : never,
|
||||
pokemon: Pokemon,
|
||||
move: PokemonMove,
|
||||
move: Move,
|
||||
source: Pokemon,
|
||||
targets: BattlerIndex[],
|
||||
hitChecks: HitCheckEntry[],
|
||||
simulated = false,
|
||||
...args: any[]
|
||||
): void {
|
||||
applyAbAttrsInternal(
|
||||
attrType,
|
||||
pokemon,
|
||||
(attr, _passive) => (attr as PostMoveUsedAbAttr).applyPostMoveUsed(pokemon, move, source, targets, simulated, args),
|
||||
(attr, _passive) =>
|
||||
(attr as PostMoveUsedAbAttr).canApplyPostMoveUsed(pokemon, move, source, targets, simulated, args),
|
||||
(attr as PostMoveUsedAbAttr).applyPostMoveUsed(pokemon, move, source, targets, hitChecks, simulated, args),
|
||||
(attr, _passive) =>
|
||||
(attr as PostMoveUsedAbAttr).canApplyPostMoveUsed(pokemon, move, source, targets, hitChecks, simulated, args),
|
||||
args,
|
||||
simulated,
|
||||
);
|
||||
|
@ -1387,11 +1387,11 @@ export class EncounterBattleAnim extends BattleAnim {
|
||||
public encounterAnim: EncounterAnim;
|
||||
public oppAnim: boolean;
|
||||
|
||||
constructor(encounterAnim: EncounterAnim, user: Pokemon, target?: Pokemon, oppAnim?: boolean) {
|
||||
super(user, target ?? user, true);
|
||||
constructor(encounterAnim: EncounterAnim, user: Pokemon, target: Pokemon = user, oppAnim = false) {
|
||||
super(user, target, true);
|
||||
|
||||
this.encounterAnim = encounterAnim;
|
||||
this.oppAnim = oppAnim ?? false;
|
||||
this.oppAnim = oppAnim;
|
||||
}
|
||||
|
||||
getAnim(): AnimConfig | null {
|
||||
|
@ -34,6 +34,7 @@ import { isNullOrUndefined } from "#app/utils/common";
|
||||
import { MoveUseMode } from "#enums/move-use-mode";
|
||||
import { invalidEncoreMoves } from "./moves/invalid-moves";
|
||||
import { BattlerTagLapseType } from "#enums/battler-tag-lapse-type";
|
||||
import type { TurnMove } from "#app/field/pokemon";
|
||||
|
||||
/**
|
||||
* A {@linkcode BattlerTag} represents a semi-persistent effect that can be attached to a {@linkcode Pokemon}.
|
||||
@ -153,21 +154,21 @@ export abstract class MoveRestrictionBattlerTag extends BattlerTag {
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets whether this tag is restricting a move.
|
||||
* Check if this tag is currently restricting a move's use.
|
||||
*
|
||||
* @param move - {@linkcode MoveId} ID to check restriction for.
|
||||
* @param user - The {@linkcode Pokemon} involved
|
||||
* @returns `true` if the move is restricted by this tag, otherwise `false`.
|
||||
* @param move - The {@linkcode MoveId} whose usability is being checked.
|
||||
* @param user - The {@linkcode Pokemon} using the move.
|
||||
* @returns Whether the given move is restricted by this tag.
|
||||
*/
|
||||
public abstract isMoveRestricted(move: MoveId, user?: Pokemon): boolean;
|
||||
|
||||
/**
|
||||
* Checks if this tag is restricting a move based on a user's decisions during the target selection phase
|
||||
*
|
||||
* @param {MoveId} _move {@linkcode MoveId} move ID to check restriction for
|
||||
* @param {Pokemon} _user {@linkcode Pokemon} the user of the above move
|
||||
* @param {Pokemon} _target {@linkcode Pokemon} the target of the above move
|
||||
* @returns {boolean} `false` unless overridden by the child tag
|
||||
* Check if this tag is restricting a move during target selection.
|
||||
* Returns `false` by default unless overridden by a child class.
|
||||
* @param _move - The {@linkcode MoveId} whose selectability is being checked.
|
||||
* @param _user - The {@linkcode Pokemon} using the move.
|
||||
* @param _target - The {@linkcode Pokemon} being targeted
|
||||
* @returns Whether the given move should be unselectable when choosing targets.
|
||||
*/
|
||||
isMoveTargetRestricted(_move: MoveId, _user: Pokemon, _target: Pokemon): boolean {
|
||||
return false;
|
||||
@ -315,9 +316,10 @@ export class DisabledTag extends MoveRestrictionBattlerTag {
|
||||
|
||||
/**
|
||||
* @override
|
||||
* @param {Pokemon} pokemon {@linkcode Pokemon} attempting to use the restricted move
|
||||
* @param {MoveId} move {@linkcode MoveId} ID of the move being interrupted
|
||||
* @returns {string} text to display when the move is interrupted
|
||||
* Display the text that occurs when a move is interrupted via Disable.
|
||||
* @param pokemon - The {@linkcode Pokemon} attempting to use the restricted move.
|
||||
* @param move - The {@linkcode MoveId} of the move being interrupted.
|
||||
* @returns The text to display when the given move is interrupted.
|
||||
*/
|
||||
override interruptedText(pokemon: Pokemon, move: MoveId): string {
|
||||
return i18next.t("battle:disableInterruptedMove", {
|
||||
@ -1876,9 +1878,9 @@ export class TruantTag extends AbilityBattlerTag {
|
||||
return super.lapse(pokemon, lapseType);
|
||||
}
|
||||
|
||||
const lastMove = pokemon.getLastXMoves()[0];
|
||||
const lastMove: TurnMove | undefined = pokemon.getLastXMoves()[0];
|
||||
|
||||
if (!lastMove) {
|
||||
if (!lastMove?.move) {
|
||||
// Don't interrupt move if last move was `Moves.NONE` OR no prior move was found
|
||||
return true;
|
||||
}
|
||||
@ -3419,7 +3421,8 @@ export class PsychoShiftTag extends BattlerTag {
|
||||
}
|
||||
|
||||
/**
|
||||
* Tag associated with the move Magic Coat.
|
||||
* Tag associated with {@linkcode Moves.MAGIC_COAT | Magic Coat} that reflects certain status moves directed at the user.
|
||||
* TODO: Move Reflection code out of `move-effect-phase` and into here
|
||||
*/
|
||||
export class MagicCoatTag extends BattlerTag {
|
||||
constructor() {
|
||||
|
@ -177,9 +177,9 @@ export default abstract class Move implements Localizable {
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a move has an attribute that matches `attrType`
|
||||
* @param attrType any attribute that extends {@linkcode MoveAttr}
|
||||
* @returns true if the move has attribute `attrType`
|
||||
* Check if a move has an attribute that matches `attrType`.
|
||||
* @param attrType - The {@linkcode MoveAttrString} for the attribute to check.
|
||||
* @returns Whether the move has an attribute that is or extends `attrType`.
|
||||
*/
|
||||
hasAttr(attrType: MoveAttrString): boolean {
|
||||
const targetAttr = MoveAttrs[attrType];
|
||||
@ -631,7 +631,9 @@ export default abstract class Move implements Localizable {
|
||||
target?: Pokemon;
|
||||
isFollowUp?: boolean;
|
||||
}): boolean {
|
||||
// special cases below, eg: if the move flag is MAKES_CONTACT, and the user pokemon has an ability that ignores contact (like "Long Reach"), then overrides and move does not make contact
|
||||
// Handle special cases
|
||||
|
||||
// Abilities that ignores contact (Long Reach) and substitute blockages
|
||||
switch (flag) {
|
||||
case MoveFlags.MAKES_CONTACT:
|
||||
if (user.hasAbilityWithAttr("IgnoreContactAbAttr") || this.hitsSubstitute(user, target)) {
|
||||
@ -639,15 +641,17 @@ export default abstract class Move implements Localizable {
|
||||
}
|
||||
break;
|
||||
case MoveFlags.IGNORE_ABILITIES:
|
||||
// Check for ability based blockages
|
||||
if (user.hasAbilityWithAttr("MoveAbilityBypassAbAttr")) {
|
||||
const abilityEffectsIgnored = new BooleanHolder(false);
|
||||
applyAbAttrs("MoveAbilityBypassAbAttr", user, abilityEffectsIgnored, false, this);
|
||||
if (abilityEffectsIgnored.value) {
|
||||
return true;
|
||||
}
|
||||
// Sunsteel strike, Moongeist beam, and photon geyser will not ignore abilities if invoked
|
||||
// by another move, such as via metronome.
|
||||
}
|
||||
|
||||
// Sunsteel strike, Moongeist beam, and photon geyser will not ignore abilities if invoked
|
||||
// by another move, such as metronome/dancer.
|
||||
return this.hasFlag(MoveFlags.IGNORE_ABILITIES) && !isFollowUp;
|
||||
case MoveFlags.IGNORE_PROTECT:
|
||||
if (user.hasAbilityWithAttr("IgnoreProtectOnContactAbAttr")
|
||||
@ -2449,6 +2453,7 @@ export class MultiHitAttr extends MoveAttr {
|
||||
break;
|
||||
case MultiHitType.BEAT_UP:
|
||||
// Estimate that half of the party can contribute to beat up.
|
||||
// TODO: The AI should be able to check this manually?
|
||||
expectedHits = Math.max(1, partySize / 2);
|
||||
break;
|
||||
}
|
||||
|
@ -147,3 +147,22 @@ export function isIgnorePP(useMode: MoveUseMode): boolean {
|
||||
export function isReflected(useMode: MoveUseMode): boolean {
|
||||
return useMode === MoveUseMode.REFLECTED;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a given {@linkcode MoveUseMode} is capable of being copied by {@linkcode PostDancingMoveAbAttr | Dancer}.
|
||||
* @param useMode - The {@linkcode MoveUseMode} to check.
|
||||
* @returns Whether {@linkcode useMode} is dancer copiable.
|
||||
* @remarks
|
||||
* This function is equivalent to the following truth table:
|
||||
*
|
||||
* | Use Type | Returns |
|
||||
* |------------------------------------|---------|
|
||||
* | {@linkcode MoveUseMode.NORMAL} | `true` |
|
||||
* | {@linkcode MoveUseMode.IGNORE_PP} | `true` |
|
||||
* | {@linkcode MoveUseMode.INDIRECT} | `false` |
|
||||
* | {@linkcode MoveUseMode.FOLLOW_UP} | `true` |
|
||||
* | {@linkcode MoveUseMode.REFLECTED} | `false` |
|
||||
*/
|
||||
export function isDancerCopiable(useMode: MoveUseMode): boolean {
|
||||
return !([MoveUseMode.INDIRECT, MoveUseMode.REFLECTED] as MoveUseMode[]).includes(useMode)
|
||||
}
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { type PokeballCounts } from "#app/battle-scene";
|
||||
import type { PokeballCounts } from "#app/battle-scene";
|
||||
import { EvolutionItem } from "#app/data/balance/pokemon-evolutions";
|
||||
import { Gender } from "#app/data/gender";
|
||||
import { FormChangeItem } from "#enums/form-change-item";
|
||||
@ -6,21 +6,21 @@ import { type ModifierOverride } from "#app/modifier/modifier-type";
|
||||
import { Variant } from "#app/sprites/variant";
|
||||
import { Unlockables } from "#enums/unlockables";
|
||||
import { AbilityId } from "#enums/ability-id";
|
||||
import { BattleType } from "#enums/battle-type";
|
||||
import type { BattleType } from "#enums/battle-type";
|
||||
import { BerryType } from "#enums/berry-type";
|
||||
import { BiomeId } from "#enums/biome-id";
|
||||
import { EggTier } from "#enums/egg-type";
|
||||
import { MoveId } from "#enums/move-id";
|
||||
import { MysteryEncounterTier } from "#enums/mystery-encounter-tier";
|
||||
import { MysteryEncounterType } from "#enums/mystery-encounter-type";
|
||||
import type { BiomeId } from "#enums/biome-id";
|
||||
import type { EggTier } from "#enums/egg-type";
|
||||
import type { MoveId } from "#enums/move-id";
|
||||
import type { MysteryEncounterTier } from "#enums/mystery-encounter-tier";
|
||||
import type { MysteryEncounterType } from "#enums/mystery-encounter-type";
|
||||
import { PokeballType } from "#enums/pokeball";
|
||||
import { PokemonType } from "#enums/pokemon-type";
|
||||
import { SpeciesId } from "#enums/species-id";
|
||||
import type { SpeciesId } from "#enums/species-id";
|
||||
import { Stat } from "#enums/stat";
|
||||
import { StatusEffect } from "#enums/status-effect";
|
||||
import { TimeOfDay } from "#enums/time-of-day";
|
||||
import { TrainerType } from "#enums/trainer-type";
|
||||
import { VariantTier } from "#enums/variant-tier";
|
||||
import type { TimeOfDay } from "#enums/time-of-day";
|
||||
import type { TrainerType } from "#enums/trainer-type";
|
||||
import type { VariantTier } from "#enums/variant-tier";
|
||||
import { WeatherType } from "#enums/weather-type";
|
||||
|
||||
/**
|
||||
@ -150,6 +150,10 @@ class DefaultOverrides {
|
||||
readonly ABILITY_OVERRIDE: AbilityId = AbilityId.NONE;
|
||||
readonly PASSIVE_ABILITY_OVERRIDE: AbilityId = AbilityId.NONE;
|
||||
readonly HAS_PASSIVE_ABILITY_OVERRIDE: boolean | null = null;
|
||||
/**
|
||||
* If set, will be added to each newly caught/obtained player Pokemon.
|
||||
* @remarks If this is set to {@linkcode StatusEffect.SLEEP}, it will always have a constant duration of 4 turns.
|
||||
*/
|
||||
readonly STATUS_OVERRIDE: StatusEffect = StatusEffect.NONE;
|
||||
readonly GENDER_OVERRIDE: Gender | null = null;
|
||||
readonly MOVESET_OVERRIDE: MoveId | Array<MoveId> = [];
|
||||
|
@ -99,6 +99,7 @@ import { UnavailablePhase } from "#app/phases/unavailable-phase";
|
||||
import { UnlockPhase } from "#app/phases/unlock-phase";
|
||||
import { VictoryPhase } from "#app/phases/victory-phase";
|
||||
import { WeatherEffectPhase } from "#app/phases/weather-effect-phase";
|
||||
import { DancerPhase } from "#app/phases/dancer-phase";
|
||||
|
||||
/**
|
||||
* Manager for phases used by battle scene.
|
||||
@ -126,6 +127,7 @@ const PHASES = Object.freeze({
|
||||
CommandPhase,
|
||||
CommonAnimPhase,
|
||||
DamageAnimPhase,
|
||||
DancerPhase,
|
||||
EggHatchPhase,
|
||||
EggLapsePhase,
|
||||
EggSummaryPhase,
|
||||
@ -567,7 +569,6 @@ export class PhaseManager {
|
||||
*/
|
||||
public queueAbilityDisplay(pokemon: Pokemon, passive: boolean, show: boolean): void {
|
||||
this.unshiftPhase(show ? new ShowAbilityPhase(pokemon.getBattlerIndex(), passive) : new HideAbilityPhase());
|
||||
this.clearPhaseQueueSplice();
|
||||
}
|
||||
|
||||
/**
|
||||
|
36
src/phases/dancer-phase.ts
Normal file
36
src/phases/dancer-phase.ts
Normal file
@ -0,0 +1,36 @@
|
||||
import type { BattlerIndex } from "#enums/battler-index";
|
||||
import { applyPostMoveUsedAbAttrs } from "#app/data/abilities/apply-ab-attrs";
|
||||
import type Move from "#app/data/moves/move";
|
||||
import { globalScene } from "#app/global-scene";
|
||||
import type { HitCheckEntry } from "#app/phases/move-effect-phase";
|
||||
import { PokemonPhase } from "#app/phases/pokemon-phase";
|
||||
|
||||
/** The phase where all on-field Pokemon trigger Dancer and Dancer-like effects. */
|
||||
export class DancerPhase extends PokemonPhase {
|
||||
public override readonly phaseName: "DancerPhase";
|
||||
|
||||
constructor(
|
||||
battlerIndex: BattlerIndex,
|
||||
private targets: BattlerIndex[],
|
||||
private move: Move,
|
||||
private hitChecks: HitCheckEntry[],
|
||||
) {
|
||||
super(battlerIndex);
|
||||
}
|
||||
|
||||
// TODO: Make iteration occur in speed order
|
||||
override start(): void {
|
||||
super.start();
|
||||
for (const pokemon of globalScene.getField(true)) {
|
||||
applyPostMoveUsedAbAttrs(
|
||||
"PostMoveUsedAbAttr",
|
||||
pokemon,
|
||||
this.move,
|
||||
this.getPokemon(),
|
||||
this.targets,
|
||||
this.hitChecks,
|
||||
);
|
||||
}
|
||||
super.end();
|
||||
}
|
||||
}
|
@ -54,10 +54,9 @@ import { HitCheckResult } from "#enums/hit-check-result";
|
||||
import type Move from "#app/data/moves/move";
|
||||
import { isFieldTargeted } from "#app/data/moves/move-utils";
|
||||
import { DamageAchv } from "#app/system/achv";
|
||||
import { isVirtual, isReflected, MoveUseMode } from "#enums/move-use-mode";
|
||||
import { isVirtual, isReflected, MoveUseMode, isDancerCopiable } from "#enums/move-use-mode";
|
||||
|
||||
export type HitCheckEntry = [HitCheckResult, TypeDamageMultiplier];
|
||||
|
||||
export class MoveEffectPhase extends PokemonPhase {
|
||||
public readonly phaseName = "MoveEffectPhase";
|
||||
public move: Move;
|
||||
@ -112,11 +111,11 @@ export class MoveEffectPhase extends PokemonPhase {
|
||||
* Compute targets and the results of hit checks of the invoked move against all targets,
|
||||
* organized by battler index.
|
||||
*
|
||||
* **This is *not* a pure function**; it has the following side effects
|
||||
* - `this.hitChecks` - The results of the hit checks against each target
|
||||
* - `this.moveHistoryEntry` - Sets success or failure based on the hit check results
|
||||
* - user.turnData.hitCount and user.turnData.hitsLeft - Both set to 1 if the
|
||||
* move was unsuccessful against all targets
|
||||
* **This is *not* a pure function** and has the following side effects:
|
||||
* - Sets `this.hitChecks` to the results of the hit checks against each target
|
||||
* - Sets success/failure of `this.moveHistoryEntry` based on the hit check results
|
||||
* - Sets `user.turnData.hitCount` and `user.turnData.hitsLeft` to 1 if the move
|
||||
* was unsuccessful against all targets (effectively canceling it)
|
||||
*
|
||||
* @returns The targets of the invoked move
|
||||
* @see {@linkcode hitCheck}
|
||||
@ -194,9 +193,9 @@ export class MoveEffectPhase extends PokemonPhase {
|
||||
}
|
||||
|
||||
/**
|
||||
* Apply the move to each of the resolved targets.
|
||||
* Apply the move to each of its resolved targets.
|
||||
* @param targets - The resolved set of targets of the move
|
||||
* @throws Error if there was an unexpected hit check result
|
||||
* @throws - Error if there was an unexpected hit check result
|
||||
*/
|
||||
private applyToTargets(user: Pokemon, targets: Pokemon[]): void {
|
||||
let firstHit = true;
|
||||
@ -368,6 +367,7 @@ export class MoveEffectPhase extends PokemonPhase {
|
||||
*/
|
||||
private postAnimCallback(user: Pokemon, targets: Pokemon[]) {
|
||||
// Add to the move history entry
|
||||
// TODO: Once Truant is fixed to not check history, don't push an entry for reflected/indirect moves
|
||||
if (this.firstHit) {
|
||||
user.pushMoveHistory(this.moveHistoryEntry);
|
||||
applyExecutedMoveAbAttrs("ExecutedMoveAbAttr", user);
|
||||
@ -428,6 +428,19 @@ export class MoveEffectPhase extends PokemonPhase {
|
||||
this.getTargets().forEach(target => {
|
||||
target.turnData.moveEffectiveness = null;
|
||||
});
|
||||
|
||||
// Dancer does not proc on other dancer moves, nor for either occurrence of a reflected move.
|
||||
// (This blocks copying on the follow-up reflected use; the initial use gets blocked by hit checks)
|
||||
if (isDancerCopiable(this.useMode)) {
|
||||
globalScene.phaseManager.appendNewToPhase(
|
||||
"MoveEndPhase",
|
||||
"DancerPhase",
|
||||
this.battlerIndex,
|
||||
this.targets,
|
||||
this.move,
|
||||
this.hitChecks,
|
||||
);
|
||||
}
|
||||
super.end();
|
||||
}
|
||||
|
||||
@ -759,7 +772,7 @@ export class MoveEffectPhase extends PokemonPhase {
|
||||
user: Pokemon,
|
||||
target: Pokemon | null,
|
||||
firstTarget?: boolean | null,
|
||||
selfTarget?: boolean,
|
||||
selfTarget = false,
|
||||
): void {
|
||||
applyFilteredMoveAttrs(
|
||||
(attr: MoveAttr) =>
|
||||
|
@ -1,6 +1,6 @@
|
||||
import { BattlerIndex } from "#enums/battler-index";
|
||||
import { globalScene } from "#app/global-scene";
|
||||
import { applyAbAttrs, applyPostMoveUsedAbAttrs, applyPreAttackAbAttrs } from "#app/data/abilities/apply-ab-attrs";
|
||||
import { applyAbAttrs, applyPreAttackAbAttrs } from "#app/data/abilities/apply-ab-attrs";
|
||||
import type { DelayedAttackTag } from "#app/data/arena-tag";
|
||||
import { CommonAnim } from "#enums/move-anims-common";
|
||||
import { CenterOfAttentionTag } from "#app/data/battler-tags";
|
||||
@ -431,16 +431,6 @@ export class MovePhase extends BattlePhase {
|
||||
// Remove the user from its semi-invulnerable state (if applicable)
|
||||
this.pokemon.lapseTags(BattlerTagLapseType.MOVE_EFFECT);
|
||||
}
|
||||
|
||||
// Handle Dancer, which triggers immediately after a move is used (rather than waiting on `this.end()`).
|
||||
// Note the MoveUseMode check here prevents an infinite Dancer loop.
|
||||
const dancerModes: MoveUseMode[] = [MoveUseMode.INDIRECT, MoveUseMode.REFLECTED] as const;
|
||||
if (this.move.getMove().hasFlag(MoveFlags.DANCE_MOVE) && !dancerModes.includes(this.useMode)) {
|
||||
// TODO: Fix in dancer PR to move to MEP for hit checks
|
||||
globalScene.getField(true).forEach(pokemon => {
|
||||
applyPostMoveUsedAbAttrs("PostMoveUsedAbAttr", pokemon, this.move, this.pokemon, this.targets);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/** Queues a {@linkcode MoveChargePhase} for this phase's invoked move. */
|
||||
|
@ -18,16 +18,16 @@ export class TurnStartPhase extends FieldPhase {
|
||||
/**
|
||||
* This orders the active Pokemon on the field by speed into an BattlerIndex array and returns that array.
|
||||
* It also checks for Trick Room and reverses the array if it is present.
|
||||
* @returns {@linkcode BattlerIndex[]} the battle indices of all pokemon on the field ordered by speed
|
||||
* @returns An array of {@linkcode BattlerIndex}es containing all on-field pokemon sorted in speed order.
|
||||
*/
|
||||
getSpeedOrder(): BattlerIndex[] {
|
||||
const playerField = globalScene.getPlayerField().filter(p => p.isActive()) as Pokemon[];
|
||||
const enemyField = globalScene.getEnemyField().filter(p => p.isActive()) as Pokemon[];
|
||||
|
||||
// We shuffle the list before sorting so speed ties produce random results
|
||||
// Shuffle the list before sorting so speed ties produce random results
|
||||
// This is seeded with the current turn to prevent an inconsistency with variable turn order
|
||||
// based on how long since you last reloaded
|
||||
let orderedTargets: Pokemon[] = playerField.concat(enemyField);
|
||||
// We seed it with the current turn to prevent an inconsistency where it
|
||||
// was varying based on how long since you last reloaded
|
||||
globalScene.executeWithSeedOffset(
|
||||
() => {
|
||||
orderedTargets = randSeedShuffle(orderedTargets);
|
||||
@ -36,11 +36,11 @@ export class TurnStartPhase extends FieldPhase {
|
||||
globalScene.waveSeed,
|
||||
);
|
||||
|
||||
// Next, a check for Trick Room is applied to determine sort order.
|
||||
// Check for Trick Room and reverse sort order if active.
|
||||
// Notably, Pokerogue does NOT have the "outspeed trick room" glitch at >1809 spd.
|
||||
const speedReversed = new BooleanHolder(false);
|
||||
globalScene.arena.applyTags(TrickRoomTag, false, speedReversed);
|
||||
|
||||
// Adjust the sort function based on whether Trick Room is active.
|
||||
orderedTargets.sort((a: Pokemon, b: Pokemon) => {
|
||||
const aSpeed = a?.getEffectiveStat(Stat.SPD) ?? 0;
|
||||
const bSpeed = b?.getEffectiveStat(Stat.SPD) ?? 0;
|
||||
@ -111,7 +111,8 @@ export class TurnStartPhase extends FieldPhase {
|
||||
}
|
||||
}
|
||||
|
||||
// If there is no difference between the move's calculated priorities, the game checks for differences in battlerBypassSpeed and returns the result.
|
||||
// If there is no difference between the move's calculated priorities,
|
||||
// check for differences in battlerBypassSpeed and returns the result.
|
||||
if (battlerBypassSpeed[a].value !== battlerBypassSpeed[b].value) {
|
||||
return battlerBypassSpeed[a].value ? -1 : 1;
|
||||
}
|
||||
|
@ -1,13 +1,20 @@
|
||||
import { BattlerIndex } from "#enums/battler-index";
|
||||
import type Pokemon from "#app/field/pokemon";
|
||||
import { MoveResult } from "#enums/move-result";
|
||||
import type { MovePhase } from "#app/phases/move-phase";
|
||||
import { MovePhase } from "#app/phases/move-phase";
|
||||
import { AbilityId } from "#enums/ability-id";
|
||||
import { BattlerTagType } from "#enums/battler-tag-type";
|
||||
import { MoveUseMode } from "#enums/move-use-mode";
|
||||
import { MoveId } from "#enums/move-id";
|
||||
import { SpeciesId } from "#enums/species-id";
|
||||
import { Stat } from "#enums/stat";
|
||||
import { StatusEffect } from "#enums/status-effect";
|
||||
import GameManager from "#test/testUtils/gameManager";
|
||||
import Phaser from "phaser";
|
||||
import { afterEach, beforeAll, beforeEach, describe, expect, it } from "vitest";
|
||||
import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import { toDmgValue } from "#app/utils/common";
|
||||
import { allMoves, allAbilities } from "#app/data/data-lists";
|
||||
import { ShowAbilityPhase } from "#app/phases/show-ability-phase";
|
||||
|
||||
describe("Abilities - Dancer", () => {
|
||||
let phaserGame: Phaser.Game;
|
||||
@ -25,119 +32,517 @@ describe("Abilities - Dancer", () => {
|
||||
|
||||
beforeEach(() => {
|
||||
game = new GameManager(phaserGame);
|
||||
game.override.battleStyle("double").enemyAbility(AbilityId.BALL_FETCH);
|
||||
game.override
|
||||
.battleStyle("single")
|
||||
.disableCrits()
|
||||
.ability(AbilityId.DANCER)
|
||||
.enemySpecies(SpeciesId.SHUCKLE)
|
||||
.enemyAbility(AbilityId.BALL_FETCH)
|
||||
.enemyMoveset(MoveId.SPLASH)
|
||||
.enemyLevel(100)
|
||||
.startingLevel(100);
|
||||
});
|
||||
|
||||
// Reference Link: https://bulbapedia.bulbagarden.net/wiki/Dancer_(Ability)
|
||||
/**
|
||||
* Check that the specified {@linkcode Pokemon} is using the specified move
|
||||
* in the current {@linkcode MovePhase} against the specified targets.
|
||||
*/
|
||||
function checkCurrentMoveUser(
|
||||
pokemon: Pokemon,
|
||||
move: MoveId,
|
||||
targets?: BattlerIndex[],
|
||||
useMode: MoveUseMode = MoveUseMode.INDIRECT,
|
||||
) {
|
||||
const currentPhase = game.scene.phaseManager.getCurrentPhase() as MovePhase;
|
||||
expect(currentPhase).not.toBeNull();
|
||||
expect(currentPhase).toBeInstanceOf(MovePhase);
|
||||
expect(currentPhase.pokemon).toBe(pokemon);
|
||||
expect(currentPhase.move.moveId).toBe(move);
|
||||
if (targets) {
|
||||
expect(currentPhase.targets).toHaveLength(targets.length);
|
||||
expect(currentPhase.targets).toEqual(expect.arrayContaining(targets));
|
||||
}
|
||||
expect(currentPhase.useMode).toBe(useMode);
|
||||
}
|
||||
|
||||
it("triggers when dance moves are used, doesn't consume extra PP", async () => {
|
||||
game.override.enemyAbility(AbilityId.DANCER).enemySpecies(SpeciesId.MAGIKARP).enemyMoveset(MoveId.VICTORY_DANCE);
|
||||
async function toNextMove() {
|
||||
await game.phaseInterceptor.to("MoveEndPhase");
|
||||
await game.phaseInterceptor.to("MovePhase", false);
|
||||
}
|
||||
|
||||
// Reference Link: https://bulbapedia.bulbagarden.net/wiki/Dancer_(Ability).
|
||||
|
||||
it("should copy dance moves without consuming extra PP", async () => {
|
||||
game.override.enemyAbility(AbilityId.DANCER);
|
||||
await game.classicMode.startBattle([SpeciesId.ORICORIO]);
|
||||
|
||||
const oricorio = game.field.getPlayerPokemon();
|
||||
const shuckle = game.field.getEnemyPokemon();
|
||||
|
||||
game.move.changeMoveset(oricorio, [MoveId.VICTORY_DANCE, MoveId.SWORDS_DANCE]);
|
||||
game.move.changeMoveset(shuckle, [MoveId.VICTORY_DANCE, MoveId.SWORDS_DANCE]);
|
||||
|
||||
game.move.select(MoveId.SWORDS_DANCE);
|
||||
await game.move.selectEnemyMove(MoveId.VICTORY_DANCE);
|
||||
await game.setTurnOrder([BattlerIndex.PLAYER, BattlerIndex.ENEMY]);
|
||||
await game.phaseInterceptor.to("BerryPhase");
|
||||
|
||||
// shpuldn't use PP if copied move is also in moveset
|
||||
expect(oricorio.moveset.map(m => m.ppUsed)).toEqual([0, 1]);
|
||||
expect(shuckle.moveset.map(m => m.ppUsed)).toEqual([1, 0]);
|
||||
|
||||
// effects were applied correctly
|
||||
expect(oricorio.getStatStage(Stat.ATK)).toBe(3);
|
||||
expect(shuckle.getStatStage(Stat.ATK)).toBe(3);
|
||||
|
||||
// moves showed up in history
|
||||
expect(oricorio.getLastXMoves(-1)).toHaveLength(2);
|
||||
expect(oricorio.getLastXMoves(-1)).toEqual([
|
||||
expect.objectContaining({ move: MoveId.VICTORY_DANCE, useMode: MoveUseMode.INDIRECT }),
|
||||
expect.objectContaining({ move: MoveId.SWORDS_DANCE, useMode: MoveUseMode.NORMAL }),
|
||||
]);
|
||||
expect(shuckle.getLastXMoves(-1)).toHaveLength(2);
|
||||
expect(shuckle.getLastXMoves(-1)).toEqual([
|
||||
expect.objectContaining({ move: MoveId.VICTORY_DANCE, useMode: MoveUseMode.NORMAL }),
|
||||
expect.objectContaining({ move: MoveId.SWORDS_DANCE, useMode: MoveUseMode.INDIRECT }),
|
||||
]);
|
||||
});
|
||||
|
||||
it("should redirect copied move if ally target faints", async () => {
|
||||
game.override.battleStyle("double").startingLevel(500);
|
||||
await game.classicMode.startBattle([SpeciesId.ORICORIO, SpeciesId.FEEBAS]);
|
||||
|
||||
const [oricorio, feebas] = game.scene.getPlayerField();
|
||||
game.move.changeMoveset(oricorio, [MoveId.SWORDS_DANCE, MoveId.VICTORY_DANCE, MoveId.SPLASH]);
|
||||
game.move.changeMoveset(feebas, [MoveId.SWORDS_DANCE, MoveId.SPLASH]);
|
||||
|
||||
game.move.select(MoveId.SPLASH);
|
||||
game.move.select(MoveId.SWORDS_DANCE, 1);
|
||||
await game.setTurnOrder([BattlerIndex.PLAYER_2, BattlerIndex.ENEMY, BattlerIndex.PLAYER, BattlerIndex.ENEMY_2]);
|
||||
await game.phaseInterceptor.to("MovePhase"); // feebas uses swords dance
|
||||
await game.phaseInterceptor.to("MovePhase", false); // oricorio copies swords dance
|
||||
game.move.use(MoveId.SPLASH, BattlerIndex.PLAYER);
|
||||
game.move.use(MoveId.REVELATION_DANCE, BattlerIndex.PLAYER_2, BattlerIndex.ENEMY_2);
|
||||
await game.setTurnOrder([BattlerIndex.PLAYER_2, BattlerIndex.PLAYER, BattlerIndex.ENEMY, BattlerIndex.ENEMY_2]);
|
||||
|
||||
let currentPhase = game.scene.phaseManager.getCurrentPhase() as MovePhase;
|
||||
expect(currentPhase.pokemon).toBe(oricorio);
|
||||
expect(currentPhase.move.moveId).toBe(MoveId.SWORDS_DANCE);
|
||||
await game.phaseInterceptor.to("MovePhase", false); // feebas rev dance
|
||||
checkCurrentMoveUser(feebas, MoveId.REVELATION_DANCE, [BattlerIndex.ENEMY_2], MoveUseMode.NORMAL);
|
||||
await toNextMove();
|
||||
|
||||
await game.phaseInterceptor.to("MoveEndPhase"); // end oricorio's move
|
||||
await game.phaseInterceptor.to("MovePhase"); // magikarp 1 copies swords dance
|
||||
await game.phaseInterceptor.to("MovePhase"); // magikarp 2 copies swords dance
|
||||
await game.phaseInterceptor.to("MovePhase"); // magikarp (left) uses victory dance
|
||||
await game.phaseInterceptor.to("MovePhase", false); // oricorio copies magikarp's victory dance
|
||||
// attack should redirect
|
||||
const [shuckle1, shuckle2] = game.scene.getEnemyField();
|
||||
expect(shuckle2.isFainted()).toBe(true);
|
||||
checkCurrentMoveUser(oricorio, MoveId.REVELATION_DANCE, [BattlerIndex.ENEMY]);
|
||||
|
||||
currentPhase = game.scene.phaseManager.getCurrentPhase() as MovePhase;
|
||||
expect(currentPhase.pokemon).toBe(oricorio);
|
||||
expect(currentPhase.move.moveId).toBe(MoveId.VICTORY_DANCE);
|
||||
await game.toEndOfTurn();
|
||||
|
||||
await game.phaseInterceptor.to("BerryPhase"); // finish the turn
|
||||
|
||||
// doesn't use PP if copied move is also in moveset
|
||||
expect(oricorio.moveset[0]?.ppUsed).toBe(0);
|
||||
expect(oricorio.moveset[1]?.ppUsed).toBe(0);
|
||||
expect(shuckle1.isFainted()).toBe(true);
|
||||
});
|
||||
|
||||
// TODO: Enable after Dancer rework to not push to move history
|
||||
it.todo("should not count as the last move used for mirror move/instruct", async () => {
|
||||
game.override
|
||||
.moveset([MoveId.FIERY_DANCE, MoveId.REVELATION_DANCE])
|
||||
.enemyMoveset([MoveId.INSTRUCT, MoveId.MIRROR_MOVE, MoveId.SPLASH])
|
||||
.enemySpecies(SpeciesId.SHUCKLE)
|
||||
.enemyLevel(10);
|
||||
await game.classicMode.startBattle([SpeciesId.ORICORIO, SpeciesId.FEEBAS]);
|
||||
|
||||
const [oricorio] = game.scene.getPlayerField();
|
||||
const [, shuckle2] = game.scene.getEnemyField();
|
||||
|
||||
game.move.select(MoveId.REVELATION_DANCE, BattlerIndex.PLAYER, BattlerIndex.ENEMY_2);
|
||||
game.move.select(MoveId.FIERY_DANCE, BattlerIndex.PLAYER_2, BattlerIndex.ENEMY_2);
|
||||
await game.move.selectEnemyMove(MoveId.INSTRUCT, BattlerIndex.PLAYER);
|
||||
await game.move.selectEnemyMove(MoveId.MIRROR_MOVE, BattlerIndex.PLAYER);
|
||||
await game.setTurnOrder([BattlerIndex.PLAYER, BattlerIndex.PLAYER_2, BattlerIndex.ENEMY_2, BattlerIndex.ENEMY]);
|
||||
await game.phaseInterceptor.to("MovePhase"); // Oricorio rev dance
|
||||
await game.phaseInterceptor.to("MovePhase"); // Feebas fiery dance
|
||||
await game.phaseInterceptor.to("MovePhase"); // Oricorio fiery dance (from dancer)
|
||||
await game.phaseInterceptor.to("MoveEndPhase", false);
|
||||
// dancer copied move doesn't appear in move history
|
||||
expect(oricorio.getLastXMoves(-1)[0].move).toBe(MoveId.REVELATION_DANCE);
|
||||
|
||||
await game.phaseInterceptor.to("MovePhase"); // shuckle 2 mirror moves oricorio
|
||||
await game.phaseInterceptor.to("MovePhase"); // calls instructed rev dance
|
||||
let currentPhase = game.scene.phaseManager.getCurrentPhase() as MovePhase;
|
||||
expect(currentPhase.pokemon).toBe(shuckle2);
|
||||
expect(currentPhase.move.moveId).toBe(MoveId.REVELATION_DANCE);
|
||||
|
||||
await game.phaseInterceptor.to("MovePhase"); // shuckle 1 instructs oricorio
|
||||
await game.phaseInterceptor.to("MovePhase");
|
||||
currentPhase = game.scene.phaseManager.getCurrentPhase() as MovePhase;
|
||||
expect(currentPhase.pokemon).toBe(oricorio);
|
||||
expect(currentPhase.move.moveId).toBe(MoveId.REVELATION_DANCE);
|
||||
});
|
||||
|
||||
it("should not break subsequent last hit only moves", async () => {
|
||||
game.override.battleStyle("single");
|
||||
await game.classicMode.startBattle([SpeciesId.ORICORIO, SpeciesId.FEEBAS]);
|
||||
|
||||
const [oricorio, feebas] = game.scene.getPlayerParty();
|
||||
|
||||
game.move.use(MoveId.BATON_PASS);
|
||||
game.doSelectPartyPokemon(1);
|
||||
await game.move.forceEnemyMove(MoveId.SWORDS_DANCE);
|
||||
await game.setTurnOrder([BattlerIndex.ENEMY, BattlerIndex.PLAYER]);
|
||||
await game.phaseInterceptor.to("TurnEndPhase");
|
||||
|
||||
expect(game.phaseInterceptor.log).toContain("SwitchSummonPhase");
|
||||
expect(game.field.getPlayerPokemon()).toBe(feebas);
|
||||
expect(feebas.getStatStage(Stat.ATK)).toBe(2);
|
||||
expect(oricorio.isOnField()).toBe(false);
|
||||
expect(oricorio.visible).toBe(false);
|
||||
});
|
||||
|
||||
it("should not trigger while flinched", async () => {
|
||||
game.override.battleStyle("double").moveset(MoveId.SPLASH).enemyMoveset([MoveId.SWORDS_DANCE, MoveId.FAKE_OUT]);
|
||||
it("should redirect copied move if source enemy faints", async () => {
|
||||
game.override.battleStyle("double");
|
||||
await game.classicMode.startBattle([SpeciesId.ORICORIO]);
|
||||
|
||||
const oricorio = game.scene.getPlayerPokemon()!;
|
||||
expect(oricorio).toBeDefined();
|
||||
const oricorio = game.field.getPlayerPokemon();
|
||||
const [shuckle1, shuckle2] = game.scene.getEnemyField();
|
||||
shuckle1.hp = 1;
|
||||
vi.spyOn(shuckle2, "getAbility").mockReturnValue(allAbilities[AbilityId.ROUGH_SKIN]);
|
||||
|
||||
// get faked out and copy swords dance
|
||||
game.move.select(MoveId.SPLASH);
|
||||
// Enemy 1 hits enemy 2 and gets pwned
|
||||
game.move.use(MoveId.SPLASH);
|
||||
await game.move.forceEnemyMove(MoveId.AQUA_STEP, BattlerIndex.ENEMY_2);
|
||||
await game.move.forceEnemyMove(MoveId.SPLASH);
|
||||
await game.setTurnOrder([BattlerIndex.ENEMY, BattlerIndex.ENEMY_2, BattlerIndex.PLAYER]);
|
||||
|
||||
await game.phaseInterceptor.to("MovePhase"); // shuckle aqua steps its ally and kills itself
|
||||
await toNextMove(); // Oricorio copies
|
||||
expect(shuckle1.isFainted()).toBe(true);
|
||||
|
||||
// attack should redirect to other shuckle
|
||||
checkCurrentMoveUser(oricorio, MoveId.AQUA_STEP, [BattlerIndex.ENEMY_2]);
|
||||
|
||||
await game.toEndOfTurn();
|
||||
expect(oricorio.isFullHp()).toBe(false);
|
||||
});
|
||||
|
||||
it("should target correctly in double battles", async () => {
|
||||
game.override.battleStyle("double");
|
||||
await game.classicMode.startBattle([SpeciesId.ORICORIO, SpeciesId.FEEBAS]);
|
||||
|
||||
const [oricorio, feebas] = game.scene.getPlayerField();
|
||||
|
||||
// oricorio feather dances feebas, everyone else dances like crazy
|
||||
game.move.use(MoveId.FEATHER_DANCE, BattlerIndex.PLAYER, BattlerIndex.PLAYER_2);
|
||||
game.move.use(MoveId.REVELATION_DANCE, BattlerIndex.PLAYER_2, BattlerIndex.ENEMY_2);
|
||||
await game.move.forceEnemyMove(MoveId.FIERY_DANCE, BattlerIndex.PLAYER);
|
||||
await game.move.forceEnemyMove(MoveId.SWORDS_DANCE);
|
||||
await game.move.forceEnemyMove(MoveId.FAKE_OUT, BattlerIndex.PLAYER);
|
||||
await game.phaseInterceptor.to("TurnEndPhase");
|
||||
await game.setTurnOrder([BattlerIndex.PLAYER, BattlerIndex.PLAYER_2, BattlerIndex.ENEMY, BattlerIndex.ENEMY_2]);
|
||||
|
||||
expect(oricorio.getLastXMoves(-1)[0]).toMatchObject({
|
||||
move: MoveId.NONE,
|
||||
result: MoveResult.FAIL,
|
||||
});
|
||||
await game.phaseInterceptor.to("MovePhase", false); // oricorio feather dance
|
||||
checkCurrentMoveUser(oricorio, MoveId.FEATHER_DANCE, [BattlerIndex.PLAYER_2], MoveUseMode.NORMAL);
|
||||
await toNextMove(); // feebas copies feather dance against oricorio
|
||||
checkCurrentMoveUser(feebas, MoveId.FEATHER_DANCE, [BattlerIndex.PLAYER]);
|
||||
|
||||
await toNextMove(); // feebas uses rev dance on shuckle #2
|
||||
checkCurrentMoveUser(feebas, MoveId.REVELATION_DANCE, [BattlerIndex.ENEMY_2], MoveUseMode.NORMAL);
|
||||
await toNextMove(); // oricorio copies rev dance against same target
|
||||
checkCurrentMoveUser(oricorio, MoveId.REVELATION_DANCE, [BattlerIndex.ENEMY_2]);
|
||||
|
||||
await toNextMove(); // shuckle 1 uses fiery dance
|
||||
await toNextMove(); // oricorio copies fiery dance against it
|
||||
checkCurrentMoveUser(oricorio, MoveId.FIERY_DANCE, [BattlerIndex.ENEMY]);
|
||||
await toNextMove(); // feebas copies fiery dance
|
||||
checkCurrentMoveUser(feebas, MoveId.FIERY_DANCE, [BattlerIndex.ENEMY]);
|
||||
|
||||
await toNextMove(); // shuckle 2 uses swords dance
|
||||
await toNextMove(); // oricorio copies swords dance
|
||||
checkCurrentMoveUser(oricorio, MoveId.SWORDS_DANCE, [BattlerIndex.PLAYER]);
|
||||
await toNextMove(); // feebas copies swords dance
|
||||
checkCurrentMoveUser(feebas, MoveId.SWORDS_DANCE, [BattlerIndex.PLAYER_2]);
|
||||
|
||||
await game.toEndOfTurn();
|
||||
});
|
||||
|
||||
it("should display ability flyouts right before move use", async () => {
|
||||
game.override.battleStyle("double").enemyAbility(AbilityId.DANCER);
|
||||
await game.classicMode.startBattle([SpeciesId.ORICORIO, SpeciesId.FEEBAS]);
|
||||
|
||||
// TODO: uncomment once dynamic spd order added
|
||||
// game.scene
|
||||
// .getField()
|
||||
// .forEach((pkmn, i) => pkmn.setStat(Stat.SPD, 5 - i));
|
||||
|
||||
game.move.use(MoveId.SWORDS_DANCE, BattlerIndex.PLAYER);
|
||||
game.move.use(MoveId.SPLASH, BattlerIndex.PLAYER_2);
|
||||
await game.phaseInterceptor.to("DancerPhase", false);
|
||||
game.phaseInterceptor.clearLogs();
|
||||
await game.toEndOfTurn();
|
||||
|
||||
const showAbPhases: number[] = [];
|
||||
const hideAbPhases: number[] = [];
|
||||
const movePhases: number[] = [];
|
||||
const moveEndPhases: number[] = [];
|
||||
|
||||
const log = game.phaseInterceptor.log;
|
||||
for (const [index, phase] of log.entries()) {
|
||||
switch (phase) {
|
||||
case "ShowAbilityPhase":
|
||||
showAbPhases.push(index);
|
||||
break;
|
||||
case "HideAbilityPhase":
|
||||
hideAbPhases.push(index);
|
||||
break;
|
||||
case "MovePhase":
|
||||
movePhases.push(index);
|
||||
break;
|
||||
case "MoveEndPhase":
|
||||
case "DancerPhase": // also count the initial dancer phase (which occurs right after the initial usage anyhow)
|
||||
moveEndPhases.push(index);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
expect(showAbPhases).toHaveLength(3);
|
||||
expect(hideAbPhases).toHaveLength(3);
|
||||
|
||||
// Each dancer's ShowAbilityPhase must be immediately preceded by a MoveEndPhase and HideAbilityPhase,
|
||||
// and followed by a MovePhase.
|
||||
// We do not check the move phases directly as other pokemon may have moved themselves.
|
||||
for (const i of showAbPhases) {
|
||||
expect(moveEndPhases).toContain(i - 2);
|
||||
expect(hideAbPhases).toContain(i - 1);
|
||||
expect(movePhases).toContain(i + 1);
|
||||
}
|
||||
});
|
||||
|
||||
// TODO: Enable once abilities start proccing in speed order
|
||||
it.todo("should respect speed order during doubles", async () => {
|
||||
game.override
|
||||
.battleStyle("double")
|
||||
.enemyAbility(AbilityId.DANCER)
|
||||
.moveset([MoveId.QUIVER_DANCE, MoveId.SPLASH])
|
||||
.enemyMoveset(MoveId.SPLASH);
|
||||
await game.classicMode.startBattle([SpeciesId.ORICORIO, SpeciesId.FEEBAS]);
|
||||
|
||||
// Set the mons in reverse speed order - P1, P2, E1, E2
|
||||
// Used in place of `setTurnOrder` as the latter only applies for turn start phase
|
||||
game.scene.getField().forEach((pkmn, i) => pkmn.setStat(Stat.SPD, 5 - i));
|
||||
const orderSpy = vi.spyOn(MovePhase.prototype, "start");
|
||||
const showAbSpy = vi.spyOn(ShowAbilityPhase.prototype, "start");
|
||||
|
||||
game.move.select(MoveId.QUIVER_DANCE, BattlerIndex.PLAYER);
|
||||
game.move.select(MoveId.SPLASH, BattlerIndex.PLAYER_2);
|
||||
await game.toEndOfTurn();
|
||||
|
||||
const [oricorio, feebas, shuckle1, shuckle2] = game.scene.getField();
|
||||
|
||||
const expectedOrder = [
|
||||
// Oricorio quiver dance, then copies
|
||||
oricorio,
|
||||
feebas,
|
||||
shuckle1,
|
||||
shuckle2,
|
||||
];
|
||||
|
||||
const order = (orderSpy.mock.contexts as MovePhase[]).map(mp => mp.pokemon);
|
||||
const abOrder = (showAbSpy.mock.contexts as ShowAbilityPhase[]).map(sap => sap.getPokemon());
|
||||
expect(order).toEqual(expectedOrder);
|
||||
expect(abOrder).toEqual(expectedOrder);
|
||||
});
|
||||
|
||||
// TODO: Currently this is bugged as counter moves don't work at all
|
||||
it.todo("should count as last attack recieved for counter moves", async () => {
|
||||
game.override.battleStyle("double");
|
||||
|
||||
await game.classicMode.startBattle([SpeciesId.ORICORIO, SpeciesId.SNIVY]);
|
||||
|
||||
const [oricorio, _snivy] = game.scene.getPlayerField();
|
||||
const [shuckle1, shuckle2] = game.scene.getEnemyField();
|
||||
|
||||
const enemyDmgSpy = vi.spyOn(shuckle2, "damageAndUpdate");
|
||||
|
||||
// snivy attacks enemy 2, prompting oricorio to do the same
|
||||
game.move.use(MoveId.SPLASH, BattlerIndex.PLAYER);
|
||||
game.move.use(MoveId.REVELATION_DANCE, BattlerIndex.PLAYER_2, BattlerIndex.ENEMY_2);
|
||||
await game.killPokemon(shuckle1);
|
||||
await game.move.forceEnemyMove(MoveId.METAL_BURST);
|
||||
await game.setTurnOrder([BattlerIndex.PLAYER, BattlerIndex.ENEMY, BattlerIndex.PLAYER_2, BattlerIndex.ENEMY_2]);
|
||||
|
||||
// ORDER:
|
||||
// oricorio splash
|
||||
// shuckle 1 splash
|
||||
// snivy rev dance vs shuckle 2
|
||||
// oricorio copies rev dance vs shuckle 2
|
||||
// shuckle 2 metal burst vs Oricorio
|
||||
|
||||
await game.toEndOfTurn();
|
||||
expect(shuckle2.getLastXMoves(-1)[0].move).toBe(MoveId.METAL_BURST);
|
||||
expect(shuckle2.getLastXMoves(-1)[0].targets).toEqual([BattlerIndex.PLAYER]);
|
||||
|
||||
expect(enemyDmgSpy).toHaveBeenCalledTimes(2);
|
||||
const lastDmgTaken = enemyDmgSpy.mock.lastCall?.[0]!;
|
||||
expect(oricorio.getInverseHp()).toBe(toDmgValue(lastDmgTaken * 1.5));
|
||||
});
|
||||
|
||||
it.each<{ name: string; move?: MoveId; enemyMove: MoveId }>([
|
||||
{ name: "protected moves", enemyMove: MoveId.FIERY_DANCE, move: MoveId.PROTECT },
|
||||
{ name: "missed moves", enemyMove: MoveId.AQUA_STEP },
|
||||
{ name: "ineffective moves", enemyMove: MoveId.REVELATION_DANCE }, // ground type
|
||||
// TODO: These currently don't work as the moves are still considered "successful"
|
||||
// if all targets are unaffected
|
||||
// { name: "failed Teeter Dance", enemyMove: Moves.TEETER_DANCE },
|
||||
// { name: "capped stat-boosting moves", enemyMove: Moves.FEATHER_DANCE },
|
||||
// { name: "capped stat-lowering moves", enemyMove: Moves.QUIVER_DANCE },
|
||||
])("should not trigger on $name", async ({ move = MoveId.SPLASH, enemyMove }) => {
|
||||
game.override.enemySpecies(SpeciesId.GROUDON);
|
||||
// force aqua step to whiff
|
||||
vi.spyOn(allMoves[MoveId.AQUA_STEP], "accuracy", "get").mockReturnValue(0);
|
||||
await game.classicMode.startBattle([SpeciesId.ORICORIO]);
|
||||
|
||||
const oricorio = game.field.getPlayerPokemon();
|
||||
|
||||
oricorio.setStatStage(Stat.ATK, -6);
|
||||
oricorio.setStatStage(Stat.SPATK, +6);
|
||||
oricorio.setStatStage(Stat.SPDEF, +6);
|
||||
oricorio.setStatStage(Stat.SPD, +6);
|
||||
oricorio.addTag(BattlerTagType.CONFUSED, 12, MoveId.CONFUSE_RAY, game.field.getEnemyPokemon().id);
|
||||
|
||||
game.move.use(move);
|
||||
await game.move.forceEnemyMove(enemyMove);
|
||||
await game.toNextTurn();
|
||||
|
||||
expect(oricorio.getLastXMoves(-1)).toEqual([
|
||||
expect.objectContaining({ move, result: MoveResult.SUCCESS, useMode: MoveUseMode.NORMAL }),
|
||||
]);
|
||||
expect(oricorio.waveData.abilityRevealed).toBe(false);
|
||||
});
|
||||
|
||||
it("should trigger confusion self-damage, even when protected against", async () => {
|
||||
game.override.confusionActivation(false); // disable confusion unless forced by mocks
|
||||
await game.classicMode.startBattle([SpeciesId.ORICORIO]);
|
||||
|
||||
const oricorio = game.field.getPlayerPokemon();
|
||||
|
||||
// get confused
|
||||
game.move.use(MoveId.SPLASH);
|
||||
await game.move.forceEnemyMove(MoveId.CONFUSE_RAY);
|
||||
await game.toNextTurn();
|
||||
|
||||
expect(oricorio.getTag(BattlerTagType.CONFUSED)).toBeDefined();
|
||||
|
||||
// Protect, then copy swords dance
|
||||
game.move.use(MoveId.PROTECT);
|
||||
await game.move.forceEnemyMove(MoveId.SWORDS_DANCE);
|
||||
|
||||
await game.phaseInterceptor.to("MovePhase"); // protect
|
||||
await game.phaseInterceptor.to("MoveEndPhase"); // Swords dance
|
||||
await game.move.forceConfusionActivation(true); // force confusion proc during swords dance copy
|
||||
await game.toEndOfTurn();
|
||||
|
||||
// took damage from confusion instead of using move; player remains confused
|
||||
expect(oricorio.hp).toBeLessThan(oricorio.getMaxHp());
|
||||
expect(oricorio.getTag(BattlerTagType.CONFUSED)).toBeDefined();
|
||||
expect(game.field.getEnemyPokemon()?.getTag(BattlerTagType.CONFUSED)).toBeUndefined();
|
||||
});
|
||||
|
||||
it("should respect full paralysis", async () => {
|
||||
game.override.statusEffect(StatusEffect.PARALYSIS).statusActivation(true);
|
||||
await game.classicMode.startBattle([SpeciesId.ORICORIO]);
|
||||
|
||||
const oricorio = game.field.getPlayerPokemon();
|
||||
expect(oricorio.status).not.toBeNull();
|
||||
|
||||
// attempt to copy swords dance and get para'd
|
||||
game.move.use(MoveId.SPLASH);
|
||||
await game.move.forceEnemyMove(MoveId.SWORDS_DANCE);
|
||||
await game.setTurnOrder([BattlerIndex.PLAYER, BattlerIndex.ENEMY]);
|
||||
await game.toNextTurn();
|
||||
|
||||
expect(oricorio.getLastXMoves(-1)[0]).toMatchObject({ move: MoveId.NONE });
|
||||
expect(oricorio.status).toBeTruthy();
|
||||
expect(oricorio.getStatStage(Stat.ATK)).toBe(0);
|
||||
});
|
||||
|
||||
it.each<{ name: string; status: StatusEffect }>([
|
||||
{ name: "Sleep", status: StatusEffect.SLEEP },
|
||||
{ name: "Freeze", status: StatusEffect.FREEZE },
|
||||
])("should cure $name when copying move", async ({ status }) => {
|
||||
game.override.statusEffect(status).statusActivation(true);
|
||||
await game.classicMode.startBattle([SpeciesId.ORICORIO]);
|
||||
|
||||
const oricorio = game.field.getPlayerPokemon();
|
||||
expect(oricorio.status?.effect).toBe(status);
|
||||
|
||||
game.move.use(MoveId.ACROBATICS);
|
||||
await game.move.forceEnemyMove(MoveId.SWORDS_DANCE);
|
||||
await game.setTurnOrder([BattlerIndex.ENEMY, BattlerIndex.PLAYER]);
|
||||
|
||||
await game.phaseInterceptor.to("MoveEffectPhase"); // enemy SD
|
||||
await game.phaseInterceptor.to("MovePhase"); // player dancer attempt (curing happens inside MP)
|
||||
expect(oricorio.status).toBeNull();
|
||||
|
||||
await game.toEndOfTurn();
|
||||
expect(oricorio.getStatStage(Stat.ATK)).toBe(2);
|
||||
expect(game.field.getEnemyPokemon().isFullHp()).toBe(false);
|
||||
});
|
||||
|
||||
// TODO: This more or less requires an overhaul of Frenzy moves
|
||||
it.todo("should not lock user into Petal Dance or reduce its duration", async () => {
|
||||
game.override.enemyMoveset(MoveId.PETAL_DANCE);
|
||||
await game.classicMode.startBattle([SpeciesId.ORICORIO]);
|
||||
|
||||
// Mock RNG to make frenzy always last for max duration
|
||||
const oricorio = game.field.getPlayerPokemon();
|
||||
vi.spyOn(oricorio, "randBattleSeedIntRange").mockImplementation((_, max) => max);
|
||||
|
||||
game.move.changeMoveset(oricorio, [MoveId.SPLASH, MoveId.PETAL_DANCE]);
|
||||
|
||||
const shuckle = game.field.getEnemyPokemon();
|
||||
|
||||
// Enemy uses petal dance and we copy
|
||||
game.move.select(MoveId.SPLASH);
|
||||
await game.toEndOfTurn();
|
||||
|
||||
// used petal dance without being locked into move
|
||||
expect(oricorio.getLastXMoves(-1)[0]).toMatchObject({ move: MoveId.PETAL_DANCE, useMode: MoveUseMode.INDIRECT });
|
||||
expect(oricorio.getMoveQueue()).toHaveLength(0);
|
||||
expect(oricorio.getTag(BattlerTagType.FRENZY)).toBeUndefined();
|
||||
expect(shuckle.turnData.attacksReceived).toHaveLength(1);
|
||||
await game.toNextTurn();
|
||||
|
||||
// Use petal dance ourselves and copy enemy one
|
||||
game.move.select(MoveId.PETAL_DANCE);
|
||||
await game.setTurnOrder([BattlerIndex.PLAYER, BattlerIndex.ENEMY]);
|
||||
await game.phaseInterceptor.to("MoveEndPhase");
|
||||
const prevQueueLength = oricorio.getMoveQueue().length;
|
||||
await game.toEndOfTurn();
|
||||
|
||||
// locked into Petal Dance for 2 more turns (not 1)
|
||||
expect(oricorio.getMoveQueue()).toHaveLength(prevQueueLength);
|
||||
expect(oricorio.getTag(BattlerTagType.FRENZY)).toBeDefined();
|
||||
expect(oricorio.getMoveset().find(m => m.moveId === MoveId.PETAL_DANCE)?.ppUsed).toBe(1);
|
||||
});
|
||||
|
||||
it("should lapse Truant and respect its disables", async () => {
|
||||
game.override.passiveAbility(AbilityId.TRUANT).moveset(MoveId.SPLASH).enemyMoveset(MoveId.SWORDS_DANCE);
|
||||
await game.classicMode.startBattle([SpeciesId.ORICORIO]);
|
||||
|
||||
const oricorio = game.field.getPlayerPokemon();
|
||||
|
||||
// turn 1: Splash --> truanted Dancer SD
|
||||
game.move.select(MoveId.SPLASH);
|
||||
await game.setTurnOrder([BattlerIndex.PLAYER, BattlerIndex.ENEMY]);
|
||||
await game.toNextTurn();
|
||||
|
||||
expect(oricorio.getLastXMoves(2)).toEqual([
|
||||
expect.objectContaining({
|
||||
move: MoveId.NONE,
|
||||
result: MoveResult.FAIL,
|
||||
useMode: MoveUseMode.INDIRECT,
|
||||
}),
|
||||
expect.objectContaining({
|
||||
move: MoveId.SPLASH,
|
||||
result: MoveResult.SUCCESS,
|
||||
useMode: MoveUseMode.NORMAL,
|
||||
}),
|
||||
]);
|
||||
expect(oricorio.getStatStage(Stat.ATK)).toBe(0);
|
||||
|
||||
// Turn 2: Dancer SD --> truanted Splash
|
||||
game.move.select(MoveId.SPLASH);
|
||||
await game.setTurnOrder([BattlerIndex.ENEMY, BattlerIndex.PLAYER]);
|
||||
await game.toEndOfTurn();
|
||||
|
||||
expect(oricorio.getLastXMoves(2)).toEqual([
|
||||
expect.objectContaining({
|
||||
move: MoveId.NONE,
|
||||
result: MoveResult.FAIL,
|
||||
useMode: MoveUseMode.NORMAL,
|
||||
}),
|
||||
expect.objectContaining({
|
||||
move: MoveId.SWORDS_DANCE,
|
||||
result: MoveResult.SUCCESS,
|
||||
useMode: MoveUseMode.INDIRECT,
|
||||
}),
|
||||
]);
|
||||
expect(oricorio.getStatStage(Stat.ATK)).toBe(2);
|
||||
});
|
||||
|
||||
it.each<{ name: string; move: MoveId }>([
|
||||
{ name: "Mirror Move", move: MoveId.MIRROR_MOVE },
|
||||
{ name: "Copycat", move: MoveId.COPYCAT },
|
||||
])("should not count as last move used for $name", async ({ move }) => {
|
||||
await game.classicMode.startBattle([SpeciesId.ORICORIO]);
|
||||
|
||||
const shuckle = game.field.getEnemyPokemon();
|
||||
|
||||
// select splash first so we have a clear indicator of what move got copied
|
||||
game.move.use(MoveId.SPLASH);
|
||||
await game.move.forceEnemyMove(MoveId.SWORDS_DANCE);
|
||||
await game.setTurnOrder([BattlerIndex.PLAYER, BattlerIndex.ENEMY]);
|
||||
await game.toNextTurn();
|
||||
|
||||
game.move.use(MoveId.SPLASH);
|
||||
await game.move.forceEnemyMove(move);
|
||||
await game.setTurnOrder([BattlerIndex.ENEMY, BattlerIndex.PLAYER]);
|
||||
await game.toEndOfTurn();
|
||||
|
||||
expect(shuckle.getLastXMoves()[0]).toMatchObject({ move: MoveId.SPLASH, useMode: MoveUseMode.FOLLOW_UP });
|
||||
});
|
||||
|
||||
it("should not count as the last move used for Instruct", async () => {
|
||||
game.override.battleStyle("double");
|
||||
await game.classicMode.startBattle([SpeciesId.ORICORIO, SpeciesId.FEEBAS]);
|
||||
|
||||
game.move.use(MoveId.SPLASH, BattlerIndex.PLAYER);
|
||||
game.move.use(MoveId.FIERY_DANCE, BattlerIndex.PLAYER_2, BattlerIndex.ENEMY_2);
|
||||
await game.move.forceEnemyMove(MoveId.SPLASH);
|
||||
await game.move.forceEnemyMove(MoveId.INSTRUCT, BattlerIndex.PLAYER);
|
||||
await game.setTurnOrder([BattlerIndex.PLAYER, BattlerIndex.PLAYER_2, BattlerIndex.ENEMY_2, BattlerIndex.ENEMY]);
|
||||
|
||||
await game.phaseInterceptor.to("MovePhase"); // Oricorio uses splash
|
||||
|
||||
await game.phaseInterceptor.to("MovePhase"); // Feebas uses fiery dance
|
||||
await game.phaseInterceptor.to("MovePhase"); // Oricorio copies fiery dance
|
||||
|
||||
await game.phaseInterceptor.to("MovePhase"); // shuckle 2 instructs oricorio
|
||||
await toNextMove(); // instructed move used
|
||||
|
||||
checkCurrentMoveUser(game.field.getPlayerPokemon(), MoveId.SPLASH, [BattlerIndex.PLAYER], MoveUseMode.NORMAL);
|
||||
});
|
||||
|
||||
("");
|
||||
});
|
||||
|
@ -91,6 +91,6 @@ export class ClassicModeHelper extends GameManagerHelper {
|
||||
}
|
||||
|
||||
await this.game.phaseInterceptor.to(CommandPhase);
|
||||
console.log("==================[New Turn]==================");
|
||||
console.log("==================[New Battle]==================");
|
||||
}
|
||||
}
|
||||
|
@ -209,9 +209,11 @@ export class OverridesHelper extends GameManagerHelper {
|
||||
}
|
||||
|
||||
/**
|
||||
* Override the player pokemon's {@linkcode StatusEffect | status-effect}
|
||||
* @param statusEffect - The {@linkcode StatusEffect | status-effect} to set
|
||||
* @returns
|
||||
* Override the player pokemon's {@linkcode StatusEffect | status effect}.
|
||||
* Will be applied to each newly spawned pokemon on battle start.
|
||||
* @param statusEffect - The {@linkcode StatusEffect | status effect} to set
|
||||
* @returns `this`
|
||||
* @remarks If this is set to {@linkcode StatusEffect.SLEEP}, it will always have a constant duration of 4 turns.
|
||||
*/
|
||||
public statusEffect(statusEffect: StatusEffect): this {
|
||||
vi.spyOn(Overrides, "STATUS_OVERRIDE", "get").mockReturnValue(statusEffect);
|
||||
|
Loading…
Reference in New Issue
Block a user