mirror of
https://github.com/pagefaultgames/pokerogue.git
synced 2024-11-25 08:16:04 +00:00
Merge branch 'beta' into feat/export-settings
This commit is contained in:
commit
f88aaadbe7
@ -1 +1 @@
|
||||
Subproject commit 3cf6d553541d79ba165387bc73fb06544d00f1f9
|
||||
Subproject commit fc4a1effd5170def3c8314208a52cd0d8e6913ef
|
@ -323,6 +323,7 @@ export default class BattleScene extends SceneBase {
|
||||
this.conditionalQueue = [];
|
||||
this.phaseQueuePrependSpliceIndex = -1;
|
||||
this.nextCommandPhaseQueue = [];
|
||||
this.eventManager = new TimedEventManager();
|
||||
this.updateGameInfo();
|
||||
}
|
||||
|
||||
@ -378,7 +379,6 @@ export default class BattleScene extends SceneBase {
|
||||
|
||||
this.fieldSpritePipeline = new FieldSpritePipeline(this.game);
|
||||
(this.renderer as Phaser.Renderer.WebGL.WebGLRenderer).pipelines.add("FieldSprite", this.fieldSpritePipeline);
|
||||
this.eventManager = new TimedEventManager();
|
||||
|
||||
this.launchBattle();
|
||||
}
|
||||
|
@ -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 { Constructor } from "#app/utils";
|
||||
import * as Utils from "../utils";
|
||||
@ -9,7 +9,7 @@ import { StatusEffect, getNonVolatileStatusEffects, getStatusEffectDescriptor, g
|
||||
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 { ArenaTagSide, ArenaTrapTag } from "./arena-tag";
|
||||
import { BerryModifier, PokemonHeldItemModifier } from "../modifier/modifier";
|
||||
import { BerryModifier, HitHealModifier, PokemonHeldItemModifier } from "../modifier/modifier";
|
||||
import { TerrainType } from "./terrain";
|
||||
import { SpeciesFormChangeManualTrigger, SpeciesFormChangeRevertWeatherFormTrigger, SpeciesFormChangeWeatherTrigger } from "./pokemon-forms";
|
||||
import i18next from "i18next";
|
||||
@ -17,7 +17,7 @@ import { Localizable } from "#app/interfaces/locales";
|
||||
import { Command } from "../ui/command-ui-handler";
|
||||
import { BerryModifierType } from "#app/modifier/modifier-type";
|
||||
import { getPokeballName } from "./pokeball";
|
||||
import { BattlerIndex } from "#app/battle";
|
||||
import { BattlerIndex, BattleType } from "#app/battle";
|
||||
import { Abilities } from "#enums/abilities";
|
||||
import { ArenaTagType } from "#enums/arena-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 { StatStageChangePhase } from "#app/phases/stat-stage-change-phase";
|
||||
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 {
|
||||
public id: Abilities;
|
||||
@ -3092,7 +3098,7 @@ export class SuppressWeatherEffectAbAttr extends PreWeatherEffectAbAttr {
|
||||
/**
|
||||
* 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:
|
||||
* 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.
|
||||
*/
|
||||
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.totalDamageDealt / 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> {
|
||||
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);
|
||||
}
|
||||
|
||||
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
|
||||
* @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)
|
||||
.ignorable(),
|
||||
new Ability(Abilities.SHIELD_DUST, 3)
|
||||
.attr(IgnoreMoveEffectsAbAttr),
|
||||
.attr(IgnoreMoveEffectsAbAttr)
|
||||
.ignorable(),
|
||||
new Ability(Abilities.OWN_TEMPO, 3)
|
||||
.attr(BattlerTagImmunityAbAttr, BattlerTagType.CONFUSED)
|
||||
.attr(IntimidateImmunityAbAttr)
|
||||
@ -5180,11 +5425,9 @@ export function initAbilities() {
|
||||
new Ability(Abilities.CUTE_CHARM, 3)
|
||||
.attr(PostDefendContactApplyTagChanceAbAttr, 30, BattlerTagType.INFATUATED),
|
||||
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)
|
||||
.ignorable(),
|
||||
.conditionalAttr(p => p.scene.currentBattle.double && [ Abilities.PLUS, Abilities.MINUS ].some(a => p.getAlly().hasAbility(a)), StatMultiplierAbAttr, Stat.SPATK, 1.5),
|
||||
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)
|
||||
.ignorable(),
|
||||
.conditionalAttr(p => p.scene.currentBattle.double && [ Abilities.PLUS, Abilities.MINUS ].some(a => p.getAlly().hasAbility(a)), StatMultiplierAbAttr, Stat.SPATK, 1.5),
|
||||
new Ability(Abilities.FORECAST, 3)
|
||||
.attr(UncopiableAbilityAbAttr)
|
||||
.attr(NoFusionAbilityAbAttr)
|
||||
@ -5522,7 +5765,8 @@ export function initAbilities() {
|
||||
.attr(PostSummonMessageAbAttr, (pokemon: Pokemon) => i18next.t("abilityTriggers:postSummonTeravolt", { pokemonNameWithAffix: getPokemonNameWithAffix(pokemon) }))
|
||||
.attr(MoveAbilityBypassAbAttr),
|
||||
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)
|
||||
.ignorable()
|
||||
.unimplemented(),
|
||||
@ -5607,11 +5851,11 @@ export function initAbilities() {
|
||||
new Ability(Abilities.STAMINA, 7)
|
||||
.attr(PostDefendStatStageChangeAbAttr, (target, user, move) => move.category !== MoveCategory.STATUS, Stat.DEF, 1),
|
||||
new Ability(Abilities.WIMP_OUT, 7)
|
||||
.condition(getSheerForceHitDisableAbCondition())
|
||||
.unimplemented(),
|
||||
.attr(PostDamageForceSwitchAbAttr)
|
||||
.edgeCase(), // Should not trigger when hurting itself in confusion
|
||||
new Ability(Abilities.EMERGENCY_EXIT, 7)
|
||||
.condition(getSheerForceHitDisableAbCondition())
|
||||
.unimplemented(),
|
||||
.attr(PostDamageForceSwitchAbAttr)
|
||||
.edgeCase(), // Should not trigger when hurting itself in confusion
|
||||
new Ability(Abilities.WATER_COMPACTION, 7)
|
||||
.attr(PostDefendStatStageChangeAbAttr, (target, user, move) => user.getMoveType(move) === Type.WATER && move.category !== MoveCategory.STATUS, Stat.DEF, 2),
|
||||
new Ability(Abilities.MERCILESS, 7)
|
||||
@ -5973,16 +6217,14 @@ export function initAbilities() {
|
||||
.ignorable(),
|
||||
new Ability(Abilities.SWORD_OF_RUIN, 9)
|
||||
.attr(FieldMultiplyStatAbAttr, Stat.DEF, 0.75)
|
||||
.attr(PostSummonMessageAbAttr, (user) => i18next.t("abilityTriggers:postSummonSwordOfRuin", { pokemonNameWithAffix: getPokemonNameWithAffix(user), statName: i18next.t(getStatKey(Stat.DEF)) }))
|
||||
.ignorable(),
|
||||
.attr(PostSummonMessageAbAttr, (user) => i18next.t("abilityTriggers:postSummonSwordOfRuin", { pokemonNameWithAffix: getPokemonNameWithAffix(user), statName: i18next.t(getStatKey(Stat.DEF)) })),
|
||||
new Ability(Abilities.TABLETS_OF_RUIN, 9)
|
||||
.attr(FieldMultiplyStatAbAttr, Stat.ATK, 0.75)
|
||||
.attr(PostSummonMessageAbAttr, (user) => i18next.t("abilityTriggers:postSummonTabletsOfRuin", { pokemonNameWithAffix: getPokemonNameWithAffix(user), statName: i18next.t(getStatKey(Stat.ATK)) }))
|
||||
.ignorable(),
|
||||
new Ability(Abilities.BEADS_OF_RUIN, 9)
|
||||
.attr(FieldMultiplyStatAbAttr, Stat.SPDEF, 0.75)
|
||||
.attr(PostSummonMessageAbAttr, (user) => i18next.t("abilityTriggers:postSummonBeadsOfRuin", { pokemonNameWithAffix: getPokemonNameWithAffix(user), statName: i18next.t(getStatKey(Stat.SPDEF)) }))
|
||||
.ignorable(),
|
||||
.attr(PostSummonMessageAbAttr, (user) => i18next.t("abilityTriggers:postSummonBeadsOfRuin", { pokemonNameWithAffix: getPokemonNameWithAffix(user), statName: i18next.t(getStatKey(Stat.SPDEF)) })),
|
||||
new Ability(Abilities.ORICHALCUM_PULSE, 9)
|
||||
.attr(PostSummonWeatherChangeAbAttr, WeatherType.SUNNY)
|
||||
.attr(PostBiomeChangeWeatherChangeAbAttr, WeatherType.SUNNY)
|
||||
|
412
src/data/move.ts
412
src/data/move.ts
@ -8,7 +8,7 @@ import { Constructor, NumberHolder } from "#app/utils";
|
||||
import * as Utils from "../utils";
|
||||
import { WeatherType } from "./weather";
|
||||
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 { BattlerIndex, BattleType } from "../battle";
|
||||
import { TerrainType } from "./terrain";
|
||||
@ -1049,31 +1049,80 @@ export enum MoveEffectTrigger {
|
||||
POST_TARGET,
|
||||
}
|
||||
|
||||
interface MoveEffectAttrOptions {
|
||||
/**
|
||||
* Defines when this effect should trigger in the move's effect order
|
||||
* @see {@linkcode MoveEffectPhase}
|
||||
*/
|
||||
trigger?: MoveEffectTrigger;
|
||||
/** Should this effect only apply on the first hit? */
|
||||
firstHitOnly?: boolean;
|
||||
/** Should this effect only apply on the last hit? */
|
||||
lastHitOnly?: boolean;
|
||||
/** Should this effect only apply on the first target hit? */
|
||||
firstTargetOnly?: boolean;
|
||||
/** Overrides the secondary effect chance for this attr if set. */
|
||||
effectChanceOverride?: number;
|
||||
}
|
||||
|
||||
/** Base class defining all Move Effect Attributes
|
||||
* @extends MoveAttr
|
||||
* @see {@linkcode apply}
|
||||
*/
|
||||
export class MoveEffectAttr extends MoveAttr {
|
||||
/** Defines when this effect should trigger in the move's effect order
|
||||
* @see {@linkcode phases.MoveEffectPhase.start}
|
||||
/**
|
||||
* A container for this attribute's optional parameters
|
||||
* @see {@linkcode MoveEffectAttrOptions} for supported params.
|
||||
*/
|
||||
public trigger: MoveEffectTrigger;
|
||||
/** Should this effect only apply on the first hit? */
|
||||
public firstHitOnly: boolean;
|
||||
/** Should this effect only apply on the last hit? */
|
||||
public lastHitOnly: boolean;
|
||||
/** Should this effect only apply on the first target hit? */
|
||||
public firstTargetOnly: boolean;
|
||||
/** Overrides the secondary effect chance for this attr if set. */
|
||||
public effectChanceOverride?: number;
|
||||
protected options?: MoveEffectAttrOptions;
|
||||
|
||||
constructor(selfTarget?: boolean, trigger?: MoveEffectTrigger, firstHitOnly: boolean = false, lastHitOnly: boolean = false, firstTargetOnly: boolean = false, effectChanceOverride?: number) {
|
||||
constructor(selfTarget?: boolean, options?: MoveEffectAttrOptions) {
|
||||
super(selfTarget);
|
||||
this.trigger = trigger ?? MoveEffectTrigger.POST_APPLY;
|
||||
this.firstHitOnly = firstHitOnly;
|
||||
this.lastHitOnly = lastHitOnly;
|
||||
this.firstTargetOnly = firstTargetOnly;
|
||||
this.effectChanceOverride = effectChanceOverride;
|
||||
this.options = options;
|
||||
}
|
||||
|
||||
/**
|
||||
* Defines when this effect should trigger in the move's effect order.
|
||||
* @default MoveEffectTrigger.POST_APPLY
|
||||
* @see {@linkcode MoveEffectTrigger}
|
||||
*/
|
||||
public get trigger () {
|
||||
return this.options?.trigger ?? MoveEffectTrigger.POST_APPLY;
|
||||
}
|
||||
|
||||
/**
|
||||
* `true` if this effect should only trigger on the first hit of
|
||||
* multi-hit moves.
|
||||
* @default false
|
||||
*/
|
||||
public get firstHitOnly () {
|
||||
return this.options?.firstHitOnly ?? false;
|
||||
}
|
||||
|
||||
/**
|
||||
* `true` if this effect should only trigger on the last hit of
|
||||
* multi-hit moves.
|
||||
* @default false
|
||||
*/
|
||||
public get lastHitOnly () {
|
||||
return this.options?.lastHitOnly ?? false;
|
||||
}
|
||||
|
||||
/**
|
||||
* `true` if this effect should apply only upon hitting a target
|
||||
* for the first time when targeting multiple {@linkcode Pokemon}.
|
||||
* @default false
|
||||
*/
|
||||
public get firstTargetOnly () {
|
||||
return this.options?.firstTargetOnly ?? false;
|
||||
}
|
||||
|
||||
/**
|
||||
* If defined, overrides the move's base chance for this
|
||||
* secondary effect to trigger.
|
||||
*/
|
||||
public get effectChanceOverride () {
|
||||
return this.options?.effectChanceOverride;
|
||||
}
|
||||
|
||||
/**
|
||||
@ -1398,7 +1447,7 @@ export class RecoilAttr extends MoveEffectAttr {
|
||||
private unblockable: boolean;
|
||||
|
||||
constructor(useHp: boolean = false, damageRatio: number = 0.25, unblockable: boolean = false) {
|
||||
super(true, MoveEffectTrigger.POST_APPLY, false, true);
|
||||
super(true, { lastHitOnly: true });
|
||||
|
||||
this.useHp = useHp;
|
||||
this.damageRatio = damageRatio;
|
||||
@ -1425,8 +1474,8 @@ export class RecoilAttr extends MoveEffectAttr {
|
||||
return false;
|
||||
}
|
||||
|
||||
const damageValue = (!this.useHp ? user.turnData.damageDealt : user.getMaxHp()) * this.damageRatio;
|
||||
const minValue = user.turnData.damageDealt ? 1 : 0;
|
||||
const damageValue = (!this.useHp ? user.turnData.totalDamageDealt : user.getMaxHp()) * this.damageRatio;
|
||||
const minValue = user.turnData.totalDamageDealt ? 1 : 0;
|
||||
const recoilDamage = Utils.toDmgValue(damageValue, minValue);
|
||||
if (!recoilDamage) {
|
||||
return false;
|
||||
@ -1456,7 +1505,7 @@ export class RecoilAttr extends MoveEffectAttr {
|
||||
**/
|
||||
export class SacrificialAttr extends MoveEffectAttr {
|
||||
constructor() {
|
||||
super(true, MoveEffectTrigger.POST_TARGET);
|
||||
super(true, { trigger: MoveEffectTrigger.POST_TARGET });
|
||||
}
|
||||
|
||||
/**
|
||||
@ -1489,7 +1538,7 @@ export class SacrificialAttr extends MoveEffectAttr {
|
||||
**/
|
||||
export class SacrificialAttrOnHit extends MoveEffectAttr {
|
||||
constructor() {
|
||||
super(true, MoveEffectTrigger.HIT);
|
||||
super(true, { trigger: MoveEffectTrigger.HIT });
|
||||
}
|
||||
|
||||
/**
|
||||
@ -1528,7 +1577,7 @@ export class SacrificialAttrOnHit extends MoveEffectAttr {
|
||||
*/
|
||||
export class HalfSacrificialAttr extends MoveEffectAttr {
|
||||
constructor() {
|
||||
super(true, MoveEffectTrigger.POST_TARGET);
|
||||
super(true, { trigger: MoveEffectTrigger.POST_TARGET });
|
||||
}
|
||||
|
||||
/**
|
||||
@ -1932,7 +1981,7 @@ export class HitHealAttr extends MoveEffectAttr {
|
||||
private healStat: EffectiveStat | null;
|
||||
|
||||
constructor(healRatio?: number | null, healStat?: EffectiveStat) {
|
||||
super(true, MoveEffectTrigger.HIT);
|
||||
super(true, { trigger: MoveEffectTrigger.HIT });
|
||||
|
||||
this.healRatio = healRatio ?? 0.5;
|
||||
this.healStat = healStat ?? null;
|
||||
@ -1957,7 +2006,7 @@ export class HitHealAttr extends MoveEffectAttr {
|
||||
message = i18next.t("battle:drainMessage", { pokemonName: getPokemonNameWithAffix(target) });
|
||||
} else {
|
||||
// Default healing formula used by draining moves like Absorb, Draining Kiss, Bitter Blade, etc.
|
||||
healAmount = Utils.toDmgValue(user.turnData.currDamageDealt * this.healRatio);
|
||||
healAmount = Utils.toDmgValue(user.turnData.singleHitDamageDealt * this.healRatio);
|
||||
message = i18next.t("battle:regainHealth", { pokemonName: getPokemonNameWithAffix(user) });
|
||||
}
|
||||
if (reverseDrain) {
|
||||
@ -2141,7 +2190,7 @@ export class StatusEffectAttr extends MoveEffectAttr {
|
||||
public overrideStatus: boolean = false;
|
||||
|
||||
constructor(effect: StatusEffect, selfTarget?: boolean, turnsRemaining?: number, overrideStatus: boolean = false) {
|
||||
super(selfTarget, MoveEffectTrigger.HIT);
|
||||
super(selfTarget, { trigger: MoveEffectTrigger.HIT });
|
||||
|
||||
this.effect = effect;
|
||||
this.turnsRemaining = turnsRemaining;
|
||||
@ -2214,7 +2263,7 @@ export class MultiStatusEffectAttr extends StatusEffectAttr {
|
||||
|
||||
export class PsychoShiftEffectAttr extends MoveEffectAttr {
|
||||
constructor() {
|
||||
super(false, MoveEffectTrigger.HIT);
|
||||
super(false, { trigger: MoveEffectTrigger.HIT });
|
||||
}
|
||||
|
||||
apply(user: Pokemon, target: Pokemon, move: Move, args: any[]): boolean {
|
||||
@ -2251,7 +2300,7 @@ export class StealHeldItemChanceAttr extends MoveEffectAttr {
|
||||
private chance: number;
|
||||
|
||||
constructor(chance: number) {
|
||||
super(false, MoveEffectTrigger.HIT);
|
||||
super(false, { trigger: MoveEffectTrigger.HIT });
|
||||
this.chance = chance;
|
||||
}
|
||||
|
||||
@ -2312,7 +2361,7 @@ export class RemoveHeldItemAttr extends MoveEffectAttr {
|
||||
private berriesOnly: boolean;
|
||||
|
||||
constructor(berriesOnly: boolean) {
|
||||
super(false, MoveEffectTrigger.HIT);
|
||||
super(false, { trigger: MoveEffectTrigger.HIT });
|
||||
this.berriesOnly = berriesOnly;
|
||||
}
|
||||
|
||||
@ -2386,7 +2435,7 @@ export class RemoveHeldItemAttr extends MoveEffectAttr {
|
||||
export class EatBerryAttr extends MoveEffectAttr {
|
||||
protected chosenBerry: BerryModifier | undefined;
|
||||
constructor() {
|
||||
super(true, MoveEffectTrigger.HIT);
|
||||
super(true, { trigger: MoveEffectTrigger.HIT });
|
||||
}
|
||||
/**
|
||||
* Causes the target to eat a berry.
|
||||
@ -2489,7 +2538,7 @@ export class HealStatusEffectAttr extends MoveEffectAttr {
|
||||
* @param ...effects - List of status effects to cure
|
||||
*/
|
||||
constructor(selfTarget: boolean, ...effects: StatusEffect[]) {
|
||||
super(selfTarget, MoveEffectTrigger.POST_APPLY, false, true);
|
||||
super(selfTarget, { lastHitOnly: true });
|
||||
|
||||
this.effects = effects;
|
||||
}
|
||||
@ -2819,35 +2868,67 @@ export class AwaitCombinedPledgeAttr extends OverrideMoveEffectAttr {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Set of optional parameters that may be applied to stat stage changing effects
|
||||
* @extends MoveEffectAttrOptions
|
||||
* @see {@linkcode StatStageChangeAttr}
|
||||
*/
|
||||
interface StatStageChangeAttrOptions extends MoveEffectAttrOptions {
|
||||
/** If defined, needs to be met in order for the stat change to apply */
|
||||
condition?: MoveConditionFunc,
|
||||
/** `true` to display a message */
|
||||
showMessage?: boolean
|
||||
}
|
||||
|
||||
/**
|
||||
* Attribute used for moves that change stat stages
|
||||
*
|
||||
* @param stats {@linkcode BattleStat} Array of stat(s) to change
|
||||
* @param stages How many stages to change the stat(s) by, [-6, 6]
|
||||
* @param selfTarget `true` if the move is self-targetting
|
||||
* @param condition {@linkcode MoveConditionFunc} Optional condition to be checked in order to apply the changes
|
||||
* @param showMessage `true` to display a message; default `true`
|
||||
* @param firstHitOnly `true` if only the first hit of a multi hit move should cause a stat stage change; default `false`
|
||||
* @param moveEffectTrigger {@linkcode MoveEffectTrigger} When the stat change should trigger; default {@linkcode MoveEffectTrigger.HIT}
|
||||
* @param firstTargetOnly `true` if a move that hits multiple pokemon should only trigger the stat change if it hits at least one pokemon, rather than once per hit pokemon; default `false`
|
||||
* @param lastHitOnly `true` if the effect should only apply after the last hit of a multi hit move; default `false`
|
||||
* @param effectChanceOverride Will override the move's normal secondary effect chance if specified
|
||||
* @param options {@linkcode StatStageChangeAttrOptions} Container for any optional parameters for this attribute.
|
||||
*
|
||||
* @extends MoveEffectAttr
|
||||
* @see {@linkcode apply}
|
||||
*/
|
||||
export class StatStageChangeAttr extends MoveEffectAttr {
|
||||
public stats: BattleStat[];
|
||||
public stages: integer;
|
||||
private condition?: MoveConditionFunc | null;
|
||||
private showMessage: boolean;
|
||||
public stages: number;
|
||||
/**
|
||||
* Container for optional parameters to this attribute.
|
||||
* @see {@linkcode StatStageChangeAttrOptions} for available optional params
|
||||
*/
|
||||
protected override options?: StatStageChangeAttrOptions;
|
||||
|
||||
constructor(stats: BattleStat[], stages: integer, selfTarget?: boolean, condition?: MoveConditionFunc | null, showMessage: boolean = true, firstHitOnly: boolean = false, moveEffectTrigger: MoveEffectTrigger = MoveEffectTrigger.HIT, firstTargetOnly: boolean = false, lastHitOnly: boolean = false, effectChanceOverride?: number) {
|
||||
super(selfTarget, moveEffectTrigger, firstHitOnly, lastHitOnly, firstTargetOnly, effectChanceOverride);
|
||||
constructor(stats: BattleStat[], stages: number, selfTarget?: boolean, options?: StatStageChangeAttrOptions) {
|
||||
super(selfTarget, options);
|
||||
this.stats = stats;
|
||||
this.stages = stages;
|
||||
this.condition = condition;
|
||||
this.showMessage = showMessage;
|
||||
this.options = options;
|
||||
}
|
||||
|
||||
/**
|
||||
* The condition required for the stat stage change to apply.
|
||||
* Defaults to `null` (i.e. no condition required).
|
||||
*/
|
||||
private get condition () {
|
||||
return this.options?.condition ?? null;
|
||||
}
|
||||
|
||||
/**
|
||||
* `true` to display a message for the stat change.
|
||||
* @default true
|
||||
*/
|
||||
private get showMessage () {
|
||||
return this.options?.showMessage ?? true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Indicates when the stat change should trigger
|
||||
* @default MoveEffectTrigger.HIT
|
||||
*/
|
||||
public override get trigger () {
|
||||
return this.options?.trigger ?? MoveEffectTrigger.HIT;
|
||||
}
|
||||
|
||||
/**
|
||||
@ -2932,20 +3013,6 @@ export class SecretPowerAttr extends MoveEffectAttr {
|
||||
super(false);
|
||||
}
|
||||
|
||||
/**
|
||||
* Used to determine if the move should apply a secondary effect based on Secret Power's 30% chance
|
||||
* @returns `true` if the move's secondary effect should apply
|
||||
*/
|
||||
override canApply(user: Pokemon, target: Pokemon, move: Move, args?: any[]): boolean {
|
||||
this.effectChanceOverride = move.chance;
|
||||
const moveChance = this.getMoveChance(user, target, move, this.selfTarget);
|
||||
if (moveChance < 0 || moveChance === 100 || user.randSeedInt(100) < moveChance) {
|
||||
return true;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Used to apply the secondary effect to the target Pokemon
|
||||
* @returns `true` if a secondary effect is successfully applied
|
||||
@ -2962,8 +3029,6 @@ export class SecretPowerAttr extends MoveEffectAttr {
|
||||
const biome = user.scene.arena.biomeType;
|
||||
secondaryEffect = this.determineBiomeEffect(biome);
|
||||
}
|
||||
// effectChanceOverride used in the application of the actual secondary effect
|
||||
secondaryEffect.effectChanceOverride = 100;
|
||||
return secondaryEffect.apply(user, target, move, []);
|
||||
}
|
||||
|
||||
@ -3139,7 +3204,7 @@ export class CutHpStatStageBoostAttr extends StatStageChangeAttr {
|
||||
private messageCallback: ((user: Pokemon) => void) | undefined;
|
||||
|
||||
constructor(stat: BattleStat[], levels: integer, cutRatio: integer, messageCallback?: ((user: Pokemon) => void) | undefined) {
|
||||
super(stat, levels, true, null, true);
|
||||
super(stat, levels, true);
|
||||
|
||||
this.cutRatio = cutRatio;
|
||||
this.messageCallback = messageCallback;
|
||||
@ -4400,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)) {
|
||||
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]];
|
||||
return true;
|
||||
@ -4889,7 +4954,7 @@ export class BypassRedirectAttr extends MoveAttr {
|
||||
|
||||
export class FrenzyAttr extends MoveEffectAttr {
|
||||
constructor() {
|
||||
super(true, MoveEffectTrigger.HIT, false, true);
|
||||
super(true, { trigger: MoveEffectTrigger.HIT, lastHitOnly: true });
|
||||
}
|
||||
|
||||
canApply(user: Pokemon, target: Pokemon, move: Move, args: any[]) {
|
||||
@ -4962,7 +5027,7 @@ export class AddBattlerTagAttr extends MoveEffectAttr {
|
||||
private failOnOverlap: boolean;
|
||||
|
||||
constructor(tagType: BattlerTagType, selfTarget: boolean = false, failOnOverlap: boolean = false, turnCountMin: integer = 0, turnCountMax?: integer, lastHitOnly: boolean = false, cancelOnFail: boolean = false) {
|
||||
super(selfTarget, MoveEffectTrigger.POST_APPLY, false, lastHitOnly);
|
||||
super(selfTarget, { lastHitOnly: lastHitOnly });
|
||||
|
||||
this.tagType = tagType;
|
||||
this.turnCountMin = turnCountMin;
|
||||
@ -5397,7 +5462,7 @@ export class AddArenaTagAttr extends MoveEffectAttr {
|
||||
public selfSideTarget: boolean;
|
||||
|
||||
constructor(tagType: ArenaTagType, turnCount?: integer | null, failOnOverlap: boolean = false, selfSideTarget: boolean = false) {
|
||||
super(true, MoveEffectTrigger.POST_APPLY);
|
||||
super(true);
|
||||
|
||||
this.tagType = tagType;
|
||||
this.turnCount = turnCount!; // TODO: is the bang correct?
|
||||
@ -5435,7 +5500,7 @@ export class RemoveArenaTagsAttr extends MoveEffectAttr {
|
||||
public selfSideTarget: boolean;
|
||||
|
||||
constructor(tagTypes: ArenaTagType[], selfSideTarget: boolean) {
|
||||
super(true, MoveEffectTrigger.POST_APPLY);
|
||||
super(true);
|
||||
|
||||
this.tagTypes = tagTypes;
|
||||
this.selfSideTarget = selfSideTarget;
|
||||
@ -5501,7 +5566,7 @@ export class RemoveArenaTrapAttr extends MoveEffectAttr {
|
||||
private targetBothSides: boolean;
|
||||
|
||||
constructor(targetBothSides: boolean = false) {
|
||||
super(true, MoveEffectTrigger.PRE_APPLY);
|
||||
super(true, { trigger: MoveEffectTrigger.PRE_APPLY });
|
||||
this.targetBothSides = targetBothSides;
|
||||
}
|
||||
|
||||
@ -5537,7 +5602,7 @@ export class RemoveScreensAttr extends MoveEffectAttr {
|
||||
private targetBothSides: boolean;
|
||||
|
||||
constructor(targetBothSides: boolean = false) {
|
||||
super(true, MoveEffectTrigger.PRE_APPLY);
|
||||
super(true, { trigger: MoveEffectTrigger.PRE_APPLY });
|
||||
this.targetBothSides = targetBothSides;
|
||||
}
|
||||
|
||||
@ -5575,7 +5640,7 @@ export class SwapArenaTagsAttr extends MoveEffectAttr {
|
||||
|
||||
|
||||
constructor(SwapTags: ArenaTagType[]) {
|
||||
super(true, MoveEffectTrigger.POST_APPLY);
|
||||
super(true);
|
||||
this.SwapTags = SwapTags;
|
||||
}
|
||||
|
||||
@ -5696,12 +5761,13 @@ export class RevivalBlessingAttr extends MoveEffectAttr {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
export class ForceSwitchOutAttr extends MoveEffectAttr {
|
||||
constructor(
|
||||
private selfSwitch: boolean = false,
|
||||
private switchType: SwitchType = SwitchType.SWITCH
|
||||
) {
|
||||
super(false, MoveEffectTrigger.POST_APPLY, false, true);
|
||||
super(false, { lastHitOnly: true });
|
||||
}
|
||||
|
||||
isBatonPass() {
|
||||
@ -5714,12 +5780,19 @@ export class ForceSwitchOutAttr extends MoveEffectAttr {
|
||||
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;
|
||||
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
|
||||
if (switchOutTarget.scene.getParty().filter((p) => p.isAllowedInBattle() && !p.isOnField()).length < 1) {
|
||||
return false;
|
||||
@ -5745,11 +5818,27 @@ export class ForceSwitchOutAttr extends MoveEffectAttr {
|
||||
false, false), MoveEndPhase);
|
||||
}
|
||||
} 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)
|
||||
if (user.scene.currentBattle.waveIndex % 10 === 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Don't allow wild mons to flee with U-turn et al
|
||||
if (this.selfSwitch && !user.isPlayer() && move.category !== MoveCategory.STATUS) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (switchOutTarget.hp > 0) {
|
||||
switchOutTarget.leaveField(false);
|
||||
user.scene.queueMessage(i18next.t("moveTriggers:fled", { pokemonName: getPokemonNameWithAffix(switchOutTarget) }), null, true, 500);
|
||||
@ -5832,8 +5921,22 @@ export class ForceSwitchOutAttr extends MoveEffectAttr {
|
||||
}
|
||||
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 {
|
||||
apply(user: Pokemon, target: Pokemon, move: Move, args: any[]): boolean {
|
||||
@ -5852,7 +5955,7 @@ export class RemoveTypeAttr extends MoveEffectAttr {
|
||||
private messageCallback: ((user: Pokemon) => void) | undefined;
|
||||
|
||||
constructor(removedType: Type, messageCallback?: (user: Pokemon) => void) {
|
||||
super(true, MoveEffectTrigger.POST_TARGET);
|
||||
super(true, { trigger: MoveEffectTrigger.POST_TARGET });
|
||||
this.removedType = removedType;
|
||||
this.messageCallback = messageCallback;
|
||||
|
||||
@ -5921,22 +6024,114 @@ export class CopyBiomeTypeAttr extends MoveEffectAttr {
|
||||
return false;
|
||||
}
|
||||
|
||||
const biomeType = user.scene.arena.getTypeForBiome();
|
||||
const terrainType = user.scene.arena.getTerrainType();
|
||||
let typeChange: Type;
|
||||
if (terrainType !== TerrainType.NONE) {
|
||||
typeChange = this.getTypeForTerrain(user.scene.arena.getTerrainType());
|
||||
} else {
|
||||
typeChange = this.getTypeForBiome(user.scene.arena.biomeType);
|
||||
}
|
||||
|
||||
user.summonData.types = [ biomeType ];
|
||||
user.summonData.types = [ typeChange ];
|
||||
user.updateInfo();
|
||||
|
||||
user.scene.queueMessage(i18next.t("moveTriggers:transformedIntoType", { pokemonName: getPokemonNameWithAffix(user), typeName: i18next.t(`pokemonInfo:Type.${Type[biomeType]}`) }));
|
||||
user.scene.queueMessage(i18next.t("moveTriggers:transformedIntoType", { pokemonName: getPokemonNameWithAffix(user), typeName: i18next.t(`pokemonInfo:Type.${Type[typeChange]}`) }));
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves a type from the current terrain
|
||||
* @param terrainType {@linkcode TerrainType}
|
||||
* @returns {@linkcode Type}
|
||||
*/
|
||||
private getTypeForTerrain(terrainType: TerrainType): Type {
|
||||
switch (terrainType) {
|
||||
case TerrainType.ELECTRIC:
|
||||
return Type.ELECTRIC;
|
||||
case TerrainType.MISTY:
|
||||
return Type.FAIRY;
|
||||
case TerrainType.GRASSY:
|
||||
return Type.GRASS;
|
||||
case TerrainType.PSYCHIC:
|
||||
return Type.PSYCHIC;
|
||||
case TerrainType.NONE:
|
||||
default:
|
||||
return Type.UNKNOWN;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves a type from the current biome
|
||||
* @param biomeType {@linkcode Biome}
|
||||
* @returns {@linkcode Type}
|
||||
*/
|
||||
private getTypeForBiome(biomeType: Biome): Type {
|
||||
switch (biomeType) {
|
||||
case Biome.TOWN:
|
||||
case Biome.PLAINS:
|
||||
case Biome.METROPOLIS:
|
||||
return Type.NORMAL;
|
||||
case Biome.GRASS:
|
||||
case Biome.TALL_GRASS:
|
||||
return Type.GRASS;
|
||||
case Biome.FOREST:
|
||||
case Biome.JUNGLE:
|
||||
return Type.BUG;
|
||||
case Biome.SLUM:
|
||||
case Biome.SWAMP:
|
||||
return Type.POISON;
|
||||
case Biome.SEA:
|
||||
case Biome.BEACH:
|
||||
case Biome.LAKE:
|
||||
case Biome.SEABED:
|
||||
return Type.WATER;
|
||||
case Biome.MOUNTAIN:
|
||||
return Type.FLYING;
|
||||
case Biome.BADLANDS:
|
||||
return Type.GROUND;
|
||||
case Biome.CAVE:
|
||||
case Biome.DESERT:
|
||||
return Type.ROCK;
|
||||
case Biome.ICE_CAVE:
|
||||
case Biome.SNOWY_FOREST:
|
||||
return Type.ICE;
|
||||
case Biome.MEADOW:
|
||||
case Biome.FAIRY_CAVE:
|
||||
case Biome.ISLAND:
|
||||
return Type.FAIRY;
|
||||
case Biome.POWER_PLANT:
|
||||
return Type.ELECTRIC;
|
||||
case Biome.VOLCANO:
|
||||
return Type.FIRE;
|
||||
case Biome.GRAVEYARD:
|
||||
case Biome.TEMPLE:
|
||||
return Type.GHOST;
|
||||
case Biome.DOJO:
|
||||
case Biome.CONSTRUCTION_SITE:
|
||||
return Type.FIGHTING;
|
||||
case Biome.FACTORY:
|
||||
case Biome.LABORATORY:
|
||||
return Type.STEEL;
|
||||
case Biome.RUINS:
|
||||
case Biome.SPACE:
|
||||
return Type.PSYCHIC;
|
||||
case Biome.WASTELAND:
|
||||
case Biome.END:
|
||||
return Type.DRAGON;
|
||||
case Biome.ABYSS:
|
||||
return Type.DARK;
|
||||
default:
|
||||
return Type.UNKNOWN;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export class ChangeTypeAttr extends MoveEffectAttr {
|
||||
private type: Type;
|
||||
|
||||
constructor(type: Type) {
|
||||
super(false, MoveEffectTrigger.HIT);
|
||||
super(false, { trigger: MoveEffectTrigger.HIT });
|
||||
|
||||
this.type = type;
|
||||
}
|
||||
@ -5959,7 +6154,7 @@ export class AddTypeAttr extends MoveEffectAttr {
|
||||
private type: Type;
|
||||
|
||||
constructor(type: Type) {
|
||||
super(false, MoveEffectTrigger.HIT);
|
||||
super(false, { trigger: MoveEffectTrigger.HIT });
|
||||
|
||||
this.type = type;
|
||||
}
|
||||
@ -6486,7 +6681,7 @@ export class AbilityChangeAttr extends MoveEffectAttr {
|
||||
public ability: Abilities;
|
||||
|
||||
constructor(ability: Abilities, selfTarget?: boolean) {
|
||||
super(selfTarget, MoveEffectTrigger.HIT);
|
||||
super(selfTarget, { trigger: MoveEffectTrigger.HIT });
|
||||
|
||||
this.ability = ability;
|
||||
}
|
||||
@ -6515,7 +6710,7 @@ export class AbilityCopyAttr extends MoveEffectAttr {
|
||||
public copyToPartner: boolean;
|
||||
|
||||
constructor(copyToPartner: boolean = false) {
|
||||
super(false, MoveEffectTrigger.HIT);
|
||||
super(false, { trigger: MoveEffectTrigger.HIT });
|
||||
|
||||
this.copyToPartner = copyToPartner;
|
||||
}
|
||||
@ -6554,7 +6749,7 @@ export class AbilityGiveAttr extends MoveEffectAttr {
|
||||
public copyToPartner: boolean;
|
||||
|
||||
constructor() {
|
||||
super(false, MoveEffectTrigger.HIT);
|
||||
super(false, { trigger: MoveEffectTrigger.HIT });
|
||||
}
|
||||
|
||||
apply(user: Pokemon, target: Pokemon, move: Move, args: any[]): boolean {
|
||||
@ -6866,7 +7061,7 @@ export class DiscourageFrequentUseAttr extends MoveAttr {
|
||||
|
||||
export class MoneyAttr extends MoveEffectAttr {
|
||||
constructor() {
|
||||
super(true, MoveEffectTrigger.HIT, true);
|
||||
super(true, { trigger: MoveEffectTrigger.HIT, firstHitOnly: true });
|
||||
}
|
||||
|
||||
apply(user: Pokemon, target: Pokemon, move: Move): boolean {
|
||||
@ -6883,7 +7078,7 @@ export class MoneyAttr extends MoveEffectAttr {
|
||||
*/
|
||||
export class DestinyBondAttr extends MoveEffectAttr {
|
||||
constructor() {
|
||||
super(true, MoveEffectTrigger.PRE_APPLY);
|
||||
super(true, { trigger: MoveEffectTrigger.PRE_APPLY });
|
||||
}
|
||||
|
||||
/**
|
||||
@ -6933,7 +7128,7 @@ export class StatusIfBoostedAttr extends MoveEffectAttr {
|
||||
public effect: StatusEffect;
|
||||
|
||||
constructor(effect: StatusEffect) {
|
||||
super(true, MoveEffectTrigger.HIT);
|
||||
super(true, { trigger: MoveEffectTrigger.HIT });
|
||||
this.effect = effect;
|
||||
}
|
||||
|
||||
@ -7058,6 +7253,11 @@ const targetSleptOrComatoseCondition: MoveConditionFunc = (user: Pokemon, target
|
||||
|
||||
const failIfLastCondition: MoveConditionFunc = (user: Pokemon, target: Pokemon, move: Move) => user.scene.phaseQueue.find(phase => phase instanceof MovePhase) !== undefined;
|
||||
|
||||
const failIfLastInPartyCondition: MoveConditionFunc = (user: Pokemon, target: Pokemon, move: Move) => {
|
||||
const party: Pokemon[] = user.isPlayer() ? user.scene.getParty() : user.scene.getEnemyParty();
|
||||
return party.some(pokemon => pokemon.isActive() && !pokemon.isOnField());
|
||||
};
|
||||
|
||||
export type MoveAttrFilter = (attr: MoveAttr) => boolean;
|
||||
|
||||
function applyMoveAttrsInternal(attrFilter: MoveAttrFilter, user: Pokemon | null, target: Pokemon | null, move: Move, args: any[]): Promise<void> {
|
||||
@ -7967,6 +8167,7 @@ export function initMoves() {
|
||||
.attr(StatusEffectAttr, StatusEffect.PARALYSIS),
|
||||
new SelfStatusMove(Moves.BATON_PASS, Type.NORMAL, -1, 40, -1, 0, 2)
|
||||
.attr(ForceSwitchOutAttr, true, SwitchType.BATON_PASS)
|
||||
.condition(failIfLastInPartyCondition)
|
||||
.hidesUser(),
|
||||
new StatusMove(Moves.ENCORE, Type.NORMAL, 100, 5, -1, 0, 2)
|
||||
.attr(AddBattlerTagAttr, BattlerTagType.ENCORE, false, true)
|
||||
@ -8985,7 +9186,7 @@ export function initMoves() {
|
||||
// If any fielded pokémon is grass-type and grounded.
|
||||
return [ ...user.scene.getEnemyParty(), ...user.scene.getParty() ].some((poke) => poke.isOfType(Type.GRASS) && poke.isGrounded());
|
||||
})
|
||||
.attr(StatStageChangeAttr, [ Stat.ATK, Stat.SPATK ], 1, false, (user, target, move) => target.isOfType(Type.GRASS) && target.isGrounded()),
|
||||
.attr(StatStageChangeAttr, [ Stat.ATK, Stat.SPATK ], 1, false, { condition: (user, target, move) => target.isOfType(Type.GRASS) && target.isGrounded() }),
|
||||
new StatusMove(Moves.STICKY_WEB, Type.BUG, -1, 20, -1, 0, 6)
|
||||
.attr(AddArenaTrapTagAttr, ArenaTagType.STICKY_WEB)
|
||||
.target(MoveTarget.ENEMY_SIDE),
|
||||
@ -9022,7 +9223,7 @@ export function initMoves() {
|
||||
.soundBased()
|
||||
.target(MoveTarget.ALL_NEAR_ENEMIES),
|
||||
new StatusMove(Moves.PARTING_SHOT, Type.DARK, 100, 20, -1, 0, 6)
|
||||
.attr(StatStageChangeAttr, [ Stat.ATK, Stat.SPATK ], -1, false, null, true, true, MoveEffectTrigger.PRE_APPLY)
|
||||
.attr(StatStageChangeAttr, [ Stat.ATK, Stat.SPATK ], -1, false, { trigger: MoveEffectTrigger.PRE_APPLY })
|
||||
.attr(ForceSwitchOutAttr, true)
|
||||
.soundBased(),
|
||||
new StatusMove(Moves.TOPSY_TURVY, Type.DARK, -1, 20, -1, 0, 6)
|
||||
@ -9037,7 +9238,7 @@ export function initMoves() {
|
||||
.condition(failIfLastCondition),
|
||||
new StatusMove(Moves.FLOWER_SHIELD, Type.FAIRY, -1, 10, -1, 0, 6)
|
||||
.target(MoveTarget.ALL)
|
||||
.attr(StatStageChangeAttr, [ Stat.DEF ], 1, false, (user, target, move) => target.getTypes().includes(Type.GRASS) && !target.getTag(SemiInvulnerableTag)),
|
||||
.attr(StatStageChangeAttr, [ Stat.DEF ], 1, false, { condition: (user, target, move) => target.getTypes().includes(Type.GRASS) && !target.getTag(SemiInvulnerableTag) }),
|
||||
new StatusMove(Moves.GRASSY_TERRAIN, Type.GRASS, -1, 10, -1, 0, 6)
|
||||
.attr(TerrainChangeAttr, TerrainType.GRASSY)
|
||||
.target(MoveTarget.BOTH_SIDES),
|
||||
@ -9069,7 +9270,7 @@ export function initMoves() {
|
||||
.attr(StatStageChangeAttr, [ Stat.SPATK ], -1)
|
||||
.soundBased(),
|
||||
new AttackMove(Moves.DIAMOND_STORM, Type.ROCK, MoveCategory.PHYSICAL, 100, 95, 5, 50, 0, 6)
|
||||
.attr(StatStageChangeAttr, [ Stat.DEF ], 2, true, undefined, undefined, undefined, undefined, true)
|
||||
.attr(StatStageChangeAttr, [ Stat.DEF ], 2, true, { firstTargetOnly: true })
|
||||
.makesContact(false)
|
||||
.target(MoveTarget.ALL_NEAR_ENEMIES),
|
||||
new AttackMove(Moves.STEAM_ERUPTION, Type.WATER, MoveCategory.SPECIAL, 110, 95, 5, 30, 0, 6)
|
||||
@ -9095,7 +9296,7 @@ export function initMoves() {
|
||||
new StatusMove(Moves.EERIE_IMPULSE, Type.ELECTRIC, 100, 15, -1, 0, 6)
|
||||
.attr(StatStageChangeAttr, [ Stat.SPATK ], -2),
|
||||
new StatusMove(Moves.VENOM_DRENCH, Type.POISON, 100, 20, -1, 0, 6)
|
||||
.attr(StatStageChangeAttr, [ Stat.ATK, Stat.SPATK, Stat.SPD ], -1, false, (user, target, move) => target.status?.effect === StatusEffect.POISON || target.status?.effect === StatusEffect.TOXIC)
|
||||
.attr(StatStageChangeAttr, [ Stat.ATK, Stat.SPATK, Stat.SPD ], -1, false, { condition: (user, target, move) => target.status?.effect === StatusEffect.POISON || target.status?.effect === StatusEffect.TOXIC })
|
||||
.target(MoveTarget.ALL_NEAR_ENEMIES),
|
||||
new StatusMove(Moves.POWDER, Type.BUG, 100, 20, -1, 1, 6)
|
||||
.ignoresSubstitute()
|
||||
@ -9106,7 +9307,7 @@ export function initMoves() {
|
||||
.attr(StatStageChangeAttr, [ Stat.SPATK, Stat.SPDEF, Stat.SPD ], 2, true)
|
||||
.ignoresVirtual(),
|
||||
new StatusMove(Moves.MAGNETIC_FLUX, Type.ELECTRIC, -1, 20, -1, 0, 6)
|
||||
.attr(StatStageChangeAttr, [ Stat.DEF, Stat.SPDEF ], 1, false, (user, target, move) => !![ Abilities.PLUS, Abilities.MINUS ].find(a => target.hasAbility(a, false)))
|
||||
.attr(StatStageChangeAttr, [ Stat.DEF, Stat.SPDEF ], 1, false, { condition: (user, target, move) => !![ Abilities.PLUS, Abilities.MINUS ].find(a => target.hasAbility(a, false)) })
|
||||
.ignoresSubstitute()
|
||||
.target(MoveTarget.USER_AND_ALLIES)
|
||||
.condition((user, target, move) => !![ user, user.getAlly() ].filter(p => p?.isActive()).find(p => !![ Abilities.PLUS, Abilities.MINUS ].find(a => p.hasAbility(a, false)))),
|
||||
@ -9326,7 +9527,7 @@ export function initMoves() {
|
||||
new SelfStatusMove(Moves.LASER_FOCUS, Type.NORMAL, -1, 30, -1, 0, 7)
|
||||
.attr(AddBattlerTagAttr, BattlerTagType.ALWAYS_CRIT, true, false),
|
||||
new StatusMove(Moves.GEAR_UP, Type.STEEL, -1, 20, -1, 0, 7)
|
||||
.attr(StatStageChangeAttr, [ Stat.ATK, Stat.SPATK ], 1, false, (user, target, move) => !![ Abilities.PLUS, Abilities.MINUS ].find(a => target.hasAbility(a, false)))
|
||||
.attr(StatStageChangeAttr, [ Stat.ATK, Stat.SPATK ], 1, false, { condition: (user, target, move) => !![ Abilities.PLUS, Abilities.MINUS ].find(a => target.hasAbility(a, false)) })
|
||||
.ignoresSubstitute()
|
||||
.target(MoveTarget.USER_AND_ALLIES)
|
||||
.condition((user, target, move) => !![ user, user.getAlly() ].filter(p => p?.isActive()).find(p => !![ Abilities.PLUS, Abilities.MINUS ].find(a => p.hasAbility(a, false)))),
|
||||
@ -9383,7 +9584,7 @@ export function initMoves() {
|
||||
.ballBombMove()
|
||||
.makesContact(false),
|
||||
new AttackMove(Moves.CLANGING_SCALES, Type.DRAGON, MoveCategory.SPECIAL, 110, 100, 5, -1, 0, 7)
|
||||
.attr(StatStageChangeAttr, [ Stat.DEF ], -1, true, null, true, false, MoveEffectTrigger.HIT, true)
|
||||
.attr(StatStageChangeAttr, [ Stat.DEF ], -1, true, { firstTargetOnly: true })
|
||||
.soundBased()
|
||||
.target(MoveTarget.ALL_NEAR_ENEMIES),
|
||||
new AttackMove(Moves.DRAGON_HAMMER, Type.DRAGON, MoveCategory.PHYSICAL, 90, 100, 15, -1, 0, 7),
|
||||
@ -9497,7 +9698,7 @@ export function initMoves() {
|
||||
.makesContact(false)
|
||||
.ignoresVirtual(),
|
||||
new AttackMove(Moves.CLANGOROUS_SOULBLAZE, Type.DRAGON, MoveCategory.SPECIAL, 185, -1, 1, 100, 0, 7)
|
||||
.attr(StatStageChangeAttr, [ Stat.ATK, Stat.DEF, Stat.SPATK, Stat.SPDEF, Stat.SPD ], 1, true, undefined, undefined, undefined, undefined, true)
|
||||
.attr(StatStageChangeAttr, [ Stat.ATK, Stat.DEF, Stat.SPATK, Stat.SPDEF, Stat.SPD ], 1, true, { firstTargetOnly: true })
|
||||
.soundBased()
|
||||
.target(MoveTarget.ALL_NEAR_ENEMIES)
|
||||
.edgeCase() // I assume it needs clanging scales and Kommo-O
|
||||
@ -9735,8 +9936,8 @@ export function initMoves() {
|
||||
.attr(ClearTerrainAttr)
|
||||
.condition((user, target, move) => !!user.scene.arena.terrain),
|
||||
new AttackMove(Moves.SCALE_SHOT, Type.DRAGON, MoveCategory.PHYSICAL, 25, 90, 20, -1, 0, 8)
|
||||
.attr(StatStageChangeAttr, [ Stat.SPD ], 1, true, null, true, false, MoveEffectTrigger.HIT, false, true)
|
||||
.attr(StatStageChangeAttr, [ Stat.DEF ], -1, true, null, true, false, MoveEffectTrigger.HIT, false, true)
|
||||
.attr(StatStageChangeAttr, [ Stat.SPD ], 1, true, { lastHitOnly: true })
|
||||
.attr(StatStageChangeAttr, [ Stat.DEF ], -1, true, { lastHitOnly: true })
|
||||
.attr(MultiHitAttr)
|
||||
.makesContact(false),
|
||||
new ChargingAttackMove(Moves.METEOR_BEAM, Type.ROCK, MoveCategory.SPECIAL, 120, 90, 10, -1, 0, 8)
|
||||
@ -9870,7 +10071,7 @@ export function initMoves() {
|
||||
new AttackMove(Moves.TRIPLE_ARROWS, Type.FIGHTING, MoveCategory.PHYSICAL, 90, 100, 10, 30, 0, 8)
|
||||
.makesContact(false)
|
||||
.attr(HighCritAttr)
|
||||
.attr(StatStageChangeAttr, [ Stat.DEF ], -1, undefined, undefined, undefined, undefined, undefined, undefined, undefined, 50)
|
||||
.attr(StatStageChangeAttr, [ Stat.DEF ], -1, false, { effectChanceOverride: 50 })
|
||||
.attr(FlinchAttr),
|
||||
new AttackMove(Moves.INFERNAL_PARADE, Type.GHOST, MoveCategory.SPECIAL, 60, 100, 15, 30, 0, 8)
|
||||
.attr(StatusEffectAttr, StatusEffect.BURN)
|
||||
@ -10006,7 +10207,7 @@ export function initMoves() {
|
||||
.attr(TeraMoveCategoryAttr)
|
||||
.attr(TeraBlastTypeAttr)
|
||||
.attr(TeraBlastPowerAttr)
|
||||
.attr(StatStageChangeAttr, [ Stat.ATK, Stat.SPATK ], -1, true, (user, target, move) => user.isTerastallized() && user.isOfType(Type.STELLAR))
|
||||
.attr(StatStageChangeAttr, [ Stat.ATK, Stat.SPATK ], -1, true, { condition: (user, target, move) => user.isTerastallized() && user.isOfType(Type.STELLAR) })
|
||||
.partial(), /** Does not ignore abilities that affect stats, relevant in determining the move's category {@see TeraMoveCategoryAttr} */
|
||||
new SelfStatusMove(Moves.SILK_TRAP, Type.BUG, -1, 10, -1, 4, 9)
|
||||
.attr(ProtectAttr, BattlerTagType.SILK_TRAP)
|
||||
@ -10090,7 +10291,7 @@ export function initMoves() {
|
||||
.attr(RemoveScreensAttr),
|
||||
new AttackMove(Moves.MAKE_IT_RAIN, Type.STEEL, MoveCategory.SPECIAL, 120, 100, 5, -1, 0, 9)
|
||||
.attr(MoneyAttr)
|
||||
.attr(StatStageChangeAttr, [ Stat.SPATK ], -1, true, null, true, false, MoveEffectTrigger.HIT, true)
|
||||
.attr(StatStageChangeAttr, [ Stat.SPATK ], -1, true, { firstTargetOnly: true })
|
||||
.target(MoveTarget.ALL_NEAR_ENEMIES),
|
||||
new AttackMove(Moves.PSYBLADE, Type.PSYCHIC, MoveCategory.PHYSICAL, 80, 100, 15, -1, 0, 9)
|
||||
.attr(MovePowerMultiplierAttr, (user, target, move) => user.scene.arena.getTerrainType() === TerrainType.ELECTRIC && user.isGrounded() ? 1.5 : 1)
|
||||
@ -10107,12 +10308,13 @@ export function initMoves() {
|
||||
.makesContact(),
|
||||
new SelfStatusMove(Moves.SHED_TAIL, Type.NORMAL, -1, 10, -1, 0, 9)
|
||||
.attr(AddSubstituteAttr, 0.5)
|
||||
.attr(ForceSwitchOutAttr, true, SwitchType.SHED_TAIL),
|
||||
.attr(ForceSwitchOutAttr, true, SwitchType.SHED_TAIL)
|
||||
.condition(failIfLastInPartyCondition),
|
||||
new SelfStatusMove(Moves.CHILLY_RECEPTION, Type.ICE, -1, 10, -1, 0, 9)
|
||||
.attr(PreMoveMessageAttr, (user, move) => i18next.t("moveTriggers:chillyReception", { pokemonName: getPokemonNameWithAffix(user) }))
|
||||
.attr(ChillyReceptionAttr, true),
|
||||
new SelfStatusMove(Moves.TIDY_UP, Type.NORMAL, -1, 10, -1, 0, 9)
|
||||
.attr(StatStageChangeAttr, [ Stat.ATK, Stat.SPD ], 1, true, null, true, true)
|
||||
.attr(StatStageChangeAttr, [ Stat.ATK, Stat.SPD ], 1, true)
|
||||
.attr(RemoveArenaTrapAttr, true)
|
||||
.attr(RemoveAllSubstitutesAttr),
|
||||
new StatusMove(Moves.SNOWSCAPE, Type.ICE, -1, 10, -1, 0, 9)
|
||||
|
@ -1,7 +1,7 @@
|
||||
import { EnemyPartyConfig, generateModifierType, initBattleWithEnemyConfig, leaveEncounterWithoutBattle, loadCustomMovesForEncounter, selectPokemonForOption, setEncounterRewards, transitionMysteryEncounterIntroVisuals } from "#app/data/mystery-encounters/utils/encounter-phase-utils";
|
||||
import { trainerConfigs, TrainerPartyCompoundTemplate, TrainerPartyTemplate, } from "#app/data/trainer-config";
|
||||
import { ModifierTier } from "#app/modifier/modifier-tier";
|
||||
import { modifierTypes, PokemonHeldItemModifierType } from "#app/modifier/modifier-type";
|
||||
import { ModifierPoolType, modifierTypes, PokemonHeldItemModifierType } from "#app/modifier/modifier-type";
|
||||
import { MysteryEncounterType } from "#enums/mystery-encounter-type";
|
||||
import { PartyMemberStrength } from "#enums/party-member-strength";
|
||||
import BattleScene from "#app/battle-scene";
|
||||
@ -280,7 +280,7 @@ export const ClowningAroundEncounter: MysteryEncounter =
|
||||
let numRogue = 0;
|
||||
items.filter(m => m.isTransferable && !(m instanceof BerryModifier))
|
||||
.forEach(m => {
|
||||
const type = m.type.withTierFromPool();
|
||||
const type = m.type.withTierFromPool(ModifierPoolType.PLAYER, party);
|
||||
const tier = type.tier ?? ModifierTier.ULTRA;
|
||||
if (type.id === "GOLDEN_EGG" || tier === ModifierTier.ROGUE) {
|
||||
numRogue += m.stackCount;
|
||||
|
@ -418,7 +418,7 @@ export function generateModifierType(scene: BattleScene, modifier: () => Modifie
|
||||
// Populates item id and tier (order matters)
|
||||
result = result
|
||||
.withIdFromFunc(modifierTypes[modifierId])
|
||||
.withTierFromPool();
|
||||
.withTierFromPool(ModifierPoolType.PLAYER, scene.getParty());
|
||||
|
||||
return result instanceof ModifierTypeGenerator ? result.generateType(scene.getParty(), pregenArgs) : result;
|
||||
}
|
||||
|
@ -47,7 +47,7 @@ export function getPokemonSpecies(species: Species | Species[] | undefined): Pok
|
||||
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
|
||||
? allSpecies.find(s => s.speciesId === species)! // TODO: is the bang correct?
|
||||
: allSpecies[species - 1];
|
||||
@ -129,26 +129,27 @@ export type PokemonSpeciesFilter = (species: PokemonSpecies) => boolean;
|
||||
|
||||
export abstract class PokemonSpeciesForm {
|
||||
public speciesId: Species;
|
||||
public formIndex: integer;
|
||||
public generation: integer;
|
||||
public type1: Type;
|
||||
public type2: Type | null;
|
||||
public height: number;
|
||||
public weight: number;
|
||||
public ability1: Abilities;
|
||||
public ability2: Abilities;
|
||||
public abilityHidden: Abilities;
|
||||
public baseTotal: integer;
|
||||
public baseStats: integer[];
|
||||
public catchRate: integer;
|
||||
public baseFriendship: integer;
|
||||
public baseExp: integer;
|
||||
public genderDiffs: boolean;
|
||||
public isStarterSelectable: boolean;
|
||||
protected _formIndex: number;
|
||||
protected _generation: number;
|
||||
readonly type1: Type;
|
||||
readonly type2: Type | null;
|
||||
readonly height: number;
|
||||
readonly weight: number;
|
||||
readonly ability1: Abilities;
|
||||
readonly ability2: Abilities;
|
||||
readonly abilityHidden: Abilities;
|
||||
readonly baseTotal: number;
|
||||
readonly baseStats: number[];
|
||||
readonly catchRate: number;
|
||||
readonly baseFriendship: number;
|
||||
readonly baseExp: number;
|
||||
readonly genderDiffs: boolean;
|
||||
readonly isStarterSelectable: boolean;
|
||||
|
||||
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,
|
||||
catchRate: integer, baseFriendship: integer, baseExp: integer, genderDiffs: boolean, isStarterSelectable: boolean) {
|
||||
baseTotal: number, baseHp: number, baseAtk: number, baseDef: number, baseSpatk: number, baseSpdef: number, baseSpd: number,
|
||||
catchRate: number, baseFriendship: number, baseExp: number, genderDiffs: boolean, isStarterSelectable: boolean
|
||||
) {
|
||||
this.type1 = type1;
|
||||
this.type2 = type2;
|
||||
this.height = height;
|
||||
@ -180,7 +181,23 @@ export abstract class PokemonSpeciesForm {
|
||||
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);
|
||||
}
|
||||
|
||||
@ -188,7 +205,7 @@ export abstract class PokemonSpeciesForm {
|
||||
* Method to get the total number of abilities a Pokemon species has.
|
||||
* @returns Number of abilities
|
||||
*/
|
||||
getAbilityCount(): integer {
|
||||
getAbilityCount(): number {
|
||||
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)
|
||||
* @returns The id of the Ability
|
||||
*/
|
||||
getAbility(abilityIndex: integer): Abilities {
|
||||
getAbility(abilityIndex: number): Abilities {
|
||||
let ret: Abilities;
|
||||
if (abilityIndex === 0) {
|
||||
ret = this.ability1;
|
||||
@ -277,12 +294,12 @@ export abstract class PokemonSpeciesForm {
|
||||
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, "/");
|
||||
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) {
|
||||
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}` : ""}`;
|
||||
}
|
||||
|
||||
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)}`;
|
||||
}
|
||||
|
||||
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
|
||||
* @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 variantDataIndex: integer | string = this.speciesId;
|
||||
let variantDataIndex: number | string = this.speciesId;
|
||||
const species = getPokemonSpecies(this.speciesId);
|
||||
if (species.forms.length > 0 && formIndex !== undefined) {
|
||||
formkey = species.forms[formIndex]?.getFormSpriteKey(formIndex);
|
||||
@ -324,13 +341,13 @@ export abstract class PokemonSpeciesForm {
|
||||
return variantDataIndex;
|
||||
}
|
||||
|
||||
getIconAtlasKey(formIndex?: integer, shiny?: boolean, variant?: integer): string {
|
||||
getIconAtlasKey(formIndex?: number, shiny?: boolean, variant?: number): string {
|
||||
const variantDataIndex = this.getVariantDataIndex(formIndex);
|
||||
const isVariant = shiny && variantData[variantDataIndex] && (variant !== undefined && variantData[variantDataIndex][variant]);
|
||||
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) {
|
||||
formIndex = this.formIndex;
|
||||
}
|
||||
@ -379,7 +396,7 @@ export abstract class PokemonSpeciesForm {
|
||||
return ret;
|
||||
}
|
||||
|
||||
getCryKey(formIndex?: integer): string {
|
||||
getCryKey(formIndex?: number): string {
|
||||
let speciesId = this.speciesId;
|
||||
if (this.speciesId > 2000) {
|
||||
switch (this.speciesId) {
|
||||
@ -446,7 +463,7 @@ export abstract class PokemonSpeciesForm {
|
||||
return ret;
|
||||
}
|
||||
|
||||
validateStarterMoveset(moveset: StarterMoveset, eggMoves: integer): boolean {
|
||||
validateStarterMoveset(moveset: StarterMoveset, eggMoves: number): boolean {
|
||||
const rootSpeciesId = this.getRootSpeciesId();
|
||||
for (const moveId of moveset) {
|
||||
if (speciesEggMoves.hasOwnProperty(rootSpeciesId)) {
|
||||
@ -467,7 +484,7 @@ export abstract class PokemonSpeciesForm {
|
||||
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 => {
|
||||
const spriteKey = this.getSpriteKey(female, formIndex, shiny, variant);
|
||||
scene.loadPokemonAtlas(spriteKey, this.getSpriteAtlasPath(female, formIndex, shiny, variant));
|
||||
@ -536,7 +553,7 @@ export abstract class PokemonSpeciesForm {
|
||||
return cry;
|
||||
}
|
||||
|
||||
generateCandyColors(scene: BattleScene): integer[][] {
|
||||
generateCandyColors(scene: BattleScene): number[][] {
|
||||
const sourceTexture = scene.textures.get(this.getSpriteKey(false));
|
||||
|
||||
const sourceFrame = sourceTexture.frames[sourceTexture.firstFrame];
|
||||
@ -544,7 +561,7 @@ export abstract class PokemonSpeciesForm {
|
||||
|
||||
const canvas = document.createElement("canvas");
|
||||
|
||||
const spriteColors: integer[][] = [];
|
||||
const spriteColors: number[][] = [];
|
||||
|
||||
const context = canvas.getContext("2d");
|
||||
const frame = sourceFrame;
|
||||
@ -567,7 +584,7 @@ export abstract class PokemonSpeciesForm {
|
||||
}
|
||||
|
||||
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) {
|
||||
continue;
|
||||
}
|
||||
@ -586,27 +603,28 @@ export abstract class PokemonSpeciesForm {
|
||||
|
||||
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 {
|
||||
public name: string;
|
||||
public subLegendary: boolean;
|
||||
public legendary: boolean;
|
||||
public mythical: boolean;
|
||||
public species: string;
|
||||
public growthRate: GrowthRate;
|
||||
public malePercent: number | null;
|
||||
public genderDiffs: boolean;
|
||||
public canChangeForm: boolean;
|
||||
public forms: PokemonForm[];
|
||||
readonly subLegendary: boolean;
|
||||
readonly legendary: boolean;
|
||||
readonly mythical: boolean;
|
||||
readonly species: string;
|
||||
readonly growthRate: GrowthRate;
|
||||
readonly malePercent: number | null;
|
||||
readonly genderDiffs: boolean;
|
||||
readonly canChangeForm: boolean;
|
||||
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,
|
||||
baseTotal: integer, baseHp: integer, baseAtk: integer, baseDef: integer, baseSpatk: integer, baseSpdef: integer, baseSpd: integer,
|
||||
catchRate: integer, baseFriendship: integer, baseExp: integer, growthRate: GrowthRate, malePercent: number | null,
|
||||
genderDiffs: boolean, canChangeForm?: boolean, ...forms: PokemonForm[]) {
|
||||
baseTotal: number, baseHp: number, baseAtk: number, baseDef: number, baseSpatk: number, baseSpdef: number, baseSpd: number,
|
||||
catchRate: number, baseFriendship: number, baseExp: number, growthRate: GrowthRate, malePercent: number | null,
|
||||
genderDiffs: boolean, canChangeForm?: boolean, ...forms: PokemonForm[]
|
||||
) {
|
||||
super(type1, type2, height, weight, ability1, ability2, abilityHidden, baseTotal, baseHp, baseAtk, baseDef, baseSpatk, baseSpdef, baseSpd,
|
||||
catchRate, baseFriendship, baseExp, genderDiffs, false);
|
||||
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) {
|
||||
const form = this.forms[formIndex];
|
||||
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()}`);
|
||||
}
|
||||
|
||||
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));
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
@ -688,7 +706,7 @@ export default class PokemonSpecies extends PokemonSpeciesForm implements Locali
|
||||
* @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.
|
||||
*/
|
||||
private getStrengthLevelDiff(strength: PartyMemberStrength): integer {
|
||||
private getStrengthLevelDiff(strength: PartyMemberStrength): number {
|
||||
switch (Math.min(strength, PartyMemberStrength.STRONGER)) {
|
||||
case PartyMemberStrength.WEAKEST:
|
||||
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();
|
||||
|
||||
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
|
||||
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[] = [];
|
||||
if (pokemonPrevolutions.hasOwnProperty(this.speciesId)) {
|
||||
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);
|
||||
}
|
||||
|
||||
getFormSpriteKey(formIndex?: integer) {
|
||||
getFormSpriteKey(formIndex?: number) {
|
||||
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`);
|
||||
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" ];
|
||||
|
||||
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,
|
||||
catchRate: integer, baseFriendship: integer, baseExp: integer, genderDiffs?: boolean, formSpriteKey?: string | null, isStarterSelectable?: boolean, ) {
|
||||
baseTotal: number, baseHp: number, baseAtk: number, baseDef: number, baseSpatk: number, baseSpdef: number, baseSpd: number,
|
||||
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,
|
||||
catchRate, baseFriendship, baseExp, !!genderDiffs, (!!isStarterSelectable || !formKey));
|
||||
catchRate, baseFriendship, baseExp, genderDiffs, (isStarterSelectable || !formKey));
|
||||
this.formName = formName;
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
@ -224,66 +224,6 @@ export class Arena {
|
||||
return 0;
|
||||
}
|
||||
|
||||
getTypeForBiome() {
|
||||
switch (this.biomeType) {
|
||||
case Biome.TOWN:
|
||||
case Biome.PLAINS:
|
||||
case Biome.METROPOLIS:
|
||||
return Type.NORMAL;
|
||||
case Biome.GRASS:
|
||||
case Biome.TALL_GRASS:
|
||||
return Type.GRASS;
|
||||
case Biome.FOREST:
|
||||
case Biome.JUNGLE:
|
||||
return Type.BUG;
|
||||
case Biome.SLUM:
|
||||
case Biome.SWAMP:
|
||||
return Type.POISON;
|
||||
case Biome.SEA:
|
||||
case Biome.BEACH:
|
||||
case Biome.LAKE:
|
||||
case Biome.SEABED:
|
||||
return Type.WATER;
|
||||
case Biome.MOUNTAIN:
|
||||
return Type.FLYING;
|
||||
case Biome.BADLANDS:
|
||||
return Type.GROUND;
|
||||
case Biome.CAVE:
|
||||
case Biome.DESERT:
|
||||
return Type.ROCK;
|
||||
case Biome.ICE_CAVE:
|
||||
case Biome.SNOWY_FOREST:
|
||||
return Type.ICE;
|
||||
case Biome.MEADOW:
|
||||
case Biome.FAIRY_CAVE:
|
||||
case Biome.ISLAND:
|
||||
return Type.FAIRY;
|
||||
case Biome.POWER_PLANT:
|
||||
return Type.ELECTRIC;
|
||||
case Biome.VOLCANO:
|
||||
return Type.FIRE;
|
||||
case Biome.GRAVEYARD:
|
||||
case Biome.TEMPLE:
|
||||
return Type.GHOST;
|
||||
case Biome.DOJO:
|
||||
case Biome.CONSTRUCTION_SITE:
|
||||
return Type.FIGHTING;
|
||||
case Biome.FACTORY:
|
||||
case Biome.LABORATORY:
|
||||
return Type.STEEL;
|
||||
case Biome.RUINS:
|
||||
case Biome.SPACE:
|
||||
return Type.PSYCHIC;
|
||||
case Biome.WASTELAND:
|
||||
case Biome.END:
|
||||
return Type.DRAGON;
|
||||
case Biome.ABYSS:
|
||||
return Type.DARK;
|
||||
default:
|
||||
return Type.UNKNOWN;
|
||||
}
|
||||
}
|
||||
|
||||
getBgTerrainColorRatioForBiome(): number {
|
||||
switch (this.biomeType) {
|
||||
case Biome.SPACE:
|
||||
|
@ -12,7 +12,7 @@ import * as Utils from "#app/utils";
|
||||
import { Type, TypeDamageMultiplier, getTypeDamageMultiplier, getTypeRgb } from "#app/data/type";
|
||||
import { getLevelTotalExp } from "#app/data/exp";
|
||||
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 { Gender } from "#app/data/gender";
|
||||
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 { WeatherType } from "#app/data/weather";
|
||||
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 { BattlerIndex } from "#app/battle";
|
||||
import { Mode } from "#app/ui/ui";
|
||||
@ -428,38 +428,26 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container {
|
||||
resolve();
|
||||
};
|
||||
if (this.shiny) {
|
||||
const populateVariantColors = (key: string, back: boolean = false): Promise<void> => {
|
||||
const populateVariantColors = (isBackSprite: boolean = false): Promise<void> => {
|
||||
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;
|
||||
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);
|
||||
const variantSet: VariantSet = config as VariantSet;
|
||||
if (variantSet && variantSet[this.variant] === 1) {
|
||||
if (variantColorCache.hasOwnProperty(key)) {
|
||||
return resolve();
|
||||
const cacheKey = this.getBattleSpriteKey(isBackSprite);
|
||||
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()) {
|
||||
Promise.all([ populateVariantColors(this.getBattleSpriteKey(false)), populateVariantColors(this.getBattleSpriteKey(true), true) ]).then(() => updateFusionPaletteAndResolve());
|
||||
Promise.all([ populateVariantColors(false), populateVariantColors(true) ]).then(() => updateFusionPaletteAndResolve());
|
||||
} else {
|
||||
populateVariantColors(this.getBattleSpriteKey(false)).then(() => updateFusionPaletteAndResolve());
|
||||
populateVariantColors(false).then(() => updateFusionPaletteAndResolve());
|
||||
}
|
||||
} else {
|
||||
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 {
|
||||
if (!this.species.forms.length || this.species.forms.length <= this.formIndex) {
|
||||
return "";
|
||||
@ -2791,7 +2818,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
|
||||
* 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 (source.isPlayer()) {
|
||||
@ -2800,10 +2827,19 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container {
|
||||
this.scene.gameData.gameStats.highestDamage = damage;
|
||||
}
|
||||
}
|
||||
source.turnData.damageDealt += damage;
|
||||
source.turnData.currDamageDealt = damage;
|
||||
source.turnData.totalDamageDealt += damage;
|
||||
source.turnData.singleHitDamageDealt = damage;
|
||||
this.turnData.damageTaken += damage;
|
||||
this.battleData.hitCount++;
|
||||
|
||||
// Multi-Lens and Parental Bond check for Wimp Out/Emergency Exit
|
||||
if (this.hasAbilityWithAttr(PostDamageForceSwitchAbAttr)) {
|
||||
const multiHitModifier = source.getHeldItems().find(m => m instanceof PokemonMultiHitModifier);
|
||||
if (multiHitModifier || source.hasAbilityWithAttr(AddSecondStrikeAbAttr)) {
|
||||
applyPostDamageAbAttrs(PostDamageAbAttr, this, damage, this.hasPassive(), false, [], source);
|
||||
}
|
||||
}
|
||||
|
||||
const attackResult = { move: move.id, result: result as DamageResult, damage: damage, critical: isCritical, sourceId: source.id, sourceBattlerIndex: source.getBattlerIndex() };
|
||||
this.turnData.attacksReceived.unshift(attackResult);
|
||||
if (source.isPlayer() && !this.isPlayer()) {
|
||||
@ -2891,7 +2927,6 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container {
|
||||
this.destroySubstitute();
|
||||
this.resetSummonData();
|
||||
}
|
||||
|
||||
return damage;
|
||||
}
|
||||
|
||||
@ -2905,12 +2940,13 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container {
|
||||
* @param ignoreFaintPhase boolean to ignore adding a FaintPhase, passsed to damage()
|
||||
* @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);
|
||||
this.scene.unshiftPhase(damagePhase);
|
||||
damage = this.damage(damage, ignoreSegments, preventEndure, ignoreFaintPhase);
|
||||
// Damage amount may have changed, but needed to be queued before calling damage function
|
||||
damagePhase.updateAmount(damage);
|
||||
applyPostDamageAbAttrs(PostDamageAbAttr, this, damage, this.hasPassive(), false, [], source);
|
||||
return damage;
|
||||
}
|
||||
|
||||
@ -5094,7 +5130,6 @@ export class PokemonSummonData {
|
||||
public tags: BattlerTag[] = [];
|
||||
public abilitySuppressed: boolean = false;
|
||||
public abilitiesApplied: Abilities[] = [];
|
||||
|
||||
public speciesForm: PokemonSpeciesForm | null;
|
||||
public fusionSpeciesForm: PokemonSpeciesForm;
|
||||
public ability: Abilities = Abilities.NONE;
|
||||
@ -5134,8 +5169,8 @@ export class PokemonTurnData {
|
||||
* - `0` = Move is finished
|
||||
*/
|
||||
public hitsLeft: number = -1;
|
||||
public damageDealt: number = 0;
|
||||
public currDamageDealt: number = 0;
|
||||
public totalDamageDealt: number = 0;
|
||||
public singleHitDamageDealt: number = 0;
|
||||
public damageTaken: number = 0;
|
||||
public attacksReceived: AttackMoveResult[] = [];
|
||||
public order: number;
|
||||
|
@ -19,7 +19,7 @@ import { Unlockables } from "#app/system/unlockables";
|
||||
import { getVoucherTypeIcon, getVoucherTypeName, VoucherType } from "#app/system/voucher";
|
||||
import PartyUiHandler, { PokemonMoveSelectFilter, PokemonSelectFilter } from "#app/ui/party-ui-handler";
|
||||
import { getModifierTierTextTint } from "#app/ui/text";
|
||||
import { formatMoney, getEnumKeys, getEnumValues, IntegerHolder, NumberHolder, padInt, randSeedInt, randSeedItem } from "#app/utils";
|
||||
import { formatMoney, getEnumKeys, getEnumValues, IntegerHolder, isNullOrUndefined, NumberHolder, padInt, randSeedInt, randSeedItem } from "#app/utils";
|
||||
import { Abilities } from "#enums/abilities";
|
||||
import { BattlerTagType } from "#enums/battler-tag-type";
|
||||
import { BerryType } from "#enums/berry-type";
|
||||
@ -121,18 +121,41 @@ export class ModifierType {
|
||||
* Populates item tier for ModifierType instance
|
||||
* Tier is a necessary field for items that appear in player shop (determines the Pokeball visual they use)
|
||||
* To find the tier, this function performs a reverse lookup of the item type in modifier pools
|
||||
* It checks the weight of the item and will use the first tier for which the weight is greater than 0
|
||||
* This is to allow items to be in multiple item pools depending on the conditions, for example for events
|
||||
* If all tiers have a weight of 0 for the item, the first tier where the item was found is used
|
||||
* @param poolType Default 'ModifierPoolType.PLAYER'. Which pool to lookup item tier from
|
||||
* @param party optional. Needed to check the weight of modifiers with conditional weight (see {@linkcode WeightedModifierTypeWeightFunc})
|
||||
* if not provided or empty, the weight check will be ignored
|
||||
* @param rerollCount Default `0`. Used to check the weight of modifiers with conditional weight (see {@linkcode WeightedModifierTypeWeightFunc})
|
||||
*/
|
||||
withTierFromPool(poolType: ModifierPoolType = ModifierPoolType.PLAYER): ModifierType {
|
||||
withTierFromPool(poolType: ModifierPoolType = ModifierPoolType.PLAYER, party?: PlayerPokemon[], rerollCount: number = 0): ModifierType {
|
||||
let defaultTier: undefined | ModifierTier;
|
||||
for (const tier of Object.values(getModifierPoolForType(poolType))) {
|
||||
for (const modifier of tier) {
|
||||
if (this.id === modifier.modifierType.id) {
|
||||
this.tier = modifier.modifierType.tier;
|
||||
return this;
|
||||
let weight: number;
|
||||
if (modifier.weight instanceof Function) {
|
||||
weight = party ? modifier.weight(party, rerollCount) : 0;
|
||||
} else {
|
||||
weight = modifier.weight;
|
||||
}
|
||||
if (weight > 0) {
|
||||
this.tier = modifier.modifierType.tier;
|
||||
return this;
|
||||
} else if (isNullOrUndefined(defaultTier)) {
|
||||
// If weight is 0, keep track of the first tier where the item was found
|
||||
defaultTier = modifier.modifierType.tier;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Didn't find a pool with weight > 0, fallback to first tier where the item was found, if any
|
||||
if (defaultTier) {
|
||||
this.tier = defaultTier;
|
||||
}
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
@ -2117,7 +2140,7 @@ export function getPlayerModifierTypeOptions(count: integer, party: PlayerPokemo
|
||||
// Populates item id and tier
|
||||
guaranteedMod = guaranteedMod
|
||||
.withIdFromFunc(modifierTypes[modifierId])
|
||||
.withTierFromPool();
|
||||
.withTierFromPool(ModifierPoolType.PLAYER, party);
|
||||
|
||||
const modType = guaranteedMod instanceof ModifierTypeGenerator ? guaranteedMod.generateType(party) : guaranteedMod;
|
||||
if (modType) {
|
||||
@ -2186,7 +2209,7 @@ export function overridePlayerModifierTypeOptions(options: ModifierTypeOption[],
|
||||
}
|
||||
|
||||
if (modifierType) {
|
||||
options[i].type = modifierType.withIdFromFunc(modifierFunc).withTierFromPool();
|
||||
options[i].type = modifierType.withIdFromFunc(modifierFunc).withTierFromPool(ModifierPoolType.PLAYER, party);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1767,10 +1767,10 @@ export class HitHealModifier extends PokemonHeldItemModifier {
|
||||
* @returns `true` if the {@linkcode Pokemon} was healed
|
||||
*/
|
||||
override apply(pokemon: Pokemon): boolean {
|
||||
if (pokemon.turnData.damageDealt && !pokemon.isFullHp()) {
|
||||
if (pokemon.turnData.totalDamageDealt && !pokemon.isFullHp()) {
|
||||
const scene = pokemon.scene;
|
||||
scene.unshiftPhase(new PokemonHealPhase(scene, pokemon.getBattlerIndex(),
|
||||
toDmgValue(pokemon.turnData.damageDealt / 8) * this.stackCount, i18next.t("modifier:hitHealApply", { pokemonNameWithAffix: getPokemonNameWithAffix(pokemon), typeName: this.type.name }), true));
|
||||
toDmgValue(pokemon.turnData.totalDamageDealt / 8) * this.stackCount, i18next.t("modifier:hitHealApply", { pokemonNameWithAffix: getPokemonNameWithAffix(pokemon), typeName: this.type.name }), true));
|
||||
}
|
||||
|
||||
return true;
|
||||
|
@ -1,20 +1,62 @@
|
||||
import BattleScene from "#app/battle-scene";
|
||||
import { BattlerIndex } from "#app/battle";
|
||||
import { applyPreAttackAbAttrs, AddSecondStrikeAbAttr, IgnoreMoveEffectsAbAttr, applyPostDefendAbAttrs, PostDefendAbAttr, applyPostAttackAbAttrs, PostAttackAbAttr, MaxMultiHitAbAttr, AlwaysHitAbAttr, TypeImmunityAbAttr } from "#app/data/ability";
|
||||
import BattleScene from "#app/battle-scene";
|
||||
import {
|
||||
AddSecondStrikeAbAttr,
|
||||
AlwaysHitAbAttr,
|
||||
applyPostAttackAbAttrs,
|
||||
applyPostDefendAbAttrs,
|
||||
applyPreAttackAbAttrs,
|
||||
IgnoreMoveEffectsAbAttr,
|
||||
MaxMultiHitAbAttr,
|
||||
PostAttackAbAttr,
|
||||
PostDefendAbAttr,
|
||||
TypeImmunityAbAttr,
|
||||
} from "#app/data/ability";
|
||||
import { ArenaTagSide, ConditionalProtectTag } from "#app/data/arena-tag";
|
||||
import { MoveAnim } from "#app/data/battle-anims";
|
||||
import { BattlerTagLapseType, DamageProtectedTag, ProtectedTag, SemiInvulnerableTag, SubstituteTag } from "#app/data/battler-tags";
|
||||
import { MoveTarget, applyMoveAttrs, OverrideMoveEffectAttr, MultiHitAttr, AttackMove, FixedDamageAttr, VariableTargetAttr, MissEffectAttr, MoveFlags, applyFilteredMoveAttrs, MoveAttr, MoveEffectAttr, OneHitKOAttr, MoveEffectTrigger, MoveCategory, NoEffectAttr, HitsTagAttr, ToxicAccuracyAttr } from "#app/data/move";
|
||||
import {
|
||||
BattlerTagLapseType,
|
||||
DamageProtectedTag,
|
||||
ProtectedTag,
|
||||
SemiInvulnerableTag,
|
||||
SubstituteTag,
|
||||
} from "#app/data/battler-tags";
|
||||
import {
|
||||
applyFilteredMoveAttrs,
|
||||
applyMoveAttrs,
|
||||
AttackMove,
|
||||
FixedDamageAttr,
|
||||
HitsTagAttr,
|
||||
MissEffectAttr,
|
||||
MoveAttr,
|
||||
MoveCategory,
|
||||
MoveEffectAttr,
|
||||
MoveEffectTrigger,
|
||||
MoveFlags,
|
||||
MoveTarget,
|
||||
MultiHitAttr,
|
||||
NoEffectAttr,
|
||||
OneHitKOAttr,
|
||||
OverrideMoveEffectAttr,
|
||||
ToxicAccuracyAttr,
|
||||
VariableTargetAttr,
|
||||
} from "#app/data/move";
|
||||
import { SpeciesFormChangePostMoveTrigger } from "#app/data/pokemon-forms";
|
||||
import { BattlerTagType } from "#app/enums/battler-tag-type";
|
||||
import { Moves } from "#app/enums/moves";
|
||||
import Pokemon, { PokemonMove, MoveResult, HitResult } from "#app/field/pokemon";
|
||||
import { getPokemonNameWithAffix } from "#app/messages";
|
||||
import { PokemonMultiHitModifier, FlinchChanceModifier, EnemyAttackStatusEffectChanceModifier, ContactHeldItemTransferChanceModifier, HitHealModifier } from "#app/modifier/modifier";
|
||||
import i18next from "i18next";
|
||||
import * as Utils from "#app/utils";
|
||||
import { PokemonPhase } from "./pokemon-phase";
|
||||
import { Type } from "#app/data/type";
|
||||
import Pokemon, { HitResult, MoveResult, PokemonMove } from "#app/field/pokemon";
|
||||
import { getPokemonNameWithAffix } from "#app/messages";
|
||||
import {
|
||||
ContactHeldItemTransferChanceModifier,
|
||||
EnemyAttackStatusEffectChanceModifier,
|
||||
FlinchChanceModifier,
|
||||
HitHealModifier,
|
||||
PokemonMultiHitModifier,
|
||||
} from "#app/modifier/modifier";
|
||||
import { BooleanHolder, executeIf, NumberHolder } from "#app/utils";
|
||||
import { BattlerTagType } from "#enums/battler-tag-type";
|
||||
import { Moves } from "#enums/moves";
|
||||
import i18next from "i18next";
|
||||
import { PokemonPhase } from "./pokemon-phase";
|
||||
|
||||
export class MoveEffectPhase extends PokemonPhase {
|
||||
public move: PokemonMove;
|
||||
@ -35,7 +77,7 @@ export class MoveEffectPhase extends PokemonPhase {
|
||||
this.targets = targets;
|
||||
}
|
||||
|
||||
start() {
|
||||
public override start(): void {
|
||||
super.start();
|
||||
|
||||
/** The Pokemon using this phase's invoked move */
|
||||
@ -52,12 +94,12 @@ export class MoveEffectPhase extends PokemonPhase {
|
||||
* Does an effect from this move override other effects on this turn?
|
||||
* e.g. Charging moves (Fly, etc.) on their first turn of use.
|
||||
*/
|
||||
const overridden = new Utils.BooleanHolder(false);
|
||||
const overridden = new BooleanHolder(false);
|
||||
/** The {@linkcode Move} object from {@linkcode allMoves} invoked by this phase */
|
||||
const move = this.move.getMove();
|
||||
|
||||
// Assume single target for override
|
||||
applyMoveAttrs(OverrideMoveEffectAttr, user, this.getTarget() ?? null, move, overridden, this.move.virtual).then(() => {
|
||||
applyMoveAttrs(OverrideMoveEffectAttr, user, this.getFirstTarget() ?? null, move, overridden, this.move.virtual).then(() => {
|
||||
// If other effects were overriden, stop this phase before they can be applied
|
||||
if (overridden.value) {
|
||||
return this.end();
|
||||
@ -71,14 +113,14 @@ export class MoveEffectPhase extends PokemonPhase {
|
||||
* effects of the move itself, Parental Bond, and Multi-Lens to do so.
|
||||
*/
|
||||
if (user.turnData.hitsLeft === -1) {
|
||||
const hitCount = new Utils.IntegerHolder(1);
|
||||
const hitCount = new NumberHolder(1);
|
||||
// Assume single target for multi hit
|
||||
applyMoveAttrs(MultiHitAttr, user, this.getTarget() ?? null, move, hitCount);
|
||||
applyMoveAttrs(MultiHitAttr, user, this.getFirstTarget() ?? null, move, hitCount);
|
||||
// If Parental Bond is applicable, double the hit count
|
||||
applyPreAttackAbAttrs(AddSecondStrikeAbAttr, user, null, move, false, targets.length, hitCount, new Utils.IntegerHolder(0));
|
||||
applyPreAttackAbAttrs(AddSecondStrikeAbAttr, user, null, move, false, targets.length, hitCount, new NumberHolder(0));
|
||||
// If Multi-Lens is applicable, multiply the hit count by 1 + the number of Multi-Lenses held by the user
|
||||
if (move instanceof AttackMove && !move.hasAttr(FixedDamageAttr)) {
|
||||
this.scene.applyModifiers(PokemonMultiHitModifier, user.isPlayer(), user, hitCount, new Utils.IntegerHolder(0));
|
||||
this.scene.applyModifiers(PokemonMultiHitModifier, user.isPlayer(), user, hitCount, new NumberHolder(0));
|
||||
}
|
||||
// Set the user's relevant turnData fields to reflect the final hit count
|
||||
user.turnData.hitCount = hitCount.value;
|
||||
@ -100,8 +142,9 @@ export class MoveEffectPhase extends PokemonPhase {
|
||||
const hasActiveTargets = targets.some(t => t.isActive(true));
|
||||
|
||||
/** Check if the target is immune via ability to the attacking move, and NOT in semi invulnerable state */
|
||||
const isImmune = targets[0].hasAbilityWithAttr(TypeImmunityAbAttr) && (targets[0].getAbility()?.getAttrs(TypeImmunityAbAttr)?.[0]?.getImmuneType() === user.getMoveType(move))
|
||||
&& !targets[0].getTag(SemiInvulnerableTag);
|
||||
const isImmune = targets[0].hasAbilityWithAttr(TypeImmunityAbAttr)
|
||||
&& (targets[0].getAbility()?.getAttrs(TypeImmunityAbAttr)?.[0]?.getImmuneType() === user.getMoveType(move))
|
||||
&& !targets[0].getTag(SemiInvulnerableTag);
|
||||
|
||||
/**
|
||||
* If no targets are left for the move to hit (FAIL), or the invoked move is single-target
|
||||
@ -111,7 +154,7 @@ export class MoveEffectPhase extends PokemonPhase {
|
||||
if (!hasActiveTargets || (!move.hasAttr(VariableTargetAttr) && !move.isMultiTarget() && !targetHitChecks[this.targets[0]] && !targets[0].getTag(ProtectedTag) && !isImmune)) {
|
||||
this.stopMultiHit();
|
||||
if (hasActiveTargets) {
|
||||
this.scene.queueMessage(i18next.t("battle:attackMissed", { pokemonNameWithAffix: this.getTarget() ? getPokemonNameWithAffix(this.getTarget()!) : "" }));
|
||||
this.scene.queueMessage(i18next.t("battle:attackMissed", { pokemonNameWithAffix: this.getFirstTarget() ? getPokemonNameWithAffix(this.getFirstTarget()!) : "" }));
|
||||
moveHistoryEntry.result = MoveResult.MISS;
|
||||
applyMoveAttrs(MissEffectAttr, user, null, move);
|
||||
} else {
|
||||
@ -127,30 +170,40 @@ export class MoveEffectPhase extends PokemonPhase {
|
||||
|
||||
const playOnEmptyField = this.scene.currentBattle?.mysteryEncounter?.hasBattleAnimationsWithoutTargets ?? false;
|
||||
// Move animation only needs one target
|
||||
new MoveAnim(move.id as Moves, user, this.getTarget()!.getBattlerIndex()!, playOnEmptyField).play(this.scene, move.hitsSubstitute(user, this.getTarget()!), () => {
|
||||
new MoveAnim(move.id as Moves, user, this.getFirstTarget()!.getBattlerIndex()!, playOnEmptyField).play(this.scene, move.hitsSubstitute(user, this.getFirstTarget()!), () => {
|
||||
/** Has the move successfully hit a target (for damage) yet? */
|
||||
let hasHit: boolean = false;
|
||||
for (const target of targets) {
|
||||
// Prevent ENEMY_SIDE targeted moves from occurring twice in double battles
|
||||
if (move.moveTarget === MoveTarget.ENEMY_SIDE && target !== targets[targets.length - 1]) {
|
||||
continue;
|
||||
}
|
||||
|
||||
/** The {@linkcode ArenaTagSide} to which the target belongs */
|
||||
const targetSide = target.isPlayer() ? ArenaTagSide.PLAYER : ArenaTagSide.ENEMY;
|
||||
/** Has the invoked move been cancelled by conditional protection (e.g Quick Guard)? */
|
||||
const hasConditionalProtectApplied = new Utils.BooleanHolder(false);
|
||||
const hasConditionalProtectApplied = new BooleanHolder(false);
|
||||
/** Does the applied conditional protection bypass Protect-ignoring effects? */
|
||||
const bypassIgnoreProtect = new Utils.BooleanHolder(false);
|
||||
const bypassIgnoreProtect = new BooleanHolder(false);
|
||||
/** If the move is not targeting a Pokemon on the user's side, try to apply conditional protection effects */
|
||||
if (!this.move.getMove().isAllyTarget()) {
|
||||
this.scene.arena.applyTagsForSide(ConditionalProtectTag, targetSide, false, hasConditionalProtectApplied, user, target, move.id, bypassIgnoreProtect);
|
||||
}
|
||||
|
||||
/** Is the target protected by Protect, etc. or a relevant conditional protection effect? */
|
||||
const isProtected = (bypassIgnoreProtect.value || !this.move.getMove().checkFlag(MoveFlags.IGNORE_PROTECT, user, target))
|
||||
&& (hasConditionalProtectApplied.value || (!target.findTags(t => t instanceof DamageProtectedTag).length && target.findTags(t => t instanceof ProtectedTag).find(t => target.lapseTag(t.tagType)))
|
||||
|| (this.move.getMove().category !== MoveCategory.STATUS && target.findTags(t => t instanceof DamageProtectedTag).find(t => target.lapseTag(t.tagType))));
|
||||
const isProtected = (
|
||||
bypassIgnoreProtect.value
|
||||
|| !this.move.getMove().checkFlag(MoveFlags.IGNORE_PROTECT, user, target))
|
||||
&& (hasConditionalProtectApplied.value
|
||||
|| (!target.findTags(t => t instanceof DamageProtectedTag).length
|
||||
&& target.findTags(t => t instanceof ProtectedTag).find(t => target.lapseTag(t.tagType)))
|
||||
|| (this.move.getMove().category !== MoveCategory.STATUS
|
||||
&& target.findTags(t => t instanceof DamageProtectedTag).find(t => target.lapseTag(t.tagType))));
|
||||
|
||||
/** Is the pokemon immune due to an ablility, and also not in a semi invulnerable state? */
|
||||
const isImmune = target.hasAbilityWithAttr(TypeImmunityAbAttr) && (target.getAbility()?.getAttrs(TypeImmunityAbAttr)?.[0]?.getImmuneType() === user.getMoveType(move))
|
||||
&& !target.getTag(SemiInvulnerableTag);
|
||||
const isImmune = target.hasAbilityWithAttr(TypeImmunityAbAttr)
|
||||
&& (target.getAbility()?.getAttrs(TypeImmunityAbAttr)?.[0]?.getImmuneType() === user.getMoveType(move))
|
||||
&& !target.getTag(SemiInvulnerableTag);
|
||||
|
||||
/**
|
||||
* If the move missed a target, stop all future hits against that target
|
||||
@ -218,7 +271,7 @@ export class MoveEffectPhase extends PokemonPhase {
|
||||
}
|
||||
|
||||
/** Does this phase represent the invoked move's last strike? */
|
||||
const lastHit = (user.turnData.hitsLeft === 1 || !this.getTarget()?.isActive());
|
||||
const lastHit = (user.turnData.hitsLeft === 1 || !this.getFirstTarget()?.isActive());
|
||||
|
||||
/**
|
||||
* If the user can change forms by using the invoked move,
|
||||
@ -234,85 +287,48 @@ export class MoveEffectPhase extends PokemonPhase {
|
||||
* These are ordered by trigger type (see {@linkcode MoveEffectTrigger}), and each trigger
|
||||
* type requires different conditions to be met with respect to the move's hit result.
|
||||
*/
|
||||
applyAttrs.push(new Promise(resolve => {
|
||||
// Apply all effects with PRE_MOVE triggers (if the target isn't immune to the move)
|
||||
applyFilteredMoveAttrs((attr: MoveAttr) => attr instanceof MoveEffectAttr && attr.trigger === MoveEffectTrigger.PRE_APPLY && (!attr.firstHitOnly || firstHit) && (!attr.lastHitOnly || lastHit) && hitResult !== HitResult.NO_EFFECT,
|
||||
user, target, move).then(() => {
|
||||
// All other effects require the move to not have failed or have been cancelled to trigger
|
||||
if (hitResult !== HitResult.FAIL) {
|
||||
/**
|
||||
* If the invoked move's effects are meant to trigger during the move's "charge turn,"
|
||||
* ignore all effects after this point.
|
||||
* Otherwise, apply all self-targeted POST_APPLY effects.
|
||||
*/
|
||||
applyFilteredMoveAttrs((attr: MoveAttr) => attr instanceof MoveEffectAttr && attr.trigger === MoveEffectTrigger.POST_APPLY
|
||||
&& attr.selfTarget && (!attr.firstHitOnly || firstHit) && (!attr.lastHitOnly || lastHit), user, target, move).then(() => {
|
||||
// All effects past this point require the move to have hit the target
|
||||
if (hitResult !== HitResult.NO_EFFECT) {
|
||||
// Apply all non-self-targeted POST_APPLY effects
|
||||
applyFilteredMoveAttrs((attr: MoveAttr) => attr instanceof MoveEffectAttr && (attr as MoveEffectAttr).trigger === MoveEffectTrigger.POST_APPLY
|
||||
&& !(attr as MoveEffectAttr).selfTarget && (!attr.firstHitOnly || firstHit) && (!attr.lastHitOnly || lastHit), user, target, this.move.getMove()).then(() => {
|
||||
/**
|
||||
* If the move hit, and the target doesn't have Shield Dust,
|
||||
* apply the chance to flinch the target gained from King's Rock
|
||||
*/
|
||||
if (dealsDamage && !target.hasAbilityWithAttr(IgnoreMoveEffectsAbAttr) && !move.hitsSubstitute(user, target)) {
|
||||
const flinched = new Utils.BooleanHolder(false);
|
||||
user.scene.applyModifiers(FlinchChanceModifier, user.isPlayer(), user, flinched);
|
||||
if (flinched.value) {
|
||||
target.addTag(BattlerTagType.FLINCHED, undefined, this.move.moveId, user.id);
|
||||
}
|
||||
}
|
||||
// If the move was not protected against, apply all HIT effects
|
||||
Utils.executeIf(!isProtected, () => applyFilteredMoveAttrs((attr: MoveAttr) => attr instanceof MoveEffectAttr && (attr as MoveEffectAttr).trigger === MoveEffectTrigger.HIT
|
||||
&& (!attr.firstHitOnly || firstHit) && (!attr.lastHitOnly || lastHit) && (!attr.firstTargetOnly || firstTarget), user, target, this.move.getMove()).then(() => {
|
||||
// Apply the target's post-defend ability effects (as long as the target is active or can otherwise apply them)
|
||||
return Utils.executeIf(!target.isFainted() || target.canApplyAbility(), () => applyPostDefendAbAttrs(PostDefendAbAttr, target, user, this.move.getMove(), hitResult).then(() => {
|
||||
// Only apply the following effects if the move was not deflected by a substitute
|
||||
if (move.hitsSubstitute(user, target)) {
|
||||
return resolve();
|
||||
}
|
||||
const k = new Promise<void>((resolve) => {
|
||||
//Start promise chain and apply PRE_APPLY move attributes
|
||||
let promiseChain: Promise<void | null> = applyFilteredMoveAttrs((attr: MoveAttr) =>
|
||||
attr instanceof MoveEffectAttr
|
||||
&& attr.trigger === MoveEffectTrigger.PRE_APPLY
|
||||
&& (!attr.firstHitOnly || firstHit)
|
||||
&& (!attr.lastHitOnly || lastHit)
|
||||
&& hitResult !== HitResult.NO_EFFECT, user, target, move);
|
||||
|
||||
// If the invoked move is an enemy attack, apply the enemy's status effect-inflicting tokens
|
||||
if (!user.isPlayer() && this.move.getMove() instanceof AttackMove) {
|
||||
user.scene.applyShuffledModifiers(this.scene, EnemyAttackStatusEffectChanceModifier, false, target);
|
||||
}
|
||||
target.lapseTags(BattlerTagLapseType.AFTER_HIT);
|
||||
/** Don't complete if the move failed */
|
||||
if (hitResult === HitResult.FAIL) {
|
||||
return resolve();
|
||||
}
|
||||
|
||||
})).then(() => {
|
||||
// Apply the user's post-attack ability effects
|
||||
applyPostAttackAbAttrs(PostAttackAbAttr, user, target, this.move.getMove(), hitResult).then(() => {
|
||||
/**
|
||||
* If the invoked move is an attack, apply the user's chance to
|
||||
* steal an item from the target granted by Grip Claw
|
||||
*/
|
||||
if (this.move.getMove() instanceof AttackMove) {
|
||||
this.scene.applyModifiers(ContactHeldItemTransferChanceModifier, this.player, user, target);
|
||||
}
|
||||
resolve();
|
||||
});
|
||||
});
|
||||
})
|
||||
).then(() => resolve());
|
||||
});
|
||||
} else {
|
||||
applyMoveAttrs(NoEffectAttr, user, null, move).then(() => resolve());
|
||||
}
|
||||
});
|
||||
} else {
|
||||
resolve();
|
||||
}
|
||||
});
|
||||
}));
|
||||
/** Apply Move/Ability Effects in correct order */
|
||||
promiseChain = promiseChain
|
||||
.then(this.applySelfTargetEffects(user, target, firstHit, lastHit));
|
||||
|
||||
if (hitResult !== HitResult.NO_EFFECT) {
|
||||
promiseChain
|
||||
.then(this.applyPostApplyEffects(user, target, firstHit, lastHit))
|
||||
.then(this.applyHeldItemFlinchCheck(user, target, dealsDamage))
|
||||
.then(this.applySuccessfulAttackEffects(user, target, firstHit, lastHit, !!isProtected, hitResult, firstTarget))
|
||||
.then(() => resolve());
|
||||
} else {
|
||||
promiseChain
|
||||
.then(() => applyMoveAttrs(NoEffectAttr, user, null, move))
|
||||
.then(resolve);
|
||||
}
|
||||
});
|
||||
|
||||
applyAttrs.push(k);
|
||||
}
|
||||
|
||||
// Apply the move's POST_TARGET effects on the move's last hit, after all targeted effects have resolved
|
||||
const postTarget = (user.turnData.hitsLeft === 1 || !this.getTarget()?.isActive()) ?
|
||||
const postTarget = (user.turnData.hitsLeft === 1 || !this.getFirstTarget()?.isActive()) ?
|
||||
applyFilteredMoveAttrs((attr: MoveAttr) => attr instanceof MoveEffectAttr && attr.trigger === MoveEffectTrigger.POST_TARGET, user, null, move) :
|
||||
null;
|
||||
|
||||
if (!!postTarget) {
|
||||
if (postTarget) {
|
||||
if (applyAttrs.length) { // If there is a pending asynchronous move effect, do this after
|
||||
applyAttrs[applyAttrs.length - 1]?.then(() => postTarget);
|
||||
applyAttrs[applyAttrs.length - 1].then(() => postTarget);
|
||||
} else { // Otherwise, push a new asynchronous move effect
|
||||
applyAttrs.push(postTarget);
|
||||
}
|
||||
@ -327,7 +343,7 @@ export class MoveEffectPhase extends PokemonPhase {
|
||||
*/
|
||||
targets.forEach(target => {
|
||||
const substitute = target.getTag(SubstituteTag);
|
||||
if (!!substitute && substitute.hp <= 0) {
|
||||
if (substitute && substitute.hp <= 0) {
|
||||
target.lapseTag(BattlerTagType.SUBSTITUTE);
|
||||
}
|
||||
});
|
||||
@ -337,7 +353,7 @@ export class MoveEffectPhase extends PokemonPhase {
|
||||
});
|
||||
}
|
||||
|
||||
end() {
|
||||
public override end(): void {
|
||||
const user = this.getUserPokemon();
|
||||
/**
|
||||
* If this phase isn't for the invoked move's last strike,
|
||||
@ -347,7 +363,7 @@ export class MoveEffectPhase extends PokemonPhase {
|
||||
* to the user.
|
||||
*/
|
||||
if (user) {
|
||||
if (user.turnData.hitsLeft && --user.turnData.hitsLeft >= 1 && this.getTarget()?.isActive()) {
|
||||
if (user.turnData.hitsLeft && --user.turnData.hitsLeft >= 1 && this.getFirstTarget()?.isActive()) {
|
||||
this.scene.unshiftPhase(this.getNewHitPhase());
|
||||
} else {
|
||||
// Queue message for number of hits made by multi-move
|
||||
@ -367,11 +383,135 @@ export class MoveEffectPhase extends PokemonPhase {
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolves whether this phase's invoked move hits or misses the given target
|
||||
* @param target {@linkcode Pokemon} the Pokemon targeted by the invoked move
|
||||
* @returns `true` if the move does not miss the target; `false` otherwise
|
||||
*/
|
||||
hitCheck(target: Pokemon): boolean {
|
||||
* Apply self-targeted effects that trigger `POST_APPLY`
|
||||
*
|
||||
* @param user - The {@linkcode Pokemon} using this phase's invoked move
|
||||
* @param target - {@linkcode Pokemon} the current target of this phase's invoked move
|
||||
* @param firstHit - `true` if this is the first hit in a multi-hit attack
|
||||
* @param lastHit - `true` if this is the last hit in a multi-hit attack
|
||||
* @returns a function intended to be passed into a `then()` call.
|
||||
*/
|
||||
protected applySelfTargetEffects(user: Pokemon, target: Pokemon, firstHit: boolean, lastHit: boolean): () => Promise<void | null> {
|
||||
return () => applyFilteredMoveAttrs((attr: MoveAttr) =>
|
||||
attr instanceof MoveEffectAttr
|
||||
&& attr.trigger === MoveEffectTrigger.POST_APPLY
|
||||
&& attr.selfTarget
|
||||
&& (!attr.firstHitOnly || firstHit)
|
||||
&& (!attr.lastHitOnly || lastHit), user, target, this.move.getMove());
|
||||
}
|
||||
|
||||
/**
|
||||
* Applies non-self-targeted effects that trigger `POST_APPLY`
|
||||
* (i.e. Smelling Salts curing Paralysis, and the forced switch from U-Turn, Dragon Tail, etc)
|
||||
* @param user - The {@linkcode Pokemon} using this phase's invoked move
|
||||
* @param target - {@linkcode Pokemon} the current target of this phase's invoked move
|
||||
* @param firstHit - `true` if this is the first hit in a multi-hit attack
|
||||
* @param lastHit - `true` if this is the last hit in a multi-hit attack
|
||||
* @returns a function intended to be passed into a `then()` call.
|
||||
*/
|
||||
protected applyPostApplyEffects(user: Pokemon, target: Pokemon, firstHit: boolean, lastHit: boolean): () => Promise<void | null> {
|
||||
return () => applyFilteredMoveAttrs((attr: MoveAttr) =>
|
||||
attr instanceof MoveEffectAttr
|
||||
&& attr.trigger === MoveEffectTrigger.POST_APPLY
|
||||
&& !attr.selfTarget
|
||||
&& (!attr.firstHitOnly || firstHit)
|
||||
&& (!attr.lastHitOnly || lastHit), user, target, this.move.getMove());
|
||||
}
|
||||
|
||||
/**
|
||||
* Applies effects that trigger on HIT
|
||||
* (i.e. Final Gambit, Power-Up Punch, Drain Punch)
|
||||
* @param user - The {@linkcode Pokemon} using this phase's invoked move
|
||||
* @param target - {@linkcode Pokemon} the current target of this phase's invoked move
|
||||
* @param firstHit - `true` if this is the first hit in a multi-hit attack
|
||||
* @param lastHit - `true` if this is the last hit in a multi-hit attack
|
||||
* @param firstTarget - `true` if {@linkcode target} is the first target hit by this strike of {@linkcode move}
|
||||
* @returns a function intended to be passed into a `then()` call.
|
||||
*/
|
||||
protected applyOnHitEffects(user: Pokemon, target: Pokemon, firstHit : boolean, lastHit: boolean, firstTarget: boolean): Promise<void> {
|
||||
return applyFilteredMoveAttrs((attr: MoveAttr) =>
|
||||
attr instanceof MoveEffectAttr
|
||||
&& attr.trigger === MoveEffectTrigger.HIT
|
||||
&& (!attr.firstHitOnly || firstHit)
|
||||
&& (!attr.lastHitOnly || lastHit)
|
||||
&& (!attr.firstTargetOnly || firstTarget), user, target, this.move.getMove());
|
||||
}
|
||||
|
||||
/**
|
||||
* Applies reactive effects that occur when a Pokémon is hit.
|
||||
* (i.e. Effect Spore, Disguise, Liquid Ooze, Beak Blast)
|
||||
* @param user - The {@linkcode Pokemon} using this phase's invoked move
|
||||
* @param target - {@linkcode Pokemon} the current target of this phase's invoked move
|
||||
* @param hitResult - The {@linkcode HitResult} of the attempted move
|
||||
* @returns a `Promise` intended to be passed into a `then()` call.
|
||||
*/
|
||||
protected applyOnGetHitAbEffects(user: Pokemon, target: Pokemon, hitResult: HitResult): Promise<void | null> {
|
||||
return executeIf(!target.isFainted() || target.canApplyAbility(), () =>
|
||||
applyPostDefendAbAttrs(PostDefendAbAttr, target, user, this.move.getMove(), hitResult)
|
||||
.then(() => {
|
||||
|
||||
if (!this.move.getMove().hitsSubstitute(user, target)) {
|
||||
if (!user.isPlayer() && this.move.getMove() instanceof AttackMove) {
|
||||
user.scene.applyShuffledModifiers(this.scene, EnemyAttackStatusEffectChanceModifier, false, target);
|
||||
}
|
||||
|
||||
target.lapseTags(BattlerTagLapseType.AFTER_HIT);
|
||||
}
|
||||
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Applies all effects and attributes that require a move to connect with a target,
|
||||
* namely reactive effects like Weak Armor, on-hit effects like that of Power-Up Punch, and item stealing effects
|
||||
* @param user - The {@linkcode Pokemon} using this phase's invoked move
|
||||
* @param target - {@linkcode Pokemon} the current target of this phase's invoked move
|
||||
* @param firstHit - `true` if this is the first hit in a multi-hit attack
|
||||
* @param lastHit - `true` if this is the last hit in a multi-hit attack
|
||||
* @param isProtected - `true` if the target is protected by effects such as Protect
|
||||
* @param hitResult - The {@linkcode HitResult} of the attempted move
|
||||
* @param firstTarget - `true` if {@linkcode target} is the first target hit by this strike of {@linkcode move}
|
||||
* @returns a function intended to be passed into a `then()` call.
|
||||
*/
|
||||
protected applySuccessfulAttackEffects(user: Pokemon, target: Pokemon, firstHit : boolean, lastHit: boolean, isProtected : boolean, hitResult: HitResult, firstTarget: boolean) : () => Promise<void | null> {
|
||||
return () => executeIf(!isProtected, () =>
|
||||
this.applyOnHitEffects(user, target, firstHit, lastHit, firstTarget).then(() =>
|
||||
this.applyOnGetHitAbEffects(user, target, hitResult)).then(() =>
|
||||
applyPostAttackAbAttrs(PostAttackAbAttr, user, target, this.move.getMove(), hitResult)).then(() => { // Item Stealing Effects
|
||||
|
||||
if (this.move.getMove() instanceof AttackMove) {
|
||||
this.scene.applyModifiers(ContactHeldItemTransferChanceModifier, this.player, user, target);
|
||||
}
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles checking for and applying Flinches
|
||||
* @param user - The {@linkcode Pokemon} using this phase's invoked move
|
||||
* @param target - {@linkcode Pokemon} the current target of this phase's invoked move
|
||||
* @param dealsDamage - `true` if the attempted move successfully dealt damage
|
||||
* @returns a function intended to be passed into a `then()` call.
|
||||
*/
|
||||
protected applyHeldItemFlinchCheck(user: Pokemon, target: Pokemon, dealsDamage: boolean) : () => void {
|
||||
return () => {
|
||||
if (dealsDamage && !target.hasAbilityWithAttr(IgnoreMoveEffectsAbAttr) && !this.move.getMove().hitsSubstitute(user, target)) {
|
||||
const flinched = new BooleanHolder(false);
|
||||
user.scene.applyModifiers(FlinchChanceModifier, user.isPlayer(), user, flinched);
|
||||
if (flinched.value) {
|
||||
target.addTag(BattlerTagType.FLINCHED, undefined, this.move.moveId, user.id);
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolves whether this phase's invoked move hits the given target
|
||||
* @param target - The {@linkcode Pokemon} targeted by the invoked move
|
||||
* @returns `true` if the move hits the target
|
||||
*/
|
||||
public hitCheck(target: Pokemon): boolean {
|
||||
// Moves targeting the user and entry hazards can't miss
|
||||
if ([ MoveTarget.USER, MoveTarget.ENEMY_SIDE ].includes(this.move.getMove().moveTarget)) {
|
||||
return true;
|
||||
@ -425,29 +565,29 @@ export class MoveEffectPhase extends PokemonPhase {
|
||||
return rand < (moveAccuracy * accuracyMultiplier);
|
||||
}
|
||||
|
||||
/** Returns the {@linkcode Pokemon} using this phase's invoked move */
|
||||
getUserPokemon(): Pokemon | undefined {
|
||||
/** @returns The {@linkcode Pokemon} using this phase's invoked move */
|
||||
public getUserPokemon(): Pokemon | undefined {
|
||||
if (this.battlerIndex > BattlerIndex.ENEMY_2) {
|
||||
return this.scene.getPokemonById(this.battlerIndex) ?? undefined;
|
||||
}
|
||||
return (this.player ? this.scene.getPlayerField() : this.scene.getEnemyField())[this.fieldIndex];
|
||||
}
|
||||
|
||||
/** Returns an array of all {@linkcode Pokemon} targeted by this phase's invoked move */
|
||||
getTargets(): Pokemon[] {
|
||||
/** @returns An array of all {@linkcode Pokemon} targeted by this phase's invoked move */
|
||||
public getTargets(): Pokemon[] {
|
||||
return this.scene.getField(true).filter(p => this.targets.indexOf(p.getBattlerIndex()) > -1);
|
||||
}
|
||||
|
||||
/** Returns the first target of this phase's invoked move */
|
||||
getTarget(): Pokemon | undefined {
|
||||
/** @returns The first target of this phase's invoked move */
|
||||
public getFirstTarget(): Pokemon | undefined {
|
||||
return this.getTargets()[0];
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes the given {@linkcode Pokemon} from this phase's target list
|
||||
* @param target {@linkcode Pokemon} the Pokemon to be removed
|
||||
* @param target - The {@linkcode Pokemon} to be removed
|
||||
*/
|
||||
removeTarget(target: Pokemon): void {
|
||||
protected removeTarget(target: Pokemon): void {
|
||||
const targetIndex = this.targets.findIndex(ind => ind === target.getBattlerIndex());
|
||||
if (targetIndex !== -1) {
|
||||
this.targets.splice(this.targets.findIndex(ind => ind === target.getBattlerIndex()), 1);
|
||||
@ -459,23 +599,25 @@ export class MoveEffectPhase extends PokemonPhase {
|
||||
* @param target {@linkcode Pokemon} if defined, only stop subsequent
|
||||
* strikes against this Pokemon
|
||||
*/
|
||||
stopMultiHit(target?: Pokemon): void {
|
||||
/** If given a specific target, remove the target from subsequent strikes */
|
||||
public stopMultiHit(target?: Pokemon): void {
|
||||
// If given a specific target, remove the target from subsequent strikes
|
||||
if (target) {
|
||||
this.removeTarget(target);
|
||||
}
|
||||
/**
|
||||
* If no target specified, or the specified target was the last of this move's
|
||||
* targets, completely cancel all subsequent strikes.
|
||||
*/
|
||||
const user = this.getUserPokemon();
|
||||
if (!user) {
|
||||
return;
|
||||
}
|
||||
// If no target specified, or the specified target was the last of this move's
|
||||
// targets, completely cancel all subsequent strikes.
|
||||
if (!target || this.targets.length === 0 ) {
|
||||
this.getUserPokemon()!.turnData.hitCount = 1; // TODO: is the bang correct here?
|
||||
this.getUserPokemon()!.turnData.hitsLeft = 1; // TODO: is the bang correct here?
|
||||
user.turnData.hitCount = 1;
|
||||
user.turnData.hitsLeft = 1;
|
||||
}
|
||||
}
|
||||
|
||||
/** Returns a new MoveEffectPhase with the same properties as this phase */
|
||||
getNewHitPhase() {
|
||||
/** @returns A new `MoveEffectPhase` with the same properties as this phase */
|
||||
protected getNewHitPhase(): MoveEffectPhase {
|
||||
return new MoveEffectPhase(this.scene, this.battlerIndex, this.targets, this.move);
|
||||
}
|
||||
}
|
||||
|
@ -1,6 +1,6 @@
|
||||
import BattleScene from "#app/battle-scene";
|
||||
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 { getStatusEffectActivationText } from "#app/data/status-effect";
|
||||
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 ...
|
||||
this.scene.damageNumberHandler.add(this.getPokemon(), pokemon.damage(damage.value, false, true));
|
||||
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());
|
||||
} else {
|
||||
|
@ -57,7 +57,7 @@ describe("Abilities - Serene Grace", () => {
|
||||
|
||||
const chance = new Utils.IntegerHolder(move.chance);
|
||||
console.log(move.chance + " Their ability is " + phase.getUserPokemon()!.getAbility().name);
|
||||
applyAbAttrs(MoveEffectChanceMultiplierAbAttr, phase.getUserPokemon()!, null, false, chance, move, phase.getTarget(), false);
|
||||
applyAbAttrs(MoveEffectChanceMultiplierAbAttr, phase.getUserPokemon()!, null, false, chance, move, phase.getFirstTarget(), false);
|
||||
expect(chance.value).toBe(30);
|
||||
|
||||
}, 20000);
|
||||
@ -83,7 +83,7 @@ describe("Abilities - Serene Grace", () => {
|
||||
expect(move.id).toBe(Moves.AIR_SLASH);
|
||||
|
||||
const chance = new Utils.IntegerHolder(move.chance);
|
||||
applyAbAttrs(MoveEffectChanceMultiplierAbAttr, phase.getUserPokemon()!, null, false, chance, move, phase.getTarget(), false);
|
||||
applyAbAttrs(MoveEffectChanceMultiplierAbAttr, phase.getUserPokemon()!, null, false, chance, move, phase.getFirstTarget(), false);
|
||||
expect(chance.value).toBe(60);
|
||||
|
||||
}, 20000);
|
||||
|
@ -60,8 +60,8 @@ describe("Abilities - Sheer Force", () => {
|
||||
const power = new Utils.IntegerHolder(move.power);
|
||||
const chance = new Utils.IntegerHolder(move.chance);
|
||||
|
||||
applyAbAttrs(MoveEffectChanceMultiplierAbAttr, phase.getUserPokemon()!, null, false, chance, move, phase.getTarget(), false);
|
||||
applyPreAttackAbAttrs(MovePowerBoostAbAttr, phase.getUserPokemon()!, phase.getTarget()!, move, false, power);
|
||||
applyAbAttrs(MoveEffectChanceMultiplierAbAttr, phase.getUserPokemon()!, null, false, chance, move, phase.getFirstTarget(), false);
|
||||
applyPreAttackAbAttrs(MovePowerBoostAbAttr, phase.getUserPokemon()!, phase.getFirstTarget()!, move, false, power);
|
||||
|
||||
expect(chance.value).toBe(0);
|
||||
expect(power.value).toBe(move.power * 5461 / 4096);
|
||||
@ -93,8 +93,8 @@ describe("Abilities - Sheer Force", () => {
|
||||
const power = new Utils.IntegerHolder(move.power);
|
||||
const chance = new Utils.IntegerHolder(move.chance);
|
||||
|
||||
applyAbAttrs(MoveEffectChanceMultiplierAbAttr, phase.getUserPokemon()!, null, false, chance, move, phase.getTarget(), false);
|
||||
applyPreAttackAbAttrs(MovePowerBoostAbAttr, phase.getUserPokemon()!, phase.getTarget()!, move, false, power);
|
||||
applyAbAttrs(MoveEffectChanceMultiplierAbAttr, phase.getUserPokemon()!, null, false, chance, move, phase.getFirstTarget(), false);
|
||||
applyPreAttackAbAttrs(MovePowerBoostAbAttr, phase.getUserPokemon()!, phase.getFirstTarget()!, move, false, power);
|
||||
|
||||
expect(chance.value).toBe(-1);
|
||||
expect(power.value).toBe(move.power);
|
||||
@ -126,8 +126,8 @@ describe("Abilities - Sheer Force", () => {
|
||||
const power = new Utils.IntegerHolder(move.power);
|
||||
const chance = new Utils.IntegerHolder(move.chance);
|
||||
|
||||
applyAbAttrs(MoveEffectChanceMultiplierAbAttr, phase.getUserPokemon()!, null, false, chance, move, phase.getTarget(), false);
|
||||
applyPreAttackAbAttrs(MovePowerBoostAbAttr, phase.getUserPokemon()!, phase.getTarget()!, move, false, power);
|
||||
applyAbAttrs(MoveEffectChanceMultiplierAbAttr, phase.getUserPokemon()!, null, false, chance, move, phase.getFirstTarget(), false);
|
||||
applyPreAttackAbAttrs(MovePowerBoostAbAttr, phase.getUserPokemon()!, phase.getFirstTarget()!, move, false, power);
|
||||
|
||||
expect(chance.value).toBe(-1);
|
||||
expect(power.value).toBe(move.power);
|
||||
@ -161,7 +161,7 @@ describe("Abilities - Sheer Force", () => {
|
||||
const power = new Utils.IntegerHolder(move.power);
|
||||
const chance = new Utils.IntegerHolder(move.chance);
|
||||
const user = phase.getUserPokemon()!;
|
||||
const target = phase.getTarget()!;
|
||||
const target = phase.getFirstTarget()!;
|
||||
const opponentType = target.getTypes()[0];
|
||||
|
||||
applyAbAttrs(MoveEffectChanceMultiplierAbAttr, user, null, false, chance, move, target, false);
|
||||
|
@ -57,8 +57,8 @@ describe("Abilities - Shield Dust", () => {
|
||||
expect(move.id).toBe(Moves.AIR_SLASH);
|
||||
|
||||
const chance = new Utils.IntegerHolder(move.chance);
|
||||
applyAbAttrs(MoveEffectChanceMultiplierAbAttr, phase.getUserPokemon()!, null, false, chance, move, phase.getTarget(), false);
|
||||
applyPreDefendAbAttrs(IgnoreMoveEffectsAbAttr, phase.getTarget()!, phase.getUserPokemon()!, null, null, false, chance);
|
||||
applyAbAttrs(MoveEffectChanceMultiplierAbAttr, phase.getUserPokemon()!, null, false, chance, move, phase.getFirstTarget(), false);
|
||||
applyPreDefendAbAttrs(IgnoreMoveEffectsAbAttr, phase.getFirstTarget()!, phase.getUserPokemon()!, null, null, false, chance);
|
||||
expect(chance.value).toBe(0);
|
||||
|
||||
}, 20000);
|
||||
|
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();
|
||||
});
|
||||
});
|
49
src/test/moves/camouflage.test.ts
Normal file
49
src/test/moves/camouflage.test.ts
Normal file
@ -0,0 +1,49 @@
|
||||
import { Abilities } from "#enums/abilities";
|
||||
import { Moves } from "#enums/moves";
|
||||
import { Species } from "#enums/species";
|
||||
import { TerrainType } from "#app/data/terrain";
|
||||
import { Type } from "#app/data/type";
|
||||
import { BattlerIndex } from "#app/battle";
|
||||
import GameManager from "#test/utils/gameManager";
|
||||
import Phaser from "phaser";
|
||||
import { afterEach, beforeAll, beforeEach, describe, expect, it } from "vitest";
|
||||
|
||||
describe("Moves - Camouflage", () => {
|
||||
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
|
||||
.moveset([ Moves.CAMOUFLAGE ])
|
||||
.ability(Abilities.BALL_FETCH)
|
||||
.battleType("single")
|
||||
.disableCrits()
|
||||
.enemySpecies(Species.REGIELEKI)
|
||||
.enemyAbility(Abilities.BALL_FETCH)
|
||||
.enemyMoveset(Moves.PSYCHIC_TERRAIN);
|
||||
});
|
||||
|
||||
it("Camouflage should look at terrain first when selecting a type to change into", async () => {
|
||||
await game.classicMode.startBattle([ Species.SHUCKLE ]);
|
||||
|
||||
const playerPokemon = game.scene.getPlayerPokemon()!;
|
||||
|
||||
game.move.select(Moves.CAMOUFLAGE);
|
||||
await game.setTurnOrder([ BattlerIndex.ENEMY, BattlerIndex.PLAYER ]);
|
||||
await game.phaseInterceptor.to("BerryPhase");
|
||||
expect(game.scene.arena.getTerrainType()).toBe(TerrainType.PSYCHIC);
|
||||
const pokemonType = playerPokemon.getTypes()[0];
|
||||
expect(pokemonType).toBe(Type.PSYCHIC);
|
||||
});
|
||||
});
|
@ -81,7 +81,7 @@ describe("Moves - Dynamax Cannon", () => {
|
||||
const phase = game.scene.getCurrentPhase() as MoveEffectPhase;
|
||||
expect(phase.move.moveId).toBe(dynamaxCannon.id);
|
||||
// Force level cap to be 100
|
||||
vi.spyOn(phase.getTarget()!.scene, "getMaxExpLevel").mockReturnValue(100);
|
||||
vi.spyOn(phase.getFirstTarget()!.scene, "getMaxExpLevel").mockReturnValue(100);
|
||||
await game.phaseInterceptor.to(DamagePhase, false);
|
||||
expect(dynamaxCannon.calculateBattlePower).toHaveLastReturnedWith(120);
|
||||
}, 20000);
|
||||
@ -98,7 +98,7 @@ describe("Moves - Dynamax Cannon", () => {
|
||||
const phase = game.scene.getCurrentPhase() as MoveEffectPhase;
|
||||
expect(phase.move.moveId).toBe(dynamaxCannon.id);
|
||||
// Force level cap to be 100
|
||||
vi.spyOn(phase.getTarget()!.scene, "getMaxExpLevel").mockReturnValue(100);
|
||||
vi.spyOn(phase.getFirstTarget()!.scene, "getMaxExpLevel").mockReturnValue(100);
|
||||
await game.phaseInterceptor.to(DamagePhase, false);
|
||||
expect(dynamaxCannon.calculateBattlePower).toHaveLastReturnedWith(140);
|
||||
}, 20000);
|
||||
@ -115,7 +115,7 @@ describe("Moves - Dynamax Cannon", () => {
|
||||
const phase = game.scene.getCurrentPhase() as MoveEffectPhase;
|
||||
expect(phase.move.moveId).toBe(dynamaxCannon.id);
|
||||
// Force level cap to be 100
|
||||
vi.spyOn(phase.getTarget()!.scene, "getMaxExpLevel").mockReturnValue(100);
|
||||
vi.spyOn(phase.getFirstTarget()!.scene, "getMaxExpLevel").mockReturnValue(100);
|
||||
await game.phaseInterceptor.to(DamagePhase, false);
|
||||
expect(dynamaxCannon.calculateBattlePower).toHaveLastReturnedWith(160);
|
||||
}, 20000);
|
||||
@ -132,7 +132,7 @@ describe("Moves - Dynamax Cannon", () => {
|
||||
const phase = game.scene.getCurrentPhase() as MoveEffectPhase;
|
||||
expect(phase.move.moveId).toBe(dynamaxCannon.id);
|
||||
// Force level cap to be 100
|
||||
vi.spyOn(phase.getTarget()!.scene, "getMaxExpLevel").mockReturnValue(100);
|
||||
vi.spyOn(phase.getFirstTarget()!.scene, "getMaxExpLevel").mockReturnValue(100);
|
||||
await game.phaseInterceptor.to(DamagePhase, false);
|
||||
expect(dynamaxCannon.calculateBattlePower).toHaveLastReturnedWith(180);
|
||||
}, 20000);
|
||||
@ -149,7 +149,7 @@ describe("Moves - Dynamax Cannon", () => {
|
||||
const phase = game.scene.getCurrentPhase() as MoveEffectPhase;
|
||||
expect(phase.move.moveId).toBe(dynamaxCannon.id);
|
||||
// Force level cap to be 100
|
||||
vi.spyOn(phase.getTarget()!.scene, "getMaxExpLevel").mockReturnValue(100);
|
||||
vi.spyOn(phase.getFirstTarget()!.scene, "getMaxExpLevel").mockReturnValue(100);
|
||||
await game.phaseInterceptor.to(DamagePhase, false);
|
||||
expect(dynamaxCannon.calculateBattlePower).toHaveLastReturnedWith(200);
|
||||
}, 20000);
|
||||
|
@ -59,7 +59,7 @@ describe("Moves - Focus Punch", () => {
|
||||
|
||||
expect(enemyPokemon.hp).toBeLessThan(enemyStartingHp);
|
||||
expect(leadPokemon.getMoveHistory().length).toBe(1);
|
||||
expect(leadPokemon.turnData.damageDealt).toBe(enemyStartingHp - enemyPokemon.hp);
|
||||
expect(leadPokemon.turnData.totalDamageDealt).toBe(enemyStartingHp - enemyPokemon.hp);
|
||||
}
|
||||
);
|
||||
|
||||
@ -86,7 +86,7 @@ describe("Moves - Focus Punch", () => {
|
||||
|
||||
expect(enemyPokemon.hp).toBe(enemyStartingHp);
|
||||
expect(leadPokemon.getMoveHistory().length).toBe(1);
|
||||
expect(leadPokemon.turnData.damageDealt).toBe(0);
|
||||
expect(leadPokemon.turnData.totalDamageDealt).toBe(0);
|
||||
}
|
||||
);
|
||||
|
||||
|
@ -2,7 +2,7 @@ import { Abilities } from "#enums/abilities";
|
||||
import { Biome } from "#enums/biome";
|
||||
import { Moves } from "#enums/moves";
|
||||
import { Stat } from "#enums/stat";
|
||||
import { allMoves, SecretPowerAttr } from "#app/data/move";
|
||||
import { allMoves } from "#app/data/move";
|
||||
import { Species } from "#enums/species";
|
||||
import GameManager from "#test/utils/gameManager";
|
||||
import Phaser from "phaser";
|
||||
@ -11,6 +11,7 @@ import { StatusEffect } from "#enums/status-effect";
|
||||
import { BattlerIndex } from "#app/battle";
|
||||
import { ArenaTagType } from "#enums/arena-tag-type";
|
||||
import { ArenaTagSide } from "#app/data/arena-tag";
|
||||
import { allAbilities, MoveEffectChanceMultiplierAbAttr } from "#app/data/ability";
|
||||
|
||||
describe("Moves - Secret Power", () => {
|
||||
let phaserGame: Phaser.Game;
|
||||
@ -60,30 +61,38 @@ describe("Moves - Secret Power", () => {
|
||||
expect(enemyPokemon.getStatStage(Stat.SPATK)).toBe(-1);
|
||||
});
|
||||
|
||||
it("the 'rainbow' effect of fire+water pledge does not double the chance of secret power's secondary effect",
|
||||
it("Secret Power's effect chance is doubled by Serene Grace, but not by the 'rainbow' effect from Fire/Water Pledge",
|
||||
async () => {
|
||||
game.override
|
||||
.moveset([ Moves.FIRE_PLEDGE, Moves.WATER_PLEDGE, Moves.SECRET_POWER, Moves.SPLASH ])
|
||||
.ability(Abilities.SERENE_GRACE)
|
||||
.enemyMoveset([ Moves.SPLASH ])
|
||||
.battleType("double");
|
||||
await game.classicMode.startBattle([ Species.BLASTOISE, Species.CHARIZARD ]);
|
||||
|
||||
const secretPowerAttr = allMoves[Moves.SECRET_POWER].getAttrs(SecretPowerAttr)[0];
|
||||
vi.spyOn(secretPowerAttr, "getMoveChance");
|
||||
const sereneGraceAttr = allAbilities[Abilities.SERENE_GRACE].getAttrs(MoveEffectChanceMultiplierAbAttr)[0];
|
||||
vi.spyOn(sereneGraceAttr, "apply");
|
||||
|
||||
game.move.select(Moves.WATER_PLEDGE, 0, BattlerIndex.ENEMY);
|
||||
game.move.select(Moves.FIRE_PLEDGE, 1, BattlerIndex.ENEMY_2);
|
||||
|
||||
await game.phaseInterceptor.to("TurnEndPhase");
|
||||
|
||||
expect(game.scene.arena.getTagOnSide(ArenaTagType.WATER_FIRE_PLEDGE, ArenaTagSide.PLAYER)).toBeDefined();
|
||||
let rainbowEffect = game.scene.arena.getTagOnSide(ArenaTagType.WATER_FIRE_PLEDGE, ArenaTagSide.PLAYER);
|
||||
expect(rainbowEffect).toBeDefined();
|
||||
|
||||
rainbowEffect = rainbowEffect!;
|
||||
vi.spyOn(rainbowEffect, "apply");
|
||||
|
||||
game.move.select(Moves.SECRET_POWER, 0, BattlerIndex.ENEMY);
|
||||
game.move.select(Moves.SPLASH, 1);
|
||||
|
||||
await game.phaseInterceptor.to("BerryPhase", false);
|
||||
|
||||
expect(secretPowerAttr.getMoveChance).toHaveLastReturnedWith(30);
|
||||
expect(sereneGraceAttr.apply).toHaveBeenCalledOnce();
|
||||
expect(sereneGraceAttr.apply).toHaveLastReturnedWith(true);
|
||||
|
||||
expect(rainbowEffect.apply).toHaveBeenCalledTimes(0);
|
||||
}
|
||||
);
|
||||
});
|
||||
|
@ -1,4 +1,5 @@
|
||||
import { SubstituteTag } from "#app/data/battler-tags";
|
||||
import { MoveResult } from "#app/field/pokemon";
|
||||
import { Abilities } from "#enums/abilities";
|
||||
import { Moves } from "#enums/moves";
|
||||
import { Species } from "#enums/species";
|
||||
@ -53,4 +54,18 @@ describe("Moves - Shed Tail", () => {
|
||||
expect(substituteTag).toBeDefined();
|
||||
expect(substituteTag?.hp).toBe(Math.floor(magikarp.getMaxHp() / 4));
|
||||
});
|
||||
|
||||
it("should fail if no ally is available to switch in", async () => {
|
||||
await game.classicMode.startBattle([ Species.MAGIKARP ]);
|
||||
|
||||
const magikarp = game.scene.getPlayerPokemon()!;
|
||||
expect(game.scene.getParty().length).toBe(1);
|
||||
|
||||
game.move.select(Moves.SHED_TAIL);
|
||||
|
||||
await game.phaseInterceptor.to("TurnEndPhase", false);
|
||||
|
||||
expect(magikarp.isOnField()).toBeTruthy();
|
||||
expect(magikarp.getLastXMoves()[0].result).toBe(MoveResult.FAIL);
|
||||
});
|
||||
});
|
||||
|
@ -24,6 +24,7 @@ import GamepadPlugin = Phaser.Input.Gamepad.GamepadPlugin;
|
||||
import EventEmitter = Phaser.Events.EventEmitter;
|
||||
import UpdateList = Phaser.GameObjects.UpdateList;
|
||||
import { version } from "../../../package.json";
|
||||
import { MockTimedEventManager } from "./mocks/mockTimedEventManager";
|
||||
|
||||
Object.defineProperty(window, "localStorage", {
|
||||
value: mockLocalStorage(),
|
||||
@ -232,6 +233,7 @@ export default class GameWrapper {
|
||||
this.scene.make = new MockGameObjectCreator(mockTextureManager);
|
||||
this.scene.time = new MockClock(this.scene);
|
||||
this.scene.remove = vi.fn(); // TODO: this should be stubbed differently
|
||||
this.scene.eventManager = new MockTimedEventManager(); // Disable Timed Events
|
||||
}
|
||||
}
|
||||
|
||||
|
17
src/test/utils/mocks/mockTimedEventManager.ts
Normal file
17
src/test/utils/mocks/mockTimedEventManager.ts
Normal file
@ -0,0 +1,17 @@
|
||||
import { TimedEventManager } from "#app/timed-event-manager";
|
||||
|
||||
/** Mock TimedEventManager so that ongoing events don't impact tests */
|
||||
export class MockTimedEventManager extends TimedEventManager {
|
||||
override activeEvent() {
|
||||
return undefined;
|
||||
}
|
||||
override isEventActive(): boolean {
|
||||
return false;
|
||||
}
|
||||
override getFriendshipMultiplier(): number {
|
||||
return 1;
|
||||
}
|
||||
override getShinyMultiplier(): number {
|
||||
return 1;
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue
Block a user