mirror of
https://github.com/pagefaultgames/pokerogue.git
synced 2025-08-21 06:49:35 +02:00
Merge remote-tracking branch 'origin/beta' into move/fairylock
This commit is contained in:
commit
93a543c3a1
@ -1,4 +1,4 @@
|
|||||||
import Pokemon, { HitResult, PlayerPokemon, PokemonMove } from "../field/pokemon";
|
import Pokemon, { EnemyPokemon, HitResult, MoveResult, PlayerPokemon, PokemonMove } from "../field/pokemon";
|
||||||
import { Type } from "./type";
|
import { Type } from "./type";
|
||||||
import { Constructor } from "#app/utils";
|
import { Constructor } from "#app/utils";
|
||||||
import * as Utils from "../utils";
|
import * as Utils from "../utils";
|
||||||
@ -9,7 +9,7 @@ import { StatusEffect, getNonVolatileStatusEffects, getStatusEffectDescriptor, g
|
|||||||
import { Gender } from "./gender";
|
import { Gender } from "./gender";
|
||||||
import Move, { AttackMove, MoveCategory, MoveFlags, MoveTarget, FlinchAttr, OneHitKOAttr, HitHealAttr, allMoves, StatusMove, SelfStatusMove, VariablePowerAttr, applyMoveAttrs, IncrementMovePriorityAttr, VariableMoveTypeAttr, RandomMovesetMoveAttr, RandomMoveAttr, NaturePowerAttr, CopyMoveAttr, MoveAttr, MultiHitAttr, SacrificialAttr, SacrificialAttrOnHit, NeutralDamageAgainstFlyingTypeMultiplierAttr, FixedDamageAttr } from "./move";
|
import Move, { AttackMove, MoveCategory, MoveFlags, MoveTarget, FlinchAttr, OneHitKOAttr, HitHealAttr, allMoves, StatusMove, SelfStatusMove, VariablePowerAttr, applyMoveAttrs, IncrementMovePriorityAttr, VariableMoveTypeAttr, RandomMovesetMoveAttr, RandomMoveAttr, NaturePowerAttr, CopyMoveAttr, MoveAttr, MultiHitAttr, SacrificialAttr, SacrificialAttrOnHit, NeutralDamageAgainstFlyingTypeMultiplierAttr, FixedDamageAttr } from "./move";
|
||||||
import { ArenaTagSide, ArenaTrapTag } from "./arena-tag";
|
import { ArenaTagSide, ArenaTrapTag } from "./arena-tag";
|
||||||
import { BerryModifier, PokemonHeldItemModifier } from "../modifier/modifier";
|
import { BerryModifier, HitHealModifier, PokemonHeldItemModifier } from "../modifier/modifier";
|
||||||
import { TerrainType } from "./terrain";
|
import { TerrainType } from "./terrain";
|
||||||
import { SpeciesFormChangeManualTrigger, SpeciesFormChangeRevertWeatherFormTrigger, SpeciesFormChangeWeatherTrigger } from "./pokemon-forms";
|
import { SpeciesFormChangeManualTrigger, SpeciesFormChangeRevertWeatherFormTrigger, SpeciesFormChangeWeatherTrigger } from "./pokemon-forms";
|
||||||
import i18next from "i18next";
|
import i18next from "i18next";
|
||||||
@ -17,7 +17,7 @@ import { Localizable } from "#app/interfaces/locales";
|
|||||||
import { Command } from "../ui/command-ui-handler";
|
import { Command } from "../ui/command-ui-handler";
|
||||||
import { BerryModifierType } from "#app/modifier/modifier-type";
|
import { BerryModifierType } from "#app/modifier/modifier-type";
|
||||||
import { getPokeballName } from "./pokeball";
|
import { getPokeballName } from "./pokeball";
|
||||||
import { BattlerIndex } from "#app/battle";
|
import { BattlerIndex, BattleType } from "#app/battle";
|
||||||
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";
|
||||||
@ -29,6 +29,12 @@ import { PokemonHealPhase } from "#app/phases/pokemon-heal-phase";
|
|||||||
import { ShowAbilityPhase } from "#app/phases/show-ability-phase";
|
import { ShowAbilityPhase } from "#app/phases/show-ability-phase";
|
||||||
import { StatStageChangePhase } from "#app/phases/stat-stage-change-phase";
|
import { StatStageChangePhase } from "#app/phases/stat-stage-change-phase";
|
||||||
import BattleScene from "#app/battle-scene";
|
import BattleScene from "#app/battle-scene";
|
||||||
|
import { SwitchType } from "#app/enums/switch-type";
|
||||||
|
import { SwitchPhase } from "#app/phases/switch-phase";
|
||||||
|
import { SwitchSummonPhase } from "#app/phases/switch-summon-phase";
|
||||||
|
import { BattleEndPhase } from "#app/phases/battle-end-phase";
|
||||||
|
import { NewBattlePhase } from "#app/phases/new-battle-phase";
|
||||||
|
import { MoveEndPhase } from "#app/phases/move-end-phase";
|
||||||
|
|
||||||
export class Ability implements Localizable {
|
export class Ability implements Localizable {
|
||||||
public id: Abilities;
|
public id: Abilities;
|
||||||
@ -3092,7 +3098,7 @@ export class SuppressWeatherEffectAbAttr extends PreWeatherEffectAbAttr {
|
|||||||
/**
|
/**
|
||||||
* Condition function to applied to abilities related to Sheer Force.
|
* Condition function to applied to abilities related to Sheer Force.
|
||||||
* Checks if last move used against target was affected by a Sheer Force user and:
|
* Checks if last move used against target was affected by a Sheer Force user and:
|
||||||
* Disables: Color Change, Pickpocket, Wimp Out, Emergency Exit, Berserk, Anger Shell
|
* Disables: Color Change, Pickpocket, Berserk, Anger Shell
|
||||||
* @returns {AbAttrCondition} If false disables the ability which the condition is applied to.
|
* @returns {AbAttrCondition} If false disables the ability which the condition is applied to.
|
||||||
*/
|
*/
|
||||||
function getSheerForceHitDisableAbCondition(): AbAttrCondition {
|
function getSheerForceHitDisableAbCondition(): AbAttrCondition {
|
||||||
@ -4833,6 +4839,239 @@ async function applyAbAttrsInternal<TAttr extends AbAttr>(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class ForceSwitchOutHelper {
|
||||||
|
constructor(private switchType: SwitchType) {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handles the logic for switching out a Pokémon based on battle conditions, HP, and the switch type.
|
||||||
|
*
|
||||||
|
* @param pokemon The {@linkcode Pokemon} attempting to switch out.
|
||||||
|
* @returns `true` if the switch is successful
|
||||||
|
*/
|
||||||
|
public switchOutLogic(pokemon: Pokemon): boolean {
|
||||||
|
const switchOutTarget = pokemon;
|
||||||
|
/**
|
||||||
|
* If the switch-out target is a player-controlled Pokémon, the function checks:
|
||||||
|
* - Whether there are available party members to switch in.
|
||||||
|
* - If the Pokémon is still alive (hp > 0), and if so, it leaves the field and a new SwitchPhase is initiated.
|
||||||
|
*/
|
||||||
|
if (switchOutTarget instanceof PlayerPokemon) {
|
||||||
|
if (switchOutTarget.scene.getParty().filter((p) => p.isAllowedInBattle() && !p.isOnField()).length < 1) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (switchOutTarget.hp > 0) {
|
||||||
|
switchOutTarget.leaveField(this.switchType === SwitchType.SWITCH);
|
||||||
|
pokemon.scene.prependToPhase(new SwitchPhase(pokemon.scene, this.switchType, switchOutTarget.getFieldIndex(), true, true), MoveEndPhase);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* For non-wild battles, it checks if the opposing party has any available Pokémon to switch in.
|
||||||
|
* If yes, the Pokémon leaves the field and a new SwitchSummonPhase is initiated.
|
||||||
|
*/
|
||||||
|
} else if (pokemon.scene.currentBattle.battleType !== BattleType.WILD) {
|
||||||
|
if (switchOutTarget.scene.getEnemyParty().filter((p) => p.isAllowedInBattle() && !p.isOnField()).length < 1) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (switchOutTarget.hp > 0) {
|
||||||
|
switchOutTarget.leaveField(this.switchType === SwitchType.SWITCH);
|
||||||
|
pokemon.scene.prependToPhase(new SwitchSummonPhase(pokemon.scene, this.switchType, switchOutTarget.getFieldIndex(),
|
||||||
|
(pokemon.scene.currentBattle.trainer ? pokemon.scene.currentBattle.trainer.getNextSummonIndex((switchOutTarget as EnemyPokemon).trainerSlot) : 0),
|
||||||
|
false, false), MoveEndPhase);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* For wild Pokémon battles, the Pokémon will flee if the conditions are met (waveIndex and double battles).
|
||||||
|
*/
|
||||||
|
} else {
|
||||||
|
if (!pokemon.scene.currentBattle.waveIndex && pokemon.scene.currentBattle.waveIndex % 10 === 0) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (switchOutTarget.hp > 0) {
|
||||||
|
switchOutTarget.leaveField(false);
|
||||||
|
pokemon.scene.queueMessage(i18next.t("moveTriggers:fled", { pokemonName: getPokemonNameWithAffix(switchOutTarget) }), null, true, 500);
|
||||||
|
|
||||||
|
if (switchOutTarget.scene.currentBattle.double) {
|
||||||
|
const allyPokemon = switchOutTarget.getAlly();
|
||||||
|
switchOutTarget.scene.redirectPokemonMoves(switchOutTarget, allyPokemon);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!switchOutTarget.getAlly()?.isActive(true)) {
|
||||||
|
pokemon.scene.clearEnemyHeldItemModifiers();
|
||||||
|
|
||||||
|
if (switchOutTarget.hp) {
|
||||||
|
pokemon.scene.pushPhase(new BattleEndPhase(pokemon.scene));
|
||||||
|
pokemon.scene.pushPhase(new NewBattlePhase(pokemon.scene));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Determines if a Pokémon can switch out based on its status, the opponent's status, and battle conditions.
|
||||||
|
*
|
||||||
|
* @param pokemon The Pokémon attempting to switch out.
|
||||||
|
* @param opponent The opponent Pokémon.
|
||||||
|
* @returns `true` if the switch-out condition is met
|
||||||
|
*/
|
||||||
|
public getSwitchOutCondition(pokemon: Pokemon, opponent: Pokemon): boolean {
|
||||||
|
const switchOutTarget = pokemon;
|
||||||
|
const player = switchOutTarget instanceof PlayerPokemon;
|
||||||
|
|
||||||
|
if (player) {
|
||||||
|
const blockedByAbility = new Utils.BooleanHolder(false);
|
||||||
|
applyAbAttrs(ForceSwitchOutImmunityAbAttr, opponent, blockedByAbility);
|
||||||
|
return !blockedByAbility.value;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!player && pokemon.scene.currentBattle.battleType === BattleType.WILD) {
|
||||||
|
if (!pokemon.scene.currentBattle.waveIndex && pokemon.scene.currentBattle.waveIndex % 10 === 0) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!player && pokemon.scene.currentBattle.isBattleMysteryEncounter() && !pokemon.scene.currentBattle.mysteryEncounter?.fleeAllowed) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const party = player ? pokemon.scene.getParty() : pokemon.scene.getEnemyParty();
|
||||||
|
return (!player && pokemon.scene.currentBattle.battleType === BattleType.WILD)
|
||||||
|
|| party.filter(p => p.isAllowedInBattle()
|
||||||
|
&& (player || (p as EnemyPokemon).trainerSlot === (switchOutTarget as EnemyPokemon).trainerSlot)).length > pokemon.scene.currentBattle.getBattlerCount();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns a message if the switch-out attempt fails due to ability effects.
|
||||||
|
*
|
||||||
|
* @param target The target Pokémon.
|
||||||
|
* @returns The failure message, or `null` if no failure.
|
||||||
|
*/
|
||||||
|
public getFailedText(target: Pokemon): string | null {
|
||||||
|
const blockedByAbility = new Utils.BooleanHolder(false);
|
||||||
|
applyAbAttrs(ForceSwitchOutImmunityAbAttr, target, blockedByAbility);
|
||||||
|
return blockedByAbility.value ? i18next.t("moveTriggers:cannotBeSwitchedOut", { pokemonName: getPokemonNameWithAffix(target) }) : null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Calculates the amount of recovery from the Shell Bell item.
|
||||||
|
*
|
||||||
|
* If the Pokémon is holding a Shell Bell, this function computes the amount of health
|
||||||
|
* recovered based on the damage dealt in the current turn. The recovery is multiplied by the
|
||||||
|
* Shell Bell's modifier (if any).
|
||||||
|
*
|
||||||
|
* @param pokemon - The Pokémon whose Shell Bell recovery is being calculated.
|
||||||
|
* @returns The amount of health recovered by Shell Bell.
|
||||||
|
*/
|
||||||
|
function calculateShellBellRecovery(pokemon: Pokemon): number {
|
||||||
|
const shellBellModifier = pokemon.getHeldItems().find(m => m instanceof HitHealModifier);
|
||||||
|
if (shellBellModifier) {
|
||||||
|
return Utils.toDmgValue(pokemon.turnData.damageDealt / 8) * shellBellModifier.stackCount;
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Triggers after the Pokemon takes any damage
|
||||||
|
* @extends AbAttr
|
||||||
|
*/
|
||||||
|
export class PostDamageAbAttr extends AbAttr {
|
||||||
|
public applyPostDamage(pokemon: Pokemon, damage: number, passive: boolean, simulated: boolean, args: any[], source?: Pokemon): boolean | Promise<boolean> {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Ability attribute for forcing a Pokémon to switch out after its health drops below half.
|
||||||
|
* This attribute checks various conditions related to the damage received, the moves used by the Pokémon
|
||||||
|
* and its opponents, and determines whether a forced switch-out should occur.
|
||||||
|
*
|
||||||
|
* Used by Wimp Out and Emergency Exit
|
||||||
|
*
|
||||||
|
* @extends PostDamageAbAttr
|
||||||
|
* @see {@linkcode applyPostDamage}
|
||||||
|
*/
|
||||||
|
export class PostDamageForceSwitchAbAttr extends PostDamageAbAttr {
|
||||||
|
private helper: ForceSwitchOutHelper = new ForceSwitchOutHelper(SwitchType.SWITCH);
|
||||||
|
private hpRatio: number;
|
||||||
|
|
||||||
|
constructor(hpRatio: number = 0.5) {
|
||||||
|
super();
|
||||||
|
this.hpRatio = hpRatio;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Applies the switch-out logic after the Pokémon takes damage.
|
||||||
|
* Checks various conditions based on the moves used by the Pokémon, the opponents' moves, and
|
||||||
|
* the Pokémon's health after damage to determine whether the switch-out should occur.
|
||||||
|
*
|
||||||
|
* @param pokemon The Pokémon that took damage.
|
||||||
|
* @param damage The amount of damage taken by the Pokémon.
|
||||||
|
* @param passive N/A
|
||||||
|
* @param simulated Whether the ability is being simulated.
|
||||||
|
* @param args N/A
|
||||||
|
* @param source The Pokemon that dealt damage
|
||||||
|
* @returns `true` if the switch-out logic was successfully applied
|
||||||
|
*/
|
||||||
|
public override applyPostDamage(pokemon: Pokemon, damage: number, passive: boolean, simulated: boolean, args: any[], source?: Pokemon): boolean | Promise<boolean> {
|
||||||
|
const moveHistory = pokemon.getMoveHistory();
|
||||||
|
// Will not activate when the Pokémon's HP is lowered by cutting its own HP
|
||||||
|
const fordbiddenAttackingMoves = [ Moves.BELLY_DRUM, Moves.SUBSTITUTE, Moves.CURSE, Moves.PAIN_SPLIT ];
|
||||||
|
if (moveHistory.length > 0) {
|
||||||
|
const lastMoveUsed = moveHistory[moveHistory.length - 1];
|
||||||
|
if (fordbiddenAttackingMoves.includes(lastMoveUsed.move)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Dragon Tail and Circle Throw switch out Pokémon before the Ability activates.
|
||||||
|
const fordbiddenDefendingMoves = [ Moves.DRAGON_TAIL, Moves.CIRCLE_THROW ];
|
||||||
|
if (source) {
|
||||||
|
const enemyMoveHistory = source.getMoveHistory();
|
||||||
|
if (enemyMoveHistory.length > 0) {
|
||||||
|
const enemyLastMoveUsed = enemyMoveHistory[enemyMoveHistory.length - 1];
|
||||||
|
// Will not activate if the Pokémon's HP falls below half while it is in the air during Sky Drop.
|
||||||
|
if (fordbiddenDefendingMoves.includes(enemyLastMoveUsed.move) || enemyLastMoveUsed.move === Moves.SKY_DROP && enemyLastMoveUsed.result === MoveResult.OTHER) {
|
||||||
|
return false;
|
||||||
|
// Will not activate if the Pokémon's HP falls below half by a move affected by Sheer Force.
|
||||||
|
} else if (allMoves[enemyLastMoveUsed.move].chance >= 0 && source.hasAbility(Abilities.SHEER_FORCE)) {
|
||||||
|
return false;
|
||||||
|
// Activate only after the last hit of multistrike moves
|
||||||
|
} else if (source.turnData.hitsLeft > 1) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (source.turnData.hitCount > 1) {
|
||||||
|
damage = pokemon.turnData.damageTaken;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (pokemon.hp + damage >= pokemon.getMaxHp() * this.hpRatio) {
|
||||||
|
// Activates if it falls below half and recovers back above half from a Shell Bell
|
||||||
|
const shellBellHeal = calculateShellBellRecovery(pokemon);
|
||||||
|
if (pokemon.hp - shellBellHeal < pokemon.getMaxHp() * this.hpRatio) {
|
||||||
|
for (const opponent of pokemon.getOpponents()) {
|
||||||
|
if (!this.helper.getSwitchOutCondition(pokemon, opponent)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return this.helper.switchOutLogic(pokemon);
|
||||||
|
} else {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
public getFailedText(user: Pokemon, target: Pokemon, move: Move, cancelled: Utils.BooleanHolder): string | null {
|
||||||
|
return this.helper.getFailedText(target);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
export function applyAbAttrs(attrType: Constructor<AbAttr>, pokemon: Pokemon, cancelled: Utils.BooleanHolder | null, simulated: boolean = false, ...args: any[]): Promise<void> {
|
export function applyAbAttrs(attrType: Constructor<AbAttr>, pokemon: Pokemon, cancelled: Utils.BooleanHolder | null, simulated: boolean = false, ...args: any[]): Promise<void> {
|
||||||
return applyAbAttrsInternal<AbAttr>(attrType, pokemon, (attr, passive) => attr.apply(pokemon, passive, simulated, cancelled, args), args, false, simulated);
|
return applyAbAttrsInternal<AbAttr>(attrType, pokemon, (attr, passive) => attr.apply(pokemon, passive, simulated, cancelled, args), args, false, simulated);
|
||||||
}
|
}
|
||||||
@ -4866,6 +5105,11 @@ export function applyPostSetStatusAbAttrs(attrType: Constructor<PostSetStatusAbA
|
|||||||
return applyAbAttrsInternal<PostSetStatusAbAttr>(attrType, pokemon, (attr, passive) => attr.applyPostSetStatus(pokemon, sourcePokemon, passive, effect, simulated, args), args, false, simulated);
|
return applyAbAttrsInternal<PostSetStatusAbAttr>(attrType, pokemon, (attr, passive) => attr.applyPostSetStatus(pokemon, sourcePokemon, passive, effect, simulated, args), args, false, simulated);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function applyPostDamageAbAttrs(attrType: Constructor<PostDamageAbAttr>,
|
||||||
|
pokemon: Pokemon, damage: number, passive: boolean, simulated: boolean = false, args: any[], source?: Pokemon): Promise<void> {
|
||||||
|
return applyAbAttrsInternal<PostDamageAbAttr>(attrType, pokemon, (attr, passive) => attr.applyPostDamage(pokemon, damage, passive, simulated, args, source), args);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Applies a field Stat multiplier attribute
|
* Applies a field Stat multiplier attribute
|
||||||
* @param attrType {@linkcode FieldMultiplyStatAbAttr} should always be FieldMultiplyBattleStatAbAttr for the time being
|
* @param attrType {@linkcode FieldMultiplyStatAbAttr} should always be FieldMultiplyBattleStatAbAttr for the time being
|
||||||
@ -5062,7 +5306,8 @@ export function initAbilities() {
|
|||||||
.attr(TypeImmunityAddBattlerTagAbAttr, Type.FIRE, BattlerTagType.FIRE_BOOST, 1)
|
.attr(TypeImmunityAddBattlerTagAbAttr, Type.FIRE, BattlerTagType.FIRE_BOOST, 1)
|
||||||
.ignorable(),
|
.ignorable(),
|
||||||
new Ability(Abilities.SHIELD_DUST, 3)
|
new Ability(Abilities.SHIELD_DUST, 3)
|
||||||
.attr(IgnoreMoveEffectsAbAttr),
|
.attr(IgnoreMoveEffectsAbAttr)
|
||||||
|
.ignorable(),
|
||||||
new Ability(Abilities.OWN_TEMPO, 3)
|
new Ability(Abilities.OWN_TEMPO, 3)
|
||||||
.attr(BattlerTagImmunityAbAttr, BattlerTagType.CONFUSED)
|
.attr(BattlerTagImmunityAbAttr, BattlerTagType.CONFUSED)
|
||||||
.attr(IntimidateImmunityAbAttr)
|
.attr(IntimidateImmunityAbAttr)
|
||||||
@ -5180,11 +5425,9 @@ export function initAbilities() {
|
|||||||
new Ability(Abilities.CUTE_CHARM, 3)
|
new Ability(Abilities.CUTE_CHARM, 3)
|
||||||
.attr(PostDefendContactApplyTagChanceAbAttr, 30, BattlerTagType.INFATUATED),
|
.attr(PostDefendContactApplyTagChanceAbAttr, 30, BattlerTagType.INFATUATED),
|
||||||
new Ability(Abilities.PLUS, 3)
|
new Ability(Abilities.PLUS, 3)
|
||||||
.conditionalAttr(p => p.scene.currentBattle.double && [ Abilities.PLUS, Abilities.MINUS ].some(a => p.getAlly().hasAbility(a)), StatMultiplierAbAttr, Stat.SPATK, 1.5)
|
.conditionalAttr(p => p.scene.currentBattle.double && [ Abilities.PLUS, Abilities.MINUS ].some(a => p.getAlly().hasAbility(a)), StatMultiplierAbAttr, Stat.SPATK, 1.5),
|
||||||
.ignorable(),
|
|
||||||
new Ability(Abilities.MINUS, 3)
|
new Ability(Abilities.MINUS, 3)
|
||||||
.conditionalAttr(p => p.scene.currentBattle.double && [ Abilities.PLUS, Abilities.MINUS ].some(a => p.getAlly().hasAbility(a)), StatMultiplierAbAttr, Stat.SPATK, 1.5)
|
.conditionalAttr(p => p.scene.currentBattle.double && [ Abilities.PLUS, Abilities.MINUS ].some(a => p.getAlly().hasAbility(a)), StatMultiplierAbAttr, Stat.SPATK, 1.5),
|
||||||
.ignorable(),
|
|
||||||
new Ability(Abilities.FORECAST, 3)
|
new Ability(Abilities.FORECAST, 3)
|
||||||
.attr(UncopiableAbilityAbAttr)
|
.attr(UncopiableAbilityAbAttr)
|
||||||
.attr(NoFusionAbilityAbAttr)
|
.attr(NoFusionAbilityAbAttr)
|
||||||
@ -5522,7 +5765,8 @@ export function initAbilities() {
|
|||||||
.attr(PostSummonMessageAbAttr, (pokemon: Pokemon) => i18next.t("abilityTriggers:postSummonTeravolt", { pokemonNameWithAffix: getPokemonNameWithAffix(pokemon) }))
|
.attr(PostSummonMessageAbAttr, (pokemon: Pokemon) => i18next.t("abilityTriggers:postSummonTeravolt", { pokemonNameWithAffix: getPokemonNameWithAffix(pokemon) }))
|
||||||
.attr(MoveAbilityBypassAbAttr),
|
.attr(MoveAbilityBypassAbAttr),
|
||||||
new Ability(Abilities.AROMA_VEIL, 6)
|
new Ability(Abilities.AROMA_VEIL, 6)
|
||||||
.attr(UserFieldBattlerTagImmunityAbAttr, [ BattlerTagType.INFATUATED, BattlerTagType.TAUNT, BattlerTagType.DISABLED, BattlerTagType.TORMENT, BattlerTagType.HEAL_BLOCK ]),
|
.attr(UserFieldBattlerTagImmunityAbAttr, [ BattlerTagType.INFATUATED, BattlerTagType.TAUNT, BattlerTagType.DISABLED, BattlerTagType.TORMENT, BattlerTagType.HEAL_BLOCK ])
|
||||||
|
.ignorable(),
|
||||||
new Ability(Abilities.FLOWER_VEIL, 6)
|
new Ability(Abilities.FLOWER_VEIL, 6)
|
||||||
.ignorable()
|
.ignorable()
|
||||||
.unimplemented(),
|
.unimplemented(),
|
||||||
@ -5607,11 +5851,11 @@ export function initAbilities() {
|
|||||||
new Ability(Abilities.STAMINA, 7)
|
new Ability(Abilities.STAMINA, 7)
|
||||||
.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)
|
||||||
.condition(getSheerForceHitDisableAbCondition())
|
.attr(PostDamageForceSwitchAbAttr)
|
||||||
.unimplemented(),
|
.edgeCase(), // Should not trigger when hurting itself in confusion
|
||||||
new Ability(Abilities.EMERGENCY_EXIT, 7)
|
new Ability(Abilities.EMERGENCY_EXIT, 7)
|
||||||
.condition(getSheerForceHitDisableAbCondition())
|
.attr(PostDamageForceSwitchAbAttr)
|
||||||
.unimplemented(),
|
.edgeCase(), // Should not trigger when hurting itself in confusion
|
||||||
new Ability(Abilities.WATER_COMPACTION, 7)
|
new Ability(Abilities.WATER_COMPACTION, 7)
|
||||||
.attr(PostDefendStatStageChangeAbAttr, (target, user, move) => user.getMoveType(move) === Type.WATER && move.category !== MoveCategory.STATUS, Stat.DEF, 2),
|
.attr(PostDefendStatStageChangeAbAttr, (target, user, move) => user.getMoveType(move) === Type.WATER && move.category !== MoveCategory.STATUS, Stat.DEF, 2),
|
||||||
new Ability(Abilities.MERCILESS, 7)
|
new Ability(Abilities.MERCILESS, 7)
|
||||||
@ -5973,16 +6217,14 @@ export function initAbilities() {
|
|||||||
.ignorable(),
|
.ignorable(),
|
||||||
new Ability(Abilities.SWORD_OF_RUIN, 9)
|
new Ability(Abilities.SWORD_OF_RUIN, 9)
|
||||||
.attr(FieldMultiplyStatAbAttr, Stat.DEF, 0.75)
|
.attr(FieldMultiplyStatAbAttr, Stat.DEF, 0.75)
|
||||||
.attr(PostSummonMessageAbAttr, (user) => i18next.t("abilityTriggers:postSummonSwordOfRuin", { pokemonNameWithAffix: getPokemonNameWithAffix(user), statName: i18next.t(getStatKey(Stat.DEF)) }))
|
.attr(PostSummonMessageAbAttr, (user) => i18next.t("abilityTriggers:postSummonSwordOfRuin", { pokemonNameWithAffix: getPokemonNameWithAffix(user), statName: i18next.t(getStatKey(Stat.DEF)) })),
|
||||||
.ignorable(),
|
|
||||||
new Ability(Abilities.TABLETS_OF_RUIN, 9)
|
new Ability(Abilities.TABLETS_OF_RUIN, 9)
|
||||||
.attr(FieldMultiplyStatAbAttr, Stat.ATK, 0.75)
|
.attr(FieldMultiplyStatAbAttr, Stat.ATK, 0.75)
|
||||||
.attr(PostSummonMessageAbAttr, (user) => i18next.t("abilityTriggers:postSummonTabletsOfRuin", { pokemonNameWithAffix: getPokemonNameWithAffix(user), statName: i18next.t(getStatKey(Stat.ATK)) }))
|
.attr(PostSummonMessageAbAttr, (user) => i18next.t("abilityTriggers:postSummonTabletsOfRuin", { pokemonNameWithAffix: getPokemonNameWithAffix(user), statName: i18next.t(getStatKey(Stat.ATK)) }))
|
||||||
.ignorable(),
|
.ignorable(),
|
||||||
new Ability(Abilities.BEADS_OF_RUIN, 9)
|
new Ability(Abilities.BEADS_OF_RUIN, 9)
|
||||||
.attr(FieldMultiplyStatAbAttr, Stat.SPDEF, 0.75)
|
.attr(FieldMultiplyStatAbAttr, Stat.SPDEF, 0.75)
|
||||||
.attr(PostSummonMessageAbAttr, (user) => i18next.t("abilityTriggers:postSummonBeadsOfRuin", { pokemonNameWithAffix: getPokemonNameWithAffix(user), statName: i18next.t(getStatKey(Stat.SPDEF)) }))
|
.attr(PostSummonMessageAbAttr, (user) => i18next.t("abilityTriggers:postSummonBeadsOfRuin", { pokemonNameWithAffix: getPokemonNameWithAffix(user), statName: i18next.t(getStatKey(Stat.SPDEF)) })),
|
||||||
.ignorable(),
|
|
||||||
new Ability(Abilities.ORICHALCUM_PULSE, 9)
|
new Ability(Abilities.ORICHALCUM_PULSE, 9)
|
||||||
.attr(PostSummonWeatherChangeAbAttr, WeatherType.SUNNY)
|
.attr(PostSummonWeatherChangeAbAttr, WeatherType.SUNNY)
|
||||||
.attr(PostBiomeChangeWeatherChangeAbAttr, WeatherType.SUNNY)
|
.attr(PostBiomeChangeWeatherChangeAbAttr, WeatherType.SUNNY)
|
||||||
|
@ -8,7 +8,7 @@ import { Constructor, NumberHolder } from "#app/utils";
|
|||||||
import * as Utils from "../utils";
|
import * as Utils from "../utils";
|
||||||
import { WeatherType } from "./weather";
|
import { WeatherType } from "./weather";
|
||||||
import { ArenaTagSide, ArenaTrapTag, WeakenMoveTypeTag } from "./arena-tag";
|
import { ArenaTagSide, ArenaTrapTag, WeakenMoveTypeTag } from "./arena-tag";
|
||||||
import { allAbilities, AllyMoveCategoryPowerBoostAbAttr, applyAbAttrs, applyPostAttackAbAttrs, applyPreAttackAbAttrs, applyPreDefendAbAttrs, BlockItemTheftAbAttr, BlockNonDirectDamageAbAttr, BlockOneHitKOAbAttr, BlockRecoilDamageAttr, ConfusionOnStatusEffectAbAttr, FieldMoveTypePowerBoostAbAttr, FieldPreventExplosiveMovesAbAttr, ForceSwitchOutImmunityAbAttr, HealFromBerryUseAbAttr, IgnoreContactAbAttr, IgnoreMoveEffectsAbAttr, IgnoreProtectOnContactAbAttr, InfiltratorAbAttr, MaxMultiHitAbAttr, MoveAbilityBypassAbAttr, MoveEffectChanceMultiplierAbAttr, MoveTypeChangeAbAttr, ReverseDrainAbAttr, UncopiableAbilityAbAttr, UnsuppressableAbilityAbAttr, UnswappableAbilityAbAttr, UserFieldMoveTypePowerBoostAbAttr, VariableMovePowerAbAttr, WonderSkinAbAttr } from "./ability";
|
import { allAbilities, AllyMoveCategoryPowerBoostAbAttr, applyAbAttrs, applyPostAttackAbAttrs, applyPreAttackAbAttrs, applyPreDefendAbAttrs, BlockItemTheftAbAttr, BlockNonDirectDamageAbAttr, BlockOneHitKOAbAttr, BlockRecoilDamageAttr, ConfusionOnStatusEffectAbAttr, FieldMoveTypePowerBoostAbAttr, FieldPreventExplosiveMovesAbAttr, ForceSwitchOutImmunityAbAttr, HealFromBerryUseAbAttr, IgnoreContactAbAttr, IgnoreMoveEffectsAbAttr, IgnoreProtectOnContactAbAttr, InfiltratorAbAttr, MaxMultiHitAbAttr, MoveAbilityBypassAbAttr, MoveEffectChanceMultiplierAbAttr, MoveTypeChangeAbAttr, PostDamageForceSwitchAbAttr, ReverseDrainAbAttr, UncopiableAbilityAbAttr, UnsuppressableAbilityAbAttr, UnswappableAbilityAbAttr, UserFieldMoveTypePowerBoostAbAttr, VariableMovePowerAbAttr, WonderSkinAbAttr } from "./ability";
|
||||||
import { AttackTypeBoosterModifier, BerryModifier, PokemonHeldItemModifier, PokemonMoveAccuracyBoosterModifier, PokemonMultiHitModifier, PreserveBerryModifier } from "../modifier/modifier";
|
import { AttackTypeBoosterModifier, BerryModifier, PokemonHeldItemModifier, PokemonMoveAccuracyBoosterModifier, PokemonMultiHitModifier, PreserveBerryModifier } from "../modifier/modifier";
|
||||||
import { BattlerIndex, BattleType } from "../battle";
|
import { BattlerIndex, BattleType } from "../battle";
|
||||||
import { TerrainType } from "./terrain";
|
import { TerrainType } from "./terrain";
|
||||||
@ -4465,7 +4465,7 @@ export class FormChangeItemTypeAttr extends VariableMoveTypeAttr {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if ([ user.species.speciesId, user.fusionSpecies?.speciesId ].includes(Species.ARCEUS) || [ user.species.speciesId, user.fusionSpecies?.speciesId ].includes(Species.SILVALLY)) {
|
if ([ user.species.speciesId, user.fusionSpecies?.speciesId ].includes(Species.ARCEUS) || [ user.species.speciesId, user.fusionSpecies?.speciesId ].includes(Species.SILVALLY)) {
|
||||||
const form = user.species.speciesId === Species.ARCEUS || user.species.speciesId === Species.SILVALLY ? user.formIndex : user.fusionSpecies?.formIndex!; // TODO: is this bang correct?
|
const form = user.species.speciesId === Species.ARCEUS || user.species.speciesId === Species.SILVALLY ? user.formIndex : user.fusionSpecies?.formIndex!;
|
||||||
|
|
||||||
moveType.value = Type[Type[form]];
|
moveType.value = Type[Type[form]];
|
||||||
return true;
|
return true;
|
||||||
@ -5761,6 +5761,7 @@ export class RevivalBlessingAttr extends MoveEffectAttr {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
export class ForceSwitchOutAttr extends MoveEffectAttr {
|
export class ForceSwitchOutAttr extends MoveEffectAttr {
|
||||||
constructor(
|
constructor(
|
||||||
private selfSwitch: boolean = false,
|
private selfSwitch: boolean = false,
|
||||||
@ -5779,12 +5780,19 @@ export class ForceSwitchOutAttr extends MoveEffectAttr {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Move the switch out logic inside the conditional block
|
|
||||||
* This ensures that the switch out only happens when the conditions are met
|
|
||||||
*/
|
|
||||||
const switchOutTarget = this.selfSwitch ? user : target;
|
const switchOutTarget = this.selfSwitch ? user : target;
|
||||||
if (switchOutTarget instanceof PlayerPokemon) {
|
if (switchOutTarget instanceof PlayerPokemon) {
|
||||||
|
/**
|
||||||
|
* Check if Wimp Out/Emergency Exit activates due to being hit by U-turn or Volt Switch
|
||||||
|
* If it did, the user of U-turn or Volt Switch will not be switched out.
|
||||||
|
*/
|
||||||
|
if (target.getAbility().hasAttr(PostDamageForceSwitchAbAttr) &&
|
||||||
|
(move.id === Moves.U_TURN || move.id === Moves.VOLT_SWITCH || move.id === Moves.FLIP_TURN)
|
||||||
|
) {
|
||||||
|
if (this.hpDroppedBelowHalf(target)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
// Switch out logic for the player's Pokemon
|
// Switch out logic for the player's Pokemon
|
||||||
if (switchOutTarget.scene.getParty().filter((p) => p.isAllowedInBattle() && !p.isOnField()).length < 1) {
|
if (switchOutTarget.scene.getParty().filter((p) => p.isAllowedInBattle() && !p.isOnField()).length < 1) {
|
||||||
return false;
|
return false;
|
||||||
@ -5810,6 +5818,17 @@ export class ForceSwitchOutAttr extends MoveEffectAttr {
|
|||||||
false, false), MoveEndPhase);
|
false, false), MoveEndPhase);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
/**
|
||||||
|
* Check if Wimp Out/Emergency Exit activates due to being hit by U-turn or Volt Switch
|
||||||
|
* If it did, the user of U-turn or Volt Switch will not be switched out.
|
||||||
|
*/
|
||||||
|
if (target.getAbility().hasAttr(PostDamageForceSwitchAbAttr) &&
|
||||||
|
(move.id === Moves.U_TURN || move.id === Moves.VOLT_SWITCH) || move.id === Moves.FLIP_TURN) {
|
||||||
|
if (this.hpDroppedBelowHalf(target)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Switch out logic for everything else (eg: WILD battles)
|
// Switch out logic for everything else (eg: WILD battles)
|
||||||
if (user.scene.currentBattle.waveIndex % 10 === 0) {
|
if (user.scene.currentBattle.waveIndex % 10 === 0) {
|
||||||
return false;
|
return false;
|
||||||
@ -5902,6 +5921,21 @@ export class ForceSwitchOutAttr extends MoveEffectAttr {
|
|||||||
}
|
}
|
||||||
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 {
|
||||||
|
@ -47,7 +47,7 @@ export function getPokemonSpecies(species: Species | Species[] | undefined): Pok
|
|||||||
return allSpecies[species - 1];
|
return allSpecies[species - 1];
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getPokemonSpeciesForm(species: Species, formIndex: integer): PokemonSpeciesForm {
|
export function getPokemonSpeciesForm(species: Species, formIndex: number): PokemonSpeciesForm {
|
||||||
const retSpecies: PokemonSpecies = species >= 2000
|
const retSpecies: PokemonSpecies = species >= 2000
|
||||||
? allSpecies.find(s => s.speciesId === species)! // TODO: is the bang correct?
|
? allSpecies.find(s => s.speciesId === species)! // TODO: is the bang correct?
|
||||||
: allSpecies[species - 1];
|
: allSpecies[species - 1];
|
||||||
@ -129,26 +129,27 @@ export type PokemonSpeciesFilter = (species: PokemonSpecies) => boolean;
|
|||||||
|
|
||||||
export abstract class PokemonSpeciesForm {
|
export abstract class PokemonSpeciesForm {
|
||||||
public speciesId: Species;
|
public speciesId: Species;
|
||||||
public formIndex: integer;
|
protected _formIndex: number;
|
||||||
public generation: integer;
|
protected _generation: number;
|
||||||
public type1: Type;
|
readonly type1: Type;
|
||||||
public type2: Type | null;
|
readonly type2: Type | null;
|
||||||
public height: number;
|
readonly height: number;
|
||||||
public weight: number;
|
readonly weight: number;
|
||||||
public ability1: Abilities;
|
readonly ability1: Abilities;
|
||||||
public ability2: Abilities;
|
readonly ability2: Abilities;
|
||||||
public abilityHidden: Abilities;
|
readonly abilityHidden: Abilities;
|
||||||
public baseTotal: integer;
|
readonly baseTotal: number;
|
||||||
public baseStats: integer[];
|
readonly baseStats: number[];
|
||||||
public catchRate: integer;
|
readonly catchRate: number;
|
||||||
public baseFriendship: integer;
|
readonly baseFriendship: number;
|
||||||
public baseExp: integer;
|
readonly baseExp: number;
|
||||||
public genderDiffs: boolean;
|
readonly genderDiffs: boolean;
|
||||||
public isStarterSelectable: boolean;
|
readonly isStarterSelectable: boolean;
|
||||||
|
|
||||||
constructor(type1: Type, type2: Type | null, height: number, weight: number, ability1: Abilities, ability2: Abilities, abilityHidden: Abilities,
|
constructor(type1: Type, type2: Type | null, height: number, weight: number, ability1: Abilities, ability2: Abilities, abilityHidden: Abilities,
|
||||||
baseTotal: integer, baseHp: integer, baseAtk: integer, baseDef: integer, baseSpatk: integer, baseSpdef: integer, baseSpd: integer,
|
baseTotal: number, baseHp: number, baseAtk: number, baseDef: number, baseSpatk: number, baseSpdef: number, baseSpd: number,
|
||||||
catchRate: integer, baseFriendship: integer, baseExp: integer, genderDiffs: boolean, isStarterSelectable: boolean) {
|
catchRate: number, baseFriendship: number, baseExp: number, genderDiffs: boolean, isStarterSelectable: boolean
|
||||||
|
) {
|
||||||
this.type1 = type1;
|
this.type1 = type1;
|
||||||
this.type2 = type2;
|
this.type2 = type2;
|
||||||
this.height = height;
|
this.height = height;
|
||||||
@ -180,7 +181,23 @@ export abstract class PokemonSpeciesForm {
|
|||||||
return ret;
|
return ret;
|
||||||
}
|
}
|
||||||
|
|
||||||
isOfType(type: integer): boolean {
|
get generation(): number {
|
||||||
|
return this._generation;
|
||||||
|
}
|
||||||
|
|
||||||
|
set generation(generation: number) {
|
||||||
|
this._generation = generation;
|
||||||
|
}
|
||||||
|
|
||||||
|
get formIndex(): number {
|
||||||
|
return this._formIndex;
|
||||||
|
}
|
||||||
|
|
||||||
|
set formIndex(formIndex: number) {
|
||||||
|
this._formIndex = formIndex;
|
||||||
|
}
|
||||||
|
|
||||||
|
isOfType(type: number): boolean {
|
||||||
return this.type1 === type || (this.type2 !== null && this.type2 === type);
|
return this.type1 === type || (this.type2 !== null && this.type2 === type);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -188,7 +205,7 @@ export abstract class PokemonSpeciesForm {
|
|||||||
* Method to get the total number of abilities a Pokemon species has.
|
* Method to get the total number of abilities a Pokemon species has.
|
||||||
* @returns Number of abilities
|
* @returns Number of abilities
|
||||||
*/
|
*/
|
||||||
getAbilityCount(): integer {
|
getAbilityCount(): number {
|
||||||
return this.abilityHidden !== Abilities.NONE ? 3 : 2;
|
return this.abilityHidden !== Abilities.NONE ? 3 : 2;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -197,7 +214,7 @@ export abstract class PokemonSpeciesForm {
|
|||||||
* @param abilityIndex Which ability to get (should only be 0-2)
|
* @param abilityIndex Which ability to get (should only be 0-2)
|
||||||
* @returns The id of the Ability
|
* @returns The id of the Ability
|
||||||
*/
|
*/
|
||||||
getAbility(abilityIndex: integer): Abilities {
|
getAbility(abilityIndex: number): Abilities {
|
||||||
let ret: Abilities;
|
let ret: Abilities;
|
||||||
if (abilityIndex === 0) {
|
if (abilityIndex === 0) {
|
||||||
ret = this.ability1;
|
ret = this.ability1;
|
||||||
@ -277,12 +294,12 @@ export abstract class PokemonSpeciesForm {
|
|||||||
return ret;
|
return ret;
|
||||||
}
|
}
|
||||||
|
|
||||||
getSpriteAtlasPath(female: boolean, formIndex?: integer, shiny?: boolean, variant?: integer): string {
|
getSpriteAtlasPath(female: boolean, formIndex?: number, shiny?: boolean, variant?: number): string {
|
||||||
const spriteId = this.getSpriteId(female, formIndex, shiny, variant).replace(/\_{2}/g, "/");
|
const spriteId = this.getSpriteId(female, formIndex, shiny, variant).replace(/\_{2}/g, "/");
|
||||||
return `${/_[1-3]$/.test(spriteId) ? "variant/" : ""}${spriteId}`;
|
return `${/_[1-3]$/.test(spriteId) ? "variant/" : ""}${spriteId}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
getSpriteId(female: boolean, formIndex?: integer, shiny?: boolean, variant: integer = 0, back?: boolean): string {
|
getSpriteId(female: boolean, formIndex?: number, shiny?: boolean, variant: number = 0, back?: boolean): string {
|
||||||
if (formIndex === undefined || this instanceof PokemonForm) {
|
if (formIndex === undefined || this instanceof PokemonForm) {
|
||||||
formIndex = this.formIndex;
|
formIndex = this.formIndex;
|
||||||
}
|
}
|
||||||
@ -299,11 +316,11 @@ export abstract class PokemonSpeciesForm {
|
|||||||
return `${back ? "back__" : ""}${shiny && (!variantSet || (!variant && !variantSet[variant || 0])) ? "shiny__" : ""}${baseSpriteKey}${shiny && variantSet && variantSet[variant] === 2 ? `_${variant + 1}` : ""}`;
|
return `${back ? "back__" : ""}${shiny && (!variantSet || (!variant && !variantSet[variant || 0])) ? "shiny__" : ""}${baseSpriteKey}${shiny && variantSet && variantSet[variant] === 2 ? `_${variant + 1}` : ""}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
getSpriteKey(female: boolean, formIndex?: integer, shiny?: boolean, variant?: integer): string {
|
getSpriteKey(female: boolean, formIndex?: number, shiny?: boolean, variant?: number): string {
|
||||||
return `pkmn__${this.getSpriteId(female, formIndex, shiny, variant)}`;
|
return `pkmn__${this.getSpriteId(female, formIndex, shiny, variant)}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
abstract getFormSpriteKey(formIndex?: integer): string;
|
abstract getFormSpriteKey(formIndex?: number): string;
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -311,9 +328,9 @@ export abstract class PokemonSpeciesForm {
|
|||||||
* @param formIndex optional form index for pokemon with different forms
|
* @param formIndex optional form index for pokemon with different forms
|
||||||
* @returns species id if no additional forms, index with formkey if a pokemon with a form
|
* @returns species id if no additional forms, index with formkey if a pokemon with a form
|
||||||
*/
|
*/
|
||||||
getVariantDataIndex(formIndex?: integer) {
|
getVariantDataIndex(formIndex?: number) {
|
||||||
let formkey: string | null = null;
|
let formkey: string | null = null;
|
||||||
let variantDataIndex: integer | string = this.speciesId;
|
let variantDataIndex: number | string = this.speciesId;
|
||||||
const species = getPokemonSpecies(this.speciesId);
|
const species = getPokemonSpecies(this.speciesId);
|
||||||
if (species.forms.length > 0 && formIndex !== undefined) {
|
if (species.forms.length > 0 && formIndex !== undefined) {
|
||||||
formkey = species.forms[formIndex]?.getFormSpriteKey(formIndex);
|
formkey = species.forms[formIndex]?.getFormSpriteKey(formIndex);
|
||||||
@ -324,13 +341,13 @@ export abstract class PokemonSpeciesForm {
|
|||||||
return variantDataIndex;
|
return variantDataIndex;
|
||||||
}
|
}
|
||||||
|
|
||||||
getIconAtlasKey(formIndex?: integer, shiny?: boolean, variant?: integer): string {
|
getIconAtlasKey(formIndex?: number, shiny?: boolean, variant?: number): string {
|
||||||
const variantDataIndex = this.getVariantDataIndex(formIndex);
|
const variantDataIndex = this.getVariantDataIndex(formIndex);
|
||||||
const isVariant = shiny && variantData[variantDataIndex] && (variant !== undefined && variantData[variantDataIndex][variant]);
|
const isVariant = shiny && variantData[variantDataIndex] && (variant !== undefined && variantData[variantDataIndex][variant]);
|
||||||
return `pokemon_icons_${this.generation}${isVariant ? "v" : ""}`;
|
return `pokemon_icons_${this.generation}${isVariant ? "v" : ""}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
getIconId(female: boolean, formIndex?: integer, shiny?: boolean, variant?: integer): string {
|
getIconId(female: boolean, formIndex?: number, shiny?: boolean, variant?: number): string {
|
||||||
if (formIndex === undefined) {
|
if (formIndex === undefined) {
|
||||||
formIndex = this.formIndex;
|
formIndex = this.formIndex;
|
||||||
}
|
}
|
||||||
@ -379,7 +396,7 @@ export abstract class PokemonSpeciesForm {
|
|||||||
return ret;
|
return ret;
|
||||||
}
|
}
|
||||||
|
|
||||||
getCryKey(formIndex?: integer): string {
|
getCryKey(formIndex?: number): string {
|
||||||
let speciesId = this.speciesId;
|
let speciesId = this.speciesId;
|
||||||
if (this.speciesId > 2000) {
|
if (this.speciesId > 2000) {
|
||||||
switch (this.speciesId) {
|
switch (this.speciesId) {
|
||||||
@ -446,7 +463,7 @@ export abstract class PokemonSpeciesForm {
|
|||||||
return ret;
|
return ret;
|
||||||
}
|
}
|
||||||
|
|
||||||
validateStarterMoveset(moveset: StarterMoveset, eggMoves: integer): boolean {
|
validateStarterMoveset(moveset: StarterMoveset, eggMoves: number): boolean {
|
||||||
const rootSpeciesId = this.getRootSpeciesId();
|
const rootSpeciesId = this.getRootSpeciesId();
|
||||||
for (const moveId of moveset) {
|
for (const moveId of moveset) {
|
||||||
if (speciesEggMoves.hasOwnProperty(rootSpeciesId)) {
|
if (speciesEggMoves.hasOwnProperty(rootSpeciesId)) {
|
||||||
@ -467,7 +484,7 @@ export abstract class PokemonSpeciesForm {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
loadAssets(scene: BattleScene, female: boolean, formIndex?: integer, shiny?: boolean, variant?: Variant, startLoad?: boolean): Promise<void> {
|
loadAssets(scene: BattleScene, female: boolean, formIndex?: number, shiny?: boolean, variant?: Variant, startLoad?: boolean): Promise<void> {
|
||||||
return new Promise(resolve => {
|
return new Promise(resolve => {
|
||||||
const spriteKey = this.getSpriteKey(female, formIndex, shiny, variant);
|
const spriteKey = this.getSpriteKey(female, formIndex, shiny, variant);
|
||||||
scene.loadPokemonAtlas(spriteKey, this.getSpriteAtlasPath(female, formIndex, shiny, variant));
|
scene.loadPokemonAtlas(spriteKey, this.getSpriteAtlasPath(female, formIndex, shiny, variant));
|
||||||
@ -536,7 +553,7 @@ export abstract class PokemonSpeciesForm {
|
|||||||
return cry;
|
return cry;
|
||||||
}
|
}
|
||||||
|
|
||||||
generateCandyColors(scene: BattleScene): integer[][] {
|
generateCandyColors(scene: BattleScene): number[][] {
|
||||||
const sourceTexture = scene.textures.get(this.getSpriteKey(false));
|
const sourceTexture = scene.textures.get(this.getSpriteKey(false));
|
||||||
|
|
||||||
const sourceFrame = sourceTexture.frames[sourceTexture.firstFrame];
|
const sourceFrame = sourceTexture.frames[sourceTexture.firstFrame];
|
||||||
@ -544,7 +561,7 @@ export abstract class PokemonSpeciesForm {
|
|||||||
|
|
||||||
const canvas = document.createElement("canvas");
|
const canvas = document.createElement("canvas");
|
||||||
|
|
||||||
const spriteColors: integer[][] = [];
|
const spriteColors: number[][] = [];
|
||||||
|
|
||||||
const context = canvas.getContext("2d");
|
const context = canvas.getContext("2d");
|
||||||
const frame = sourceFrame;
|
const frame = sourceFrame;
|
||||||
@ -567,7 +584,7 @@ export abstract class PokemonSpeciesForm {
|
|||||||
}
|
}
|
||||||
|
|
||||||
for (let i = 0; i < pixelData.length; i += 4) {
|
for (let i = 0; i < pixelData.length; i += 4) {
|
||||||
const total = pixelData.slice(i, i + 3).reduce((total: integer, value: integer) => total + value, 0);
|
const total = pixelData.slice(i, i + 3).reduce((total: number, value: number) => total + value, 0);
|
||||||
if (!total) {
|
if (!total) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
@ -586,27 +603,28 @@ export abstract class PokemonSpeciesForm {
|
|||||||
|
|
||||||
Math.random = originalRandom;
|
Math.random = originalRandom;
|
||||||
|
|
||||||
return Array.from(paletteColors.keys()).map(c => Object.values(rgbaFromArgb(c)) as integer[]);
|
return Array.from(paletteColors.keys()).map(c => Object.values(rgbaFromArgb(c)) as number[]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export default class PokemonSpecies extends PokemonSpeciesForm implements Localizable {
|
export default class PokemonSpecies extends PokemonSpeciesForm implements Localizable {
|
||||||
public name: string;
|
public name: string;
|
||||||
public subLegendary: boolean;
|
readonly subLegendary: boolean;
|
||||||
public legendary: boolean;
|
readonly legendary: boolean;
|
||||||
public mythical: boolean;
|
readonly mythical: boolean;
|
||||||
public species: string;
|
readonly species: string;
|
||||||
public growthRate: GrowthRate;
|
readonly growthRate: GrowthRate;
|
||||||
public malePercent: number | null;
|
readonly malePercent: number | null;
|
||||||
public genderDiffs: boolean;
|
readonly genderDiffs: boolean;
|
||||||
public canChangeForm: boolean;
|
readonly canChangeForm: boolean;
|
||||||
public forms: PokemonForm[];
|
readonly forms: PokemonForm[];
|
||||||
|
|
||||||
constructor(id: Species, generation: integer, subLegendary: boolean, legendary: boolean, mythical: boolean, species: string,
|
constructor(id: Species, generation: number, subLegendary: boolean, legendary: boolean, mythical: boolean, species: string,
|
||||||
type1: Type, type2: Type | null, height: number, weight: number, ability1: Abilities, ability2: Abilities, abilityHidden: Abilities,
|
type1: Type, type2: Type | null, height: number, weight: number, ability1: Abilities, ability2: Abilities, abilityHidden: Abilities,
|
||||||
baseTotal: integer, baseHp: integer, baseAtk: integer, baseDef: integer, baseSpatk: integer, baseSpdef: integer, baseSpd: integer,
|
baseTotal: number, baseHp: number, baseAtk: number, baseDef: number, baseSpatk: number, baseSpdef: number, baseSpd: number,
|
||||||
catchRate: integer, baseFriendship: integer, baseExp: integer, growthRate: GrowthRate, malePercent: number | null,
|
catchRate: number, baseFriendship: number, baseExp: number, growthRate: GrowthRate, malePercent: number | null,
|
||||||
genderDiffs: boolean, canChangeForm?: boolean, ...forms: PokemonForm[]) {
|
genderDiffs: boolean, canChangeForm?: boolean, ...forms: PokemonForm[]
|
||||||
|
) {
|
||||||
super(type1, type2, height, weight, ability1, ability2, abilityHidden, baseTotal, baseHp, baseAtk, baseDef, baseSpatk, baseSpdef, baseSpd,
|
super(type1, type2, height, weight, ability1, ability2, abilityHidden, baseTotal, baseHp, baseAtk, baseDef, baseSpatk, baseSpdef, baseSpd,
|
||||||
catchRate, baseFriendship, baseExp, genderDiffs, false);
|
catchRate, baseFriendship, baseExp, genderDiffs, false);
|
||||||
this.speciesId = id;
|
this.speciesId = id;
|
||||||
@ -631,7 +649,7 @@ export default class PokemonSpecies extends PokemonSpeciesForm implements Locali
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
getName(formIndex?: integer): string {
|
getName(formIndex?: number): string {
|
||||||
if (formIndex !== undefined && this.forms.length) {
|
if (formIndex !== undefined && this.forms.length) {
|
||||||
const form = this.forms[formIndex];
|
const form = this.forms[formIndex];
|
||||||
let key: string | null;
|
let key: string | null;
|
||||||
@ -662,11 +680,11 @@ export default class PokemonSpecies extends PokemonSpeciesForm implements Locali
|
|||||||
this.name = i18next.t(`pokemon:${Species[this.speciesId].toLowerCase()}`);
|
this.name = i18next.t(`pokemon:${Species[this.speciesId].toLowerCase()}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
getWildSpeciesForLevel(level: integer, allowEvolving: boolean, isBoss: boolean, gameMode: GameMode): Species {
|
getWildSpeciesForLevel(level: number, allowEvolving: boolean, isBoss: boolean, gameMode: GameMode): Species {
|
||||||
return this.getSpeciesForLevel(level, allowEvolving, false, (isBoss ? PartyMemberStrength.WEAKER : PartyMemberStrength.AVERAGE) + (gameMode?.isEndless ? 1 : 0));
|
return this.getSpeciesForLevel(level, allowEvolving, false, (isBoss ? PartyMemberStrength.WEAKER : PartyMemberStrength.AVERAGE) + (gameMode?.isEndless ? 1 : 0));
|
||||||
}
|
}
|
||||||
|
|
||||||
getTrainerSpeciesForLevel(level: integer, allowEvolving: boolean = false, strength: PartyMemberStrength, currentWave: number = 0): Species {
|
getTrainerSpeciesForLevel(level: number, allowEvolving: boolean = false, strength: PartyMemberStrength, currentWave: number = 0): Species {
|
||||||
return this.getSpeciesForLevel(level, allowEvolving, true, strength, currentWave);
|
return this.getSpeciesForLevel(level, allowEvolving, true, strength, currentWave);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -688,7 +706,7 @@ export default class PokemonSpecies extends PokemonSpeciesForm implements Locali
|
|||||||
* @param strength {@linkcode PartyMemberStrength} The strength of the party member in question
|
* @param strength {@linkcode PartyMemberStrength} The strength of the party member in question
|
||||||
* @returns {@linkcode integer} The level difference from expected evolution level tolerated for a mon to be unevolved. Lower value = higher evolution chance.
|
* @returns {@linkcode integer} The level difference from expected evolution level tolerated for a mon to be unevolved. Lower value = higher evolution chance.
|
||||||
*/
|
*/
|
||||||
private getStrengthLevelDiff(strength: PartyMemberStrength): integer {
|
private getStrengthLevelDiff(strength: PartyMemberStrength): number {
|
||||||
switch (Math.min(strength, PartyMemberStrength.STRONGER)) {
|
switch (Math.min(strength, PartyMemberStrength.STRONGER)) {
|
||||||
case PartyMemberStrength.WEAKEST:
|
case PartyMemberStrength.WEAKEST:
|
||||||
return 60;
|
return 60;
|
||||||
@ -705,7 +723,7 @@ export default class PokemonSpecies extends PokemonSpeciesForm implements Locali
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
getSpeciesForLevel(level: integer, allowEvolving: boolean = false, forTrainer: boolean = false, strength: PartyMemberStrength = PartyMemberStrength.WEAKER, currentWave: number = 0): Species {
|
getSpeciesForLevel(level: number, allowEvolving: boolean = false, forTrainer: boolean = false, strength: PartyMemberStrength = PartyMemberStrength.WEAKER, currentWave: number = 0): Species {
|
||||||
const prevolutionLevels = this.getPrevolutionLevels();
|
const prevolutionLevels = this.getPrevolutionLevels();
|
||||||
|
|
||||||
if (prevolutionLevels.length) {
|
if (prevolutionLevels.length) {
|
||||||
@ -847,7 +865,7 @@ export default class PokemonSpecies extends PokemonSpeciesForm implements Locali
|
|||||||
}
|
}
|
||||||
|
|
||||||
// This could definitely be written better and more accurate to the getSpeciesForLevel logic, but it is only for generating movesets for evolved Pokemon
|
// This could definitely be written better and more accurate to the getSpeciesForLevel logic, but it is only for generating movesets for evolved Pokemon
|
||||||
getSimulatedEvolutionChain(currentLevel: integer, forTrainer: boolean = false, isBoss: boolean = false, player: boolean = false): EvolutionLevel[] {
|
getSimulatedEvolutionChain(currentLevel: number, forTrainer: boolean = false, isBoss: boolean = false, player: boolean = false): EvolutionLevel[] {
|
||||||
const ret: EvolutionLevel[] = [];
|
const ret: EvolutionLevel[] = [];
|
||||||
if (pokemonPrevolutions.hasOwnProperty(this.speciesId)) {
|
if (pokemonPrevolutions.hasOwnProperty(this.speciesId)) {
|
||||||
const prevolutionLevels = this.getPrevolutionLevels().reverse();
|
const prevolutionLevels = this.getPrevolutionLevels().reverse();
|
||||||
@ -899,7 +917,7 @@ export default class PokemonSpecies extends PokemonSpeciesForm implements Locali
|
|||||||
return variantData.hasOwnProperty(variantDataIndex) || variantData.hasOwnProperty(this.speciesId);
|
return variantData.hasOwnProperty(variantDataIndex) || variantData.hasOwnProperty(this.speciesId);
|
||||||
}
|
}
|
||||||
|
|
||||||
getFormSpriteKey(formIndex?: integer) {
|
getFormSpriteKey(formIndex?: number) {
|
||||||
if (this.forms.length && (formIndex !== undefined && formIndex >= this.forms.length)) {
|
if (this.forms.length && (formIndex !== undefined && formIndex >= this.forms.length)) {
|
||||||
console.warn(`Attempted accessing form with index ${formIndex} of species ${this.getName()} with only ${this.forms.length || 0} forms`);
|
console.warn(`Attempted accessing form with index ${formIndex} of species ${this.getName()} with only ${this.forms.length || 0} forms`);
|
||||||
formIndex = Math.min(formIndex, this.forms.length - 1);
|
formIndex = Math.min(formIndex, this.forms.length - 1);
|
||||||
@ -919,16 +937,17 @@ export class PokemonForm extends PokemonSpeciesForm {
|
|||||||
private starterSelectableKeys: string[] = [ "10", "50", "10-pc", "50-pc", "red", "orange", "yellow", "green", "blue", "indigo", "violet" ];
|
private starterSelectableKeys: string[] = [ "10", "50", "10-pc", "50-pc", "red", "orange", "yellow", "green", "blue", "indigo", "violet" ];
|
||||||
|
|
||||||
constructor(formName: string, formKey: string, type1: Type, type2: Type | null, height: number, weight: number, ability1: Abilities, ability2: Abilities, abilityHidden: Abilities,
|
constructor(formName: string, formKey: string, type1: Type, type2: Type | null, height: number, weight: number, ability1: Abilities, ability2: Abilities, abilityHidden: Abilities,
|
||||||
baseTotal: integer, baseHp: integer, baseAtk: integer, baseDef: integer, baseSpatk: integer, baseSpdef: integer, baseSpd: integer,
|
baseTotal: number, baseHp: number, baseAtk: number, baseDef: number, baseSpatk: number, baseSpdef: number, baseSpd: number,
|
||||||
catchRate: integer, baseFriendship: integer, baseExp: integer, genderDiffs?: boolean, formSpriteKey?: string | null, isStarterSelectable?: boolean, ) {
|
catchRate: number, baseFriendship: number, baseExp: number, genderDiffs: boolean = false, formSpriteKey: string | null = null, isStarterSelectable: boolean = false
|
||||||
|
) {
|
||||||
super(type1, type2, height, weight, ability1, ability2, abilityHidden, baseTotal, baseHp, baseAtk, baseDef, baseSpatk, baseSpdef, baseSpd,
|
super(type1, type2, height, weight, ability1, ability2, abilityHidden, baseTotal, baseHp, baseAtk, baseDef, baseSpatk, baseSpdef, baseSpd,
|
||||||
catchRate, baseFriendship, baseExp, !!genderDiffs, (!!isStarterSelectable || !formKey));
|
catchRate, baseFriendship, baseExp, genderDiffs, (isStarterSelectable || !formKey));
|
||||||
this.formName = formName;
|
this.formName = formName;
|
||||||
this.formKey = formKey;
|
this.formKey = formKey;
|
||||||
this.formSpriteKey = formSpriteKey !== undefined ? formSpriteKey : null;
|
this.formSpriteKey = formSpriteKey;
|
||||||
}
|
}
|
||||||
|
|
||||||
getFormSpriteKey(_formIndex?: integer) {
|
getFormSpriteKey(_formIndex?: number) {
|
||||||
return this.formSpriteKey !== null ? this.formSpriteKey : this.formKey;
|
return this.formSpriteKey !== null ? this.formSpriteKey : this.formKey;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -12,7 +12,7 @@ import * as Utils from "#app/utils";
|
|||||||
import { Type, TypeDamageMultiplier, getTypeDamageMultiplier, getTypeRgb } from "#app/data/type";
|
import { Type, TypeDamageMultiplier, getTypeDamageMultiplier, getTypeRgb } from "#app/data/type";
|
||||||
import { getLevelTotalExp } from "#app/data/exp";
|
import { getLevelTotalExp } from "#app/data/exp";
|
||||||
import { Stat, type PermanentStat, type BattleStat, type EffectiveStat, PERMANENT_STATS, BATTLE_STATS, EFFECTIVE_STATS } from "#enums/stat";
|
import { Stat, type PermanentStat, type BattleStat, type EffectiveStat, PERMANENT_STATS, BATTLE_STATS, EFFECTIVE_STATS } from "#enums/stat";
|
||||||
import { DamageMoneyRewardModifier, EnemyDamageBoosterModifier, EnemyDamageReducerModifier, EnemyEndureChanceModifier, EnemyFusionChanceModifier, HiddenAbilityRateBoosterModifier, BaseStatModifier, PokemonFriendshipBoosterModifier, PokemonHeldItemModifier, PokemonNatureWeightModifier, ShinyRateBoosterModifier, SurviveDamageModifier, TempStatStageBoosterModifier, TempCritBoosterModifier, StatBoosterModifier, CritBoosterModifier, TerastallizeModifier, PokemonBaseStatFlatModifier, PokemonBaseStatTotalModifier, PokemonIncrementingStatModifier, EvoTrackerModifier } from "#app/modifier/modifier";
|
import { DamageMoneyRewardModifier, EnemyDamageBoosterModifier, EnemyDamageReducerModifier, EnemyEndureChanceModifier, EnemyFusionChanceModifier, HiddenAbilityRateBoosterModifier, BaseStatModifier, PokemonFriendshipBoosterModifier, PokemonHeldItemModifier, PokemonNatureWeightModifier, ShinyRateBoosterModifier, SurviveDamageModifier, TempStatStageBoosterModifier, TempCritBoosterModifier, StatBoosterModifier, CritBoosterModifier, TerastallizeModifier, PokemonBaseStatFlatModifier, PokemonBaseStatTotalModifier, PokemonIncrementingStatModifier, EvoTrackerModifier, PokemonMultiHitModifier } from "#app/modifier/modifier";
|
||||||
import { PokeballType } from "#app/data/pokeball";
|
import { PokeballType } from "#app/data/pokeball";
|
||||||
import { Gender } from "#app/data/gender";
|
import { Gender } from "#app/data/gender";
|
||||||
import { initMoveAnim, loadMoveAnimAssets } from "#app/data/battle-anims";
|
import { initMoveAnim, loadMoveAnimAssets } from "#app/data/battle-anims";
|
||||||
@ -22,7 +22,7 @@ import { reverseCompatibleTms, tmSpecies, tmPoolTiers } from "#app/data/balance/
|
|||||||
import { BattlerTag, BattlerTagLapseType, EncoreTag, GroundedTag, HighestStatBoostTag, SubstituteTag, TypeImmuneTag, getBattlerTag, SemiInvulnerableTag, TypeBoostTag, MoveRestrictionBattlerTag, ExposedTag, DragonCheerTag, CritBoostTag, TrappedTag, TarShotTag, AutotomizedTag, PowerTrickTag } from "../data/battler-tags";
|
import { BattlerTag, BattlerTagLapseType, EncoreTag, GroundedTag, HighestStatBoostTag, SubstituteTag, TypeImmuneTag, getBattlerTag, SemiInvulnerableTag, TypeBoostTag, MoveRestrictionBattlerTag, ExposedTag, DragonCheerTag, CritBoostTag, TrappedTag, TarShotTag, AutotomizedTag, PowerTrickTag } from "../data/battler-tags";
|
||||||
import { WeatherType } from "#app/data/weather";
|
import { WeatherType } from "#app/data/weather";
|
||||||
import { ArenaTagSide, NoCritTag, WeakenMoveScreenTag } from "#app/data/arena-tag";
|
import { ArenaTagSide, NoCritTag, WeakenMoveScreenTag } from "#app/data/arena-tag";
|
||||||
import { Ability, AbAttr, StatMultiplierAbAttr, BlockCritAbAttr, BonusCritAbAttr, BypassBurnDamageReductionAbAttr, FieldPriorityMoveImmunityAbAttr, IgnoreOpponentStatStagesAbAttr, MoveImmunityAbAttr, PreDefendFullHpEndureAbAttr, ReceivedMoveDamageMultiplierAbAttr, StabBoostAbAttr, StatusEffectImmunityAbAttr, TypeImmunityAbAttr, WeightMultiplierAbAttr, allAbilities, applyAbAttrs, applyStatMultiplierAbAttrs, applyPreApplyBattlerTagAbAttrs, applyPreAttackAbAttrs, applyPreDefendAbAttrs, applyPreSetStatusAbAttrs, UnsuppressableAbilityAbAttr, SuppressFieldAbilitiesAbAttr, NoFusionAbilityAbAttr, MultCritAbAttr, IgnoreTypeImmunityAbAttr, DamageBoostAbAttr, IgnoreTypeStatusEffectImmunityAbAttr, ConditionalCritAbAttr, applyFieldStatMultiplierAbAttrs, FieldMultiplyStatAbAttr, AddSecondStrikeAbAttr, UserFieldStatusEffectImmunityAbAttr, UserFieldBattlerTagImmunityAbAttr, BattlerTagImmunityAbAttr, MoveTypeChangeAbAttr, FullHpResistTypeAbAttr, applyCheckTrappedAbAttrs, CheckTrappedAbAttr, PostSetStatusAbAttr, applyPostSetStatusAbAttrs, InfiltratorAbAttr, AlliedFieldDamageReductionAbAttr } from "#app/data/ability";
|
import { Ability, AbAttr, StatMultiplierAbAttr, BlockCritAbAttr, BonusCritAbAttr, BypassBurnDamageReductionAbAttr, FieldPriorityMoveImmunityAbAttr, IgnoreOpponentStatStagesAbAttr, MoveImmunityAbAttr, PreDefendFullHpEndureAbAttr, ReceivedMoveDamageMultiplierAbAttr, StabBoostAbAttr, StatusEffectImmunityAbAttr, TypeImmunityAbAttr, WeightMultiplierAbAttr, allAbilities, applyAbAttrs, applyStatMultiplierAbAttrs, applyPreApplyBattlerTagAbAttrs, applyPreAttackAbAttrs, applyPreDefendAbAttrs, applyPreSetStatusAbAttrs, UnsuppressableAbilityAbAttr, SuppressFieldAbilitiesAbAttr, NoFusionAbilityAbAttr, MultCritAbAttr, IgnoreTypeImmunityAbAttr, DamageBoostAbAttr, IgnoreTypeStatusEffectImmunityAbAttr, ConditionalCritAbAttr, applyFieldStatMultiplierAbAttrs, FieldMultiplyStatAbAttr, AddSecondStrikeAbAttr, UserFieldStatusEffectImmunityAbAttr, UserFieldBattlerTagImmunityAbAttr, BattlerTagImmunityAbAttr, MoveTypeChangeAbAttr, FullHpResistTypeAbAttr, applyCheckTrappedAbAttrs, CheckTrappedAbAttr, PostSetStatusAbAttr, applyPostSetStatusAbAttrs, InfiltratorAbAttr, AlliedFieldDamageReductionAbAttr, PostDamageAbAttr, applyPostDamageAbAttrs, PostDamageForceSwitchAbAttr } from "#app/data/ability";
|
||||||
import PokemonData from "#app/system/pokemon-data";
|
import PokemonData from "#app/system/pokemon-data";
|
||||||
import { BattlerIndex } from "#app/battle";
|
import { BattlerIndex } from "#app/battle";
|
||||||
import { Mode } from "#app/ui/ui";
|
import { Mode } from "#app/ui/ui";
|
||||||
@ -428,38 +428,26 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container {
|
|||||||
resolve();
|
resolve();
|
||||||
};
|
};
|
||||||
if (this.shiny) {
|
if (this.shiny) {
|
||||||
const populateVariantColors = (key: string, back: boolean = false): Promise<void> => {
|
const populateVariantColors = (isBackSprite: boolean = false): Promise<void> => {
|
||||||
return new Promise(resolve => {
|
return new Promise(resolve => {
|
||||||
const battleSpritePath = this.getBattleSpriteAtlasPath(back, ignoreOverride).replace("variant/", "").replace(/_[1-3]$/, "");
|
const battleSpritePath = this.getBattleSpriteAtlasPath(isBackSprite, ignoreOverride).replace("variant/", "").replace(/_[1-3]$/, "");
|
||||||
let config = variantData;
|
let config = variantData;
|
||||||
const useExpSprite = this.scene.experimentalSprites && this.scene.hasExpSprite(this.getBattleSpriteKey(back, ignoreOverride));
|
const useExpSprite = this.scene.experimentalSprites && this.scene.hasExpSprite(this.getBattleSpriteKey(isBackSprite, ignoreOverride));
|
||||||
battleSpritePath.split("/").map(p => config ? config = config[p] : null);
|
battleSpritePath.split("/").map(p => config ? config = config[p] : null);
|
||||||
const variantSet: VariantSet = config as VariantSet;
|
const variantSet: VariantSet = config as VariantSet;
|
||||||
if (variantSet && variantSet[this.variant] === 1) {
|
if (variantSet && variantSet[this.variant] === 1) {
|
||||||
if (variantColorCache.hasOwnProperty(key)) {
|
const cacheKey = this.getBattleSpriteKey(isBackSprite);
|
||||||
return resolve();
|
if (!variantColorCache.hasOwnProperty(cacheKey)) {
|
||||||
|
this.populateVariantColorCache(cacheKey, useExpSprite, battleSpritePath);
|
||||||
}
|
}
|
||||||
this.scene.cachedFetch(`./images/pokemon/variant/${useExpSprite ? "exp/" : ""}${battleSpritePath}.json`).
|
|
||||||
then(res => {
|
|
||||||
// Prevent the JSON from processing if it failed to load
|
|
||||||
if (!res.ok) {
|
|
||||||
console.error(`Could not load ${res.url}!`);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
return res.json();
|
|
||||||
}).then(c => {
|
|
||||||
variantColorCache[key] = c;
|
|
||||||
resolve();
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
resolve();
|
|
||||||
}
|
}
|
||||||
|
resolve();
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
if (this.isPlayer()) {
|
if (this.isPlayer()) {
|
||||||
Promise.all([ populateVariantColors(this.getBattleSpriteKey(false)), populateVariantColors(this.getBattleSpriteKey(true), true) ]).then(() => updateFusionPaletteAndResolve());
|
Promise.all([ populateVariantColors(false), populateVariantColors(true) ]).then(() => updateFusionPaletteAndResolve());
|
||||||
} else {
|
} else {
|
||||||
populateVariantColors(this.getBattleSpriteKey(false)).then(() => updateFusionPaletteAndResolve());
|
populateVariantColors(false).then(() => updateFusionPaletteAndResolve());
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
updateFusionPaletteAndResolve();
|
updateFusionPaletteAndResolve();
|
||||||
@ -472,6 +460,45 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gracefully handle errors loading a variant sprite. Log if it fails and attempt to fall back on
|
||||||
|
* non-experimental sprites before giving up.
|
||||||
|
*
|
||||||
|
* @param cacheKey the cache key for the variant color sprite
|
||||||
|
* @param attemptedSpritePath the sprite path that failed to load
|
||||||
|
* @param useExpSprite was the attempted sprite experimental
|
||||||
|
* @param battleSpritePath the filename of the sprite
|
||||||
|
* @param optionalParams any additional params to log
|
||||||
|
*/
|
||||||
|
fallbackVariantColor(cacheKey: string, attemptedSpritePath: string, useExpSprite: boolean, battleSpritePath: string, ...optionalParams: any[]) {
|
||||||
|
console.warn(`Could not load ${attemptedSpritePath}!`, ...optionalParams);
|
||||||
|
if (useExpSprite) {
|
||||||
|
this.populateVariantColorCache(cacheKey, false, battleSpritePath);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Attempt to process variant sprite.
|
||||||
|
*
|
||||||
|
* @param cacheKey the cache key for the variant color sprite
|
||||||
|
* @param useExpSprite should the experimental sprite be used
|
||||||
|
* @param battleSpritePath the filename of the sprite
|
||||||
|
*/
|
||||||
|
populateVariantColorCache(cacheKey: string, useExpSprite: boolean, battleSpritePath: string) {
|
||||||
|
const spritePath = `./images/pokemon/variant/${useExpSprite ? "exp/" : ""}${battleSpritePath}.json`;
|
||||||
|
this.scene.cachedFetch(spritePath).then(res => {
|
||||||
|
// Prevent the JSON from processing if it failed to load
|
||||||
|
if (!res.ok) {
|
||||||
|
return this.fallbackVariantColor(cacheKey, res.url, useExpSprite, battleSpritePath, res.status, res.statusText);
|
||||||
|
}
|
||||||
|
return res.json();
|
||||||
|
}).catch(error => {
|
||||||
|
this.fallbackVariantColor(cacheKey, spritePath, useExpSprite, battleSpritePath, error);
|
||||||
|
}).then(c => {
|
||||||
|
variantColorCache[cacheKey] = c;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
getFormKey(): string {
|
getFormKey(): string {
|
||||||
if (!this.species.forms.length || this.species.forms.length <= this.formIndex) {
|
if (!this.species.forms.length || this.species.forms.length <= this.formIndex) {
|
||||||
return "";
|
return "";
|
||||||
@ -2792,7 +2819,7 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container {
|
|||||||
* We explicitly require to ignore the faint phase here, as we want to show the messages
|
* We explicitly require to ignore the faint phase here, as we want to show the messages
|
||||||
* about the critical hit and the super effective/not very effective messages before the faint phase.
|
* about the critical hit and the super effective/not very effective messages before the faint phase.
|
||||||
*/
|
*/
|
||||||
const damage = this.damageAndUpdate(isBlockedBySubstitute ? 0 : dmg, result as DamageResult, isCritical, isOneHitKo, isOneHitKo, true);
|
const damage = this.damageAndUpdate(isBlockedBySubstitute ? 0 : dmg, result as DamageResult, isCritical, isOneHitKo, isOneHitKo, true, source);
|
||||||
|
|
||||||
if (damage > 0) {
|
if (damage > 0) {
|
||||||
if (source.isPlayer()) {
|
if (source.isPlayer()) {
|
||||||
@ -2805,6 +2832,15 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container {
|
|||||||
source.turnData.currDamageDealt = damage;
|
source.turnData.currDamageDealt = damage;
|
||||||
this.turnData.damageTaken += damage;
|
this.turnData.damageTaken += damage;
|
||||||
this.battleData.hitCount++;
|
this.battleData.hitCount++;
|
||||||
|
|
||||||
|
// Multi-Lens and Parental Bond check for Wimp Out/Emergency Exit
|
||||||
|
if (this.hasAbilityWithAttr(PostDamageForceSwitchAbAttr)) {
|
||||||
|
const multiHitModifier = source.getHeldItems().find(m => m instanceof PokemonMultiHitModifier);
|
||||||
|
if (multiHitModifier || source.hasAbilityWithAttr(AddSecondStrikeAbAttr)) {
|
||||||
|
applyPostDamageAbAttrs(PostDamageAbAttr, this, damage, this.hasPassive(), false, [], source);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const attackResult = { move: move.id, result: result as DamageResult, damage: damage, critical: isCritical, sourceId: source.id, sourceBattlerIndex: source.getBattlerIndex() };
|
const attackResult = { move: move.id, result: result as DamageResult, damage: damage, critical: isCritical, sourceId: source.id, sourceBattlerIndex: source.getBattlerIndex() };
|
||||||
this.turnData.attacksReceived.unshift(attackResult);
|
this.turnData.attacksReceived.unshift(attackResult);
|
||||||
if (source.isPlayer() && !this.isPlayer()) {
|
if (source.isPlayer() && !this.isPlayer()) {
|
||||||
@ -2892,7 +2928,6 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container {
|
|||||||
this.destroySubstitute();
|
this.destroySubstitute();
|
||||||
this.resetSummonData();
|
this.resetSummonData();
|
||||||
}
|
}
|
||||||
|
|
||||||
return damage;
|
return damage;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -2906,12 +2941,13 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container {
|
|||||||
* @param ignoreFaintPhase boolean to ignore adding a FaintPhase, passsed to damage()
|
* @param ignoreFaintPhase boolean to ignore adding a FaintPhase, passsed to damage()
|
||||||
* @returns integer of damage done
|
* @returns integer of damage done
|
||||||
*/
|
*/
|
||||||
damageAndUpdate(damage: integer, result?: DamageResult, critical: boolean = false, ignoreSegments: boolean = false, preventEndure: boolean = false, ignoreFaintPhase: boolean = false): integer {
|
damageAndUpdate(damage: integer, result?: DamageResult, critical: boolean = false, ignoreSegments: boolean = false, preventEndure: boolean = false, ignoreFaintPhase: boolean = false, source?: Pokemon): integer {
|
||||||
const damagePhase = new DamagePhase(this.scene, this.getBattlerIndex(), damage, result as DamageResult, critical);
|
const damagePhase = new DamagePhase(this.scene, this.getBattlerIndex(), damage, result as DamageResult, critical);
|
||||||
this.scene.unshiftPhase(damagePhase);
|
this.scene.unshiftPhase(damagePhase);
|
||||||
damage = this.damage(damage, ignoreSegments, preventEndure, ignoreFaintPhase);
|
damage = this.damage(damage, ignoreSegments, preventEndure, ignoreFaintPhase);
|
||||||
// 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);
|
||||||
|
applyPostDamageAbAttrs(PostDamageAbAttr, this, damage, this.hasPassive(), false, [], source);
|
||||||
return damage;
|
return damage;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
import BattleScene from "#app/battle-scene";
|
import BattleScene from "#app/battle-scene";
|
||||||
import { BattlerIndex } from "#app/battle";
|
import { BattlerIndex } from "#app/battle";
|
||||||
import { applyAbAttrs, BlockNonDirectDamageAbAttr, BlockStatusDamageAbAttr, ReduceBurnDamageAbAttr } from "#app/data/ability";
|
import { applyAbAttrs, applyPostDamageAbAttrs, BlockNonDirectDamageAbAttr, BlockStatusDamageAbAttr, PostDamageAbAttr, ReduceBurnDamageAbAttr } from "#app/data/ability";
|
||||||
import { CommonBattleAnim, CommonAnim } from "#app/data/battle-anims";
|
import { CommonBattleAnim, CommonAnim } from "#app/data/battle-anims";
|
||||||
import { getStatusEffectActivationText } from "#app/data/status-effect";
|
import { getStatusEffectActivationText } from "#app/data/status-effect";
|
||||||
import { BattleSpec } from "#app/enums/battle-spec";
|
import { BattleSpec } from "#app/enums/battle-spec";
|
||||||
@ -41,6 +41,7 @@ export class PostTurnStatusEffectPhase extends PokemonPhase {
|
|||||||
// Set preventEndure flag to avoid pokemon surviving thanks to focus band, sturdy, endure ...
|
// Set preventEndure flag to avoid pokemon surviving thanks to focus band, sturdy, endure ...
|
||||||
this.scene.damageNumberHandler.add(this.getPokemon(), pokemon.damage(damage.value, false, true));
|
this.scene.damageNumberHandler.add(this.getPokemon(), pokemon.damage(damage.value, false, true));
|
||||||
pokemon.updateInfo();
|
pokemon.updateInfo();
|
||||||
|
applyPostDamageAbAttrs(PostDamageAbAttr, pokemon, damage.value, pokemon.hasPassive(), false, []);
|
||||||
}
|
}
|
||||||
new CommonBattleAnim(CommonAnim.POISON + (pokemon.status.effect - 1), pokemon).play(this.scene, false, () => this.end());
|
new CommonBattleAnim(CommonAnim.POISON + (pokemon.status.effect - 1), pokemon).play(this.scene, false, () => this.end());
|
||||||
} else {
|
} else {
|
||||||
|
614
src/test/abilities/wimp_out.test.ts
Normal file
614
src/test/abilities/wimp_out.test.ts
Normal file
@ -0,0 +1,614 @@
|
|||||||
|
import { BattlerIndex } from "#app/battle";
|
||||||
|
import { ArenaTagSide } from "#app/data/arena-tag";
|
||||||
|
import { allMoves } from "#app/data/move";
|
||||||
|
import GameManager from "#app/test/utils/gameManager";
|
||||||
|
import { Abilities } from "#enums/abilities";
|
||||||
|
import { ArenaTagType } from "#enums/arena-tag-type";
|
||||||
|
import { BattlerTagType } from "#enums/battler-tag-type";
|
||||||
|
import { Moves } from "#enums/moves";
|
||||||
|
import { Species } from "#enums/species";
|
||||||
|
import { Stat } from "#enums/stat";
|
||||||
|
import { StatusEffect } from "#enums/status-effect";
|
||||||
|
import { WeatherType } from "#enums/weather-type";
|
||||||
|
import Phaser from "phaser";
|
||||||
|
import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest";
|
||||||
|
|
||||||
|
describe("Abilities - Wimp Out", () => {
|
||||||
|
let phaserGame: Phaser.Game;
|
||||||
|
let game: GameManager;
|
||||||
|
|
||||||
|
beforeAll(() => {
|
||||||
|
phaserGame = new Phaser.Game({
|
||||||
|
type: Phaser.HEADLESS,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
game.phaseInterceptor.restoreOg();
|
||||||
|
});
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
game = new GameManager(phaserGame);
|
||||||
|
game.override
|
||||||
|
.battleType("single")
|
||||||
|
.ability(Abilities.WIMP_OUT)
|
||||||
|
.enemySpecies(Species.NINJASK)
|
||||||
|
.enemyPassiveAbility(Abilities.NO_GUARD)
|
||||||
|
.startingLevel(90)
|
||||||
|
.enemyLevel(70)
|
||||||
|
.moveset([ Moves.SPLASH, Moves.FALSE_SWIPE, Moves.ENDURE ])
|
||||||
|
.enemyMoveset(Moves.FALSE_SWIPE)
|
||||||
|
.disableCrits();
|
||||||
|
});
|
||||||
|
|
||||||
|
function confirmSwitch(): void {
|
||||||
|
const [ pokemon1, pokemon2 ] = game.scene.getParty();
|
||||||
|
|
||||||
|
expect(game.phaseInterceptor.log).toContain("SwitchSummonPhase");
|
||||||
|
|
||||||
|
expect(pokemon1.species.speciesId).not.toBe(Species.WIMPOD);
|
||||||
|
|
||||||
|
expect(pokemon2.species.speciesId).toBe(Species.WIMPOD);
|
||||||
|
expect(pokemon2.isFainted()).toBe(false);
|
||||||
|
expect(pokemon2.getHpRatio()).toBeLessThan(0.5);
|
||||||
|
}
|
||||||
|
|
||||||
|
function confirmNoSwitch(): void {
|
||||||
|
const [ pokemon1, pokemon2 ] = game.scene.getParty();
|
||||||
|
|
||||||
|
expect(game.phaseInterceptor.log).not.toContain("SwitchSummonPhase");
|
||||||
|
|
||||||
|
expect(pokemon2.species.speciesId).not.toBe(Species.WIMPOD);
|
||||||
|
|
||||||
|
expect(pokemon1.species.speciesId).toBe(Species.WIMPOD);
|
||||||
|
expect(pokemon1.isFainted()).toBe(false);
|
||||||
|
expect(pokemon1.getHpRatio()).toBeLessThan(0.5);
|
||||||
|
}
|
||||||
|
|
||||||
|
it("triggers regenerator passive single time when switching out with wimp out", async () => {
|
||||||
|
game.override
|
||||||
|
.passiveAbility(Abilities.REGENERATOR)
|
||||||
|
.startingLevel(5)
|
||||||
|
.enemyLevel(100);
|
||||||
|
await game.classicMode.startBattle([
|
||||||
|
Species.WIMPOD,
|
||||||
|
Species.TYRUNT
|
||||||
|
]);
|
||||||
|
|
||||||
|
const wimpod = game.scene.getPlayerPokemon()!;
|
||||||
|
|
||||||
|
game.move.select(Moves.SPLASH);
|
||||||
|
game.doSelectPartyPokemon(1);
|
||||||
|
await game.phaseInterceptor.to("TurnEndPhase");
|
||||||
|
|
||||||
|
expect(wimpod.hp).toEqual(Math.floor(wimpod.getMaxHp() * 0.33 + 1));
|
||||||
|
confirmSwitch();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("It makes wild pokemon flee if triggered", async () => {
|
||||||
|
game.override.enemyAbility(Abilities.WIMP_OUT);
|
||||||
|
await game.classicMode.startBattle([
|
||||||
|
Species.GOLISOPOD,
|
||||||
|
Species.TYRUNT
|
||||||
|
]);
|
||||||
|
|
||||||
|
const enemyPokemon = game.scene.getEnemyPokemon()!;
|
||||||
|
enemyPokemon.hp *= 0.52;
|
||||||
|
|
||||||
|
game.move.select(Moves.FALSE_SWIPE);
|
||||||
|
await game.phaseInterceptor.to("BerryPhase");
|
||||||
|
|
||||||
|
const isVisible = enemyPokemon.visible;
|
||||||
|
const hasFled = enemyPokemon.switchOutStatus;
|
||||||
|
expect(!isVisible && hasFled).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("Does not trigger when HP already below half", async () => {
|
||||||
|
await game.classicMode.startBattle([
|
||||||
|
Species.WIMPOD,
|
||||||
|
Species.TYRUNT
|
||||||
|
]);
|
||||||
|
const wimpod = game.scene.getPlayerPokemon()!;
|
||||||
|
wimpod.hp = 5;
|
||||||
|
|
||||||
|
game.move.select(Moves.SPLASH);
|
||||||
|
await game.phaseInterceptor.to("TurnEndPhase");
|
||||||
|
|
||||||
|
expect(wimpod.hp).toEqual(1);
|
||||||
|
confirmNoSwitch();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("Trapping moves do not prevent Wimp Out from activating.", async () => {
|
||||||
|
game.override
|
||||||
|
.enemyMoveset([ Moves.SPIRIT_SHACKLE ])
|
||||||
|
.startingLevel(53)
|
||||||
|
.enemyLevel(45);
|
||||||
|
await game.classicMode.startBattle([
|
||||||
|
Species.WIMPOD,
|
||||||
|
Species.TYRUNT
|
||||||
|
]);
|
||||||
|
|
||||||
|
game.move.select(Moves.SPLASH);
|
||||||
|
game.doSelectPartyPokemon(1);
|
||||||
|
|
||||||
|
await game.phaseInterceptor.to("TurnEndPhase");
|
||||||
|
|
||||||
|
expect(game.phaseInterceptor.log).toContain("SwitchSummonPhase");
|
||||||
|
expect(game.scene.getPlayerPokemon()!.getTag(BattlerTagType.TRAPPED)).toBeUndefined();
|
||||||
|
expect(game.scene.getParty()[1].getTag(BattlerTagType.TRAPPED)).toBeUndefined();
|
||||||
|
confirmSwitch();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("If this Ability activates due to being hit by U-turn or Volt Switch, the user of that move will not be switched out.", async () => {
|
||||||
|
game.override
|
||||||
|
.startingLevel(95)
|
||||||
|
.enemyMoveset([ Moves.U_TURN ]);
|
||||||
|
await game.classicMode.startBattle([
|
||||||
|
Species.WIMPOD,
|
||||||
|
Species.TYRUNT
|
||||||
|
]);
|
||||||
|
|
||||||
|
game.move.select(Moves.SPLASH);
|
||||||
|
game.doSelectPartyPokemon(1);
|
||||||
|
await game.phaseInterceptor.to("TurnEndPhase");
|
||||||
|
|
||||||
|
const enemyPokemon = game.scene.getEnemyPokemon()!;
|
||||||
|
const hasFled = enemyPokemon.switchOutStatus;
|
||||||
|
expect(hasFled).toBe(false);
|
||||||
|
confirmSwitch();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("If this Ability does not activate due to being hit by U-turn or Volt Switch, the user of that move will be switched out.", async () => {
|
||||||
|
game.override
|
||||||
|
.startingLevel(190)
|
||||||
|
.startingWave(8)
|
||||||
|
.enemyMoveset([ Moves.U_TURN ]);
|
||||||
|
await game.classicMode.startBattle([
|
||||||
|
Species.GOLISOPOD,
|
||||||
|
Species.TYRUNT
|
||||||
|
]);
|
||||||
|
const RIVAL_NINJASK1 = game.scene.getEnemyPokemon()?.id;
|
||||||
|
game.move.select(Moves.SPLASH);
|
||||||
|
await game.phaseInterceptor.to("BerryPhase", false);
|
||||||
|
expect(game.scene.getEnemyPokemon()?.id !== RIVAL_NINJASK1);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("Dragon Tail and Circle Throw switch out Pokémon before the Ability activates.", async () => {
|
||||||
|
game.override
|
||||||
|
.startingLevel(69)
|
||||||
|
.enemyMoveset([ Moves.DRAGON_TAIL ]);
|
||||||
|
await game.classicMode.startBattle([
|
||||||
|
Species.WIMPOD,
|
||||||
|
Species.TYRUNT
|
||||||
|
]);
|
||||||
|
|
||||||
|
const wimpod = game.scene.getPlayerPokemon()!;
|
||||||
|
|
||||||
|
game.move.select(Moves.SPLASH);
|
||||||
|
game.doSelectPartyPokemon(1);
|
||||||
|
await game.phaseInterceptor.to("SwitchSummonPhase", false);
|
||||||
|
|
||||||
|
expect(wimpod.summonData.abilitiesApplied).not.toContain(Abilities.WIMP_OUT);
|
||||||
|
|
||||||
|
await game.phaseInterceptor.to("TurnEndPhase");
|
||||||
|
|
||||||
|
expect(game.scene.getPlayerPokemon()!.species.speciesId).not.toBe(Species.WIMPOD);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("triggers when recoil damage is taken", 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("It does not activate when the Pokémon cuts its own HP", async () => {
|
||||||
|
game.override
|
||||||
|
.moveset([ Moves.SUBSTITUTE ])
|
||||||
|
.enemyMoveset([ Moves.SPLASH ]);
|
||||||
|
await game.classicMode.startBattle([
|
||||||
|
Species.WIMPOD,
|
||||||
|
Species.TYRUNT
|
||||||
|
]);
|
||||||
|
|
||||||
|
const wimpod = game.scene.getPlayerPokemon()!;
|
||||||
|
wimpod.hp *= 0.52;
|
||||||
|
|
||||||
|
game.move.select(Moves.SUBSTITUTE);
|
||||||
|
await game.phaseInterceptor.to("TurnEndPhase");
|
||||||
|
|
||||||
|
confirmNoSwitch();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("Does not trigger when neutralized", async () => {
|
||||||
|
game.override
|
||||||
|
.enemyAbility(Abilities.NEUTRALIZING_GAS)
|
||||||
|
.startingLevel(5);
|
||||||
|
await game.classicMode.startBattle([
|
||||||
|
Species.WIMPOD,
|
||||||
|
Species.TYRUNT
|
||||||
|
]);
|
||||||
|
|
||||||
|
game.move.select(Moves.SPLASH);
|
||||||
|
await game.phaseInterceptor.to("TurnEndPhase");
|
||||||
|
|
||||||
|
confirmNoSwitch();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("If it falls below half and recovers back above half from a Shell Bell, Wimp Out will activate even after the Shell Bell recovery", async () => {
|
||||||
|
game.override
|
||||||
|
.moveset([ Moves.DOUBLE_EDGE ])
|
||||||
|
.enemyMoveset([ Moves.SPLASH ])
|
||||||
|
.startingHeldItems([
|
||||||
|
{ name: "SHELL_BELL", count: 3 },
|
||||||
|
{ name: "HEALING_CHARM", count: 5 },
|
||||||
|
]);
|
||||||
|
await game.classicMode.startBattle([
|
||||||
|
Species.WIMPOD,
|
||||||
|
Species.TYRUNT
|
||||||
|
]);
|
||||||
|
|
||||||
|
game.scene.getPlayerPokemon()!.hp *= 0.75;
|
||||||
|
|
||||||
|
game.move.select(Moves.DOUBLE_EDGE);
|
||||||
|
game.doSelectPartyPokemon(1);
|
||||||
|
await game.phaseInterceptor.to("TurnEndPhase");
|
||||||
|
|
||||||
|
expect(game.scene.getParty()[1].getHpRatio()).toBeGreaterThan(0.5);
|
||||||
|
expect(game.phaseInterceptor.log).toContain("SwitchSummonPhase");
|
||||||
|
expect(game.scene.getPlayerPokemon()!.species.speciesId).toBe(Species.TYRUNT);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("Wimp Out will activate due to weather damage", async () => {
|
||||||
|
game.override
|
||||||
|
.weather(WeatherType.HAIL)
|
||||||
|
.enemyMoveset([ Moves.SPLASH ]);
|
||||||
|
await game.classicMode.startBattle([
|
||||||
|
Species.WIMPOD,
|
||||||
|
Species.TYRUNT
|
||||||
|
]);
|
||||||
|
|
||||||
|
game.scene.getPlayerPokemon()!.hp *= 0.51;
|
||||||
|
|
||||||
|
game.move.select(Moves.SPLASH);
|
||||||
|
game.doSelectPartyPokemon(1);
|
||||||
|
await game.phaseInterceptor.to("TurnEndPhase");
|
||||||
|
|
||||||
|
confirmSwitch();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("Does not trigger when enemy has sheer force", async () => {
|
||||||
|
game.override
|
||||||
|
.enemyAbility(Abilities.SHEER_FORCE)
|
||||||
|
.enemyMoveset(Moves.SLUDGE_BOMB)
|
||||||
|
.startingLevel(95);
|
||||||
|
await game.classicMode.startBattle([
|
||||||
|
Species.WIMPOD,
|
||||||
|
Species.TYRUNT
|
||||||
|
]);
|
||||||
|
|
||||||
|
game.move.select(Moves.SPLASH);
|
||||||
|
await game.phaseInterceptor.to("TurnEndPhase");
|
||||||
|
|
||||||
|
confirmNoSwitch();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("Wimp Out will activate due to post turn status damage", async () => {
|
||||||
|
game.override
|
||||||
|
.statusEffect(StatusEffect.POISON)
|
||||||
|
.enemyMoveset([ Moves.SPLASH ]);
|
||||||
|
await game.classicMode.startBattle([
|
||||||
|
Species.WIMPOD,
|
||||||
|
Species.TYRUNT
|
||||||
|
]);
|
||||||
|
|
||||||
|
game.scene.getPlayerPokemon()!.hp *= 0.51;
|
||||||
|
|
||||||
|
game.move.select(Moves.SPLASH);
|
||||||
|
game.doSelectPartyPokemon(1);
|
||||||
|
await game.toNextTurn();
|
||||||
|
|
||||||
|
confirmSwitch();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("Wimp Out will activate due to bad dreams", async () => {
|
||||||
|
game.override
|
||||||
|
.statusEffect(StatusEffect.SLEEP)
|
||||||
|
.enemyAbility(Abilities.BAD_DREAMS);
|
||||||
|
await game.classicMode.startBattle([
|
||||||
|
Species.WIMPOD,
|
||||||
|
Species.TYRUNT
|
||||||
|
]);
|
||||||
|
|
||||||
|
game.scene.getPlayerPokemon()!.hp *= 0.52;
|
||||||
|
|
||||||
|
game.move.select(Moves.SPLASH);
|
||||||
|
game.doSelectPartyPokemon(1);
|
||||||
|
await game.toNextTurn();
|
||||||
|
|
||||||
|
confirmSwitch();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("Wimp Out will activate due to leech seed", async () => {
|
||||||
|
game.override
|
||||||
|
.enemyMoveset([ Moves.LEECH_SEED ]);
|
||||||
|
await game.classicMode.startBattle([
|
||||||
|
Species.WIMPOD,
|
||||||
|
Species.TYRUNT
|
||||||
|
]);
|
||||||
|
game.scene.getPlayerPokemon()!.hp *= 0.52;
|
||||||
|
|
||||||
|
game.move.select(Moves.SPLASH);
|
||||||
|
game.doSelectPartyPokemon(1);
|
||||||
|
await game.toNextTurn();
|
||||||
|
|
||||||
|
confirmSwitch();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("Wimp Out will activate due to curse damage", async () => {
|
||||||
|
game.override
|
||||||
|
.enemySpecies(Species.DUSKNOIR)
|
||||||
|
.enemyMoveset([ Moves.CURSE ]);
|
||||||
|
await game.classicMode.startBattle([
|
||||||
|
Species.WIMPOD,
|
||||||
|
Species.TYRUNT
|
||||||
|
]);
|
||||||
|
game.scene.getPlayerPokemon()!.hp *= 0.52;
|
||||||
|
|
||||||
|
game.move.select(Moves.SPLASH);
|
||||||
|
game.doSelectPartyPokemon(1);
|
||||||
|
await game.toNextTurn();
|
||||||
|
|
||||||
|
confirmSwitch();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("Wimp Out will activate due to salt cure damage", async () => {
|
||||||
|
game.override
|
||||||
|
.enemySpecies(Species.NACLI)
|
||||||
|
.enemyMoveset([ Moves.SALT_CURE ])
|
||||||
|
.enemyLevel(1);
|
||||||
|
await game.classicMode.startBattle([
|
||||||
|
Species.WIMPOD,
|
||||||
|
Species.TYRUNT
|
||||||
|
]);
|
||||||
|
game.scene.getPlayerPokemon()!.hp *= 0.70;
|
||||||
|
|
||||||
|
game.move.select(Moves.SPLASH);
|
||||||
|
game.doSelectPartyPokemon(1);
|
||||||
|
await game.toNextTurn();
|
||||||
|
|
||||||
|
confirmSwitch();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("Wimp Out will activate due to damaging trap damage", async () => {
|
||||||
|
game.override
|
||||||
|
.enemySpecies(Species.MAGIKARP)
|
||||||
|
.enemyMoveset([ Moves.WHIRLPOOL ])
|
||||||
|
.enemyLevel(1);
|
||||||
|
await game.classicMode.startBattle([
|
||||||
|
Species.WIMPOD,
|
||||||
|
Species.TYRUNT
|
||||||
|
]);
|
||||||
|
game.scene.getPlayerPokemon()!.hp *= 0.55;
|
||||||
|
|
||||||
|
game.move.select(Moves.SPLASH);
|
||||||
|
game.doSelectPartyPokemon(1);
|
||||||
|
await game.toNextTurn();
|
||||||
|
|
||||||
|
confirmSwitch();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("Magic Guard passive should not allow indirect damage to trigger Wimp Out", 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
|
||||||
|
.passiveAbility(Abilities.MAGIC_GUARD)
|
||||||
|
.enemyMoveset([ Moves.LEECH_SEED ])
|
||||||
|
.weather(WeatherType.HAIL)
|
||||||
|
.statusEffect(StatusEffect.POISON);
|
||||||
|
await game.classicMode.startBattle([
|
||||||
|
Species.WIMPOD,
|
||||||
|
Species.TYRUNT
|
||||||
|
]);
|
||||||
|
game.scene.getPlayerPokemon()!.hp *= 0.51;
|
||||||
|
|
||||||
|
game.move.select(Moves.SPLASH);
|
||||||
|
await game.phaseInterceptor.to("TurnEndPhase");
|
||||||
|
|
||||||
|
expect(game.scene.getParty()[0].getHpRatio()).toEqual(0.51);
|
||||||
|
expect(game.phaseInterceptor.log).not.toContain("SwitchSummonPhase");
|
||||||
|
expect(game.scene.getPlayerPokemon()!.species.speciesId).toBe(Species.WIMPOD);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("Wimp Out activating should not cancel a double battle", async () => {
|
||||||
|
game.override
|
||||||
|
.battleType("double")
|
||||||
|
.enemyAbility(Abilities.WIMP_OUT)
|
||||||
|
.enemyMoveset([ Moves.SPLASH ])
|
||||||
|
.enemyLevel(1);
|
||||||
|
await game.classicMode.startBattle([
|
||||||
|
Species.WIMPOD,
|
||||||
|
Species.TYRUNT
|
||||||
|
]);
|
||||||
|
const enemyLeadPokemon = game.scene.getEnemyParty()[0];
|
||||||
|
const enemySecPokemon = game.scene.getEnemyParty()[1];
|
||||||
|
|
||||||
|
game.move.select(Moves.FALSE_SWIPE, 0, BattlerIndex.ENEMY);
|
||||||
|
game.move.select(Moves.SPLASH, 1);
|
||||||
|
|
||||||
|
await game.phaseInterceptor.to("BerryPhase");
|
||||||
|
|
||||||
|
const isVisibleLead = enemyLeadPokemon.visible;
|
||||||
|
const hasFledLead = enemyLeadPokemon.switchOutStatus;
|
||||||
|
const isVisibleSec = enemySecPokemon.visible;
|
||||||
|
const hasFledSec = enemySecPokemon.switchOutStatus;
|
||||||
|
expect(!isVisibleLead && hasFledLead && isVisibleSec && !hasFledSec).toBe(true);
|
||||||
|
expect(enemyLeadPokemon.hp).toBeLessThan(enemyLeadPokemon.getMaxHp());
|
||||||
|
expect(enemySecPokemon.hp).toEqual(enemySecPokemon.getMaxHp());
|
||||||
|
});
|
||||||
|
|
||||||
|
it("Wimp Out will activate due to aftermath", async () => {
|
||||||
|
game.override
|
||||||
|
.moveset([ Moves.THUNDER_PUNCH ])
|
||||||
|
.enemySpecies(Species.MAGIKARP)
|
||||||
|
.enemyAbility(Abilities.AFTERMATH)
|
||||||
|
.enemyMoveset([ Moves.SPLASH ])
|
||||||
|
.enemyLevel(1);
|
||||||
|
await game.classicMode.startBattle([
|
||||||
|
Species.WIMPOD,
|
||||||
|
Species.TYRUNT
|
||||||
|
]);
|
||||||
|
game.scene.getPlayerPokemon()!.hp *= 0.51;
|
||||||
|
|
||||||
|
game.move.select(Moves.THUNDER_PUNCH);
|
||||||
|
game.doSelectPartyPokemon(1);
|
||||||
|
await game.phaseInterceptor.to("TurnEndPhase");
|
||||||
|
|
||||||
|
confirmSwitch();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("Activates due to entry hazards", 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("Wimp Out will activate due to Nightmare", async () => {
|
||||||
|
game.override
|
||||||
|
.enemyMoveset([ Moves.NIGHTMARE ])
|
||||||
|
.statusEffect(StatusEffect.SLEEP);
|
||||||
|
await game.classicMode.startBattle([
|
||||||
|
Species.WIMPOD,
|
||||||
|
Species.TYRUNT
|
||||||
|
]);
|
||||||
|
game.scene.getPlayerPokemon()!.hp *= 0.65;
|
||||||
|
|
||||||
|
game.move.select(Moves.SPLASH);
|
||||||
|
game.doSelectPartyPokemon(1);
|
||||||
|
await game.toNextTurn();
|
||||||
|
|
||||||
|
confirmSwitch();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("triggers status on the wimp out user before a new pokemon is switched in", async () => {
|
||||||
|
game.override
|
||||||
|
.enemyMoveset(Moves.SLUDGE_BOMB)
|
||||||
|
.startingLevel(80);
|
||||||
|
await game.classicMode.startBattle([
|
||||||
|
Species.WIMPOD,
|
||||||
|
Species.TYRUNT
|
||||||
|
]);
|
||||||
|
vi.spyOn(allMoves[Moves.SLUDGE_BOMB], "chance", "get").mockReturnValue(100);
|
||||||
|
|
||||||
|
game.move.select(Moves.SPLASH);
|
||||||
|
game.doSelectPartyPokemon(1);
|
||||||
|
await game.phaseInterceptor.to("TurnEndPhase");
|
||||||
|
|
||||||
|
expect(game.scene.getParty()[1].status?.effect).toEqual(StatusEffect.POISON);
|
||||||
|
confirmSwitch();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("triggers after last hit of multi hit move", async () => {
|
||||||
|
game.override
|
||||||
|
.enemyMoveset(Moves.BULLET_SEED)
|
||||||
|
.enemyAbility(Abilities.SKILL_LINK);
|
||||||
|
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(5);
|
||||||
|
confirmSwitch();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("triggers after last hit of multi hit move (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("Wimp Out will 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");
|
||||||
|
}
|
||||||
|
|
||||||
|
confirmNoSwitch();
|
||||||
|
});
|
||||||
|
});
|
Loading…
Reference in New Issue
Block a user