mirror of
https://github.com/pagefaultgames/pokerogue.git
synced 2025-07-05 16:02:20 +02:00
Fixed things with switching moves; refactored code and removed extraneous resetSummonData
calls
This commit is contained in:
parent
09bdfa395b
commit
02a3a56ef6
@ -872,48 +872,51 @@ export default class BattleScene extends SceneBase {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Return all {@linkcode Pokemon} that are **not** currently {@linkcode Pokemon.isOnField | on field}
|
* Return the party positions of all {@linkcode Pokemon} that are **not** currently {@linkcode Pokemon.isOnField | on field}
|
||||||
* but are still {@linkcode Pokemon.isAllowedInBattle | allowed in battle}.
|
* but are still {@linkcode Pokemon.isAllowedInBattle | allowed in battle}.
|
||||||
|
|
||||||
* Used for switch out logic checks.
|
* Used for switch out logic checks.
|
||||||
* @param player - Whether to search the player (`true`) or enemy (`false`) party; default `true`
|
* @param player - Whether to search the player (`true`) or enemy (`false`) party; default `true`
|
||||||
* @returns An array of all {@linkcode PlayerPokemon} in reserve able to be switched into.
|
* @returns An array containing the **INDICES** of all {@linkcode Pokemon} in reserve able to be switched into.
|
||||||
* @overload
|
* @overload
|
||||||
*/
|
*/
|
||||||
public getBackupPartyMembers(player: true): PlayerPokemon[];
|
public getBackupPartyMemberIndices(player: true): number[];
|
||||||
/**
|
/**
|
||||||
* Return all {@linkcode Pokemon} that are **not** currently {@linkcode Pokemon.isOnField | on field}
|
* Return the party positions of all {@linkcode Pokemon} that are **not** currently {@linkcode Pokemon.isOnField | on field}
|
||||||
* but are still {@linkcode Pokemon.isAllowedInBattle | allowed in battle}.
|
* but are still {@linkcode Pokemon.isAllowedInBattle | allowed in battle}.
|
||||||
|
|
||||||
* Used for switch out logic checks.
|
* Used for switch out logic checks.
|
||||||
* @param player - Whether to search the player (`true`) or enemy (`false`) party; default `true`
|
* @param player - Whether to search the player (`true`) or enemy (`false`) party; default `true`
|
||||||
* @param trainerSlot - The {@linkcode EnemyPokemon.trainerSlot | trainer slot} of the Pokemon being switched out;
|
* @param trainerSlot - The {@linkcode TrainerSlot | trainer slot} to check against for enemy trainers;
|
||||||
* used to verify ownership in multi battles.
|
* used to verify ownership in multi battles.
|
||||||
* @returns An array of all {@linkcode EnemyPokemon} in reserve able to be switched into.
|
* @returns An array containing the **INDICES** of all {@linkcode Pokemon} in reserve able to be switched into.
|
||||||
* @overload
|
* @overload
|
||||||
*/
|
*/
|
||||||
public getBackupPartyMembers(player: false, trainerSlot: number): EnemyPokemon[];
|
public getBackupPartyMemberIndices(player: false, trainerSlot: TrainerSlot): number[];
|
||||||
/**
|
/**
|
||||||
* Return all {@linkcode Pokemon} that are **not** currently {@linkcode Pokemon.isOnField | on field}
|
* Return the party positions of all {@linkcode Pokemon} that are **not** currently {@linkcode Pokemon.isOnField | on field}
|
||||||
* but are still {@linkcode Pokemon.isAllowedInBattle | allowed in battle}.
|
* but are still {@linkcode Pokemon.isAllowedInBattle | allowed in battle}.
|
||||||
|
|
||||||
* Used for switch out logic checks.
|
* Used for switch out logic checks.
|
||||||
* @param player - Whether to search the player (`true`) or enemy (`false`) party; default `true`
|
* @param player - Whether to search the player (`true`) or enemy (`false`) party; default `true`
|
||||||
* @param trainerSlot - The enemy Pokemon's {@linkcode EnemyPokemon.trainerSlot | trainer slot} for opposing trainers;
|
* @param trainerSlot - The {@linkcode TrainerSlot | trainer slot} to check against for enemy trainers;
|
||||||
* used to verify ownership in multi battles and unused for player pokemon.
|
* used to verify ownership in multi battles and unused for player pokemon.
|
||||||
* @returns An array of all {@linkcode PlayerPokemon}/{@linkcode EnemyPokemon} in reserve able to be switched into.
|
* @returns An array containing the **INDICES** of all {@linkcode Pokemon} in reserve able to be switched into.
|
||||||
* @overload
|
* @overload
|
||||||
*/
|
*/
|
||||||
public getBackupPartyMembers(player: boolean, trainerSlot: number | undefined): PlayerPokemon | EnemyPokemon[];
|
public getBackupPartyMemberIndices(player: boolean, trainerSlot: TrainerSlot | undefined): number[];
|
||||||
|
|
||||||
public getBackupPartyMembers<B extends boolean = never, R = B extends true ? PlayerPokemon : EnemyPokemon>(
|
public getBackupPartyMemberIndices(player: boolean, trainerSlot?: number): number[] {
|
||||||
player: B,
|
// Note: We return the indices instead of the actual Pokemon because `SwitchSummonPhase` and co. take an index instead of a pokemon.
|
||||||
trainerSlot?: number,
|
// If this is ever changed, this can be replaced with a simpler version involving `filter` and conditional type annotations.
|
||||||
): R[] {
|
const indices: number[] = [];
|
||||||
return (player ? this.getPlayerParty() : this.getEnemyParty()).filter(
|
const party = player ? this.getPlayerParty() : this.getEnemyParty();
|
||||||
(p: PlayerPokemon | EnemyPokemon) =>
|
party.forEach((p: PlayerPokemon | EnemyPokemon, i: number) => {
|
||||||
p.isAllowedInBattle() && !p.isOnField() && (p instanceof PlayerPokemon || p.trainerSlot !== trainerSlot),
|
if (p.isAllowedInBattle() && !p.isOnField() && (player || (p as EnemyPokemon).trainerSlot === trainerSlot)) {
|
||||||
) as R[];
|
indices.push(i);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return indices;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -80,6 +80,7 @@ import type { ArenaTrapTag, SuppressAbilitiesTag } from "#app/data/arena-tag";
|
|||||||
import { SelectBiomePhase } from "#app/phases/select-biome-phase";
|
import { SelectBiomePhase } from "#app/phases/select-biome-phase";
|
||||||
import { noAbilityTypeOverrideMoves } from "../moves/invalid-moves";
|
import { noAbilityTypeOverrideMoves } from "../moves/invalid-moves";
|
||||||
import { ForceSwitch } from "../mixins/force-switch";
|
import { ForceSwitch } from "../mixins/force-switch";
|
||||||
|
import { MoveEffectPhase } from "#app/phases/move-effect-phase";
|
||||||
|
|
||||||
export class BlockRecoilDamageAttr extends AbAttr {
|
export class BlockRecoilDamageAttr extends AbAttr {
|
||||||
constructor() {
|
constructor() {
|
||||||
@ -1301,7 +1302,6 @@ export class PokemonTypeChangeAbAttr extends PreAttackAbAttr {
|
|||||||
if (!pokemon.isTerastallized &&
|
if (!pokemon.isTerastallized &&
|
||||||
move.id !== Moves.STRUGGLE &&
|
move.id !== Moves.STRUGGLE &&
|
||||||
/**
|
/**
|
||||||
* Skip moves that call other moves because these moves generate a following move that will trigger this ability attribute
|
|
||||||
* @see {@link https://bulbapedia.bulbagarden.net/wiki/Category:Moves_that_call_other_moves}
|
* @see {@link https://bulbapedia.bulbagarden.net/wiki/Category:Moves_that_call_other_moves}
|
||||||
*/
|
*/
|
||||||
!move.findAttr((attr) =>
|
!move.findAttr((attr) =>
|
||||||
@ -2818,8 +2818,12 @@ export class CommanderAbAttr extends AbAttr {
|
|||||||
|
|
||||||
// TODO: Should this work with X + Dondozo fusions?
|
// TODO: Should this work with X + Dondozo fusions?
|
||||||
const ally = pokemon.getAlly();
|
const ally = pokemon.getAlly();
|
||||||
return globalScene.currentBattle?.double && !isNullOrUndefined(ally) && ally.species.speciesId === Species.DONDOZO
|
return (
|
||||||
&& !(ally.isFainted() || ally.getTag(BattlerTagType.COMMANDED));
|
globalScene.currentBattle?.double
|
||||||
|
&& ally?.species.speciesId === Species.DONDOZO
|
||||||
|
&& !ally.isFainted()
|
||||||
|
&& !ally.getTag(BattlerTagType.COMMANDED)
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
override apply(pokemon: Pokemon, passive: boolean, simulated: boolean, cancelled: null, args: any[]): void {
|
override apply(pokemon: Pokemon, passive: boolean, simulated: boolean, cancelled: null, args: any[]): void {
|
||||||
@ -4105,7 +4109,6 @@ export class PostTurnRestoreBerryAbAttr extends PostTurnAbAttr {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Clamp procChance to [0, 1]. Skip if didn't proc (less than pass)
|
|
||||||
const pass = Phaser.Math.RND.realInRange(0, 1);
|
const pass = Phaser.Math.RND.realInRange(0, 1);
|
||||||
return Phaser.Math.Clamp(this.procChance(pokemon), 0, 1) >= pass;
|
return Phaser.Math.Clamp(this.procChance(pokemon), 0, 1) >= pass;
|
||||||
}
|
}
|
||||||
@ -5578,7 +5581,7 @@ export class PostDamageForceSwitchAbAttr extends ForceSwitch(PostDamageAbAttr) {
|
|||||||
|
|
||||||
constructor(switchType: NormalSwitchType = SwitchType.SWITCH, hpRatio = 0.5) {
|
constructor(switchType: NormalSwitchType = SwitchType.SWITCH, hpRatio = 0.5) {
|
||||||
super();
|
super();
|
||||||
this.selfSwitch = false; // TODO: change if any abilities get damage
|
this.selfSwitch = false; // TODO: change if any force switch abilities with red card exist
|
||||||
this.switchType = switchType;
|
this.switchType = switchType;
|
||||||
this.hpRatio = hpRatio;
|
this.hpRatio = hpRatio;
|
||||||
}
|
}
|
||||||
@ -5600,34 +5603,64 @@ export class PostDamageForceSwitchAbAttr extends ForceSwitch(PostDamageAbAttr) {
|
|||||||
source: Pokemon | undefined,
|
source: Pokemon | undefined,
|
||||||
_args: any[],
|
_args: any[],
|
||||||
): boolean {
|
): boolean {
|
||||||
const userLastMove = pokemon.getLastXMoves()[0];
|
|
||||||
// Will not activate when the Pokémon's HP is lowered by cutting its own HP
|
// Skip move checks for damage not occurring due to a move (eg: hazards)
|
||||||
const forbiddenAttackingMoves = new Set<Moves>([ Moves.BELLY_DRUM, Moves.SUBSTITUTE, Moves.CURSE, Moves.PAIN_SPLIT ]);
|
const currentPhase = globalScene.getCurrentPhase();
|
||||||
if (!isNullOrUndefined(userLastMove) && forbiddenAttackingMoves.has(userLastMove.move)) {
|
if (currentPhase instanceof MoveEffectPhase && !this.passesMoveChecks(pokemon, source)) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Skip last move checks if no enemy move
|
if (!this.wasKnockedBelowHalf(pokemon, damage)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.canSwitchOut(pokemon)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Perform move checks to determine if this pokemon should switch out.
|
||||||
|
* @param pokemon - The {@linkcode Pokemon} with this ability
|
||||||
|
* @param source - The {@linkcode Pokemon} whose attack caused the user to switch out,
|
||||||
|
* or `undefined` if the damage source was indirect.
|
||||||
|
* @returns `true` if this Pokemon should be allowed to switch out.
|
||||||
|
*/
|
||||||
|
private passesMoveChecks(pokemon: Pokemon, source: Pokemon | undefined): boolean {
|
||||||
|
// Wimp Out and Emergency Exit...
|
||||||
|
const currentPhase = globalScene.getCurrentPhase() as MoveEffectPhase;
|
||||||
|
const currentMove = currentPhase.move;
|
||||||
|
|
||||||
|
// will not activate from self-induced HP cutting...
|
||||||
|
// TODO: Verify that Fillet Away and Clangorous Soul proc wimp out
|
||||||
|
const hpCutMoves = new Set<Moves>([ Moves.CURSE, Moves.BELLY_DRUM, Moves.SUBSTITUTE, Moves.PAIN_SPLIT, Moves.CLANGOROUS_SOUL, Moves.FILLET_AWAY]);
|
||||||
|
// NB: Given this attribute is only applied after _taking damage_ or recieving a damaging attack,
|
||||||
|
// a failed Substitute or non-ghost type Curse will not trigger this ability to begin with.
|
||||||
|
const notHpCut = !hpCutMoves.has(currentMove.id)
|
||||||
|
|
||||||
|
// will not activate for forced switch moves (which trigger before wimp out activates)...
|
||||||
|
const notForceSwitched = ![Moves.DRAGON_TAIL, Moves.CIRCLE_THROW].includes(currentMove.id)
|
||||||
|
|
||||||
|
// and will not activate if the Pokemon is currently in the air from Sky Drop.
|
||||||
|
// TODO: Make this check the user's tags once Sky Drop is fully implemented -
|
||||||
|
// we could be sky dropped by another Pokemon or take indirect damage while skybound (both of which render this check moot)
|
||||||
const lastMove = source?.getLastXMoves()[0]
|
const lastMove = source?.getLastXMoves()[0]
|
||||||
if (
|
const notSkyDropped = !(lastMove?.move === Moves.SKY_DROP && lastMove.result === MoveResult.OTHER)
|
||||||
lastMove &&
|
|
||||||
// Will not activate for forced switch moves (triggers before wimp out activates)
|
|
||||||
(allMoves[lastMove.move].hasAttr(ForceSwitchOutAttr)
|
|
||||||
// Will not activate if the Pokémon's HP falls below half while it is in the air during Sky Drop
|
|
||||||
// TODO: Make this check the user's tags rather than the last move used by the target - we could be lifted by another pokemon
|
|
||||||
|| (lastMove.move === Moves.SKY_DROP && lastMove.result === MoveResult.OTHER))
|
|
||||||
) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check for HP percents - don't switch if the move didn't knock us below our switch threshold
|
return notHpCut && notForceSwitched && notSkyDropped;
|
||||||
// (either because we were below it to begin with or are still above it after the hit).
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Perform HP checks to determine if this pokemon should switch out.
|
||||||
|
* The switch fails if the pokemon was below {@linkcode hpRatio} before being hit
|
||||||
|
* or is still above it after the hit.
|
||||||
|
* @param pokemon - The {@linkcode Pokemon} with this ability
|
||||||
|
* @param damage - The amount of damage taken.
|
||||||
|
* @returns `true` if this Pokemon was knocked below half after `damage` was applied
|
||||||
|
*/
|
||||||
|
private wasKnockedBelowHalf(pokemon: Pokemon, damage: number) {
|
||||||
|
// NB: This occurs in `MoveEffectPhase` _after_ attack damage has been dealt,
|
||||||
|
// so `pokemon.hp` contains the post-taking damage hp value.
|
||||||
const hpNeededToSwitch = pokemon.getMaxHp() * this.hpRatio;
|
const hpNeededToSwitch = pokemon.getMaxHp() * this.hpRatio;
|
||||||
if (pokemon.hp + damage < hpNeededToSwitch || pokemon.hp >= hpNeededToSwitch) {
|
return pokemon.hp < hpNeededToSwitch && pokemon.hp + damage >= hpNeededToSwitch
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
return this.canSwitchOut(pokemon, undefined)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -6913,12 +6946,10 @@ export function initAbilities() {
|
|||||||
.attr(PostDefendStatStageChangeAbAttr, (target, user, move) => move.category !== MoveCategory.STATUS, Stat.DEF, 1),
|
.attr(PostDefendStatStageChangeAbAttr, (target, user, move) => move.category !== MoveCategory.STATUS, Stat.DEF, 1),
|
||||||
new Ability(Abilities.WIMP_OUT, 7)
|
new Ability(Abilities.WIMP_OUT, 7)
|
||||||
.attr(PostDamageForceSwitchAbAttr)
|
.attr(PostDamageForceSwitchAbAttr)
|
||||||
.condition(getSheerForceHitDisableAbCondition())
|
.condition(getSheerForceHitDisableAbCondition()),
|
||||||
.edgeCase(), // Should not trigger when hurting itself in confusion, causes Fake Out to fail turn 1 and succeed turn 2 if pokemon is switched out before battle start via playing in Switch Mode
|
|
||||||
new Ability(Abilities.EMERGENCY_EXIT, 7)
|
new Ability(Abilities.EMERGENCY_EXIT, 7)
|
||||||
.attr(PostDamageForceSwitchAbAttr)
|
.attr(PostDamageForceSwitchAbAttr)
|
||||||
.condition(getSheerForceHitDisableAbCondition())
|
.condition(getSheerForceHitDisableAbCondition()),
|
||||||
.edgeCase(), // Should not trigger when hurting itself in confusion, causes Fake Out to fail turn 1 and succeed turn 2 if pokemon is switched out before battle start via playing in Switch Mode
|
|
||||||
new Ability(Abilities.WATER_COMPACTION, 7)
|
new Ability(Abilities.WATER_COMPACTION, 7)
|
||||||
.attr(PostDefendStatStageChangeAbAttr, (target, user, move) => user.getMoveType(move) === PokemonType.WATER && move.category !== MoveCategory.STATUS, Stat.DEF, 2),
|
.attr(PostDefendStatStageChangeAbAttr, (target, user, move) => user.getMoveType(move) === PokemonType.WATER && move.category !== MoveCategory.STATUS, Stat.DEF, 2),
|
||||||
new Ability(Abilities.MERCILESS, 7)
|
new Ability(Abilities.MERCILESS, 7)
|
||||||
|
@ -751,10 +751,16 @@ export class ConfusedTag extends BattlerTag {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Tick down this Pokemon's confusion duration, randomly interrupting its move if not cured/
|
||||||
|
* @param pokemon - The {@linkcode Pokemon} with this tag
|
||||||
|
* @param lapseType - The {@linkcode BattlerTagLapseType | lapse type} triggering this tag's effects.
|
||||||
|
* @returns Whether the tag should be kept.
|
||||||
|
*/
|
||||||
lapse(pokemon: Pokemon, lapseType: BattlerTagLapseType): boolean {
|
lapse(pokemon: Pokemon, lapseType: BattlerTagLapseType): boolean {
|
||||||
const shouldLapse = lapseType !== BattlerTagLapseType.CUSTOM && super.lapse(pokemon, lapseType);
|
const shouldRemain = super.lapse(pokemon, lapseType);
|
||||||
|
|
||||||
if (!shouldLapse) {
|
if (!shouldRemain) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -766,7 +772,9 @@ export class ConfusedTag extends BattlerTag {
|
|||||||
globalScene.unshiftPhase(new CommonAnimPhase(pokemon.getBattlerIndex(), undefined, CommonAnim.CONFUSION));
|
globalScene.unshiftPhase(new CommonAnimPhase(pokemon.getBattlerIndex(), undefined, CommonAnim.CONFUSION));
|
||||||
|
|
||||||
// 1/3 chance of hitting self with a 40 base power move
|
// 1/3 chance of hitting self with a 40 base power move
|
||||||
if (pokemon.randSeedInt(3) === 0 || Overrides.CONFUSION_ACTIVATION_OVERRIDE === true) {
|
const shouldInterruptMove = Overrides.CONFUSION_ACTIVATION_OVERRIDE ?? pokemon.randSeedInt(3) === 0;
|
||||||
|
if (shouldInterruptMove) {
|
||||||
|
// TODO: Are these calculations correct? We really shouldn't hardcode the damage formula here...
|
||||||
const atk = pokemon.getEffectiveStat(Stat.ATK);
|
const atk = pokemon.getEffectiveStat(Stat.ATK);
|
||||||
const def = pokemon.getEffectiveStat(Stat.DEF);
|
const def = pokemon.getEffectiveStat(Stat.DEF);
|
||||||
const damage = toDmgValue(
|
const damage = toDmgValue(
|
||||||
|
@ -17,51 +17,49 @@ import { applyAbAttrs, ForceSwitchOutImmunityAbAttr } from "../abilities/ability
|
|||||||
import type { MoveAttr } from "../moves/move";
|
import type { MoveAttr } from "../moves/move";
|
||||||
import { getPokemonNameWithAffix } from "#app/messages";
|
import { getPokemonNameWithAffix } from "#app/messages";
|
||||||
import type { AbAttr } from "../abilities/ab-attrs/ab-attr";
|
import type { AbAttr } from "../abilities/ab-attrs/ab-attr";
|
||||||
import type { TrainerSlot } from "#enums/trainer-slot";
|
|
||||||
|
|
||||||
// NB: This shouldn't be terribly hard to extend from if switching items are added (à la Eject Button)
|
// NB: This shouldn't be terribly hard to extend from items if switching items are added (à la Eject Button/Red Card);
|
||||||
type SubMoveOrAbAttr = (new (...args: any[]) => MoveAttr) | (new (...args: any[]) => AbAttr);
|
type SubMoveOrAbAttr = (new (...args: any[]) => MoveAttr) | (new (...args: any[]) => AbAttr);
|
||||||
|
|
||||||
/** Mixin to handle shared logic for switch-in moves and abilities. */
|
/** Mixin to handle shared logic for switching moves and abilities. */
|
||||||
export function ForceSwitch<TBase extends SubMoveOrAbAttr>(Base: TBase) {
|
export function ForceSwitch<TBase extends SubMoveOrAbAttr>(Base: TBase) {
|
||||||
return class ForceSwitchClass extends Base {
|
return class ForceSwitchClass extends Base {
|
||||||
protected selfSwitch = false;
|
protected selfSwitch = false;
|
||||||
protected switchType: NormalSwitchType = SwitchType.SWITCH;
|
protected switchType: NormalSwitchType = SwitchType.SWITCH;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Determines if a Pokémon can be forcibly switched out based on its status, the opponent's status and battle conditions.
|
* Determines if a Pokémon can be forcibly switched out based on its status and battle conditions.
|
||||||
* @see {@linkcode performOpponentChecks} for opponent-related check code.
|
* @param switchOutTarget - The {@linkcode Pokemon} being switched out.
|
||||||
|
|
||||||
* @param switchOutTarget - The {@linkcode Pokemon} attempting to switch out.
|
|
||||||
* @param opponent - The {@linkcode Pokemon} opposing the currently switched out Pokemon.
|
|
||||||
* Unused if {@linkcode selfSwitch} is `true`, as it is only used to check Suction Cups.
|
|
||||||
* @returns Whether {@linkcode switchOutTarget} can be switched out by the current Move or Ability.
|
* @returns Whether {@linkcode switchOutTarget} can be switched out by the current Move or Ability.
|
||||||
*/
|
*/
|
||||||
protected canSwitchOut(switchOutTarget: Pokemon, opponent: Pokemon | undefined): boolean {
|
protected canSwitchOut(switchOutTarget: Pokemon): boolean {
|
||||||
const isPlayer = switchOutTarget instanceof PlayerPokemon;
|
const isPlayer = switchOutTarget instanceof PlayerPokemon;
|
||||||
|
|
||||||
if (!this.selfSwitch && opponent && !this.performOpponentChecks(switchOutTarget, opponent)) {
|
// If we aren't switching ourself out, ensure the target in question can actually be switched out by us
|
||||||
|
if (!this.selfSwitch && !this.performForceSwitchChecks(switchOutTarget)) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Wild enemies should not be allowed to flee with baton pass, nor by any means on X0 waves (don't want easy boss wins)
|
||||||
if (!isPlayer && globalScene.currentBattle.battleType === BattleType.WILD) {
|
if (!isPlayer && globalScene.currentBattle.battleType === BattleType.WILD) {
|
||||||
// enemies should not be allowed to flee with baton pass, nor by any means on X0 waves (don't want easy boss wins)
|
|
||||||
return this.switchType !== SwitchType.BATON_PASS && globalScene.currentBattle.waveIndex % 10 !== 0;
|
return this.switchType !== SwitchType.BATON_PASS && globalScene.currentBattle.waveIndex % 10 !== 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Finally, ensure that we have valid switch out targets.
|
// Finally, ensure that a trainer switching out has at least 1 valid reserve member to send in.
|
||||||
const reservePartyMembers = globalScene.getBackupPartyMembers(
|
const reservePartyMembers = globalScene.getBackupPartyMemberIndices(
|
||||||
isPlayer,
|
isPlayer,
|
||||||
(switchOutTarget as EnemyPokemon).trainerSlot as TrainerSlot | undefined,
|
!isPlayer ? (switchOutTarget as EnemyPokemon).trainerSlot : undefined,
|
||||||
); // evaluates to `undefined` if not present
|
);
|
||||||
if (reservePartyMembers.length === 0) {
|
return reservePartyMembers.length > 0;
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
protected performOpponentChecks(switchOutTarget: Pokemon, opponent: Pokemon): boolean {
|
/**
|
||||||
|
* Perform various checks to confirm the switched out target can be forcibly removed from the field
|
||||||
|
* by another Pokemon.
|
||||||
|
* @param switchOutTarget - The {@linkcode Pokemon} being switched out
|
||||||
|
* @returns Whether {@linkcode switchOutTarget} can be switched out by another Pokemon.
|
||||||
|
*/
|
||||||
|
private performForceSwitchChecks(switchOutTarget: Pokemon): boolean {
|
||||||
// Dondozo with an allied Tatsugiri in its mouth cannot be forced out by enemies
|
// Dondozo with an allied Tatsugiri in its mouth cannot be forced out by enemies
|
||||||
const commandedTag = switchOutTarget.getTag(BattlerTagType.COMMANDED);
|
const commandedTag = switchOutTarget.getTag(BattlerTagType.COMMANDED);
|
||||||
if (commandedTag?.getSourcePokemon()?.isActive(true)) {
|
if (commandedTag?.getSourcePokemon()?.isActive(true)) {
|
||||||
@ -70,8 +68,8 @@ export function ForceSwitch<TBase extends SubMoveOrAbAttr>(Base: TBase) {
|
|||||||
|
|
||||||
// Check for opposing switch block abilities (Suction Cups and co)
|
// Check for opposing switch block abilities (Suction Cups and co)
|
||||||
const blockedByAbility = new BooleanHolder(false);
|
const blockedByAbility = new BooleanHolder(false);
|
||||||
applyAbAttrs(ForceSwitchOutImmunityAbAttr, opponent, blockedByAbility);
|
applyAbAttrs(ForceSwitchOutImmunityAbAttr, switchOutTarget, blockedByAbility);
|
||||||
if (!blockedByAbility.value) {
|
if (blockedByAbility.value) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -97,7 +95,10 @@ export function ForceSwitch<TBase extends SubMoveOrAbAttr>(Base: TBase) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (!(switchOutTarget instanceof EnemyPokemon)) {
|
if (!(switchOutTarget instanceof EnemyPokemon)) {
|
||||||
console.warn("Switched out target not instance of Player or enemy Pokemon!");
|
console.warn(
|
||||||
|
"Switched out target (index %i) neither player nor enemy Pokemon!",
|
||||||
|
switchOutTarget.getFieldIndex(),
|
||||||
|
);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -119,12 +120,12 @@ export function ForceSwitch<TBase extends SubMoveOrAbAttr>(Base: TBase) {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Pick a random player pokemon to switch out.
|
// Pick a random eligible player pokemon to replace the switched out one.
|
||||||
const reservePartyMembers = globalScene.getBackupPartyMembers(true);
|
const reservePartyMembers = globalScene.getBackupPartyMemberIndices(true);
|
||||||
const switchOutIndex = switchOutTarget.randSeedInt(reservePartyMembers.length);
|
const switchInIndex = reservePartyMembers[switchOutTarget.randSeedInt(reservePartyMembers.length)];
|
||||||
|
|
||||||
globalScene.appendToPhase(
|
globalScene.appendToPhase(
|
||||||
new SwitchSummonPhase(this.switchType, switchOutTarget.getFieldIndex(), switchOutIndex, false, true),
|
new SwitchSummonPhase(this.switchType, switchOutTarget.getFieldIndex(), switchInIndex, false, true),
|
||||||
MoveEndPhase,
|
MoveEndPhase,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -132,15 +133,16 @@ export function ForceSwitch<TBase extends SubMoveOrAbAttr>(Base: TBase) {
|
|||||||
private trySwitchTrainerPokemon(switchOutTarget: EnemyPokemon): void {
|
private trySwitchTrainerPokemon(switchOutTarget: EnemyPokemon): void {
|
||||||
// fallback for no trainer
|
// fallback for no trainer
|
||||||
if (!globalScene.currentBattle.trainer) {
|
if (!globalScene.currentBattle.trainer) {
|
||||||
console.warn("Enemy trainer switch logic approached without a trainer!");
|
console.warn("Enemy trainer switch logic triggered without a trainer!");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
// Forced switches will to pick a random eligible pokemon, while
|
|
||||||
// choice-based switching uses the trainer's default switch behavior
|
// Forced switches will pick a random eligible pokemon from this trainer's side, while
|
||||||
const reservePartyMembers = globalScene.getBackupPartyMembers(false, switchOutTarget.trainerSlot);
|
// choice-based switching uses the trainer's default switch behavior.
|
||||||
|
const reservePartyIndices = globalScene.getBackupPartyMemberIndices(false, switchOutTarget.trainerSlot);
|
||||||
const summonIndex =
|
const summonIndex =
|
||||||
this.switchType === SwitchType.FORCE_SWITCH
|
this.switchType === SwitchType.FORCE_SWITCH
|
||||||
? switchOutTarget.randSeedInt(reservePartyMembers.length)
|
? reservePartyIndices[switchOutTarget.randSeedInt(reservePartyIndices.length)]
|
||||||
: (globalScene.currentBattle.trainer.getNextSummonIndex(switchOutTarget.trainerSlot) ?? 0);
|
: (globalScene.currentBattle.trainer.getNextSummonIndex(switchOutTarget.trainerSlot) ?? 0);
|
||||||
globalScene.appendToPhase(
|
globalScene.appendToPhase(
|
||||||
new SwitchSummonPhase(this.switchType, switchOutTarget.getFieldIndex(), summonIndex, false, false),
|
new SwitchSummonPhase(this.switchType, switchOutTarget.getFieldIndex(), summonIndex, false, false),
|
||||||
@ -180,5 +182,9 @@ export function ForceSwitch<TBase extends SubMoveOrAbAttr>(Base: TBase) {
|
|||||||
public isBatonPass(): boolean {
|
public isBatonPass(): boolean {
|
||||||
return this.switchType === SwitchType.BATON_PASS;
|
return this.switchType === SwitchType.BATON_PASS;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public isForcedSwitch(): boolean {
|
||||||
|
return this.switchType === SwitchType.FORCE_SWITCH;
|
||||||
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
@ -2002,6 +2002,13 @@ export class FlameBurstAttr extends MoveEffectAttr {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Attribute to KO the user while fully restoring HP/status of the next switched in Pokemon.
|
||||||
|
*
|
||||||
|
* Used for {@linkcode Moves.HEALING_WISH} and {@linkcode Moves.LUNAR_DANCE}.
|
||||||
|
* TODO: Implement "heal storing" if switched in pokemon is at full HP (likely with an end-of-turn ArenaTag).
|
||||||
|
* Will likely be blocked by the need for a "slot dependent ArenaTag" similar to Future Sight
|
||||||
|
*/
|
||||||
export class SacrificialFullRestoreAttr extends SacrificialAttr {
|
export class SacrificialFullRestoreAttr extends SacrificialAttr {
|
||||||
protected restorePP: boolean;
|
protected restorePP: boolean;
|
||||||
protected moveMessage: string;
|
protected moveMessage: string;
|
||||||
@ -2019,8 +2026,8 @@ export class SacrificialFullRestoreAttr extends SacrificialAttr {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// We don't know which party member will be chosen, so pick the highest max HP in the party
|
// We don't know which party member will be chosen, so pick the highest max HP in the party
|
||||||
const party = user.isPlayer() ? globalScene.getPlayerParty() : globalScene.getEnemyParty();
|
const party: Pokemon[] = user.isPlayer() ? globalScene.getPlayerParty() : globalScene.getEnemyParty();
|
||||||
const maxPartyMemberHp = party.map(p => p.getMaxHp()).reduce((maxHp: number, hp: number) => Math.max(hp, maxHp), 0);
|
const maxPartyMemberHp = Math.max(...party.map(p => p.getMaxHp()));
|
||||||
|
|
||||||
globalScene.pushPhase(
|
globalScene.pushPhase(
|
||||||
new PokemonHealPhase(
|
new PokemonHealPhase(
|
||||||
@ -6210,7 +6217,9 @@ export class RevivalBlessingAttr extends MoveEffectAttr {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Attribute to forcibly switch out the user or target of a Move.
|
||||||
|
*/
|
||||||
export class ForceSwitchOutAttr extends ForceSwitch(MoveEffectAttr) {
|
export class ForceSwitchOutAttr extends ForceSwitch(MoveEffectAttr) {
|
||||||
constructor(
|
constructor(
|
||||||
selfSwitch: boolean = false,
|
selfSwitch: boolean = false,
|
||||||
@ -6221,54 +6230,67 @@ export class ForceSwitchOutAttr extends ForceSwitch(MoveEffectAttr) {
|
|||||||
this.switchType = switchType;
|
this.switchType = switchType;
|
||||||
}
|
}
|
||||||
|
|
||||||
apply(user: Pokemon, target: Pokemon, _move: Move, _args: any[]): boolean {
|
apply(user: Pokemon, target: Pokemon, move: Move, _args: any[]): boolean {
|
||||||
|
if (!this.getSwitchOutCondition()(user, target, move)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
this.doSwitch(this.selfSwitch ? user : target)
|
this.doSwitch(this.selfSwitch ? user : target)
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
getCondition(): MoveConditionFunc {
|
getCondition(): MoveConditionFunc {
|
||||||
return this.getSwitchOutCondition();
|
// Damaging switch moves should not "fail" _per se_ upon a failed switch -
|
||||||
|
// they still succeed and deal damage (but just without actually switching out).
|
||||||
|
return (user, target, move) => (move.category !== MoveCategory.STATUS || this.getSwitchOutCondition()(user, target, move));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check whether the target can be switched out.
|
||||||
|
* @returns A {@linkcode MoveConditionFunc} that returns `true` if the switch out attempt should succeed.
|
||||||
|
*/
|
||||||
getSwitchOutCondition(): MoveConditionFunc {
|
getSwitchOutCondition(): MoveConditionFunc {
|
||||||
return (user, target, move) => {
|
return (user, target, move) => {
|
||||||
const [switchOutTarget, opponent] = this.selfSwitch ? [user, target] : [target, user];
|
const switchOutTarget = this.selfSwitch ? user : target;
|
||||||
|
|
||||||
// Don't allow wild mons to flee with U-turn et al.
|
// Don't allow wild mons to flee with U-turn et al.
|
||||||
if (switchOutTarget instanceof EnemyPokemon && globalScene.currentBattle.battleType === BattleType.WILD && !(this.selfSwitch && move.category !== MoveCategory.STATUS)) {
|
if (
|
||||||
|
switchOutTarget instanceof EnemyPokemon
|
||||||
|
&& globalScene.currentBattle.battleType === BattleType.WILD
|
||||||
|
&& this.selfSwitch
|
||||||
|
&& move.category !== MoveCategory.STATUS
|
||||||
|
) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
return this.canSwitchOut(switchOutTarget, opponent)
|
// Check for Wimp Out edge case - self-switching moves cannot proc if the attack also triggers Wimp Out/EE
|
||||||
|
const moveDmgDealt = user.turnData.lastMoveDamageDealt[target.getBattlerIndex()]
|
||||||
|
if (
|
||||||
|
this.selfSwitch
|
||||||
|
&& moveDmgDealt
|
||||||
|
&& target.getAbilityAttrs(PostDamageForceSwitchAbAttr).some(
|
||||||
|
p => p.canApplyPostDamage(target, moveDmgDealt, false, user, []))
|
||||||
|
) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.canSwitchOut(switchOutTarget)
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
getUserBenefitScore(user: Pokemon, target: Pokemon, move: Move): number {
|
getUserBenefitScore(user: Pokemon, target: Pokemon, move: Move): number {
|
||||||
if (!globalScene.getEnemyParty().find(p => p.isActive() && !p.isOnField())) {
|
const reservePartyMembers = globalScene.getBackupPartyMemberIndices(user.isPlayer() === this.selfSwitch, !user.isPlayer() ? (user as EnemyPokemon).trainerSlot : undefined)
|
||||||
|
if (reservePartyMembers.length === 0) {
|
||||||
return -20;
|
return -20;
|
||||||
}
|
}
|
||||||
|
|
||||||
let ret = this.selfSwitch ? Math.floor((1 - user.getHpRatio()) * 20) : super.getUserBenefitScore(user, target, move);
|
let ret = this.selfSwitch ? Math.floor((1 - user.getHpRatio()) * 20) : super.getUserBenefitScore(user, target, move);
|
||||||
if (this.selfSwitch && this.isBatonPass()) {
|
if (this.selfSwitch && this.isBatonPass()) {
|
||||||
const statStageTotal = user.getStatStages().reduce((s: number, total: number) => total += s, 0);
|
const statStageTotal = user.getStatStages().reduce((s: number, total: number) => total += s, 0);
|
||||||
|
// TODO: Why do we use a sine tween?
|
||||||
ret = ret / 2 + (Phaser.Tweens.Builders.GetEaseFunction("Sine.easeOut")(Math.min(Math.abs(statStageTotal), 10) / 10) * (statStageTotal >= 0 ? 10 : -10));
|
ret = ret / 2 + (Phaser.Tweens.Builders.GetEaseFunction("Sine.easeOut")(Math.min(Math.abs(statStageTotal), 10) / 10) * (statStageTotal >= 0 ? 10 : -10));
|
||||||
}
|
}
|
||||||
return ret;
|
return ret;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Helper function to check if the Pokémon's health is below half after taking damage.
|
|
||||||
* Used for an edge case interaction with Wimp Out/Emergency Exit.
|
|
||||||
* If the Ability activates due to being hit by U-turn or Volt Switch, the user of that move will not be switched out.
|
|
||||||
*/
|
|
||||||
hpDroppedBelowHalf(target: Pokemon): boolean {
|
|
||||||
const pokemonHealth = target.hp;
|
|
||||||
const maxPokemonHealth = target.getMaxHp();
|
|
||||||
const damageTaken = target.turnData.damageTaken;
|
|
||||||
const initialHealth = pokemonHealth + damageTaken;
|
|
||||||
|
|
||||||
// Check if the Pokémon's health has dropped below half after the damage
|
|
||||||
return initialHealth >= maxPokemonHealth / 2 && pokemonHealth < maxPokemonHealth / 2;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export class ChillyReceptionAttr extends ForceSwitchOutAttr {
|
export class ChillyReceptionAttr extends ForceSwitchOutAttr {
|
||||||
|
@ -344,12 +344,13 @@ export default class MysteryEncounter implements IMysteryEncounter {
|
|||||||
*/
|
*/
|
||||||
private meetsPrimaryRequirementAndPrimaryPokemonSelected(): boolean {
|
private meetsPrimaryRequirementAndPrimaryPokemonSelected(): boolean {
|
||||||
let qualified: PlayerPokemon[] = globalScene.getPlayerParty();
|
let qualified: PlayerPokemon[] = globalScene.getPlayerParty();
|
||||||
if (!this.primaryPokemonRequirements || this.primaryPokemonRequirements.length === 0) {
|
if (!this.primaryPokemonRequirements?.length) {
|
||||||
// If we lack specified criterion, grab the first on-field pokemon, or else the first pokemon allowed in battle
|
// If we lack specified criterion, grab the first on-field pokemon, or else the first pokemon allowed in battle
|
||||||
const activeMons = qualified.filter(p => p.isAllowedInBattle());
|
const activeMons = qualified.filter(p => p.isAllowedInBattle());
|
||||||
this.primaryPokemon = activeMons.find(p => p.isOnField()) ?? activeMons[0];
|
this.primaryPokemon = activeMons.find(p => p.isOnField()) ?? activeMons[0];
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
for (const req of this.primaryPokemonRequirements) {
|
for (const req of this.primaryPokemonRequirements) {
|
||||||
if (req.meetsRequirement()) {
|
if (req.meetsRequirement()) {
|
||||||
qualified = qualified.filter(pkmn => req.queryParty(globalScene.getPlayerParty()).includes(pkmn));
|
qualified = qualified.filter(pkmn => req.queryParty(globalScene.getPlayerParty()).includes(pkmn));
|
||||||
|
@ -4757,7 +4757,7 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container {
|
|||||||
* @param ignoreFaintPhase - Whether to ignore adding a faint phase if the damage causes the target to faint; default `false`
|
* @param ignoreFaintPhase - Whether to ignore adding a faint phase if the damage causes the target to faint; default `false`
|
||||||
* @returns The amount of damage actually dealt.
|
* @returns The amount of damage actually dealt.
|
||||||
* @remarks
|
* @remarks
|
||||||
* This will not trigger "on damage" effects for direct damage moves, which instead occurs at the end of `MoveEffectPhase`.
|
* This will not trigger "on damage" effects for direct damage moves, instead occuring at the end of `MoveEffectPhase`.
|
||||||
*/
|
*/
|
||||||
damageAndUpdate(damage: number,
|
damageAndUpdate(damage: number,
|
||||||
{
|
{
|
||||||
@ -4782,7 +4782,8 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container {
|
|||||||
);
|
);
|
||||||
globalScene.unshiftPhase(damagePhase);
|
globalScene.unshiftPhase(damagePhase);
|
||||||
|
|
||||||
// TODO: Review if wimp out battle skip actually needs this anymore
|
// Prevent enemies not on field from taking damage.
|
||||||
|
// TODO: Review if wimp out actually needs this anymore
|
||||||
if (this.switchOutStatus) {
|
if (this.switchOutStatus) {
|
||||||
damage = 0;
|
damage = 0;
|
||||||
}
|
}
|
||||||
@ -4803,8 +4804,9 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container {
|
|||||||
// Damage amount may have changed, but needed to be queued before calling damage function
|
// Damage amount may have changed, but needed to be queued before calling damage function
|
||||||
damagePhase.updateAmount(damage);
|
damagePhase.updateAmount(damage);
|
||||||
|
|
||||||
// Trigger PostDamageAbAttr (ie wimp out) for indirect damage only.
|
// Trigger PostDamageAbAttr (ie wimp out) for indirect, non-confusion damage instances.
|
||||||
if (isIndirectDamage) {
|
// We leave `source` as undefined for indirect hits to specify that the damage instance is indirect.
|
||||||
|
if (isIndirectDamage && result !== HitResult.CONFUSION) {
|
||||||
applyPostDamageAbAttrs(
|
applyPostDamageAbAttrs(
|
||||||
PostDamageAbAttr,
|
PostDamageAbAttr,
|
||||||
this,
|
this,
|
||||||
@ -5726,8 +5728,8 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container {
|
|||||||
* Reset this Pokemon's {@linkcode PokemonSummonData | SummonData} and {@linkcode PokemonTempSummonData | TempSummonData}
|
* Reset this Pokemon's {@linkcode PokemonSummonData | SummonData} and {@linkcode PokemonTempSummonData | TempSummonData}
|
||||||
* in preparation for switching pokemon, as well as removing any relevant on-switch tags.
|
* in preparation for switching pokemon, as well as removing any relevant on-switch tags.
|
||||||
* @remarks
|
* @remarks
|
||||||
* This **SHOULD NOT** be called when a `SummonPhase` or `SwitchSummonPhase` is already being added,
|
* This **SHOULD NOT** be called when {@linkcode leaveField} is already being called,
|
||||||
* both of which call this method (directly or indirectly) on both pokemon changing positions.
|
* which already calls this function.
|
||||||
*/
|
*/
|
||||||
resetSummonData(): void {
|
resetSummonData(): void {
|
||||||
console.log(`resetSummonData called on Pokemon ${this.name}`)
|
console.log(`resetSummonData called on Pokemon ${this.name}`)
|
||||||
@ -5801,6 +5803,7 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container {
|
|||||||
}
|
}
|
||||||
|
|
||||||
resetTurnData(): void {
|
resetTurnData(): void {
|
||||||
|
console.log(`resetTurnData called on Pokemon ${this.name}`)
|
||||||
this.turnData = new PokemonTurnData();
|
this.turnData = new PokemonTurnData();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -6311,7 +6314,7 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container {
|
|||||||
* @param destroy - Whether to destroy this Pokemon once it leaves the field; default `false`
|
* @param destroy - Whether to destroy this Pokemon once it leaves the field; default `false`
|
||||||
* @remarks
|
* @remarks
|
||||||
* This **SHOULD NOT** be called when a `SummonPhase` or `SwitchSummonPhase` is already being added,
|
* This **SHOULD NOT** be called when a `SummonPhase` or `SwitchSummonPhase` is already being added,
|
||||||
* which can lead to erroneous resetting of {@linkcode turnData} or {@linkcode summonData}.
|
* which can lead to premature resetting of {@klinkcode turnData} and {@linkcode summonData}.
|
||||||
*/
|
*/
|
||||||
leaveField(clearEffects = true, hideInfo = true, destroy = false) {
|
leaveField(clearEffects = true, hideInfo = true, destroy = false) {
|
||||||
console.log(`leaveField called on Pokemon ${this.name}`)
|
console.log(`leaveField called on Pokemon ${this.name}`)
|
||||||
@ -7960,12 +7963,20 @@ export class PokemonTurnData {
|
|||||||
*/
|
*/
|
||||||
public hitsLeft = -1;
|
public hitsLeft = -1;
|
||||||
/**
|
/**
|
||||||
* The amount of damage dealt by this Pokemon's last attack.
|
* The final amount of damage dealt by this Pokemon's last attack against each of its targets,
|
||||||
* Reset upon successfully using a move and used to enable internal tracking of damage amounts.
|
* mapped by their respective `BattlerIndex`es.
|
||||||
|
* Reset to an empty array upon attempting to use a move,
|
||||||
|
* and is used to calculate various damage-related effects (Shell Bell, U-Turn + Wimp Out interactions, etc.).
|
||||||
|
*/
|
||||||
|
public lastMoveDamageDealt: number[] = [];
|
||||||
|
/**
|
||||||
|
* The amount of damage dealt by this Pokemon's last hit.
|
||||||
|
* Used to calculate recoil damage amounts.
|
||||||
|
* TODO: Merge with `lastMoveDamageDealt` if any spread recoil moves are added
|
||||||
*/
|
*/
|
||||||
public lastMoveDamageDealt = 0;
|
|
||||||
public singleHitDamageDealt = 0;
|
public singleHitDamageDealt = 0;
|
||||||
// TODO: Rework this into "damage taken last" counter for metal burst and co.
|
// TODO: Make this into a "damage taken last" counter for metal burst and co.
|
||||||
|
// This is currently ONLY USED FOR ASSURANCE
|
||||||
public damageTaken = 0;
|
public damageTaken = 0;
|
||||||
public attacksReceived: AttackMoveResult[] = [];
|
public attacksReceived: AttackMoveResult[] = [];
|
||||||
public order: number;
|
public order: number;
|
||||||
|
@ -1783,21 +1783,22 @@ export class HitHealModifier extends PokemonHeldItemModifier {
|
|||||||
* @returns `true` if the {@linkcode Pokemon} was healed
|
* @returns `true` if the {@linkcode Pokemon} was healed
|
||||||
*/
|
*/
|
||||||
override apply(pokemon: Pokemon): boolean {
|
override apply(pokemon: Pokemon): boolean {
|
||||||
if (pokemon.turnData.lastMoveDamageDealt && !pokemon.isFullHp()) {
|
if (pokemon.isFullHp()) {
|
||||||
// TODO: this shouldn't be undefined AFAIK
|
return false;
|
||||||
globalScene.unshiftPhase(
|
|
||||||
new PokemonHealPhase(
|
|
||||||
pokemon.getBattlerIndex(),
|
|
||||||
toDmgValue(pokemon.turnData.lastMoveDamageDealt / 8) * this.stackCount,
|
|
||||||
i18next.t("modifier:hitHealApply", {
|
|
||||||
pokemonNameWithAffix: getPokemonNameWithAffix(pokemon),
|
|
||||||
typeName: this.type.name,
|
|
||||||
}),
|
|
||||||
true,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const totalDmgDealt = pokemon.turnData.lastMoveDamageDealt.reduce((r, d) => r + d, 0);
|
||||||
|
globalScene.unshiftPhase(
|
||||||
|
new PokemonHealPhase(
|
||||||
|
pokemon.getBattlerIndex(),
|
||||||
|
toDmgValue(totalDmgDealt / 8) * this.stackCount,
|
||||||
|
i18next.t("modifier:hitHealApply", {
|
||||||
|
pokemonNameWithAffix: getPokemonNameWithAffix(pokemon),
|
||||||
|
typeName: this.type.name,
|
||||||
|
}),
|
||||||
|
true,
|
||||||
|
),
|
||||||
|
);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -61,8 +61,6 @@ export class FaintPhase extends PokemonPhase {
|
|||||||
faintPokemon.getTag(BattlerTagType.GRUDGE)?.lapse(faintPokemon, BattlerTagLapseType.CUSTOM, this.source);
|
faintPokemon.getTag(BattlerTagType.GRUDGE)?.lapse(faintPokemon, BattlerTagLapseType.CUSTOM, this.source);
|
||||||
}
|
}
|
||||||
|
|
||||||
faintPokemon.resetSummonData();
|
|
||||||
|
|
||||||
if (!this.preventInstantRevive) {
|
if (!this.preventInstantRevive) {
|
||||||
const instantReviveModifier = globalScene.applyModifier(
|
const instantReviveModifier = globalScene.applyModifier(
|
||||||
PokemonInstantReviveModifier,
|
PokemonInstantReviveModifier,
|
||||||
@ -71,6 +69,7 @@ export class FaintPhase extends PokemonPhase {
|
|||||||
) as PokemonInstantReviveModifier;
|
) as PokemonInstantReviveModifier;
|
||||||
|
|
||||||
if (instantReviveModifier) {
|
if (instantReviveModifier) {
|
||||||
|
faintPokemon.resetSummonData();
|
||||||
faintPokemon.loseHeldItem(instantReviveModifier);
|
faintPokemon.loseHeldItem(instantReviveModifier);
|
||||||
globalScene.updateModifiers(this.player);
|
globalScene.updateModifiers(this.player);
|
||||||
return this.end();
|
return this.end();
|
||||||
|
@ -3,6 +3,7 @@ import { globalScene } from "#app/global-scene";
|
|||||||
import {
|
import {
|
||||||
AddSecondStrikeAbAttr,
|
AddSecondStrikeAbAttr,
|
||||||
AlwaysHitAbAttr,
|
AlwaysHitAbAttr,
|
||||||
|
applyAbAttrs,
|
||||||
applyPostAttackAbAttrs,
|
applyPostAttackAbAttrs,
|
||||||
applyPostDamageAbAttrs,
|
applyPostDamageAbAttrs,
|
||||||
applyPostDefendAbAttrs,
|
applyPostDefendAbAttrs,
|
||||||
@ -78,6 +79,7 @@ import type Move from "#app/data/moves/move";
|
|||||||
import { isFieldTargeted } from "#app/data/moves/move-utils";
|
import { isFieldTargeted } from "#app/data/moves/move-utils";
|
||||||
import { FaintPhase } from "./faint-phase";
|
import { FaintPhase } from "./faint-phase";
|
||||||
import { DamageAchv } from "#app/system/achv";
|
import { DamageAchv } from "#app/system/achv";
|
||||||
|
import { userInfo } from "node:os";
|
||||||
|
|
||||||
type HitCheckEntry = [HitCheckResult, TypeDamageMultiplier];
|
type HitCheckEntry = [HitCheckResult, TypeDamageMultiplier];
|
||||||
|
|
||||||
@ -95,7 +97,7 @@ export class MoveEffectPhase extends PokemonPhase {
|
|||||||
|
|
||||||
/** Is this the first strike of a move? */
|
/** Is this the first strike of a move? */
|
||||||
private firstHit: boolean;
|
private firstHit: boolean;
|
||||||
/** Is this the last strike of a move? */
|
/** Is this the last strike of a move (either due to running out of hits or all targets being fainted/immune)? */
|
||||||
private lastHit: boolean;
|
private lastHit: boolean;
|
||||||
|
|
||||||
/** Phases queued during moves */
|
/** Phases queued during moves */
|
||||||
@ -181,7 +183,6 @@ export class MoveEffectPhase extends PokemonPhase {
|
|||||||
* Queue the phaes that should occur when the target reflects the move back to the user
|
* Queue the phaes that should occur when the target reflects the move back to the user
|
||||||
* @param user - The {@linkcode Pokemon} using this phase's invoked move
|
* @param user - The {@linkcode Pokemon} using this phase's invoked move
|
||||||
* @param target - The {@linkcode Pokemon} that is reflecting the move
|
* @param target - The {@linkcode Pokemon} that is reflecting the move
|
||||||
*
|
|
||||||
*/
|
*/
|
||||||
private queueReflectedMove(user: Pokemon, target: Pokemon): void {
|
private queueReflectedMove(user: Pokemon, target: Pokemon): void {
|
||||||
const newTargets = this.move.isMultiTarget()
|
const newTargets = this.move.isMultiTarget()
|
||||||
@ -342,7 +343,7 @@ export class MoveEffectPhase extends PokemonPhase {
|
|||||||
const targets = this.conductHitChecks(user, fieldMove);
|
const targets = this.conductHitChecks(user, fieldMove);
|
||||||
|
|
||||||
this.firstHit = user.turnData.hitCount === user.turnData.hitsLeft;
|
this.firstHit = user.turnData.hitCount === user.turnData.hitsLeft;
|
||||||
this.lastHit = user.turnData.hitsLeft === 1 || !targets.some(t => t.isActive(true));
|
this.lastHit = user.turnData.hitsLeft === 1 || targets.every(t => !t.isActive(true));
|
||||||
|
|
||||||
// Play the animation if the move was successful against any of its targets or it has a POST_TARGET effect (like self destruct)
|
// Play the animation if the move was successful against any of its targets or it has a POST_TARGET effect (like self destruct)
|
||||||
if (
|
if (
|
||||||
@ -766,6 +767,7 @@ export class MoveEffectPhase extends PokemonPhase {
|
|||||||
* - Triggering form changes and emergency exit / wimp out if this is the last hit
|
* - Triggering form changes and emergency exit / wimp out if this is the last hit
|
||||||
*
|
*
|
||||||
* @param target - the {@linkcode Pokemon} hit by this phase's move.
|
* @param target - the {@linkcode Pokemon} hit by this phase's move.
|
||||||
|
* @param targetIndex - The index of the target (used to update damage dealt amounts)
|
||||||
* @param effectiveness - The effectiveness of the move (as previously evaluated in {@linkcode hitCheck})
|
* @param effectiveness - The effectiveness of the move (as previously evaluated in {@linkcode hitCheck})
|
||||||
* @param firstTarget - Whether this is the first target successfully struck by the move
|
* @param firstTarget - Whether this is the first target successfully struck by the move
|
||||||
*/
|
*/
|
||||||
@ -785,7 +787,13 @@ export class MoveEffectPhase extends PokemonPhase {
|
|||||||
}
|
}
|
||||||
if (this.lastHit) {
|
if (this.lastHit) {
|
||||||
globalScene.triggerPokemonFormChange(user, SpeciesFormChangePostMoveTrigger);
|
globalScene.triggerPokemonFormChange(user, SpeciesFormChangePostMoveTrigger);
|
||||||
applyPostDamageAbAttrs(PostDamageAbAttr, target, target.turnData.lastMoveDamageDealt);
|
applyPostDamageAbAttrs(
|
||||||
|
PostDamageAbAttr,
|
||||||
|
target,
|
||||||
|
user.turnData.lastMoveDamageDealt[target.getBattlerIndex()],
|
||||||
|
false,
|
||||||
|
user,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -863,7 +871,7 @@ export class MoveEffectPhase extends PokemonPhase {
|
|||||||
globalScene.gameData.gameStats.highestDamage = Math.max(damage, globalScene.gameData.gameStats.highestDamage);
|
globalScene.gameData.gameStats.highestDamage = Math.max(damage, globalScene.gameData.gameStats.highestDamage);
|
||||||
}
|
}
|
||||||
|
|
||||||
user.turnData.lastMoveDamageDealt += damage;
|
user.turnData.lastMoveDamageDealt[target.getBattlerIndex()] += damage;
|
||||||
user.turnData.singleHitDamageDealt = damage;
|
user.turnData.singleHitDamageDealt = damage;
|
||||||
target.battleData.hitCount++;
|
target.battleData.hitCount++;
|
||||||
// TODO: this might be incorrect for counter moves
|
// TODO: this might be incorrect for counter moves
|
||||||
|
@ -43,7 +43,7 @@ import { CommonAnimPhase } from "#app/phases/common-anim-phase";
|
|||||||
import { MoveChargePhase } from "#app/phases/move-charge-phase";
|
import { MoveChargePhase } from "#app/phases/move-charge-phase";
|
||||||
import { MoveEffectPhase } from "#app/phases/move-effect-phase";
|
import { MoveEffectPhase } from "#app/phases/move-effect-phase";
|
||||||
import { MoveEndPhase } from "#app/phases/move-end-phase";
|
import { MoveEndPhase } from "#app/phases/move-end-phase";
|
||||||
import { NumberHolder } from "#app/utils/common";
|
import { getEnumValues, NumberHolder } from "#app/utils/common";
|
||||||
import { Abilities } from "#enums/abilities";
|
import { Abilities } from "#enums/abilities";
|
||||||
import { ArenaTagType } from "#enums/arena-tag-type";
|
import { ArenaTagType } from "#enums/arena-tag-type";
|
||||||
import { BattlerTagType } from "#enums/battler-tag-type";
|
import { BattlerTagType } from "#enums/battler-tag-type";
|
||||||
@ -160,7 +160,7 @@ export class MovePhase extends BattlePhase {
|
|||||||
}
|
}
|
||||||
|
|
||||||
this.pokemon.turnData.acted = true;
|
this.pokemon.turnData.acted = true;
|
||||||
this.pokemon.turnData.lastMoveDamageDealt = 0;
|
this.pokemon.turnData.lastMoveDamageDealt = Array(Math.max(...getEnumValues(BattlerIndex))).fill(0);
|
||||||
|
|
||||||
// Reset hit-related turn data when starting follow-up moves (e.g. Metronomed moves, Dancer repeats)
|
// Reset hit-related turn data when starting follow-up moves (e.g. Metronomed moves, Dancer repeats)
|
||||||
if (this.followUp) {
|
if (this.followUp) {
|
||||||
|
@ -274,6 +274,7 @@ export class SummonPhase extends PartyMemberPokemonPhase {
|
|||||||
globalScene.unshiftPhase(new ShinySparklePhase(pokemon.getBattlerIndex()));
|
globalScene.unshiftPhase(new ShinySparklePhase(pokemon.getBattlerIndex()));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TODO: This might be a duplicate - check to see if can be removed without breaking things
|
||||||
pokemon.resetTurnData();
|
pokemon.resetTurnData();
|
||||||
|
|
||||||
if (
|
if (
|
||||||
|
@ -37,7 +37,7 @@ export class SwitchPhase extends BattlePhase {
|
|||||||
super.start();
|
super.start();
|
||||||
|
|
||||||
// Failsafe: skip modal switches if impossible (no eligible party members in reserve).
|
// Failsafe: skip modal switches if impossible (no eligible party members in reserve).
|
||||||
if (this.isModal && globalScene.getBackupPartyMembers(true).length === 0) {
|
if (this.isModal && globalScene.getBackupPartyMemberIndices(true).length === 0) {
|
||||||
return super.end();
|
return super.end();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -52,11 +52,11 @@ export class SwitchPhase extends BattlePhase {
|
|||||||
return super.end();
|
return super.end();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check if there is any space still in field
|
// Check if there is any space still on field.
|
||||||
|
// TODO: Do we need this?
|
||||||
if (
|
if (
|
||||||
this.isModal &&
|
this.isModal &&
|
||||||
globalScene.getPlayerField().filter(p => p.isAllowedInBattle() && p.isActive(true)).length >=
|
globalScene.getPlayerField().filter(p => p.isActive(true)).length > globalScene.currentBattle.getBattlerCount()
|
||||||
globalScene.currentBattle.getBattlerCount()
|
|
||||||
) {
|
) {
|
||||||
return super.end();
|
return super.end();
|
||||||
}
|
}
|
||||||
|
@ -2,18 +2,15 @@ import { globalScene } from "#app/global-scene";
|
|||||||
import {
|
import {
|
||||||
applyPreSummonAbAttrs,
|
applyPreSummonAbAttrs,
|
||||||
applyPreSwitchOutAbAttrs,
|
applyPreSwitchOutAbAttrs,
|
||||||
PostDamageForceSwitchAbAttr,
|
|
||||||
PreSummonAbAttr,
|
PreSummonAbAttr,
|
||||||
PreSwitchOutAbAttr,
|
PreSwitchOutAbAttr,
|
||||||
} from "#app/data/abilities/ability";
|
} from "#app/data/abilities/ability";
|
||||||
import { allMoves, ForceSwitchOutAttr } from "#app/data/moves/move";
|
|
||||||
import { getPokeballTintColor } from "#app/data/pokeball";
|
import { getPokeballTintColor } from "#app/data/pokeball";
|
||||||
import { SpeciesFormChangeActiveTrigger } from "#app/data/pokemon-forms";
|
import { SpeciesFormChangeActiveTrigger } from "#app/data/pokemon-forms";
|
||||||
import { TrainerSlot } from "#enums/trainer-slot";
|
import { TrainerSlot } from "#enums/trainer-slot";
|
||||||
import type Pokemon from "#app/field/pokemon";
|
import type Pokemon from "#app/field/pokemon";
|
||||||
import { getPokemonNameWithAffix } from "#app/messages";
|
import { getPokemonNameWithAffix } from "#app/messages";
|
||||||
import { SwitchEffectTransferModifier } from "#app/modifier/modifier";
|
import { SwitchEffectTransferModifier } from "#app/modifier/modifier";
|
||||||
import { Command } from "#app/ui/command-ui-handler";
|
|
||||||
import i18next from "i18next";
|
import i18next from "i18next";
|
||||||
import { PostSummonPhase } from "./post-summon-phase";
|
import { PostSummonPhase } from "./post-summon-phase";
|
||||||
import { SummonPhase } from "./summon-phase";
|
import { SummonPhase } from "./summon-phase";
|
||||||
@ -74,13 +71,13 @@ export class SwitchSummonPhase extends SummonPhase {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const pokemon = this.getPokemon();
|
const lastPokemon = this.getPokemon();
|
||||||
(this.player ? globalScene.getEnemyField() : globalScene.getPlayerField()).forEach(enemyPokemon =>
|
(this.player ? globalScene.getEnemyField() : globalScene.getPlayerField()).forEach(enemyPokemon =>
|
||||||
enemyPokemon.removeTagsBySourceId(pokemon.id),
|
enemyPokemon.removeTagsBySourceId(lastPokemon.id),
|
||||||
);
|
);
|
||||||
|
|
||||||
if (this.switchType === SwitchType.SWITCH || this.switchType === SwitchType.INITIAL_SWITCH) {
|
if (this.switchType === SwitchType.SWITCH || this.switchType === SwitchType.INITIAL_SWITCH) {
|
||||||
const substitute = pokemon.getTag(SubstituteTag);
|
const substitute = lastPokemon.getTag(SubstituteTag);
|
||||||
if (substitute) {
|
if (substitute) {
|
||||||
globalScene.tweens.add({
|
globalScene.tweens.add({
|
||||||
targets: substitute.sprite,
|
targets: substitute.sprite,
|
||||||
@ -95,26 +92,26 @@ export class SwitchSummonPhase extends SummonPhase {
|
|||||||
globalScene.ui.showText(
|
globalScene.ui.showText(
|
||||||
this.player
|
this.player
|
||||||
? i18next.t("battle:playerComeBack", {
|
? i18next.t("battle:playerComeBack", {
|
||||||
pokemonName: getPokemonNameWithAffix(pokemon),
|
pokemonName: getPokemonNameWithAffix(lastPokemon),
|
||||||
})
|
})
|
||||||
: i18next.t("battle:trainerComeBack", {
|
: i18next.t("battle:trainerComeBack", {
|
||||||
trainerName: globalScene.currentBattle.trainer?.getName(
|
trainerName: globalScene.currentBattle.trainer?.getName(
|
||||||
!(this.fieldIndex % 2) ? TrainerSlot.TRAINER : TrainerSlot.TRAINER_PARTNER,
|
!(this.fieldIndex % 2) ? TrainerSlot.TRAINER : TrainerSlot.TRAINER_PARTNER,
|
||||||
),
|
),
|
||||||
pokemonName: pokemon.getNameToRender(),
|
pokemonName: lastPokemon.getNameToRender(),
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
globalScene.playSound("se/pb_rel");
|
globalScene.playSound("se/pb_rel");
|
||||||
pokemon.hideInfo();
|
lastPokemon.hideInfo();
|
||||||
pokemon.tint(getPokeballTintColor(pokemon.getPokeball(true)), 1, 250, "Sine.easeIn");
|
lastPokemon.tint(getPokeballTintColor(lastPokemon.getPokeball(true)), 1, 250, "Sine.easeIn");
|
||||||
globalScene.tweens.add({
|
globalScene.tweens.add({
|
||||||
targets: pokemon,
|
targets: lastPokemon,
|
||||||
duration: 250,
|
duration: 250,
|
||||||
ease: "Sine.easeIn",
|
ease: "Sine.easeIn",
|
||||||
scale: 0.5,
|
scale: 0.5,
|
||||||
onComplete: () => {
|
onComplete: () => {
|
||||||
globalScene.time.delayedCall(750, () => this.switchAndSummon());
|
globalScene.time.delayedCall(750, () => this.switchAndSummon());
|
||||||
pokemon.leaveField(this.switchType === SwitchType.SWITCH, false);
|
lastPokemon.leaveField(this.switchType === SwitchType.SWITCH, false);
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@ -192,8 +189,6 @@ export class SwitchSummonPhase extends SummonPhase {
|
|||||||
switchedInPokemon.y += this.lastPokemon.getSubstituteOffset()[1];
|
switchedInPokemon.y += this.lastPokemon.getSubstituteOffset()[1];
|
||||||
switchedInPokemon.setAlpha(0.5);
|
switchedInPokemon.setAlpha(0.5);
|
||||||
}
|
}
|
||||||
} else {
|
|
||||||
switchedInPokemon.resetSummonData();
|
|
||||||
}
|
}
|
||||||
this.summon();
|
this.summon();
|
||||||
};
|
};
|
||||||
@ -214,26 +209,6 @@ export class SwitchSummonPhase extends SummonPhase {
|
|||||||
|
|
||||||
const pokemon = this.getPokemon();
|
const pokemon = this.getPokemon();
|
||||||
|
|
||||||
const moveId = globalScene.currentBattle.lastMove;
|
|
||||||
const lastUsedMove = moveId ? allMoves[moveId] : undefined;
|
|
||||||
|
|
||||||
const currentCommand = globalScene.currentBattle.turnCommands[this.fieldIndex]?.command;
|
|
||||||
const lastPokemonIsForceSwitchedAndNotFainted =
|
|
||||||
lastUsedMove?.hasAttr(ForceSwitchOutAttr) && !this.lastPokemon.isFainted();
|
|
||||||
const lastPokemonHasForceSwitchAbAttr =
|
|
||||||
this.lastPokemon.hasAbilityWithAttr(PostDamageForceSwitchAbAttr) && !this.lastPokemon.isFainted();
|
|
||||||
|
|
||||||
// Compensate for turn spent summoning/forced switch if switched out pokemon is not fainted.
|
|
||||||
// Needed as we increment turn counters in `TurnEndPhase`.
|
|
||||||
if (
|
|
||||||
currentCommand === Command.POKEMON ||
|
|
||||||
lastPokemonIsForceSwitchedAndNotFainted ||
|
|
||||||
lastPokemonHasForceSwitchAbAttr
|
|
||||||
) {
|
|
||||||
pokemon.tempSummonData.turnCount--;
|
|
||||||
pokemon.tempSummonData.waveTurnCount--;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this.switchType === SwitchType.BATON_PASS && pokemon) {
|
if (this.switchType === SwitchType.BATON_PASS && pokemon) {
|
||||||
pokemon.transferSummon(this.lastPokemon);
|
pokemon.transferSummon(this.lastPokemon);
|
||||||
} else if (this.switchType === SwitchType.SHED_TAIL && pokemon) {
|
} else if (this.switchType === SwitchType.SHED_TAIL && pokemon) {
|
||||||
@ -243,14 +218,17 @@ export class SwitchSummonPhase extends SummonPhase {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Reset turn data if not initial switch (since it gets initialized to an empty object on turn start)
|
// If not switching at start of battle, reset turn counts and temp data.
|
||||||
|
// Needed as we increment turn counters in `TurnEndPhase`.
|
||||||
if (this.switchType !== SwitchType.INITIAL_SWITCH) {
|
if (this.switchType !== SwitchType.INITIAL_SWITCH) {
|
||||||
|
pokemon.tempSummonData.turnCount--;
|
||||||
|
pokemon.tempSummonData.waveTurnCount--;
|
||||||
|
// No need to reset turn/summon data for initial switch since both get initialized to an empty object on object creation
|
||||||
pokemon.resetTurnData();
|
pokemon.resetTurnData();
|
||||||
|
pokemon.resetSummonData();
|
||||||
pokemon.turnData.switchedInThisTurn = true;
|
pokemon.turnData.switchedInThisTurn = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
this.lastPokemon.resetSummonData();
|
|
||||||
|
|
||||||
globalScene.triggerPokemonFormChange(pokemon, SpeciesFormChangeActiveTrigger, true);
|
globalScene.triggerPokemonFormChange(pokemon, SpeciesFormChangeActiveTrigger, true);
|
||||||
// Reverts to weather-based forms when weather suppressors (Cloud Nine/Air Lock) are switched out
|
// Reverts to weather-based forms when weather suppressors (Cloud Nine/Air Lock) are switched out
|
||||||
globalScene.arena.triggerWeatherBasedFormChanges();
|
globalScene.arena.triggerWeatherBasedFormChanges();
|
||||||
|
@ -566,3 +566,55 @@ export function animationFileName(move: Moves): string {
|
|||||||
export function camelCaseToKebabCase(str: string): string {
|
export function camelCaseToKebabCase(str: string): string {
|
||||||
return str.replace(/[A-Z]+(?![a-z])|[A-Z]/g, (s, o) => (o ? "-" : "") + s.toLowerCase());
|
return str.replace(/[A-Z]+(?![a-z])|[A-Z]/g, (s, o) => (o ? "-" : "") + s.toLowerCase());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Split an array into a pair of arrays based on a conditional function.
|
||||||
|
* @param array - The array to split into 2.
|
||||||
|
* @param predicate - A function accepting up to 3 arguments. The split function calls the predicate function once per element of the array.
|
||||||
|
* @param thisArg - An object to which the this keyword can refer in the predicate function. If thisArg is omitted, `undefined` is used as the this value.
|
||||||
|
* @returns A pair of shallowly-copied arrays containing every element for which `predicate` did or did not return a value coercible to the boolean `true`.
|
||||||
|
*/
|
||||||
|
export function splitArray<T, S extends T>(
|
||||||
|
array: T[],
|
||||||
|
predicate: (value: T, index: number, array: T[]) => value is S,
|
||||||
|
thisArg?: unknown,
|
||||||
|
): [matches: S[], nonMatches: S[]];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Split an array into a pair of arrays based on a conditional function.
|
||||||
|
* @param array - The array to split into 2.
|
||||||
|
* @param predicate - A function accepting up to 3 arguments. The split function calls the function once per element of the array.
|
||||||
|
* @param thisArg - An object to which the this keyword can refer in the predicate function. If thisArg is omitted, `undefined` is used as the this value.
|
||||||
|
* @returns A pair of shallowly-copied arrays containing every element for which `predicate` did or did not return a value coercible to the boolean `true`.
|
||||||
|
*/
|
||||||
|
export function splitArray<T>(
|
||||||
|
array: T[],
|
||||||
|
predicate: (value: T, index: number, array: T[]) => unknown,
|
||||||
|
thisArg?: unknown,
|
||||||
|
): [matches: T[], nonMatches: T[]];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Split an array into a pair of arrays based on a conditional function.
|
||||||
|
* @param array - The array to split into 2.
|
||||||
|
* @param predicate - A function accepting up to 3 arguments. The split function calls the function once per element of the array.
|
||||||
|
* @param thisArg - An object to which the this keyword can refer in the predicate function. If thisArg is omitted, `undefined` is used as the this value.
|
||||||
|
* @returns A pair of shallowly-copied arrays containing every element for which `predicate` did or did not return a value coercible to the boolean `true`.
|
||||||
|
*/
|
||||||
|
export function splitArray<T>(
|
||||||
|
array: T[],
|
||||||
|
predicate: (value: T, index: number, array: T[]) => unknown,
|
||||||
|
thisArg?: any,
|
||||||
|
): [matches: T[], nonMatches: T[]] {
|
||||||
|
const matches: T[] = [];
|
||||||
|
const nonMatches: T[] = [];
|
||||||
|
|
||||||
|
const p = predicate.bind(thisArg) as typeof predicate;
|
||||||
|
array.forEach((val, index, ar) => {
|
||||||
|
if (p(val, index, ar)) {
|
||||||
|
matches.push(val);
|
||||||
|
} else {
|
||||||
|
nonMatches.push(val);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return [matches, nonMatches];
|
||||||
|
}
|
||||||
|
@ -8,11 +8,12 @@ import { ArenaTagType } from "#enums/arena-tag-type";
|
|||||||
import { BattlerTagType } from "#enums/battler-tag-type";
|
import { BattlerTagType } from "#enums/battler-tag-type";
|
||||||
import { Moves } from "#enums/moves";
|
import { Moves } from "#enums/moves";
|
||||||
import { Species } from "#enums/species";
|
import { Species } from "#enums/species";
|
||||||
import { Stat } from "#enums/stat";
|
|
||||||
import { StatusEffect } from "#enums/status-effect";
|
import { StatusEffect } from "#enums/status-effect";
|
||||||
import { WeatherType } from "#enums/weather-type";
|
|
||||||
import Phaser from "phaser";
|
import Phaser from "phaser";
|
||||||
import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest";
|
import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest";
|
||||||
|
import { BattleType } from "#enums/battle-type";
|
||||||
|
import { HitResult } from "#app/field/pokemon";
|
||||||
|
import type { ModifierOverride } from "#app/modifier/modifier-type";
|
||||||
|
|
||||||
describe("Abilities - Wimp Out", () => {
|
describe("Abilities - Wimp Out", () => {
|
||||||
let phaserGame: Phaser.Game;
|
let phaserGame: Phaser.Game;
|
||||||
@ -37,7 +38,7 @@ describe("Abilities - Wimp Out", () => {
|
|||||||
.enemyPassiveAbility(Abilities.NO_GUARD)
|
.enemyPassiveAbility(Abilities.NO_GUARD)
|
||||||
.startingLevel(90)
|
.startingLevel(90)
|
||||||
.enemyLevel(70)
|
.enemyLevel(70)
|
||||||
.moveset([Moves.SPLASH, Moves.FALSE_SWIPE, Moves.ENDURE, Moves.THUNDER_PUNCH])
|
.moveset([Moves.SPLASH, Moves.FALSE_SWIPE, Moves.ENDURE, Moves.GUILLOTINE])
|
||||||
.enemyMoveset(Moves.FALSE_SWIPE)
|
.enemyMoveset(Moves.FALSE_SWIPE)
|
||||||
.disableCrits();
|
.disableCrits();
|
||||||
});
|
});
|
||||||
@ -72,16 +73,17 @@ describe("Abilities - Wimp Out", () => {
|
|||||||
const wimpod = game.scene.getPlayerPokemon()!;
|
const wimpod = game.scene.getPlayerPokemon()!;
|
||||||
wimpod.hp *= 0.52;
|
wimpod.hp *= 0.52;
|
||||||
|
|
||||||
game.move.select(Moves.THUNDER_PUNCH);
|
game.move.select(Moves.SPLASH);
|
||||||
game.doSelectPartyPokemon(1);
|
game.doSelectPartyPokemon(1);
|
||||||
await game.phaseInterceptor.to("TurnEndPhase");
|
await game.phaseInterceptor.to("TurnEndPhase");
|
||||||
|
|
||||||
// Wimpod switched out after taking a hit, canceling its upcoming MovePhase before it could attack
|
// Wimpod switched out after taking a hit, canceling its upcoming MoveEffectPhase before it could attack
|
||||||
confirmSwitch();
|
confirmSwitch();
|
||||||
expect(game.scene.getEnemyPokemon()!.getInverseHp()).toBe(0);
|
expect(game.scene.getEnemyPokemon()!.getInverseHp()).toBe(0);
|
||||||
expect(game.phaseInterceptor.log.reduce((count, phase) => count + (phase === "MoveEffectPhase" ? 1 : 0), 0)).toBe(
|
expect(game.phaseInterceptor.log.reduce((count, phase) => count + (phase === "MoveEffectPhase" ? 1 : 0), 0)).toBe(
|
||||||
1,
|
1,
|
||||||
);
|
);
|
||||||
|
expect(wimpod.turnData.acted).toBe(false);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should not trigger if user faints from damage", async () => {
|
it("should not trigger if user faints from damage", async () => {
|
||||||
@ -91,12 +93,12 @@ describe("Abilities - Wimp Out", () => {
|
|||||||
const wimpod = game.scene.getPlayerPokemon()!;
|
const wimpod = game.scene.getPlayerPokemon()!;
|
||||||
wimpod.hp *= 0.52;
|
wimpod.hp *= 0.52;
|
||||||
|
|
||||||
game.move.select(Moves.THUNDER_PUNCH);
|
game.move.select(Moves.SPLASH);
|
||||||
game.doSelectPartyPokemon(1);
|
game.doSelectPartyPokemon(1);
|
||||||
await game.phaseInterceptor.to("TurnEndPhase");
|
await game.phaseInterceptor.to("TurnEndPhase");
|
||||||
|
|
||||||
expect(wimpod.isFainted()).toBe(true);
|
expect(wimpod.isFainted()).toBe(true);
|
||||||
confirmNoSwitch();
|
expect(wimpod.waveData.abilitiesApplied).not.toContain(Abilities.WIMP_OUT);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should trigger regenerator passive when switching out", async () => {
|
it("should trigger regenerator passive when switching out", async () => {
|
||||||
@ -128,15 +130,15 @@ describe("Abilities - Wimp Out", () => {
|
|||||||
expect(!isVisible && hasFled).toBe(true);
|
expect(!isVisible && hasFled).toBe(true);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should not trigger when HP already below half", async () => {
|
it("should not trigger if HP already below half", async () => {
|
||||||
await game.classicMode.startBattle([Species.WIMPOD, Species.TYRUNT]);
|
await game.classicMode.startBattle([Species.WIMPOD, Species.TYRUNT]);
|
||||||
const wimpod = game.scene.getPlayerPokemon()!;
|
const wimpod = game.scene.getPlayerPokemon()!;
|
||||||
wimpod.hp = 5;
|
wimpod.hp *= 0.1;
|
||||||
|
|
||||||
game.move.select(Moves.SPLASH);
|
game.move.select(Moves.SPLASH);
|
||||||
await game.phaseInterceptor.to("TurnEndPhase");
|
await game.phaseInterceptor.to("TurnEndPhase");
|
||||||
|
|
||||||
expect(wimpod.hp).toEqual(1);
|
expect(wimpod.getHpRatio()).toBeLessThan(0.1);
|
||||||
confirmNoSwitch();
|
confirmNoSwitch();
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -155,8 +157,35 @@ describe("Abilities - Wimp Out", () => {
|
|||||||
confirmSwitch();
|
confirmSwitch();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// TODO: Enable when dynamic speed order happens
|
||||||
|
it.todo("should trigger separately for each Pokemon hit in speed order", async () => {
|
||||||
|
game.override.battleStyle("double").enemyLevel(600).enemyMoveset(Moves.DRAGON_ENERGY);
|
||||||
|
await game.classicMode.startBattle([Species.WIMPOD, Species.GOLISOPOD, Species.TYRANITAR, Species.KINGAMBIT]);
|
||||||
|
|
||||||
|
// Golisopod switches out, Wimpod switches back in immediately afterwards
|
||||||
|
game.move.select(Moves.ENDURE, BattlerIndex.PLAYER);
|
||||||
|
game.move.select(Moves.ENDURE, BattlerIndex.PLAYER_2);
|
||||||
|
game.doSelectPartyPokemon(3);
|
||||||
|
game.doSelectPartyPokemon(3);
|
||||||
|
await game.phaseInterceptor.to("TurnEndPhase");
|
||||||
|
|
||||||
|
expect(game.scene.getPlayerParty().map(p => p.species.speciesId)).toBe([
|
||||||
|
Species.TYRANITAR,
|
||||||
|
Species.WIMPOD,
|
||||||
|
Species.KINGAMBIT,
|
||||||
|
Species.GOLISOPOD,
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Ttar and Kingambit should be at full HP; wimpod and golisopod should not
|
||||||
|
// Ttar and Wimpod should be on field; kingambit and golisopod should not
|
||||||
|
game.scene.getPlayerParty().forEach((p, i) => {
|
||||||
|
expect(p.isOnField()).toBe(i < 2);
|
||||||
|
expect(p.isFullHp()).toBe(i % 2 === 1);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
it("should block U-turn or Volt Switch on activation", async () => {
|
it("should block U-turn or Volt Switch on activation", async () => {
|
||||||
game.override.startingLevel(95).enemyMoveset([Moves.U_TURN]);
|
game.override.enemyMoveset(Moves.U_TURN);
|
||||||
await game.classicMode.startBattle([Species.WIMPOD, Species.TYRUNT]);
|
await game.classicMode.startBattle([Species.WIMPOD, Species.TYRUNT]);
|
||||||
|
|
||||||
game.move.select(Moves.SPLASH);
|
game.move.select(Moves.SPLASH);
|
||||||
@ -170,12 +199,20 @@ describe("Abilities - Wimp Out", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("should not block U-turn or Volt Switch if not activated", async () => {
|
it("should not block U-turn or Volt Switch if not activated", async () => {
|
||||||
game.override.startingLevel(190).startingWave(8).enemyMoveset([Moves.U_TURN]);
|
game.override.enemyMoveset(Moves.U_TURN).battleType(BattleType.TRAINER);
|
||||||
await game.classicMode.startBattle([Species.GOLISOPOD, Species.TYRUNT]);
|
await game.classicMode.startBattle([Species.GOLISOPOD, Species.TYRUNT]);
|
||||||
const RIVAL_NINJASK1 = game.scene.getEnemyPokemon()?.id;
|
const ninjask1 = game.scene.getEnemyPokemon()!;
|
||||||
|
|
||||||
|
vi.spyOn(game.scene.getPlayerPokemon()!, "getAttackDamage").mockReturnValue({
|
||||||
|
cancelled: false,
|
||||||
|
damage: 1,
|
||||||
|
result: HitResult.EFFECTIVE,
|
||||||
|
});
|
||||||
|
|
||||||
game.move.select(Moves.SPLASH);
|
game.move.select(Moves.SPLASH);
|
||||||
await game.phaseInterceptor.to("BerryPhase", false);
|
await game.phaseInterceptor.to("TurnEndPhase");
|
||||||
expect(game.scene.getEnemyPokemon()?.id !== RIVAL_NINJASK1);
|
|
||||||
|
expect(ninjask1.isOnField()).toBe(true);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should not activate from Dragon Tail and Circle Throw", async () => {
|
it("should not activate from Dragon Tail and Circle Throw", async () => {
|
||||||
@ -185,7 +222,6 @@ describe("Abilities - Wimp Out", () => {
|
|||||||
const wimpod = game.scene.getPlayerPokemon()!;
|
const wimpod = game.scene.getPlayerPokemon()!;
|
||||||
|
|
||||||
game.move.select(Moves.SPLASH);
|
game.move.select(Moves.SPLASH);
|
||||||
game.doSelectPartyPokemon(1);
|
|
||||||
await game.phaseInterceptor.to("SwitchSummonPhase", false);
|
await game.phaseInterceptor.to("SwitchSummonPhase", false);
|
||||||
|
|
||||||
expect(wimpod.waveData.abilitiesApplied).not.toContain(Abilities.WIMP_OUT);
|
expect(wimpod.waveData.abilitiesApplied).not.toContain(Abilities.WIMP_OUT);
|
||||||
@ -193,42 +229,78 @@ describe("Abilities - Wimp Out", () => {
|
|||||||
await game.phaseInterceptor.to("TurnEndPhase");
|
await game.phaseInterceptor.to("TurnEndPhase");
|
||||||
|
|
||||||
expect(game.scene.getPlayerPokemon()!.species.speciesId).not.toBe(Species.WIMPOD);
|
expect(game.scene.getPlayerPokemon()!.species.speciesId).not.toBe(Species.WIMPOD);
|
||||||
|
// Force switches directly call `SwitchSummonPhase` to send in a random opponent,
|
||||||
|
// as opposed to `SwitchPhase` which allows for player choice
|
||||||
|
expect(game.phaseInterceptor.log).not.toContain("SwitchPhase");
|
||||||
});
|
});
|
||||||
|
|
||||||
it.each<{ type: string; enemyMove?: Moves; enemyAbility?: Abilities }>([
|
it.each<{ type: string; playerMove?: Moves; playerPassive?: Abilities; enemyMove?: Moves; enemyAbility?: Abilities }>(
|
||||||
{ type: "weather", enemyMove: Moves.HAIL },
|
[
|
||||||
{ type: "status", enemyMove: Moves.TOXIC },
|
{ type: "variable recoil moves", playerMove: Moves.HEAD_SMASH },
|
||||||
{ type: "Curse", enemyMove: Moves.CURSE },
|
{ type: "HP-based recoil moves", playerMove: Moves.CHLOROBLAST },
|
||||||
{ type: "Salt Cure", enemyMove: Moves.SALT_CURE },
|
{ type: "weather", enemyMove: Moves.HAIL },
|
||||||
{ type: "partial trapping moves", enemyMove: Moves.WHIRLPOOL },
|
{ type: "status", enemyMove: Moves.TOXIC },
|
||||||
{ type: "Leech Seed", enemyMove: Moves.LEECH_SEED },
|
{ type: "Ghost-type Curse", enemyMove: Moves.CURSE },
|
||||||
{ type: "Nightmare", enemyMove: Moves.NIGHTMARE },
|
{ type: "Salt Cure", enemyMove: Moves.SALT_CURE },
|
||||||
{ type: "Aftermath", enemyAbility: Abilities.AFTERMATH },
|
{ type: "partial trapping moves", enemyMove: Moves.WHIRLPOOL }, // no guard passive makes this guaranteed
|
||||||
{ type: "Bad Dreams", enemyAbility: Abilities.BAD_DREAMS },
|
{ type: "Leech Seed", enemyMove: Moves.LEECH_SEED },
|
||||||
])(
|
{ type: "Powder", playerMove: Moves.EMBER, enemyMove: Moves.POWDER },
|
||||||
"should activate from damage caused by $name",
|
{ type: "Nightmare", playerPassive: Abilities.COMATOSE, enemyMove: Moves.NIGHTMARE },
|
||||||
async ({ enemyMove = Moves.SPLASH, enemyAbility = Abilities.BALL_FETCH }) => {
|
{ type: "Bad Dreams", playerPassive: Abilities.COMATOSE, enemyAbility: Abilities.BAD_DREAMS },
|
||||||
|
],
|
||||||
|
)(
|
||||||
|
"should activate from damage caused by $type",
|
||||||
|
async ({
|
||||||
|
playerMove = Moves.SPLASH,
|
||||||
|
playerPassive = Abilities.NONE,
|
||||||
|
enemyMove = Moves.SPLASH,
|
||||||
|
enemyAbility = Abilities.BALL_FETCH,
|
||||||
|
}) => {
|
||||||
game.override
|
game.override
|
||||||
.passiveAbility(Abilities.COMATOSE)
|
.moveset(playerMove)
|
||||||
|
.passiveAbility(playerPassive)
|
||||||
.enemySpecies(Species.GASTLY)
|
.enemySpecies(Species.GASTLY)
|
||||||
.enemyMoveset(enemyMove)
|
.enemyMoveset(enemyMove)
|
||||||
.enemyAbility(enemyAbility)
|
.enemyAbility(enemyAbility);
|
||||||
.enemyLevel(1);
|
|
||||||
await game.classicMode.startBattle([Species.WIMPOD, Species.TYRUNT]);
|
await game.classicMode.startBattle([Species.WIMPOD, Species.TYRUNT]);
|
||||||
|
|
||||||
const wimpod = game.scene.getPlayerPokemon()!;
|
const wimpod = game.scene.getPlayerPokemon()!;
|
||||||
expect(wimpod).toBeDefined();
|
expect(wimpod).toBeDefined();
|
||||||
wimpod.hp *= 0.55;
|
wimpod.hp = toDmgValue(wimpod.getMaxHp() / 2 + 5);
|
||||||
|
// mock enemy attack damage func to only do 1 dmg (for whirlpool)
|
||||||
|
vi.spyOn(wimpod, "getAttackDamage").mockReturnValueOnce({
|
||||||
|
cancelled: false,
|
||||||
|
result: HitResult.EFFECTIVE,
|
||||||
|
damage: 1,
|
||||||
|
});
|
||||||
|
|
||||||
game.move.select(Moves.THUNDER_PUNCH);
|
game.move.select(playerMove);
|
||||||
game.doSelectPartyPokemon(1);
|
game.doSelectPartyPokemon(1);
|
||||||
await game.setTurnOrder([BattlerIndex.ENEMY, BattlerIndex.PLAYER]);
|
await game.toNextTurn();
|
||||||
await game.phaseInterceptor.to("TurnEndPhase");
|
|
||||||
|
|
||||||
confirmSwitch();
|
confirmSwitch();
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
|
it.each<[name: string, ability: Abilities]>([
|
||||||
|
["Innards Out", Abilities.INNARDS_OUT],
|
||||||
|
["Aftermath", Abilities.AFTERMATH],
|
||||||
|
["Rough Skin", Abilities.ROUGH_SKIN],
|
||||||
|
])("should trigger after taking damage from %s ability", async (_, ability) => {
|
||||||
|
game.override.enemyAbility(ability).enemyMoveset(Moves.SPLASH);
|
||||||
|
await game.classicMode.startBattle([Species.WIMPOD, Species.TYRUNT]);
|
||||||
|
|
||||||
|
const wimpod = game.scene.getPlayerPokemon()!;
|
||||||
|
wimpod.hp *= 0.51;
|
||||||
|
game.scene.getEnemyPokemon()!.hp = wimpod.hp - 1; // Ensure innards out doesn't KO
|
||||||
|
|
||||||
|
game.move.select(Moves.GUILLOTINE);
|
||||||
|
game.doSelectPartyPokemon(1);
|
||||||
|
await game.toNextWave();
|
||||||
|
|
||||||
|
confirmSwitch();
|
||||||
|
});
|
||||||
|
|
||||||
it("should not trigger from Sheer Force-boosted moves", async () => {
|
it("should not trigger from Sheer Force-boosted moves", async () => {
|
||||||
game.override.enemyAbility(Abilities.SHEER_FORCE).enemyMoveset(Moves.SLUDGE_BOMB).startingLevel(95);
|
game.override.enemyAbility(Abilities.SHEER_FORCE).enemyMoveset(Moves.SLUDGE_BOMB).startingLevel(95);
|
||||||
await game.classicMode.startBattle([Species.WIMPOD, Species.TYRUNT]);
|
await game.classicMode.startBattle([Species.WIMPOD, Species.TYRUNT]);
|
||||||
@ -241,18 +313,7 @@ describe("Abilities - Wimp Out", () => {
|
|||||||
confirmNoSwitch();
|
confirmNoSwitch();
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should trigger from recoil damage", async () => {
|
it("should trigger from Flame Burst splash damage in doubles", async () => {
|
||||||
game.override.moveset(Moves.HEAD_SMASH).enemyMoveset(Moves.SPLASH);
|
|
||||||
await game.classicMode.startBattle([Species.WIMPOD, Species.TYRUNT]);
|
|
||||||
|
|
||||||
game.move.select(Moves.HEAD_SMASH);
|
|
||||||
game.doSelectPartyPokemon(1);
|
|
||||||
await game.phaseInterceptor.to("TurnEndPhase");
|
|
||||||
|
|
||||||
confirmSwitch();
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should trigger from Flame Burst ally damage in doubles", async () => {
|
|
||||||
game.override.battleStyle("double").enemyMoveset([Moves.FLAME_BURST, Moves.SPLASH]);
|
game.override.battleStyle("double").enemyMoveset([Moves.FLAME_BURST, Moves.SPLASH]);
|
||||||
await game.classicMode.startBattle([Species.WIMPOD, Species.ZYGARDE, Species.TYRUNT]);
|
await game.classicMode.startBattle([Species.WIMPOD, Species.ZYGARDE, Species.TYRUNT]);
|
||||||
|
|
||||||
@ -267,20 +328,36 @@ describe("Abilities - Wimp Out", () => {
|
|||||||
game.doSelectPartyPokemon(2);
|
game.doSelectPartyPokemon(2);
|
||||||
await game.phaseInterceptor.to("TurnEndPhase");
|
await game.phaseInterceptor.to("TurnEndPhase");
|
||||||
|
|
||||||
confirmSwitch();
|
expect(wimpod.isOnField()).toBe(false);
|
||||||
|
expect(wimpod.getHpRatio()).toBeLessThan(0.5);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should not activate when the Pokémon cuts its own HP", async () => {
|
it("should not activate when the Pokémon cuts its own HP below half", async () => {
|
||||||
game.override.moveset(Moves.SUBSTITUTE).enemyMoveset(Moves.SPLASH);
|
game.override.moveset(Moves.SUBSTITUTE).enemyMoveset([Moves.TIDY_UP, Moves.ROUND]);
|
||||||
await game.classicMode.startBattle([Species.WIMPOD, Species.TYRUNT]);
|
await game.classicMode.startBattle([Species.WIMPOD, Species.TYRUNT]);
|
||||||
|
|
||||||
|
// Turn 1: Substitute knocks below half; no switch
|
||||||
const wimpod = game.scene.getPlayerPokemon()!;
|
const wimpod = game.scene.getPlayerPokemon()!;
|
||||||
wimpod.hp *= 0.52;
|
wimpod.hp *= 0.52;
|
||||||
|
|
||||||
game.move.select(Moves.SUBSTITUTE);
|
game.move.select(Moves.SUBSTITUTE);
|
||||||
await game.phaseInterceptor.to("TurnEndPhase");
|
await game.forceEnemyMove(Moves.TIDY_UP);
|
||||||
|
game.doSelectPartyPokemon(1);
|
||||||
|
await game.setTurnOrder([BattlerIndex.PLAYER, BattlerIndex.ENEMY]);
|
||||||
|
await game.toNextTurn();
|
||||||
|
|
||||||
confirmNoSwitch();
|
confirmNoSwitch();
|
||||||
|
|
||||||
|
// Turn 2: get back enough HP that substitute doesn't put us under
|
||||||
|
wimpod.hp = wimpod.getMaxHp() * 0.8;
|
||||||
|
|
||||||
|
game.move.select(Moves.SUBSTITUTE);
|
||||||
|
game.doSelectPartyPokemon(1);
|
||||||
|
await game.forceEnemyMove(Moves.ROUND);
|
||||||
|
await game.setTurnOrder([BattlerIndex.PLAYER, BattlerIndex.ENEMY]);
|
||||||
|
await game.phaseInterceptor.to("TurnEndPhase");
|
||||||
|
|
||||||
|
confirmSwitch();
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should not trigger when neutralized", async () => {
|
it("should not trigger when neutralized", async () => {
|
||||||
@ -295,9 +372,9 @@ describe("Abilities - Wimp Out", () => {
|
|||||||
|
|
||||||
it("should disregard Shell Bell recovery while still activating it before switching", async () => {
|
it("should disregard Shell Bell recovery while still activating it before switching", async () => {
|
||||||
game.override
|
game.override
|
||||||
.moveset([Moves.DOUBLE_EDGE])
|
.moveset(Moves.DOUBLE_EDGE)
|
||||||
.enemyMoveset([Moves.SPLASH])
|
.enemyMoveset([Moves.SPLASH])
|
||||||
.startingHeldItems([{ name: "SHELL_BELL", count: 4 }]);
|
.startingHeldItems([{ name: "SHELL_BELL", count: 4 }]); // heals 50% of damage dealt, more than recoil takes away
|
||||||
await game.classicMode.startBattle([Species.WIMPOD, Species.TYRUNT]);
|
await game.classicMode.startBattle([Species.WIMPOD, Species.TYRUNT]);
|
||||||
|
|
||||||
const wimpod = game.scene.getPlayerPokemon()!;
|
const wimpod = game.scene.getPlayerPokemon()!;
|
||||||
@ -307,29 +384,34 @@ describe("Abilities - Wimp Out", () => {
|
|||||||
game.doSelectPartyPokemon(1);
|
game.doSelectPartyPokemon(1);
|
||||||
await game.phaseInterceptor.to("TurnEndPhase");
|
await game.phaseInterceptor.to("TurnEndPhase");
|
||||||
|
|
||||||
expect(game.scene.getPlayerParty()[1]).toBe(wimpod);
|
// Wimp out activated before shell bell healing
|
||||||
expect(wimpod.hp).toBeGreaterThan(toDmgValue(wimpod.getMaxHp() / 2));
|
expect(wimpod.getHpRatio()).toBeGreaterThan(0.5);
|
||||||
expect(game.phaseInterceptor.log).toContain("SwitchSummonPhase");
|
confirmSwitch();
|
||||||
expect(game.scene.getPlayerPokemon()!.species.speciesId).toBe(Species.TYRUNT);
|
expect(game.phaseInterceptor.log).toContain("PokemonHealPhase");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should activate from entry hazard damage", async () => {
|
||||||
|
game.scene.arena.addTag(ArenaTagType.STEALTH_ROCK, 1, Moves.STEALTH_ROCK, 0, ArenaTagSide.ENEMY);
|
||||||
|
game.scene.arena.addTag(ArenaTagType.SPIKES, 1, Moves.SPIKES, 0, ArenaTagSide.ENEMY);
|
||||||
|
game.override.enemySpecies(Species.CENTISKORCH).enemyAbility(Abilities.WIMP_OUT);
|
||||||
|
await game.classicMode.startBattle([Species.TYRUNT]);
|
||||||
|
|
||||||
|
expect(game.phaseInterceptor.log).not.toContain("MovePhase");
|
||||||
|
expect(game.phaseInterceptor.log).toContain("BattleEndPhase");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should not switch if Magic Guard prevents damage", async () => {
|
it("should not switch if Magic Guard prevents damage", async () => {
|
||||||
game.scene.arena.addTag(ArenaTagType.STEALTH_ROCK, 1, Moves.STEALTH_ROCK, 0, ArenaTagSide.ENEMY);
|
game.override.passiveAbility(Abilities.MAGIC_GUARD).enemyMoveset(Moves.LEECH_SEED);
|
||||||
game.scene.arena.addTag(ArenaTagType.SPIKES, 1, Moves.SPIKES, 0, ArenaTagSide.ENEMY);
|
|
||||||
game.override
|
|
||||||
.passiveAbility(Abilities.MAGIC_GUARD)
|
|
||||||
.enemyMoveset([Moves.LEECH_SEED])
|
|
||||||
.weather(WeatherType.HAIL)
|
|
||||||
.statusEffect(StatusEffect.POISON);
|
|
||||||
await game.classicMode.startBattle([Species.WIMPOD, Species.TYRUNT]);
|
await game.classicMode.startBattle([Species.WIMPOD, Species.TYRUNT]);
|
||||||
game.scene.getPlayerPokemon()!.hp *= 0.51;
|
|
||||||
|
const wimpod = game.scene.getPlayerPokemon()!;
|
||||||
|
wimpod.hp *= 0.51;
|
||||||
|
|
||||||
game.move.select(Moves.SPLASH);
|
game.move.select(Moves.SPLASH);
|
||||||
await game.phaseInterceptor.to("TurnEndPhase");
|
await game.toNextTurn();
|
||||||
|
|
||||||
expect(game.scene.getPlayerParty()[0].getHpRatio()).toEqual(0.51);
|
confirmNoSwitch();
|
||||||
expect(game.phaseInterceptor.log).not.toContain("SwitchSummonPhase");
|
expect(wimpod.getHpRatio()).toBeCloseTo(0.51);
|
||||||
expect(game.scene.getPlayerPokemon()!.species.speciesId).toBe(Species.WIMPOD);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should not cancel a double battle on activation", async () => {
|
it("should not cancel a double battle on activation", async () => {
|
||||||
@ -352,17 +434,7 @@ describe("Abilities - Wimp Out", () => {
|
|||||||
expect(enemySecPokemon.hp).toEqual(enemySecPokemon.getMaxHp());
|
expect(enemySecPokemon.hp).toEqual(enemySecPokemon.getMaxHp());
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should activate from entry hazard damage", async () => {
|
it("triggers move effects on the wimp out user before switching", async () => {
|
||||||
game.scene.arena.addTag(ArenaTagType.STEALTH_ROCK, 1, Moves.STEALTH_ROCK, 0, ArenaTagSide.ENEMY);
|
|
||||||
game.scene.arena.addTag(ArenaTagType.SPIKES, 1, Moves.SPIKES, 0, ArenaTagSide.ENEMY);
|
|
||||||
game.override.enemySpecies(Species.CENTISKORCH).enemyAbility(Abilities.WIMP_OUT).startingWave(4);
|
|
||||||
await game.classicMode.startBattle([Species.TYRUNT]);
|
|
||||||
|
|
||||||
expect(game.phaseInterceptor.log).not.toContain("MovePhase");
|
|
||||||
expect(game.phaseInterceptor.log).toContain("BattleEndPhase");
|
|
||||||
});
|
|
||||||
|
|
||||||
it("triggers status on the wimp out user before a new pokemon is switched in", async () => {
|
|
||||||
game.override.enemyMoveset(Moves.SLUDGE_BOMB).startingLevel(80);
|
game.override.enemyMoveset(Moves.SLUDGE_BOMB).startingLevel(80);
|
||||||
await game.classicMode.startBattle([Species.WIMPOD, Species.TYRUNT]);
|
await game.classicMode.startBattle([Species.WIMPOD, Species.TYRUNT]);
|
||||||
vi.spyOn(allMoves[Moves.SLUDGE_BOMB], "chance", "get").mockReturnValue(100);
|
vi.spyOn(allMoves[Moves.SLUDGE_BOMB], "chance", "get").mockReturnValue(100);
|
||||||
@ -371,92 +443,64 @@ describe("Abilities - Wimp Out", () => {
|
|||||||
game.doSelectPartyPokemon(1);
|
game.doSelectPartyPokemon(1);
|
||||||
await game.phaseInterceptor.to("TurnEndPhase");
|
await game.phaseInterceptor.to("TurnEndPhase");
|
||||||
|
|
||||||
expect(game.scene.getPlayerParty()[1].status?.effect).toEqual(StatusEffect.POISON);
|
|
||||||
confirmSwitch();
|
confirmSwitch();
|
||||||
|
expect(game.scene.getPlayerParty()[1].status?.effect).toBe(StatusEffect.POISON);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("triggers after last hit of multi hit moves", async () => {
|
it.each<{ type: string; move?: Moves; ability?: Abilities; items?: ModifierOverride[] }>([
|
||||||
game.override.enemyMoveset(Moves.BULLET_SEED).enemyAbility(Abilities.SKILL_LINK);
|
{ type: "normal", move: Moves.DUAL_CHOP },
|
||||||
await game.classicMode.startBattle([Species.WIMPOD, Species.TYRUNT]);
|
{ type: "Parental Bond", ability: Abilities.PARENTAL_BOND },
|
||||||
|
{ type: "Multi Lens", items: [{ name: "MULTI_LENS", count: 1 }] },
|
||||||
|
])(
|
||||||
|
"should trigger after the last hit of $type multi-strike moves",
|
||||||
|
async ({ move = Moves.TACKLE, ability = Abilities.COMPOUND_EYES, items = [] }) => {
|
||||||
|
game.override.enemyMoveset(move).enemyAbility(ability).enemyHeldItems(items);
|
||||||
|
await game.classicMode.startBattle([Species.WIMPOD, Species.TYRUNT]);
|
||||||
|
|
||||||
game.scene.getPlayerPokemon()!.hp *= 0.51;
|
const wimpod = game.scene.getPlayerPokemon()!;
|
||||||
|
wimpod.hp *= 0.51;
|
||||||
|
|
||||||
game.move.select(Moves.ENDURE);
|
game.move.select(Moves.ENDURE);
|
||||||
game.doSelectPartyPokemon(1);
|
game.doSelectPartyPokemon(1);
|
||||||
await game.phaseInterceptor.to("TurnEndPhase");
|
|
||||||
|
|
||||||
const enemyPokemon = game.scene.getEnemyPokemon()!;
|
|
||||||
expect(enemyPokemon.turnData.hitsLeft).toBe(0);
|
|
||||||
expect(enemyPokemon.turnData.hitCount).toBe(5);
|
|
||||||
confirmSwitch();
|
|
||||||
});
|
|
||||||
|
|
||||||
it("triggers after last hit of multi hit moves from multi lens", async () => {
|
|
||||||
game.override.enemyMoveset(Moves.TACKLE).enemyHeldItems([{ name: "MULTI_LENS", count: 1 }]);
|
|
||||||
await game.classicMode.startBattle([Species.WIMPOD, Species.TYRUNT]);
|
|
||||||
|
|
||||||
game.scene.getPlayerPokemon()!.hp *= 0.51;
|
|
||||||
|
|
||||||
game.move.select(Moves.ENDURE);
|
|
||||||
game.doSelectPartyPokemon(1);
|
|
||||||
await game.phaseInterceptor.to("TurnEndPhase");
|
|
||||||
|
|
||||||
const enemyPokemon = game.scene.getEnemyPokemon()!;
|
|
||||||
expect(enemyPokemon.turnData.hitsLeft).toBe(0);
|
|
||||||
expect(enemyPokemon.turnData.hitCount).toBe(2);
|
|
||||||
confirmSwitch();
|
|
||||||
});
|
|
||||||
|
|
||||||
it("triggers after last hit of Parental Bond", async () => {
|
|
||||||
game.override.enemyMoveset(Moves.TACKLE).enemyAbility(Abilities.PARENTAL_BOND);
|
|
||||||
await game.classicMode.startBattle([Species.WIMPOD, Species.TYRUNT]);
|
|
||||||
|
|
||||||
game.scene.getPlayerPokemon()!.hp *= 0.51;
|
|
||||||
|
|
||||||
game.move.select(Moves.ENDURE);
|
|
||||||
game.doSelectPartyPokemon(1);
|
|
||||||
await game.phaseInterceptor.to("TurnEndPhase");
|
|
||||||
|
|
||||||
const enemyPokemon = game.scene.getEnemyPokemon()!;
|
|
||||||
expect(enemyPokemon.turnData.hitsLeft).toBe(0);
|
|
||||||
expect(enemyPokemon.turnData.hitCount).toBe(2);
|
|
||||||
confirmSwitch();
|
|
||||||
});
|
|
||||||
|
|
||||||
// TODO: This interaction is not implemented yet
|
|
||||||
it.todo("should not activate if the Pokémon's HP falls below half due to hurting itself in confusion", async () => {
|
|
||||||
game.override.moveset([Moves.SWORDS_DANCE]).enemyMoveset([Moves.SWAGGER]);
|
|
||||||
await game.classicMode.startBattle([Species.WIMPOD, Species.TYRUNT]);
|
|
||||||
const playerPokemon = game.scene.getPlayerPokemon()!;
|
|
||||||
playerPokemon.hp *= 0.51;
|
|
||||||
playerPokemon.setStatStage(Stat.ATK, 6);
|
|
||||||
playerPokemon.addTag(BattlerTagType.CONFUSED);
|
|
||||||
|
|
||||||
// TODO: add helper function to force confusion self-hits
|
|
||||||
|
|
||||||
while (playerPokemon.getHpRatio() > 0.49) {
|
|
||||||
game.move.select(Moves.SWORDS_DANCE);
|
|
||||||
await game.phaseInterceptor.to("TurnEndPhase");
|
await game.phaseInterceptor.to("TurnEndPhase");
|
||||||
}
|
|
||||||
|
const enemyPokemon = game.scene.getEnemyPokemon()!;
|
||||||
|
expect(enemyPokemon.turnData.hitsLeft).toBe(0);
|
||||||
|
expect(enemyPokemon.turnData.hitCount).toBe(2);
|
||||||
|
confirmSwitch();
|
||||||
|
|
||||||
|
// Switch triggered after the MEPs for both hits finished
|
||||||
|
const phaseLogs = game.phaseInterceptor.log;
|
||||||
|
expect(phaseLogs.filter(l => l === "MoveEffectPhase")).toHaveLength(3); // 1 for endure + 2 for dual hit
|
||||||
|
expect(phaseLogs.lastIndexOf("SwitchSummonPhase")).toBeGreaterThan(phaseLogs.lastIndexOf("MoveEffectPhase"));
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
it("should not activate from confusion damage", async () => {
|
||||||
|
game.override.enemyMoveset(Moves.CONFUSE_RAY).confusionActivation(true);
|
||||||
|
await game.classicMode.startBattle([Species.WIMPOD, Species.TYRUNT]);
|
||||||
|
|
||||||
|
const wimpod = game.scene.getPlayerPokemon()!;
|
||||||
|
wimpod.hp *= 0.51;
|
||||||
|
|
||||||
|
game.move.select(Moves.SPLASH);
|
||||||
|
await game.setTurnOrder([BattlerIndex.ENEMY, BattlerIndex.PLAYER]);
|
||||||
|
await game.phaseInterceptor.to("TurnEndPhase");
|
||||||
|
|
||||||
confirmNoSwitch();
|
confirmNoSwitch();
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should not activate on wave X0 bosses", async () => {
|
it("should not activate on wave X0 bosses", async () => {
|
||||||
game.override.enemyAbility(Abilities.WIMP_OUT).startingLevel(5850).startingWave(10);
|
game.override.enemyAbility(Abilities.WIMP_OUT).startingLevel(5850).startingWave(10).enemyHealthSegments(3);
|
||||||
await game.classicMode.startBattle([Species.GOLISOPOD]);
|
await game.classicMode.startBattle([Species.GOLISOPOD]);
|
||||||
|
|
||||||
const enemyPokemon = game.scene.getEnemyPokemon()!;
|
const enemyPokemon = game.scene.getEnemyPokemon()!;
|
||||||
|
|
||||||
// Use 2 turns of False Swipe due to opponent's health bar shield
|
|
||||||
game.move.select(Moves.FALSE_SWIPE);
|
|
||||||
await game.toNextTurn();
|
|
||||||
game.move.select(Moves.FALSE_SWIPE);
|
game.move.select(Moves.FALSE_SWIPE);
|
||||||
await game.toNextTurn();
|
await game.toNextTurn();
|
||||||
|
|
||||||
const isVisible = enemyPokemon.visible;
|
expect(enemyPokemon.visible).toBe(true);
|
||||||
const hasFled = enemyPokemon.switchOutStatus;
|
expect(enemyPokemon.switchOutStatus).toBe(false);
|
||||||
expect(isVisible && !hasFled).toBe(true);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should not skip battles when triggered in a double battle", async () => {
|
it("should not skip battles when triggered in a double battle", async () => {
|
||||||
|
@ -106,9 +106,10 @@ describe("Items - Reviver Seed", () => {
|
|||||||
|
|
||||||
// Self-damage tests
|
// Self-damage tests
|
||||||
it.each([
|
it.each([
|
||||||
{ moveType: "Recoil", move: Moves.DOUBLE_EDGE },
|
{ moveType: "Relative Recoil", move: Moves.DOUBLE_EDGE },
|
||||||
|
{ moveType: "HP% Recoil", move: Moves.CHLOROBLAST },
|
||||||
{ moveType: "Self-KO", move: Moves.EXPLOSION },
|
{ moveType: "Self-KO", move: Moves.EXPLOSION },
|
||||||
{ moveType: "Self-Deduction", move: Moves.CURSE },
|
{ moveType: "Ghost-type Curse", move: Moves.CURSE },
|
||||||
{ moveType: "Liquid Ooze", move: Moves.GIGA_DRAIN },
|
{ moveType: "Liquid Ooze", move: Moves.GIGA_DRAIN },
|
||||||
])("should not activate the holder's reviver seed from $moveType", async ({ move }) => {
|
])("should not activate the holder's reviver seed from $moveType", async ({ move }) => {
|
||||||
game.override
|
game.override
|
||||||
|
@ -1,8 +1,6 @@
|
|||||||
import { BattlerIndex } from "#app/battle";
|
import { BattlerIndex } from "#app/battle";
|
||||||
import { allMoves } from "#app/data/moves/move";
|
import { allMoves } from "#app/data/moves/move";
|
||||||
import { Status } from "#app/data/status-effect";
|
|
||||||
import { Challenges } from "#enums/challenges";
|
import { Challenges } from "#enums/challenges";
|
||||||
import { StatusEffect } from "#enums/status-effect";
|
|
||||||
import { PokemonType } from "#enums/pokemon-type";
|
import { PokemonType } from "#enums/pokemon-type";
|
||||||
import { Abilities } from "#enums/abilities";
|
import { Abilities } from "#enums/abilities";
|
||||||
import { Moves } from "#enums/moves";
|
import { Moves } from "#enums/moves";
|
||||||
@ -10,6 +8,11 @@ import { Species } from "#enums/species";
|
|||||||
import GameManager from "#test/testUtils/gameManager";
|
import GameManager from "#test/testUtils/gameManager";
|
||||||
import Phaser from "phaser";
|
import Phaser from "phaser";
|
||||||
import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest";
|
import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest";
|
||||||
|
import { BattleType } from "#enums/battle-type";
|
||||||
|
import { TrainerSlot } from "#enums/trainer-slot";
|
||||||
|
import { TrainerType } from "#enums/trainer-type";
|
||||||
|
import { splitArray } from "#app/utils/common";
|
||||||
|
import { BattlerTagType } from "#enums/battler-tag-type";
|
||||||
|
|
||||||
describe("Moves - Dragon Tail", () => {
|
describe("Moves - Dragon Tail", () => {
|
||||||
let phaserGame: Phaser.Game;
|
let phaserGame: Phaser.Game;
|
||||||
@ -38,23 +41,6 @@ describe("Moves - Dragon Tail", () => {
|
|||||||
vi.spyOn(allMoves[Moves.DRAGON_TAIL], "accuracy", "get").mockReturnValue(100);
|
vi.spyOn(allMoves[Moves.DRAGON_TAIL], "accuracy", "get").mockReturnValue(100);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should cause opponent to flee, and not crash", async () => {
|
|
||||||
await game.classicMode.startBattle([Species.DRATINI]);
|
|
||||||
|
|
||||||
const enemyPokemon = game.scene.getEnemyPokemon()!;
|
|
||||||
|
|
||||||
game.move.select(Moves.DRAGON_TAIL);
|
|
||||||
|
|
||||||
await game.phaseInterceptor.to("BerryPhase");
|
|
||||||
|
|
||||||
const isVisible = enemyPokemon.visible;
|
|
||||||
const hasFled = enemyPokemon.switchOutStatus;
|
|
||||||
expect(!isVisible && hasFled).toBe(true);
|
|
||||||
|
|
||||||
// simply want to test that the game makes it this far without crashing
|
|
||||||
await game.phaseInterceptor.to("BattleEndPhase");
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should cause opponent to flee, display ability, and not crash", async () => {
|
it("should cause opponent to flee, display ability, and not crash", async () => {
|
||||||
game.override.enemyAbility(Abilities.ROUGH_SKIN);
|
game.override.enemyAbility(Abilities.ROUGH_SKIN);
|
||||||
await game.classicMode.startBattle([Species.DRATINI]);
|
await game.classicMode.startBattle([Species.DRATINI]);
|
||||||
@ -68,7 +54,8 @@ describe("Moves - Dragon Tail", () => {
|
|||||||
|
|
||||||
const isVisible = enemyPokemon.visible;
|
const isVisible = enemyPokemon.visible;
|
||||||
const hasFled = enemyPokemon.switchOutStatus;
|
const hasFled = enemyPokemon.switchOutStatus;
|
||||||
expect(!isVisible && hasFled).toBe(true);
|
expect(isVisible).toBe(false);
|
||||||
|
expect(hasFled).toBe(true);
|
||||||
expect(leadPokemon.hp).toBeLessThan(leadPokemon.getMaxHp());
|
expect(leadPokemon.hp).toBeLessThan(leadPokemon.getMaxHp());
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -101,6 +88,47 @@ describe("Moves - Dragon Tail", () => {
|
|||||||
expect(enemySecPokemon.hp).toBeLessThan(enemySecPokemon.getMaxHp());
|
expect(enemySecPokemon.hp).toBeLessThan(enemySecPokemon.getMaxHp());
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("should force trainers to switch randomly without selecting from a partner's party", async () => {
|
||||||
|
game.override
|
||||||
|
.battleStyle("double")
|
||||||
|
.enemyMoveset(Moves.SPLASH)
|
||||||
|
.enemyAbility(Abilities.STURDY)
|
||||||
|
.battleType(BattleType.TRAINER)
|
||||||
|
.randomTrainer({ trainerType: TrainerType.TATE, alwaysDouble: true })
|
||||||
|
.enemySpecies(0);
|
||||||
|
await game.classicMode.startBattle([Species.WIMPOD, Species.TYRANITAR]);
|
||||||
|
|
||||||
|
// Grab each trainer's pokemon based on species name
|
||||||
|
const [tateParty, lizaParty] = splitArray(
|
||||||
|
game.scene.getEnemyParty(),
|
||||||
|
pkmn => pkmn.trainerSlot === TrainerSlot.TRAINER,
|
||||||
|
).map(a => a.map(p => p.species.name));
|
||||||
|
|
||||||
|
expect(tateParty).not.toEqual(lizaParty);
|
||||||
|
|
||||||
|
// Force enemy trainers to switch to the first mon available.
|
||||||
|
// Due to how enemy trainer parties are laid out, this prevents false positives
|
||||||
|
// as Tate's pokemon are placed immediately before Liza's corresponding members.
|
||||||
|
vi.fn(Phaser.Math.RND.integerInRange).mockImplementation(min => min);
|
||||||
|
|
||||||
|
// Spy on the function responsible for making informed switches
|
||||||
|
const choiceSwitchSpy = vi.spyOn(game.scene.currentBattle.trainer!, "getNextSummonIndex");
|
||||||
|
|
||||||
|
game.move.select(Moves.DRAGON_TAIL, BattlerIndex.PLAYER, BattlerIndex.ENEMY_2);
|
||||||
|
game.move.select(Moves.SPLASH, BattlerIndex.PLAYER_2);
|
||||||
|
await game.phaseInterceptor.to("BerryPhase");
|
||||||
|
|
||||||
|
const [tatePartyNew, lizaPartyNew] = splitArray(
|
||||||
|
game.scene.getEnemyParty(),
|
||||||
|
pkmn => pkmn.trainerSlot === TrainerSlot.TRAINER,
|
||||||
|
).map(a => a.map(p => p.species.name));
|
||||||
|
|
||||||
|
// Forced switch move should have switched Liza's Pokemon with another one of her own at random
|
||||||
|
expect(tatePartyNew).toEqual(tateParty);
|
||||||
|
expect(lizaPartyNew).not.toEqual(lizaParty);
|
||||||
|
expect(choiceSwitchSpy).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
it("should redirect targets upon opponent flee", async () => {
|
it("should redirect targets upon opponent flee", async () => {
|
||||||
game.override.battleStyle("double").enemyMoveset(Moves.SPLASH).enemyAbility(Abilities.ROUGH_SKIN);
|
game.override.battleStyle("double").enemyMoveset(Moves.SPLASH).enemyAbility(Abilities.ROUGH_SKIN);
|
||||||
await game.classicMode.startBattle([Species.DRATINI, Species.DRATINI, Species.WAILORD, Species.WAILORD]);
|
await game.classicMode.startBattle([Species.DRATINI, Species.DRATINI, Species.WAILORD, Species.WAILORD]);
|
||||||
@ -128,7 +156,7 @@ describe("Moves - Dragon Tail", () => {
|
|||||||
expect(enemySecPokemon.hp).toBeLessThan(enemySecPokemon.getMaxHp());
|
expect(enemySecPokemon.hp).toBeLessThan(enemySecPokemon.getMaxHp());
|
||||||
});
|
});
|
||||||
|
|
||||||
it("doesn't switch out if the target has suction cups", async () => {
|
it("should not switch out a target with suction cups", async () => {
|
||||||
game.override.enemyAbility(Abilities.SUCTION_CUPS);
|
game.override.enemyAbility(Abilities.SUCTION_CUPS);
|
||||||
await game.classicMode.startBattle([Species.REGIELEKI]);
|
await game.classicMode.startBattle([Species.REGIELEKI]);
|
||||||
|
|
||||||
@ -137,9 +165,25 @@ describe("Moves - Dragon Tail", () => {
|
|||||||
game.move.select(Moves.DRAGON_TAIL);
|
game.move.select(Moves.DRAGON_TAIL);
|
||||||
await game.phaseInterceptor.to("TurnEndPhase");
|
await game.phaseInterceptor.to("TurnEndPhase");
|
||||||
|
|
||||||
|
expect(enemy.isOnField()).toBe(true);
|
||||||
expect(enemy.isFullHp()).toBe(false);
|
expect(enemy.isFullHp()).toBe(false);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("should not switch out a Commanded Dondozo", async () => {
|
||||||
|
game.override.battleStyle("double").enemySpecies(Species.DONDOZO);
|
||||||
|
await game.classicMode.startBattle([Species.REGIELEKI]);
|
||||||
|
|
||||||
|
// pretend dondozo 2 commanded dondozo 1 (silly I know, but it works)
|
||||||
|
const [dondozo1, dondozo2] = game.scene.getEnemyField();
|
||||||
|
dondozo1.addTag(BattlerTagType.COMMANDED, 1, Moves.NONE, dondozo2.id);
|
||||||
|
|
||||||
|
game.move.select(Moves.DRAGON_TAIL);
|
||||||
|
await game.phaseInterceptor.to("TurnEndPhase");
|
||||||
|
|
||||||
|
expect(dondozo1.isOnField()).toBe(true);
|
||||||
|
expect(dondozo1.isFullHp()).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
it("should force a switch upon fainting an opponent normally", async () => {
|
it("should force a switch upon fainting an opponent normally", async () => {
|
||||||
game.override.startingWave(5).startingLevel(1000); // To make sure Dragon Tail KO's the opponent
|
game.override.startingWave(5).startingLevel(1000); // To make sure Dragon Tail KO's the opponent
|
||||||
await game.classicMode.startBattle([Species.DRATINI]);
|
await game.classicMode.startBattle([Species.DRATINI]);
|
||||||
@ -227,81 +271,55 @@ describe("Moves - Dragon Tail", () => {
|
|||||||
expect(charmander.getInverseHp()).toBeGreaterThan(0);
|
expect(charmander.getInverseHp()).toBeGreaterThan(0);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should not force a switch to a challenge-ineligible Pokemon", async () => {
|
it("should not force a switch to a fainted or challenge-ineligible Pokemon", async () => {
|
||||||
game.override.enemyMoveset(Moves.DRAGON_TAIL).startingLevel(100).enemyLevel(1);
|
game.override.enemyMoveset(Moves.DRAGON_TAIL).startingLevel(100).enemyLevel(1);
|
||||||
// Mono-Water challenge, Eevee is ineligible
|
// Mono-Water challenge, Eevee is ineligible
|
||||||
game.challengeMode.addChallenge(Challenges.SINGLE_TYPE, PokemonType.WATER + 1, 0);
|
game.challengeMode.addChallenge(Challenges.SINGLE_TYPE, PokemonType.WATER + 1, 0);
|
||||||
await game.challengeMode.startBattle([Species.LAPRAS, Species.EEVEE, Species.TOXAPEX, Species.PRIMARINA]);
|
await game.challengeMode.startBattle([Species.LAPRAS, Species.EEVEE, Species.TOXAPEX, Species.PRIMARINA]);
|
||||||
|
|
||||||
const [lapras, eevee, toxapex, primarina] = game.scene.getPlayerParty();
|
const [lapras, eevee, toxapex, primarina] = game.scene.getPlayerParty();
|
||||||
|
expect(toxapex).toBeDefined();
|
||||||
|
|
||||||
// Turn 1: Mock an RNG call that would normally call for switching to Eevee, but it is ineligible
|
// Mock an RNG call to switch to the first eligible pokemon.
|
||||||
|
// Eevee is ineligible and Toxapex is fainted, so it should proc on Primarina instead
|
||||||
vi.spyOn(game.scene, "randBattleSeedInt").mockImplementation((_range, min = 0) => {
|
vi.spyOn(game.scene, "randBattleSeedInt").mockImplementation((_range, min = 0) => {
|
||||||
return min;
|
return min;
|
||||||
});
|
});
|
||||||
game.move.select(Moves.SPLASH);
|
game.move.select(Moves.SPLASH);
|
||||||
|
await game.killPokemon(toxapex);
|
||||||
await game.toNextTurn();
|
await game.toNextTurn();
|
||||||
|
|
||||||
expect(lapras.isOnField()).toBe(false);
|
expect(lapras.isOnField()).toBe(false);
|
||||||
expect(eevee.isOnField()).toBe(false);
|
expect(eevee.isOnField()).toBe(false);
|
||||||
expect(toxapex.isOnField()).toBe(true);
|
expect(toxapex.isOnField()).toBe(false);
|
||||||
expect(primarina.isOnField()).toBe(false);
|
expect(primarina.isOnField()).toBe(true);
|
||||||
expect(lapras.getInverseHp()).toBeGreaterThan(0);
|
expect(lapras.getInverseHp()).toBeGreaterThan(0);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should not force a switch to a fainted Pokemon", async () => {
|
it("should deal damage without switching if there are no available backup Pokemon to switch into", async () => {
|
||||||
game.override.enemyMoveset([Moves.SPLASH, Moves.DRAGON_TAIL]).startingLevel(100).enemyLevel(1);
|
game.override.enemyMoveset(Moves.DRAGON_TAIL).battleStyle("double").startingLevel(100).enemyLevel(1);
|
||||||
await game.classicMode.startBattle([Species.LAPRAS, Species.EEVEE, Species.TOXAPEX, Species.PRIMARINA]);
|
// Mono-Water challenge
|
||||||
|
game.challengeMode.addChallenge(Challenges.SINGLE_TYPE, PokemonType.WATER + 1, 0);
|
||||||
|
await game.challengeMode.startBattle([Species.LAPRAS, Species.KYOGRE, Species.EEVEE, Species.CLOYSTER]);
|
||||||
|
|
||||||
const [lapras, eevee, toxapex, primarina] = game.scene.getPlayerParty();
|
const [lapras, kyogre, eevee, cloyster] = game.scene.getPlayerParty();
|
||||||
|
expect(cloyster).toBeDefined();
|
||||||
|
|
||||||
// Turn 1: Eevee faints
|
game.move.select(Moves.SPLASH, BattlerIndex.PLAYER);
|
||||||
eevee.hp = 0;
|
game.move.select(Moves.SPLASH, BattlerIndex.PLAYER_2);
|
||||||
eevee.status = new Status(StatusEffect.FAINT);
|
await game.forceEnemyMove(Moves.DRAGON_TAIL, BattlerIndex.PLAYER);
|
||||||
expect(eevee.isFainted()).toBe(true);
|
await game.forceEnemyMove(Moves.DRAGON_TAIL, BattlerIndex.PLAYER_2);
|
||||||
game.move.select(Moves.SPLASH);
|
await game.killPokemon(cloyster);
|
||||||
await game.forceEnemyMove(Moves.SPLASH);
|
|
||||||
await game.toNextTurn();
|
|
||||||
|
|
||||||
// Turn 2: Mock an RNG call that would normally call for switching to Eevee, but it is fainted
|
|
||||||
vi.spyOn(game.scene, "randBattleSeedInt").mockImplementation((_range, min = 0) => {
|
|
||||||
return min;
|
|
||||||
});
|
|
||||||
game.move.select(Moves.SPLASH);
|
|
||||||
await game.forceEnemyMove(Moves.DRAGON_TAIL);
|
|
||||||
await game.toNextTurn();
|
|
||||||
|
|
||||||
expect(lapras.isOnField()).toBe(false);
|
|
||||||
expect(eevee.isOnField()).toBe(false);
|
|
||||||
expect(toxapex.isOnField()).toBe(true);
|
|
||||||
expect(primarina.isOnField()).toBe(false);
|
|
||||||
expect(lapras.getInverseHp()).toBeGreaterThan(0);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should not force a switch if there are no available Pokemon to switch into", async () => {
|
|
||||||
game.override.enemyMoveset([Moves.SPLASH, Moves.DRAGON_TAIL]).startingLevel(100).enemyLevel(1);
|
|
||||||
await game.classicMode.startBattle([Species.LAPRAS, Species.EEVEE]);
|
|
||||||
|
|
||||||
const [lapras, eevee] = game.scene.getPlayerParty();
|
|
||||||
|
|
||||||
// Turn 1: Eevee faints
|
|
||||||
eevee.hp = 0;
|
|
||||||
eevee.status = new Status(StatusEffect.FAINT);
|
|
||||||
expect(eevee.isFainted()).toBe(true);
|
|
||||||
game.move.select(Moves.SPLASH);
|
|
||||||
await game.forceEnemyMove(Moves.SPLASH);
|
|
||||||
await game.toNextTurn();
|
|
||||||
|
|
||||||
// Turn 2: Mock an RNG call that would normally call for switching to Eevee, but it is fainted
|
|
||||||
vi.spyOn(game.scene, "randBattleSeedInt").mockImplementation((_range, min = 0) => {
|
|
||||||
return min;
|
|
||||||
});
|
|
||||||
game.move.select(Moves.SPLASH);
|
|
||||||
await game.forceEnemyMove(Moves.DRAGON_TAIL);
|
|
||||||
await game.toNextTurn();
|
await game.toNextTurn();
|
||||||
|
|
||||||
|
// Eevee is ineligble due to challenge and cloyster is fainted, leaving no backup pokemon able to switch in
|
||||||
expect(lapras.isOnField()).toBe(true);
|
expect(lapras.isOnField()).toBe(true);
|
||||||
|
expect(kyogre.isOnField()).toBe(true);
|
||||||
expect(eevee.isOnField()).toBe(false);
|
expect(eevee.isOnField()).toBe(false);
|
||||||
|
expect(cloyster.isOnField()).toBe(false);
|
||||||
expect(lapras.getInverseHp()).toBeGreaterThan(0);
|
expect(lapras.getInverseHp()).toBeGreaterThan(0);
|
||||||
|
expect(kyogre.getInverseHp()).toBeGreaterThan(0);
|
||||||
|
expect(game.scene.getBackupPartyMemberIndices(true)).toHaveLength(0);
|
||||||
|
expect(game.phaseInterceptor.log).not.toContain("SwitchSummonPhase");
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@ -7,9 +7,9 @@ import { Species } from "#enums/species";
|
|||||||
import { BerryPhase } from "#app/phases/berry-phase";
|
import { BerryPhase } from "#app/phases/berry-phase";
|
||||||
import { MoveResult, PokemonMove } from "#app/field/pokemon";
|
import { MoveResult, PokemonMove } from "#app/field/pokemon";
|
||||||
import { PokemonType } from "#enums/pokemon-type";
|
import { PokemonType } from "#enums/pokemon-type";
|
||||||
import { MoveEffectPhase } from "#app/phases/move-effect-phase";
|
|
||||||
import { StatusEffect } from "#enums/status-effect";
|
import { StatusEffect } from "#enums/status-effect";
|
||||||
import { BattlerIndex } from "#app/battle";
|
import { BattlerIndex } from "#app/battle";
|
||||||
|
import { toDmgValue } from "#app/utils/common";
|
||||||
|
|
||||||
describe("Moves - Powder", () => {
|
describe("Moves - Powder", () => {
|
||||||
let phaserGame: Phaser.Game;
|
let phaserGame: Phaser.Game;
|
||||||
@ -168,18 +168,13 @@ describe("Moves - Powder", () => {
|
|||||||
game.move.select(Moves.FIERY_DANCE, 0, BattlerIndex.ENEMY);
|
game.move.select(Moves.FIERY_DANCE, 0, BattlerIndex.ENEMY);
|
||||||
game.move.select(Moves.POWDER, 1, BattlerIndex.ENEMY);
|
game.move.select(Moves.POWDER, 1, BattlerIndex.ENEMY);
|
||||||
|
|
||||||
await game.phaseInterceptor.to(MoveEffectPhase);
|
await game.phaseInterceptor.to("BerryPhase", false);
|
||||||
const enemyStartingHp = enemyPokemon.hp;
|
|
||||||
|
|
||||||
await game.phaseInterceptor.to(BerryPhase, false);
|
|
||||||
|
|
||||||
// player should not take damage
|
// player should not take damage
|
||||||
expect(enemyPokemon.getLastXMoves()[0].result).toBe(MoveResult.FAIL);
|
expect(enemyPokemon.getLastXMoves()[0].result).toBe(MoveResult.FAIL);
|
||||||
expect(playerPokemon.hp).toBe(playerPokemon.getMaxHp());
|
expect(playerPokemon.hp).toBe(playerPokemon.getMaxHp());
|
||||||
// enemy should have taken damage from player's Fiery Dance + 2 Powder procs
|
// enemy should have taken damage from player's Fiery Dance + 2 Powder procs
|
||||||
expect(enemyPokemon.hp).toBe(
|
expect(enemyPokemon.hp).toBeLessThan(2 * toDmgValue(enemyPokemon.getMaxHp() / 4));
|
||||||
enemyStartingHp - playerPokemon.turnData.lastMoveDamageDealt - 2 * Math.floor(enemyPokemon.getMaxHp() / 4),
|
|
||||||
);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should cancel Fiery Dance, then prevent it from triggering Dancer", async () => {
|
it("should cancel Fiery Dance, then prevent it from triggering Dancer", async () => {
|
||||||
|
Loading…
Reference in New Issue
Block a user