mirror of
https://github.com/pagefaultgames/pokerogue.git
synced 2025-06-29 04:52:43 +02:00
Added MoveUseType
and refactored MEP
This commit is contained in:
parent
718c8cfbb9
commit
3f02493f79
21
biome.jsonc
21
biome.jsonc
@ -58,7 +58,7 @@
|
||||
},
|
||||
"style": {
|
||||
"noVar": "error",
|
||||
"useEnumInitializers": "off", // large enums like Moves/Species would make this cumbersome
|
||||
"useEnumInitializers": "off", // large enums like Moves/Species would make this cumbersome
|
||||
"useBlockStatements": "error",
|
||||
"useConst": "error",
|
||||
"useImportType": "error",
|
||||
@ -73,9 +73,9 @@
|
||||
},
|
||||
"suspicious": {
|
||||
"noDoubleEquals": "error",
|
||||
// While this would be a nice rule to enable, the current structure of the codebase makes this infeasible
|
||||
// While this would be a nice rule to enable, the current structure of the codebase makes this infeasible
|
||||
// due to being used for move/ability `args` params and save data-related code.
|
||||
// This can likely be enabled for all non-utils files once these are eventually reworked, but until then we leave it off.
|
||||
// This can likely be enabled for all non-utils files once these are eventually reworked, but until then we leave it off.
|
||||
"noExplicitAny": "off",
|
||||
"noAssignInExpressions": "off",
|
||||
"noPrototypeBuiltins": "off",
|
||||
@ -112,6 +112,21 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
// Overrides to prevent unused import removal inside `overrides.ts` and enums files (for jsdoc)
|
||||
{
|
||||
"include": ["src/overrides.ts", "src/enums/*"],
|
||||
"linter": {
|
||||
"rules": {
|
||||
"correctness": {
|
||||
"noUnusedImports": "off"
|
||||
},
|
||||
"style": {
|
||||
"useImportType": "error"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
|
@ -1,8 +1,8 @@
|
||||
import { HitResult, MoveResult, PlayerPokemon } from "#app/field/pokemon";
|
||||
import { HitResult, MoveResult, PlayerPokemon, type TurnMove } from "#app/field/pokemon";
|
||||
import { BooleanHolder, NumberHolder, toDmgValue, isNullOrUndefined, randSeedItem, randSeedInt, type Constructor } from "#app/utils/common";
|
||||
import { getPokemonNameWithAffix } from "#app/messages";
|
||||
import { BattlerTagLapseType, GroundedTag } from "#app/data/battler-tags";
|
||||
import { getNonVolatileStatusEffects, getStatusEffectDescriptor, getStatusEffectHealText } from "#app/data/status-effect";
|
||||
import { getNonVolatileStatusEffects, getStatusEffectDescriptor, getStatusEffectHealText, type Status } from "#app/data/status-effect";
|
||||
import { Gender } from "#app/data/gender";
|
||||
import {
|
||||
AttackMove,
|
||||
@ -67,8 +67,8 @@ import { BerryUsedEvent } from "#app/events/battle-scene";
|
||||
|
||||
|
||||
// Type imports
|
||||
import type { EnemyPokemon, PokemonMove } from "#app/field/pokemon";
|
||||
import type Pokemon from "#app/field/pokemon";
|
||||
import { EnemyPokemon, PokemonMove } from "#app/field/pokemon";
|
||||
import Pokemon from "#app/field/pokemon";
|
||||
import type { Weather } from "#app/data/weather";
|
||||
import type { BattlerTag } from "#app/data/battler-tags";
|
||||
import type { AbAttrCondition, PokemonDefendCondition, PokemonStatStageChangeCondition, PokemonAttackCondition, AbAttrApplyFunc, AbAttrSuccessFunc } from "#app/@types/ability-types";
|
||||
@ -76,6 +76,7 @@ import type { BattlerIndex } from "#app/battle";
|
||||
import type Move from "#app/data/moves/move";
|
||||
import type { ArenaTrapTag, SuppressAbilitiesTag } from "#app/data/arena-tag";
|
||||
import { SelectBiomePhase } from "#app/phases/select-biome-phase";
|
||||
import { MoveUseType } from "#enums/move-use-type";
|
||||
import { noAbilityTypeOverrideMoves } from "../moves/invalid-moves";
|
||||
|
||||
export class BlockRecoilDamageAttr extends AbAttr {
|
||||
@ -1067,7 +1068,7 @@ export class PostDefendMoveDisableAbAttr extends PostDefendAbAttr {
|
||||
}
|
||||
|
||||
override canApplyPostDefend(pokemon: Pokemon, passive: boolean, simulated: boolean, attacker: Pokemon, move: Move, hitResult: HitResult | null, args: any[]): boolean {
|
||||
return attacker.getTag(BattlerTagType.DISABLED) === null
|
||||
return !isNullOrUndefined(attacker.getTag(BattlerTagType.DISABLED))
|
||||
&& move.doesFlagEffectApply({flag: MoveFlags.MAKES_CONTACT, user: attacker, target: pokemon}) && (this.chance === -1 || pokemon.randSeedInt(100) < this.chance);
|
||||
}
|
||||
|
||||
@ -1246,7 +1247,7 @@ export class MoveTypeChangeAbAttr extends PreAttackAbAttr {
|
||||
|
||||
/**
|
||||
* Determine if the move type change attribute can be applied
|
||||
*
|
||||
*
|
||||
* Can be applied if:
|
||||
* - The ability's condition is met, e.g. pixilate only boosts normal moves,
|
||||
* - The move is not forbidden from having its type changed by an ability, e.g. {@linkcode Moves.MULTI_ATTACK}
|
||||
@ -1262,7 +1263,7 @@ export class MoveTypeChangeAbAttr extends PreAttackAbAttr {
|
||||
*/
|
||||
override canApplyPreAttack(pokemon: Pokemon, _passive: boolean, _simulated: boolean, _defender: Pokemon | null, move: Move, _args: [NumberHolder?, NumberHolder?, ...any]): boolean {
|
||||
return (!this.condition || this.condition(pokemon, _defender, move)) &&
|
||||
!noAbilityTypeOverrideMoves.has(move.id) &&
|
||||
!noAbilityTypeOverrideMoves.has(move.id) &&
|
||||
(!pokemon.isTerastallized ||
|
||||
(move.id !== Moves.TERA_BLAST &&
|
||||
(move.id !== Moves.TERA_STARSTORM || pokemon.getTeraType() !== PokemonType.STELLAR || !pokemon.hasSpecies(Species.TERAPAGOS))));
|
||||
@ -2334,18 +2335,18 @@ export class PostSummonStatStageChangeAbAttr extends PostSummonAbAttr {
|
||||
// phase list (which could be after CommandPhase for example)
|
||||
globalScene.unshiftPhase(new StatStageChangePhase(pokemon.getBattlerIndex(), true, this.stats, this.stages));
|
||||
} else {
|
||||
for (const opponent of pokemon.getOpponents()) {
|
||||
const cancelled = new BooleanHolder(false);
|
||||
if (this.intimidate) {
|
||||
applyAbAttrs(IntimidateImmunityAbAttr, opponent, cancelled, simulated);
|
||||
applyAbAttrs(PostIntimidateStatStageChangeAbAttr, opponent, cancelled, simulated);
|
||||
for (const opponent of pokemon.getOpponents()) {
|
||||
const cancelled = new BooleanHolder(false);
|
||||
if (this.intimidate) {
|
||||
applyAbAttrs(IntimidateImmunityAbAttr, opponent, cancelled, simulated);
|
||||
applyAbAttrs(PostIntimidateStatStageChangeAbAttr, opponent, cancelled, simulated);
|
||||
|
||||
if (opponent.getTag(BattlerTagType.SUBSTITUTE)) {
|
||||
cancelled.value = true;
|
||||
}
|
||||
if (opponent.getTag(BattlerTagType.SUBSTITUTE)) {
|
||||
cancelled.value = true;
|
||||
}
|
||||
if (!cancelled.value) {
|
||||
globalScene.unshiftPhase(new StatStageChangePhase(opponent.getBattlerIndex(), false, this.stats, this.stages));
|
||||
}
|
||||
if (!cancelled.value) {
|
||||
globalScene.unshiftPhase(new StatStageChangePhase(opponent.getBattlerIndex(), false, this.stats, this.stages));
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -4398,12 +4399,12 @@ export class PostBiomeChangeTerrainChangeAbAttr extends PostBiomeChangeAbAttr {
|
||||
* @extends AbAttr
|
||||
*/
|
||||
export class PostMoveUsedAbAttr extends AbAttr {
|
||||
canApplyPostMoveUsed(
|
||||
canApplyPostMoveUsed(
|
||||
pokemon: Pokemon,
|
||||
move: PokemonMove,
|
||||
source: Pokemon,
|
||||
targets: BattlerIndex[],
|
||||
simulated: boolean,
|
||||
simulated: boolean,
|
||||
args: any[]): boolean {
|
||||
return true;
|
||||
}
|
||||
@ -4413,7 +4414,7 @@ export class PostMoveUsedAbAttr extends AbAttr {
|
||||
move: PokemonMove,
|
||||
source: Pokemon,
|
||||
targets: BattlerIndex[],
|
||||
simulated: boolean,
|
||||
simulated: boolean,
|
||||
args: any[],
|
||||
): void {}
|
||||
}
|
||||
@ -4446,16 +4447,17 @@ export class PostDancingMoveAbAttr extends PostMoveUsedAbAttr {
|
||||
move: PokemonMove,
|
||||
source: Pokemon,
|
||||
targets: BattlerIndex[],
|
||||
simulated: boolean,
|
||||
simulated: boolean,
|
||||
args: any[]): void {
|
||||
if (!simulated) {
|
||||
// If the move is an AttackMove or a StatusMove the Dancer must replicate the move on the source of the Dance
|
||||
// TODO: fix in main dancer PR (currently keeping this purely semantic rather than actually fixing bug)
|
||||
if (move.getMove() instanceof AttackMove || move.getMove() instanceof StatusMove) {
|
||||
const target = this.getTarget(dancer, source, targets);
|
||||
globalScene.unshiftPhase(new MovePhase(dancer, target, move, true, true));
|
||||
globalScene.unshiftPhase(new MovePhase(dancer, target, move, MoveUseType.FOLLOW_UP));
|
||||
} else if (move.getMove() instanceof SelfStatusMove) {
|
||||
// If the move is a SelfStatusMove (ie. Swords Dance) the Dancer should replicate it on itself
|
||||
globalScene.unshiftPhase(new MovePhase(dancer, [ dancer.getBattlerIndex() ], move, true, true));
|
||||
globalScene.unshiftPhase(new MovePhase(dancer, [ dancer.getBattlerIndex() ], move, MoveUseType.FOLLOW_UP))
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -4897,7 +4899,7 @@ export class BlockRedirectAbAttr extends AbAttr { }
|
||||
* @see {@linkcode apply}
|
||||
*/
|
||||
export class ReduceStatusEffectDurationAbAttr extends AbAttr {
|
||||
private statusEffect: StatusEffect;
|
||||
private statusEffect: StatusEffect;
|
||||
|
||||
constructor(statusEffect: StatusEffect) {
|
||||
super(false);
|
||||
@ -4906,7 +4908,7 @@ export class ReduceStatusEffectDurationAbAttr extends AbAttr {
|
||||
}
|
||||
|
||||
override canApply(pokemon: Pokemon, passive: boolean, simulated: boolean, args: any[]): boolean {
|
||||
return args[1] instanceof NumberHolder && args[0] === this.statusEffect;
|
||||
return args[1] instanceof NumberHolder && args[0] === this.statusEffect;
|
||||
}
|
||||
|
||||
/**
|
||||
@ -5545,6 +5547,7 @@ class ForceSwitchOutHelper {
|
||||
*
|
||||
* @param pokemon The {@linkcode Pokemon} attempting to switch out.
|
||||
* @returns `true` if the switch is successful
|
||||
* TODO: Make this actually cancel pending move phases on the switched out target
|
||||
*/
|
||||
public switchOutLogic(pokemon: Pokemon): boolean {
|
||||
const switchOutTarget = pokemon;
|
||||
@ -5554,7 +5557,7 @@ class ForceSwitchOutHelper {
|
||||
* - 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 (globalScene.getPlayerParty().filter((p) => p.isAllowedInBattle() && !p.isOnField()).length < 1) {
|
||||
if (globalScene.getPlayerParty().every(p => !p.isActive(true))) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@ -5887,7 +5890,7 @@ export function applyPostMoveUsedAbAttrs(
|
||||
move: PokemonMove,
|
||||
source: Pokemon,
|
||||
targets: BattlerIndex[],
|
||||
simulated = false,
|
||||
simulated = false,
|
||||
...args: any[]
|
||||
): void {
|
||||
applyAbAttrsInternal<PostMoveUsedAbAttr>(
|
||||
@ -6897,8 +6900,9 @@ export function initAbilities() {
|
||||
.ignorable(),
|
||||
new Ability(Abilities.ANALYTIC, 5)
|
||||
.attr(MovePowerBoostAbAttr, (user, target, move) => {
|
||||
const movePhase = globalScene.findPhase((phase) => phase instanceof MovePhase && phase.pokemon.id !== user?.id);
|
||||
return isNullOrUndefined(movePhase);
|
||||
// Boost power if all other Pokemon have already moved (no other moves are slated to execute)
|
||||
const laterMovePhase = globalScene.findPhase((phase) => phase instanceof MovePhase && phase.pokemon.id !== user?.id);
|
||||
return isNullOrUndefined(laterMovePhase);
|
||||
}, 1.3),
|
||||
new Ability(Abilities.ILLUSION, 5)
|
||||
// The Pokemon generate an illusion if it's available
|
||||
|
@ -30,6 +30,7 @@ import { MoveEffectPhase } from "#app/phases/move-effect-phase";
|
||||
import { PokemonHealPhase } from "#app/phases/pokemon-heal-phase";
|
||||
import { StatStageChangePhase } from "#app/phases/stat-stage-change-phase";
|
||||
import { CommonAnimPhase } from "#app/phases/common-anim-phase";
|
||||
import { MoveUseType } from "#enums/move-use-type";
|
||||
|
||||
export enum ArenaTagSide {
|
||||
BOTH,
|
||||
@ -892,7 +893,7 @@ export class DelayedAttackTag extends ArenaTag {
|
||||
|
||||
if (!ret) {
|
||||
globalScene.unshiftPhase(
|
||||
new MoveEffectPhase(this.sourceId!, [this.targetIndex], allMoves[this.sourceMove!], false, true),
|
||||
new MoveEffectPhase(this.sourceId!, [this.targetIndex], allMoves[this.sourceMove!], MoveUseType.REFLECTED), // Reflected ensures this doesn't check status, use PP or be copied
|
||||
); // TODO: are those bangs correct?
|
||||
}
|
||||
|
||||
|
@ -44,12 +44,17 @@ import { EFFECTIVE_STATS, getStatKey, Stat, type BattleStat, type EffectiveStat
|
||||
import { StatusEffect } from "#enums/status-effect";
|
||||
import { WeatherType } from "#enums/weather-type";
|
||||
import { isNullOrUndefined } from "#app/utils/common";
|
||||
import { MoveUseType } from "#enums/move-use-type";
|
||||
|
||||
export enum BattlerTagLapseType {
|
||||
FAINT,
|
||||
MOVE,
|
||||
PRE_MOVE,
|
||||
AFTER_MOVE,
|
||||
/**
|
||||
* TODO: Stop treating this like a catch-all "semi invulnerability" tag;
|
||||
* we may want to use this for other stuff later
|
||||
*/
|
||||
MOVE_EFFECT,
|
||||
TURN_END,
|
||||
HIT,
|
||||
@ -60,7 +65,7 @@ export enum BattlerTagLapseType {
|
||||
|
||||
export class BattlerTag {
|
||||
public tagType: BattlerTagType;
|
||||
public lapseTypes: BattlerTagLapseType[];
|
||||
public lapseTypes: BattlerTagLapseType[]; // TODO: Make this a set
|
||||
public turnCount: number;
|
||||
public sourceMove: Moves;
|
||||
public sourceId?: number;
|
||||
@ -93,7 +98,7 @@ export class BattlerTag {
|
||||
onOverlap(_pokemon: Pokemon): void {}
|
||||
|
||||
/**
|
||||
* Tick down this {@linkcode BattlerTag}'s duration.
|
||||
* Trigger and tick down this {@linkcode BattlerTag}'s duration.
|
||||
* @returns `true` if the tag should be kept (`turnCount` > 0`)
|
||||
*/
|
||||
lapse(_pokemon: Pokemon, _lapseType: BattlerTagLapseType): boolean {
|
||||
@ -181,21 +186,21 @@ export abstract class MoveRestrictionBattlerTag extends BattlerTag {
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets whether this tag is restricting a move.
|
||||
* Check if this tag is currently restricting a move's use.
|
||||
*
|
||||
* @param move - {@linkcode Moves} ID to check restriction for.
|
||||
* @param user - The {@linkcode Pokemon} involved
|
||||
* @returns `true` if the move is restricted by this tag, otherwise `false`.
|
||||
* @param move - The {@linkcode Moves | move ID} whose usability is being checked.
|
||||
* @param user - The {@linkcode Pokemon} using the move.
|
||||
* @returns Whether the given move is restricted by this tag.
|
||||
*/
|
||||
public abstract isMoveRestricted(move: Moves, user?: Pokemon): boolean;
|
||||
|
||||
/**
|
||||
* Checks if this tag is restricting a move based on a user's decisions during the target selection phase
|
||||
*
|
||||
* @param {Moves} _move {@linkcode Moves} move ID to check restriction for
|
||||
* @param {Pokemon} _user {@linkcode Pokemon} the user of the above move
|
||||
* @param {Pokemon} _target {@linkcode Pokemon} the target of the above move
|
||||
* @returns {boolean} `false` unless overridden by the child tag
|
||||
* Check if this tag is restricting a move during target selection.
|
||||
* Returns `false` by default unless overridden by a child class.
|
||||
* @param _move - The {@linkcode Moves | move ID} whose selectability is being checked
|
||||
* @param _user - The {@linkcode Pokemon} using the move.
|
||||
* @param _target - The {@linkcode Pokemon} being targeted
|
||||
* @returns Whether the given move should be unselectable when choosing targets.
|
||||
*/
|
||||
isMoveTargetRestricted(_move: Moves, _user: Pokemon, _target: Pokemon): boolean {
|
||||
return false;
|
||||
@ -302,14 +307,14 @@ export class DisabledTag extends MoveRestrictionBattlerTag {
|
||||
/**
|
||||
* @override
|
||||
*
|
||||
* Ensures that move history exists on `pokemon` and has a valid move. If so, sets the {@linkcode moveId} and shows a message.
|
||||
* Otherwise the move ID will not get assigned and this tag will get removed next turn.
|
||||
* Attempt to disable the target's last move by setting this tag's {@linkcode moveId}
|
||||
* and showing a message.
|
||||
*/
|
||||
override onAdd(pokemon: Pokemon): void {
|
||||
super.onAdd(pokemon);
|
||||
|
||||
const move = pokemon.getLastXMoves(-1).find(m => !m.virtual);
|
||||
if (isNullOrUndefined(move) || move.move === Moves.STRUGGLE || move.move === Moves.NONE) {
|
||||
// Disable fails against struggle or an empty move history, but we still need the nullish check
|
||||
// for cursed body
|
||||
const move = pokemon.getLastNonVirtualMove();
|
||||
if (isNullOrUndefined(move)) {
|
||||
return;
|
||||
}
|
||||
|
||||
@ -342,9 +347,10 @@ export class DisabledTag extends MoveRestrictionBattlerTag {
|
||||
|
||||
/**
|
||||
* @override
|
||||
* @param {Pokemon} pokemon {@linkcode Pokemon} attempting to use the restricted move
|
||||
* @param {Moves} move {@linkcode Moves} ID of the move being interrupted
|
||||
* @returns {string} text to display when the move is interrupted
|
||||
* Display the text that occurs when a move is interrupted via Disable.
|
||||
* @param pokemon - The {@linkcode Pokemon} attempting to use the restricted move
|
||||
* @param move - The {@linkcode Moves | move ID} of the move being interrupted
|
||||
* @returns The text to display when the given move is interrupted
|
||||
*/
|
||||
override interruptedText(pokemon: Pokemon, move: Moves): string {
|
||||
return i18next.t("battle:disableInterruptedMove", {
|
||||
@ -382,7 +388,7 @@ export class GorillaTacticsTag extends MoveRestrictionBattlerTag {
|
||||
* @returns `true` if the pokemon has a valid move and no existing {@linkcode GorillaTacticsTag}; `false` otherwise
|
||||
*/
|
||||
override canAdd(pokemon: Pokemon): boolean {
|
||||
return this.getLastValidMove(pokemon) !== undefined && !pokemon.getTag(GorillaTacticsTag);
|
||||
return !isNullOrUndefined(pokemon.getLastNonVirtualMove(true)) && !pokemon.getTag(GorillaTacticsTag);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -392,13 +398,12 @@ export class GorillaTacticsTag extends MoveRestrictionBattlerTag {
|
||||
* @param {Pokemon} pokemon the {@linkcode Pokemon} to add the tag to
|
||||
*/
|
||||
override onAdd(pokemon: Pokemon): void {
|
||||
const lastValidMove = this.getLastValidMove(pokemon);
|
||||
|
||||
if (!lastValidMove) {
|
||||
const lastValidMove = pokemon.getLastNonVirtualMove(true); // TODO: Check if should work with struggle or not
|
||||
if (isNullOrUndefined(lastValidMove)) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.moveId = lastValidMove;
|
||||
this.moveId = lastValidMove.move;
|
||||
pokemon.setStat(Stat.ATK, pokemon.getStat(Stat.ATK, false) * 1.5, false);
|
||||
}
|
||||
|
||||
@ -425,17 +430,6 @@ export class GorillaTacticsTag extends MoveRestrictionBattlerTag {
|
||||
pokemonName: getPokemonNameWithAffix(pokemon),
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the last valid move from the pokemon's move history.
|
||||
* @param {Pokemon} pokemon {@linkcode Pokemon} to get the last valid move from
|
||||
* @returns {Moves | undefined} the last valid move from the pokemon's move history
|
||||
*/
|
||||
getLastValidMove(pokemon: Pokemon): Moves | undefined {
|
||||
const move = pokemon.getLastXMoves().find(m => m.move !== Moves.NONE && m.move !== Moves.STRUGGLE && !m.virtual);
|
||||
|
||||
return move?.move;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@ -449,8 +443,8 @@ export class RechargingTag extends BattlerTag {
|
||||
onAdd(pokemon: Pokemon): void {
|
||||
super.onAdd(pokemon);
|
||||
|
||||
// Queue a placeholder move for the Pokemon to "use" next turn
|
||||
pokemon.getMoveQueue().push({ move: Moves.NONE, targets: [] });
|
||||
// Queue a placeholder move for the Pokemon to "use" next turn.
|
||||
pokemon.pushMoveQueue({ move: Moves.NONE, targets: [], useType: MoveUseType.NORMAL });
|
||||
}
|
||||
|
||||
/** Cancels the source's move this turn and queues a "__ must recharge!" message */
|
||||
@ -649,7 +643,7 @@ class NoRetreatTag extends TrappedTag {
|
||||
*/
|
||||
export class FlinchedTag extends BattlerTag {
|
||||
constructor(sourceMove: Moves) {
|
||||
super(BattlerTagType.FLINCHED, [BattlerTagLapseType.PRE_MOVE, BattlerTagLapseType.TURN_END], 0, sourceMove);
|
||||
super(BattlerTagType.FLINCHED, [BattlerTagLapseType.PRE_MOVE, BattlerTagLapseType.TURN_END], 1, sourceMove);
|
||||
}
|
||||
|
||||
onAdd(pokemon: Pokemon): void {
|
||||
@ -699,6 +693,7 @@ export class InterruptedTag extends BattlerTag {
|
||||
move: Moves.NONE,
|
||||
result: MoveResult.OTHER,
|
||||
targets: [],
|
||||
useType: MoveUseType.NORMAL,
|
||||
});
|
||||
}
|
||||
|
||||
@ -1018,42 +1013,45 @@ export class PowderTag extends BattlerTag {
|
||||
}
|
||||
|
||||
/**
|
||||
* Applies Powder's effects before the tag owner uses a Fire-type move.
|
||||
* Also causes the tag to expire at the end of turn.
|
||||
* @param pokemon {@linkcode Pokemon} the owner of this tag
|
||||
* @param lapseType {@linkcode BattlerTagLapseType} the type of lapse functionality to carry out
|
||||
* @returns `true` if the tag should not expire after this lapse; `false` otherwise.
|
||||
* Applies Powder's effects before the tag owner uses a Fire-type move, damaging and canceling its action.
|
||||
* Lasts until the end of the turn.
|
||||
* @param pokemon - The {@linkcode Pokemon} with this tag.
|
||||
* @param lapseType - The {@linkcode BattlerTagLapseType} dictating how this tag is being activated
|
||||
* @returns `true` if the tag should remain active.
|
||||
*/
|
||||
lapse(pokemon: Pokemon, lapseType: BattlerTagLapseType): boolean {
|
||||
if (lapseType === BattlerTagLapseType.PRE_MOVE) {
|
||||
const movePhase = globalScene.getCurrentPhase();
|
||||
if (movePhase instanceof MovePhase) {
|
||||
const move = movePhase.move.getMove();
|
||||
const weather = globalScene.arena.weather;
|
||||
if (
|
||||
pokemon.getMoveType(move) === PokemonType.FIRE &&
|
||||
!(weather && weather.weatherType === WeatherType.HEAVY_RAIN && !weather.isEffectSuppressed())
|
||||
) {
|
||||
movePhase.fail();
|
||||
movePhase.showMoveText();
|
||||
if (lapseType !== BattlerTagLapseType.PRE_MOVE) {
|
||||
return super.lapse(pokemon, lapseType);
|
||||
}
|
||||
|
||||
globalScene.unshiftPhase(
|
||||
new CommonAnimPhase(pokemon.getBattlerIndex(), pokemon.getBattlerIndex(), CommonAnim.POWDER),
|
||||
);
|
||||
|
||||
const cancelDamage = new BooleanHolder(false);
|
||||
applyAbAttrs(BlockNonDirectDamageAbAttr, pokemon, cancelDamage);
|
||||
if (!cancelDamage.value) {
|
||||
pokemon.damageAndUpdate(Math.floor(pokemon.getMaxHp() / 4), { result: HitResult.INDIRECT });
|
||||
}
|
||||
|
||||
// "When the flame touched the powder\non the Pokémon, it exploded!"
|
||||
globalScene.queueMessage(i18next.t("battlerTags:powderLapse", { moveName: move.name }));
|
||||
}
|
||||
}
|
||||
const movePhase = globalScene.getCurrentPhase() as MovePhase;
|
||||
const move = movePhase.move.getMove();
|
||||
const weather = globalScene.arena.weather;
|
||||
if (
|
||||
pokemon.getMoveType(move) !== PokemonType.FIRE ||
|
||||
(weather?.weatherType === WeatherType.HEAVY_RAIN && !weather.isEffectSuppressed()) // Heavy rain takes priority over powder
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
return super.lapse(pokemon, lapseType);
|
||||
|
||||
// Disable the target's fire type move and damage it (subject to Magic Guard)
|
||||
movePhase.showMoveText();
|
||||
movePhase.fail();
|
||||
|
||||
globalScene.unshiftPhase(
|
||||
new CommonAnimPhase(pokemon.getBattlerIndex(), pokemon.getBattlerIndex(), CommonAnim.POWDER),
|
||||
);
|
||||
|
||||
const cancelDamage = new BooleanHolder(false);
|
||||
applyAbAttrs(BlockNonDirectDamageAbAttr, pokemon, cancelDamage);
|
||||
if (!cancelDamage.value) {
|
||||
pokemon.damageAndUpdate(Math.floor(pokemon.getMaxHp() / 4), { result: HitResult.INDIRECT });
|
||||
}
|
||||
|
||||
// "When the flame touched the powder\non the Pokémon, it exploded!"
|
||||
globalScene.queueMessage(i18next.t("battlerTags:powderLapse", { moveName: move.name }));
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
@ -1128,6 +1126,7 @@ export class FrenzyTag extends BattlerTag {
|
||||
* Applies the effects of {@linkcode Moves.ENCORE} onto the target Pokemon.
|
||||
* Encore forces the target Pokemon to use its most-recent move for 3 turns.
|
||||
*/
|
||||
// TODO: Refactor and fix the bugs involving struggle and lock ons
|
||||
export class EncoreTag extends MoveRestrictionBattlerTag {
|
||||
public moveId: Moves;
|
||||
|
||||
@ -1147,29 +1146,26 @@ export class EncoreTag extends MoveRestrictionBattlerTag {
|
||||
}
|
||||
|
||||
canAdd(pokemon: Pokemon): boolean {
|
||||
const lastMoves = pokemon.getLastXMoves(1);
|
||||
if (!lastMoves.length) {
|
||||
const lastMove = pokemon.getLastNonVirtualMove(false);
|
||||
if (!lastMove) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const repeatableMove = lastMoves[0];
|
||||
const unEncoreableMoves = new Set<Moves>([
|
||||
Moves.MIMIC,
|
||||
Moves.MIRROR_MOVE,
|
||||
Moves.TRANSFORM,
|
||||
Moves.STRUGGLE,
|
||||
Moves.SKETCH,
|
||||
Moves.SLEEP_TALK,
|
||||
Moves.ENCORE,
|
||||
]);
|
||||
|
||||
if (!repeatableMove.move || repeatableMove.virtual) {
|
||||
if (unEncoreableMoves.has(lastMove.move)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
switch (repeatableMove.move) {
|
||||
case Moves.MIMIC:
|
||||
case Moves.MIRROR_MOVE:
|
||||
case Moves.TRANSFORM:
|
||||
case Moves.STRUGGLE:
|
||||
case Moves.SKETCH:
|
||||
case Moves.SLEEP_TALK:
|
||||
case Moves.ENCORE:
|
||||
return false;
|
||||
}
|
||||
|
||||
this.moveId = repeatableMove.move;
|
||||
this.moveId = lastMove.move;
|
||||
|
||||
return true;
|
||||
}
|
||||
@ -1187,10 +1183,11 @@ export class EncoreTag extends MoveRestrictionBattlerTag {
|
||||
if (movePhase) {
|
||||
const movesetMove = pokemon.getMoveset().find(m => m.moveId === this.moveId);
|
||||
if (movesetMove) {
|
||||
// TODO: Check encore + calling move interactions and change to `pokemon.getLastNonVirtualMove()` if needed
|
||||
const lastMove = pokemon.getLastXMoves(1)[0];
|
||||
globalScene.tryReplacePhase(
|
||||
m => m instanceof MovePhase && m.pokemon === pokemon,
|
||||
new MovePhase(pokemon, lastMove.targets ?? [], movesetMove),
|
||||
new MovePhase(pokemon, lastMove.targets ?? [], movesetMove, MoveUseType.NORMAL),
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -1487,10 +1484,6 @@ export class WrapTag extends DamagingTrapTag {
|
||||
}
|
||||
|
||||
export abstract class VortexTrapTag extends DamagingTrapTag {
|
||||
constructor(tagType: BattlerTagType, commonAnim: CommonAnim, turnCount: number, sourceMove: Moves, sourceId: number) {
|
||||
super(tagType, commonAnim, turnCount, sourceMove, sourceId);
|
||||
}
|
||||
|
||||
getTrapMessage(pokemon: Pokemon): string {
|
||||
return i18next.t("battlerTags:vortexOnTrap", {
|
||||
pokemonNameWithAffix: getPokemonNameWithAffix(pokemon),
|
||||
@ -1909,13 +1902,15 @@ export class TruantTag extends AbilityBattlerTag {
|
||||
|
||||
lapse(pokemon: Pokemon, lapseType: BattlerTagLapseType): boolean {
|
||||
if (!pokemon.hasAbility(Abilities.TRUANT)) {
|
||||
// remove tag if mon lacks ability
|
||||
return super.lapse(pokemon, lapseType);
|
||||
}
|
||||
const passive = pokemon.getAbility().id !== Abilities.TRUANT;
|
||||
|
||||
const lastMove = pokemon.getLastXMoves().find(() => true);
|
||||
const lastMove = pokemon.getLastXMoves()[0];
|
||||
|
||||
if (lastMove && lastMove.move !== Moves.NONE) {
|
||||
// ignore if just slacked off OR first turn of battle
|
||||
const passive = pokemon.getAbility().id !== Abilities.TRUANT;
|
||||
(globalScene.getCurrentPhase() as MovePhase).cancel();
|
||||
// TODO: Ability displays should be handled by the ability
|
||||
globalScene.queueAbilityDisplay(pokemon, passive, true);
|
||||
@ -2730,7 +2725,7 @@ export class ExposedTag extends BattlerTag {
|
||||
|
||||
/**
|
||||
* Tag that prevents HP recovery from held items and move effects. It also blocks the usage of recovery moves.
|
||||
* Applied by moves: {@linkcode Moves.HEAL_BLOCK | Heal Block (5 turns)}, {@linkcode Moves.PSYCHIC_NOISE | Psychic Noise (2 turns)}
|
||||
* Applied by moves: {@linkcode Moves.HEAL_BLOCK | Heal Block (5 turns)}, {@linkcode Moves.PSYCHIC_NOISE | Psychic Noise (2 turns)}
|
||||
*
|
||||
* @extends MoveRestrictionBattlerTag
|
||||
*/
|
||||
@ -2756,10 +2751,7 @@ export class HealBlockTag extends MoveRestrictionBattlerTag {
|
||||
* @returns `true` if the move has a TRIAGE_MOVE flag and is a status move
|
||||
*/
|
||||
override isMoveRestricted(move: Moves): boolean {
|
||||
if (allMoves[move].hasFlag(MoveFlags.TRIAGE_MOVE) && allMoves[move].category === MoveCategory.STATUS) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
return allMoves[move].hasFlag(MoveFlags.TRIAGE_MOVE) && allMoves[move].category === MoveCategory.STATUS;
|
||||
}
|
||||
|
||||
/**
|
||||
@ -3433,7 +3425,8 @@ export class PsychoShiftTag extends BattlerTag {
|
||||
}
|
||||
|
||||
/**
|
||||
* Tag associated with the move Magic Coat.
|
||||
* Tag associated with {@linkcode Moves.MAGIC_COAT | Magic Coat} that reflects certain status moves directed at the user.
|
||||
* TODO: Move Reflection code out of `move-effect-phase` and into here
|
||||
*/
|
||||
export class MagicCoatTag extends BattlerTag {
|
||||
constructor() {
|
||||
|
@ -1,6 +1,6 @@
|
||||
import { Moves } from "#enums/moves";
|
||||
|
||||
/** Set of moves that cannot be called by {@linkcode Moves.METRONOME Metronome} */
|
||||
/** Set of moves that cannot be called by {@linkcode Moves.METRONOME | Metronome} */
|
||||
export const invalidMetronomeMoves: ReadonlySet<Moves> = new Set([
|
||||
Moves.AFTER_YOU,
|
||||
Moves.ASSIST,
|
||||
@ -255,3 +255,17 @@ export const noAbilityTypeOverrideMoves: ReadonlySet<Moves> = new Set([
|
||||
Moves.TECHNO_BLAST,
|
||||
Moves.HIDDEN_POWER,
|
||||
]);
|
||||
|
||||
/** Set of all moves that cannot be copied by {@linkcode Moves.SKETCH}. */
|
||||
export const invalidSketchMoves: ReadonlySet<Moves> = new Set([
|
||||
Moves.NONE,
|
||||
Moves.CHATTER,
|
||||
Moves.MIRROR_MOVE,
|
||||
Moves.SLEEP_TALK,
|
||||
Moves.STRUGGLE,
|
||||
Moves.SKETCH,
|
||||
Moves.REVIVAL_BLESSING,
|
||||
Moves.TERA_STARSTORM,
|
||||
Moves.BREAKNECK_BLITZ__PHYSICAL,
|
||||
Moves.BREAKNECK_BLITZ__SPECIAL,
|
||||
]);
|
||||
|
@ -121,8 +121,9 @@ import { MoveTarget } from "#enums/MoveTarget";
|
||||
import { MoveFlags } from "#enums/MoveFlags";
|
||||
import { MoveEffectTrigger } from "#enums/MoveEffectTrigger";
|
||||
import { MultiHitType } from "#enums/MultiHitType";
|
||||
import { invalidAssistMoves, invalidCopycatMoves, invalidMetronomeMoves, invalidMirrorMoveMoves, invalidSleepTalkMoves } from "./invalid-moves";
|
||||
import { invalidAssistMoves, invalidCopycatMoves, invalidMetronomeMoves, invalidMirrorMoveMoves, invalidSleepTalkMoves, invalidSketchMoves } from "./invalid-moves";
|
||||
import { SelectBiomePhase } from "#app/phases/select-biome-phase";
|
||||
import { MoveUseType } from "#enums/move-use-type";
|
||||
|
||||
type MoveConditionFunc = (user: Pokemon, target: Pokemon, move: Move) => boolean;
|
||||
type UserMoveConditionFunc = (user: Pokemon, move: Move) => boolean;
|
||||
@ -701,10 +702,10 @@ export default class Move implements Localizable {
|
||||
|
||||
/**
|
||||
* Sees if a move has a custom failure text (by looking at each {@linkcode MoveAttr} of this move)
|
||||
* @param user {@linkcode Pokemon} using the move
|
||||
* @param target {@linkcode Pokemon} target of the move
|
||||
* @param move {@linkcode Move} with this attribute
|
||||
* @returns string of the custom failure text, or `null` if it uses the default text ("But it failed!")
|
||||
* @param user - The {@linkcode Pokemon} using this move
|
||||
* @param target - The {@linkcode Pokemon} targeted by this move
|
||||
* @param move - The {@linkcode Move} being used
|
||||
* @returns A string containing the custom failure text, or `undefined` if no custom text exists.
|
||||
*/
|
||||
getFailedText(user: Pokemon, target: Pokemon, move: Move): string | undefined {
|
||||
for (const attr of this.attrs) {
|
||||
@ -1185,7 +1186,7 @@ export class MoveEffectAttr extends MoveAttr {
|
||||
*/
|
||||
protected options?: MoveEffectAttrOptions;
|
||||
|
||||
constructor(selfTarget?: boolean, options?: MoveEffectAttrOptions) {
|
||||
constructor(selfTarget?: boolean, options?: MoveEffectAttrOptions) {
|
||||
super(selfTarget);
|
||||
this.options = options;
|
||||
}
|
||||
@ -1372,8 +1373,8 @@ export class PreMoveMessageAttr extends MoveAttr {
|
||||
|
||||
/**
|
||||
* Attribute for moves that can be conditionally interrupted to be considered to
|
||||
* have failed before their "useMove" message is displayed. Currently used by
|
||||
* Focus Punch.
|
||||
* have failed before their "useMove" message is displayed.
|
||||
* Currently used by {@linkcode Moves.FOCUS_PUNCH}.
|
||||
* @extends MoveAttr
|
||||
*/
|
||||
export class PreUseInterruptAttr extends MoveAttr {
|
||||
@ -1382,38 +1383,40 @@ export class PreUseInterruptAttr extends MoveAttr {
|
||||
protected conditionFunc: MoveConditionFunc;
|
||||
|
||||
/**
|
||||
* Create a new MoveInterruptedMessageAttr.
|
||||
* @param message The message to display when the move is interrupted, or a function that formats the message based on the user, target, and move.
|
||||
* Create a new PreUseInterruptAttr.
|
||||
* @param message - Custom failure text to display when the move is interrupted, either as a string or a function producing one.
|
||||
* If ommitted, will display the default failure text upon cancellation.
|
||||
* @param conditionFunc - A {@linkcode MoveConditionFunc} that returns `true` if the move should be canceled.
|
||||
*/
|
||||
constructor(message?: string | ((user: Pokemon, target: Pokemon, move: Move) => string), conditionFunc?: MoveConditionFunc) {
|
||||
constructor(message: string | undefined | ((user: Pokemon, target: Pokemon, move: Move) => string), conditionFunc: MoveConditionFunc) {
|
||||
super();
|
||||
this.message = message;
|
||||
this.conditionFunc = conditionFunc ?? (() => true);
|
||||
this.conditionFunc = conditionFunc;
|
||||
}
|
||||
|
||||
/**
|
||||
* Message to display when a move is interrupted.
|
||||
* @param user {@linkcode Pokemon} using the move
|
||||
* @param target {@linkcode Pokemon} target of the move
|
||||
* @param move {@linkcode Move} with this attribute
|
||||
* Conditionally cancel this pokemon's current move.
|
||||
* @param user - The {@linkcode Pokemon} using this move
|
||||
* @param target - The {@linkcode Pokemon} targeted by this move
|
||||
* @param move - The {@linkcode Move} being used
|
||||
* @returns `true` if the move should be cancelled.
|
||||
*/
|
||||
override apply(user: Pokemon, target: Pokemon, move: Move): boolean {
|
||||
return this.conditionFunc(user, target, move);
|
||||
}
|
||||
|
||||
/**
|
||||
* Message to display when a move is interrupted.
|
||||
* @param user {@linkcode Pokemon} using the move
|
||||
* @param target {@linkcode Pokemon} target of the move
|
||||
* @param move {@linkcode Move} with this attribute
|
||||
* Obtain the text displayed upon this move's interruption.
|
||||
* @param user - The {@linkcode Pokemon} using this move
|
||||
* @param target - The {@linkcode Pokemon} targeted by this move
|
||||
* @param move - The {@linkcode Move} being used
|
||||
* @returns A string containing the custom failure text, or `undefined` if no custom text exists.
|
||||
*/
|
||||
override getFailedText(user: Pokemon, target: Pokemon, move: Move): string | undefined {
|
||||
if (this.message && this.conditionFunc(user, target, move)) {
|
||||
const message =
|
||||
typeof this.message === "string"
|
||||
? (this.message as string)
|
||||
: this.message(user, target, move);
|
||||
return message;
|
||||
if (this.conditionFunc(user, target, move)) {
|
||||
return typeof this.message !== "function"
|
||||
? this.message
|
||||
: this.message(user, target, move);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1513,7 +1516,7 @@ export class TargetHalfHpDamageAttr extends FixedDamageAttr {
|
||||
case 0:
|
||||
// first hit of move; update initialHp tracker
|
||||
this.initialHp = target.hp;
|
||||
default:
|
||||
default:
|
||||
// multi lens added hit; use initialHp tracker to ensure correct damage
|
||||
(args[0] as NumberHolder).value = toDmgValue(this.initialHp / 2);
|
||||
return true;
|
||||
@ -3056,7 +3059,17 @@ export class DelayedAttackAttr extends OverrideMoveEffectAttr {
|
||||
this.chargeText = chargeText;
|
||||
}
|
||||
|
||||
apply(user: Pokemon, target: Pokemon, move: Move, args: any[]): boolean {
|
||||
/**
|
||||
* Apply the delayed attack, either setting it up or triggering the attack.
|
||||
* @param user - The {@linkcode Pokemon} using the move
|
||||
* @param target - The {@linkcode Pokemon} being targeted
|
||||
* @param move - The {@linkcode Move} being used
|
||||
* @param args -
|
||||
* `[0]` - {@linkcode BooleanHolder} containing whether the move was overriden
|
||||
* `[1]` - Whether the move is supposed to set up a delayed attack (`true`) or activate (`false`)
|
||||
* @returns always `true`
|
||||
*/
|
||||
apply(user: Pokemon, target: Pokemon, move: Move, args: [BooleanHolder, boolean]): boolean {
|
||||
// Edge case for the move applied on a pokemon that has fainted
|
||||
if (!target) {
|
||||
return true;
|
||||
@ -3069,7 +3082,7 @@ export class DelayedAttackAttr extends OverrideMoveEffectAttr {
|
||||
overridden.value = true;
|
||||
globalScene.unshiftPhase(new MoveAnimPhase(new MoveChargeAnim(this.chargeAnim, move.id, user)));
|
||||
globalScene.queueMessage(this.chargeText.replace("{TARGET}", getPokemonNameWithAffix(target)).replace("{USER}", getPokemonNameWithAffix(user)));
|
||||
user.pushMoveHistory({ move: move.id, targets: [ target.getBattlerIndex() ], result: MoveResult.OTHER });
|
||||
user.pushMoveHistory({ move: move.id, targets: [ target.getBattlerIndex() ], result: MoveResult.OTHER, useType: MoveUseType.NORMAL });
|
||||
const side = target.isPlayer() ? ArenaTagSide.PLAYER : ArenaTagSide.ENEMY;
|
||||
globalScene.arena.addTag(this.tagType, 3, move.id, user.id, side, false, target.getBattlerIndex());
|
||||
} else {
|
||||
@ -3092,9 +3105,9 @@ export class AwaitCombinedPledgeAttr extends OverrideMoveEffectAttr {
|
||||
/**
|
||||
* If the user's ally is set to use a different move with this attribute,
|
||||
* defer this move's effects for a combined move on the ally's turn.
|
||||
* @param user the {@linkcode Pokemon} using this move
|
||||
* @param target n/a
|
||||
* @param move the {@linkcode Move} being used
|
||||
* @param user - The {@linkcode Pokemon} using the move
|
||||
* @param target - Unused
|
||||
* @param move - The {@linkcode Move} being used
|
||||
* @param args
|
||||
* - [0] a {@linkcode BooleanHolder} indicating whether the move's base
|
||||
* effects should be overridden this turn.
|
||||
@ -3683,7 +3696,7 @@ export class LessPPMorePowerAttr extends VariablePowerAttr {
|
||||
const ppMax = move.pp;
|
||||
const ppUsed = user.moveset.find((m) => m.moveId === move.id)?.ppUsed ?? 0;
|
||||
|
||||
let ppRemains = ppMax - ppUsed;
|
||||
let ppRemains = ppMax - ppUsed;
|
||||
/** Reduce to 0 to avoid negative numbers if user has 1PP before attack and target has Ability.PRESSURE */
|
||||
if (ppRemains < 0) {
|
||||
ppRemains = 0;
|
||||
@ -3801,26 +3814,30 @@ export class DoublePowerChanceAttr extends VariablePowerAttr {
|
||||
|
||||
export abstract class ConsecutiveUsePowerMultiplierAttr extends MovePowerMultiplierAttr {
|
||||
constructor(limit: number, resetOnFail: boolean, resetOnLimit?: boolean, ...comboMoves: Moves[]) {
|
||||
super((user: Pokemon, target: Pokemon, move: Move): number => {
|
||||
const moveHistory = user.getLastXMoves(limit + 1).slice(1);
|
||||
super((user: Pokemon, _target: Pokemon, move: Move): number => {
|
||||
const moveHistory = user.getLastXMoves(-1).slice(1, limit+1); // don't count the first history entry (ie the current move)
|
||||
|
||||
let count = 0;
|
||||
let turnMove: TurnMove | undefined;
|
||||
let count = 1;
|
||||
|
||||
while (
|
||||
(
|
||||
(turnMove = moveHistory.shift())?.move === move.id
|
||||
|| (comboMoves.length && comboMoves.includes(turnMove?.move ?? Moves.NONE))
|
||||
)
|
||||
&& (!resetOnFail || turnMove?.result === MoveResult.SUCCESS)
|
||||
) {
|
||||
if (count < (limit - 1)) {
|
||||
count++;
|
||||
} else if (resetOnLimit) {
|
||||
count = 0;
|
||||
} else {
|
||||
// TODO: Confirm whether mirror moving an echoed voice counts for and/or resets a boost
|
||||
for (const tm of moveHistory) {
|
||||
if (
|
||||
!(tm.move === move.id || comboMoves.includes(tm.move))
|
||||
|| (resetOnFail && tm.result !== MoveResult.SUCCESS)
|
||||
) {
|
||||
break;
|
||||
}
|
||||
|
||||
if (count < limit - 1) {
|
||||
count++;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (resetOnLimit) {
|
||||
count = 0;
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
return this.getMultiplier(count);
|
||||
@ -4000,7 +4017,7 @@ export class HpPowerAttr extends VariablePowerAttr {
|
||||
|
||||
/**
|
||||
* Attribute used for moves whose base power scales with the opponent's HP
|
||||
* Used for Crush Grip, Wring Out, and Hard Press
|
||||
* Used for {@linkcode Moves.CRUSH_GRIP}, {@linkcode Moves.WRING_OUT}, and {@linkcode Moves.HARD_PRESS}
|
||||
* maxBasePower 100 for Hard Press, 120 for others
|
||||
*/
|
||||
export class OpponentHighHpPowerAttr extends VariablePowerAttr {
|
||||
@ -4026,15 +4043,27 @@ export class OpponentHighHpPowerAttr extends VariablePowerAttr {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Attribute to double this move's power if the target hasn't acted yet in the current turn.
|
||||
* Used by {@linkcode Moves.BOLT_BEAK} and {@linkcode Moves.FISHIOUS_REND}
|
||||
*/
|
||||
export class FirstAttackDoublePowerAttr extends VariablePowerAttr {
|
||||
apply(user: Pokemon, target: Pokemon, move: Move, args: any[]): boolean {
|
||||
console.log(target.getLastXMoves(1), globalScene.currentBattle.turn);
|
||||
if (!target.getLastXMoves(1).find(m => m.turn === globalScene.currentBattle.turn)) {
|
||||
(args[0] as NumberHolder).value *= 2;
|
||||
return true;
|
||||
/**
|
||||
* Double this move's power if the user is acting before the target.
|
||||
* @param user - Unused
|
||||
* @param target - The {@linkcode Pokemon} being targeted by this move
|
||||
* @param move - Unused
|
||||
* @param args `[0]` - A {@linkcode NumberHolder} containing move base power
|
||||
* @returns Whether the attribute was successfully applied
|
||||
*/
|
||||
apply(_user: Pokemon, target: Pokemon, move: Move, args: [NumberHolder]): boolean {
|
||||
const lastMove: TurnMove | undefined = target.getLastXMoves()[0]; // undefined needed as array might be empty
|
||||
if (lastMove?.turn === globalScene.currentBattle.turn) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return false;
|
||||
args[0].value *= 2;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
@ -4304,6 +4333,7 @@ const hasStockpileStacksCondition: MoveConditionFunc = (user) => {
|
||||
return !!hasStockpilingTag && hasStockpilingTag.stockpiledCount > 0;
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Attribute used for multi-hit moves that increase power in increments of the
|
||||
* move's base power for each hit, namely Triple Kick and Triple Axel.
|
||||
@ -5403,13 +5433,22 @@ export class FrenzyAttr extends MoveEffectAttr {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!user.getTag(BattlerTagType.FRENZY) && !user.getMoveQueue().length) {
|
||||
const turnCount = user.randSeedIntRange(1, 2);
|
||||
new Array(turnCount).fill(null).map(() => user.getMoveQueue().push({ move: move.id, targets: [ target.getBattlerIndex() ], ignorePP: true }));
|
||||
// TODO: Disable if used via dancer
|
||||
|
||||
// TODO: Add support for moves that don't add the frenzy tag (Uproar, Rollout, etc.)
|
||||
|
||||
// If frenzy is not in effect and we don't have anything queued up,
|
||||
// add 1-2 extra instances of the move to the move queue.
|
||||
// If frenzy is already in effect, tick down the tag.
|
||||
if (!user.getTag(BattlerTagType.FRENZY) && user.getMoveQueue().length === 0) {
|
||||
const turnCount = user.randSeedIntRange(1, 2); // excludes initial use
|
||||
for (let i = 0; i < turnCount; i++) {
|
||||
user.pushMoveQueue({ move: move.id, targets: [ target.getBattlerIndex() ], useType: MoveUseType.IGNORE_PP });
|
||||
}
|
||||
user.addTag(BattlerTagType.FRENZY, turnCount, move.id, user.id);
|
||||
} else {
|
||||
applyMoveAttrs(AddBattlerTagAttr, user, target, move, args);
|
||||
user.lapseTag(BattlerTagType.FRENZY); // if FRENZY is already in effect (moveQueue.length > 0), lapse the tag
|
||||
user.lapseTag(BattlerTagType.FRENZY);
|
||||
}
|
||||
|
||||
return true;
|
||||
@ -5778,23 +5817,24 @@ export class ProtectAttr extends AddBattlerTagAttr {
|
||||
super(tagType, true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Condition to fail a protect usage based on random chance.
|
||||
* Chance starts at 100% and is thirded for each prior successful proctect usage.
|
||||
* @returns a function that fails the function if its proc chance roll fails
|
||||
*/
|
||||
getCondition(): MoveConditionFunc {
|
||||
return ((user, target, move): boolean => {
|
||||
let timesUsed = 0;
|
||||
const moveHistory = user.getLastXMoves();
|
||||
let turnMove: TurnMove | undefined;
|
||||
const lastMoves = user.getLastXMoves(-1)
|
||||
|
||||
while (moveHistory.length) {
|
||||
turnMove = moveHistory.shift();
|
||||
if (!allMoves[turnMove?.move ?? Moves.NONE].hasAttr(ProtectAttr) || turnMove?.result !== MoveResult.SUCCESS) {
|
||||
let threshold = 1;
|
||||
for (const tm of lastMoves) {
|
||||
if (!allMoves[tm.move].hasAttr(ProtectAttr) || tm.result !== MoveResult.SUCCESS) {
|
||||
break;
|
||||
}
|
||||
timesUsed++;
|
||||
threshold *= 3;
|
||||
}
|
||||
if (timesUsed) {
|
||||
return !user.randSeedInt(Math.pow(3, timesUsed));
|
||||
}
|
||||
return true;
|
||||
|
||||
return threshold === 1 || user.randSeedInt(threshold) === 0;
|
||||
});
|
||||
}
|
||||
}
|
||||
@ -6180,6 +6220,7 @@ export class RevivalBlessingAttr extends MoveEffectAttr {
|
||||
globalScene.tryRemovePhase((phase: SwitchSummonPhase) => phase instanceof SwitchSummonPhase && phase.getPokemon() === pokemon);
|
||||
// If the pokemon being revived was alive earlier in the turn, cancel its move
|
||||
// (revived pokemon can't move in the turn they're brought back)
|
||||
// TODO: might make sense to move this to `FaintPhase` rather than handling it in the move
|
||||
globalScene.findPhase((phase: MovePhase) => phase.pokemon === pokemon)?.cancel();
|
||||
if (user.fieldPosition === FieldPosition.CENTER) {
|
||||
user.setFieldPosition(FieldPosition.LEFT);
|
||||
@ -6715,20 +6756,23 @@ export class FirstMoveTypeAttr extends MoveEffectAttr {
|
||||
class CallMoveAttr extends OverrideMoveEffectAttr {
|
||||
protected invalidMoves: ReadonlySet<Moves>;
|
||||
protected hasTarget: boolean;
|
||||
|
||||
apply(user: Pokemon, target: Pokemon, move: Move, args: any[]): boolean {
|
||||
// Get eligible targets for move, failing if we can't target anything
|
||||
const replaceMoveTarget = move.moveTarget === MoveTarget.NEAR_OTHER ? MoveTarget.NEAR_ENEMY : undefined;
|
||||
const moveTargets = getMoveTargets(user, move.id, replaceMoveTarget);
|
||||
if (moveTargets.targets.length === 0) {
|
||||
globalScene.queueMessage(i18next.t("battle:attackFailed"));
|
||||
console.log("CallMoveAttr failed due to no targets.");
|
||||
return false;
|
||||
}
|
||||
|
||||
// Spread moves and ones with only 1 valid target will use their normal targeting.
|
||||
// If not, target the Mirror Move recipient or else a random enemy in our target list
|
||||
const targets = moveTargets.multiple || moveTargets.targets.length === 1
|
||||
? moveTargets.targets
|
||||
: [ this.hasTarget ? target.getBattlerIndex() : moveTargets.targets[user.randSeedInt(moveTargets.targets.length)] ]; // account for Mirror Move having a target already
|
||||
user.getMoveQueue().push({ move: move.id, targets: targets, virtual: true, ignorePP: true });
|
||||
globalScene.unshiftPhase(new LoadMoveAnimPhase(move.id));
|
||||
globalScene.unshiftPhase(new MovePhase(user, targets, new PokemonMove(move.id, 0, 0, true), true, true));
|
||||
globalScene.unshiftPhase(new MovePhase(user, targets, new PokemonMove(move.id), MoveUseType.FOLLOW_UP));
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@ -6825,7 +6869,7 @@ export class RandomMovesetMoveAttr extends CallMoveAttr {
|
||||
|
||||
export class NaturePowerAttr extends OverrideMoveEffectAttr {
|
||||
apply(user: Pokemon, target: Pokemon, move: Move, args: any[]): boolean {
|
||||
let moveId;
|
||||
let moveId: Moves;
|
||||
switch (globalScene.arena.getTerrainType()) {
|
||||
// this allows terrains to 'override' the biome move
|
||||
case TerrainType.NONE:
|
||||
@ -6950,14 +6994,14 @@ export class NaturePowerAttr extends OverrideMoveEffectAttr {
|
||||
moveId = Moves.PSYCHIC;
|
||||
break;
|
||||
default:
|
||||
// Just in case there's no match
|
||||
// Just in case there's no match
|
||||
moveId = Moves.TRI_ATTACK;
|
||||
break;
|
||||
}
|
||||
|
||||
user.getMoveQueue().push({ move: moveId, targets: [ target.getBattlerIndex() ], ignorePP: true });
|
||||
// Load the move's animation if we didn't already and unshift a new usage phase
|
||||
globalScene.unshiftPhase(new LoadMoveAnimPhase(moveId));
|
||||
globalScene.unshiftPhase(new MovePhase(user, [ target.getBattlerIndex() ], new PokemonMove(moveId, 0, 0, true), true));
|
||||
globalScene.unshiftPhase(new MovePhase(user, [ target.getBattlerIndex() ], new PokemonMove(moveId), MoveUseType.FOLLOW_UP));
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@ -6976,27 +7020,23 @@ export class CopyMoveAttr extends CallMoveAttr {
|
||||
this.invalidMoves = invalidMoves;
|
||||
}
|
||||
|
||||
apply(user: Pokemon, target: Pokemon, move: Move, args: any[]): boolean {
|
||||
apply(user: Pokemon, target: Pokemon, _move: Move, args: any[]): boolean {
|
||||
this.hasTarget = this.mirrorMove;
|
||||
const lastMove = this.mirrorMove ? target.getLastXMoves()[0].move : globalScene.currentBattle.lastMove;
|
||||
// TODO: Confirm whether Mirror Move and co. can copy struggle
|
||||
const lastMove = this.mirrorMove ? target.getLastNonVirtualMove(false, false)!.move : globalScene.currentBattle.lastMove;
|
||||
return super.apply(user, target, allMoves[lastMove], args);
|
||||
}
|
||||
|
||||
getCondition(): MoveConditionFunc {
|
||||
return (user, target, move) => {
|
||||
if (this.mirrorMove) {
|
||||
const lastMove = target.getLastXMoves()[0]?.move;
|
||||
return !!lastMove && !this.invalidMoves.has(lastMove);
|
||||
} else {
|
||||
const lastMove = globalScene.currentBattle.lastMove;
|
||||
return lastMove !== undefined && !this.invalidMoves.has(lastMove);
|
||||
}
|
||||
return (_user, target, _move) => {
|
||||
const lastMove = this.mirrorMove ? target.getLastNonVirtualMove(false, true)?.move : globalScene.currentBattle.lastMove;
|
||||
return !isNullOrUndefined(lastMove) && !this.invalidMoves.has(lastMove);
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Attribute used for moves that causes the target to repeat their last used move.
|
||||
* Attribute used for moves that cause the target to repeat their last used move.
|
||||
*
|
||||
* Used for [Instruct](https://bulbapedia.bulbagarden.net/wiki/Instruct_(move)).
|
||||
*/
|
||||
@ -7006,34 +7046,41 @@ export class RepeatMoveAttr extends MoveEffectAttr {
|
||||
}
|
||||
|
||||
/**
|
||||
* Forces the target to re-use their last used move again
|
||||
*
|
||||
* @param user {@linkcode Pokemon} that used the attack
|
||||
* @param target {@linkcode Pokemon} targeted by the attack
|
||||
* @param move N/A
|
||||
* @param args N/A
|
||||
* Forces the target to re-use their last used move again.
|
||||
* @param user - The {@linkcode Pokemon} using the attack
|
||||
* @param target - The {@linkcode Pokemon} being targeted by the attack
|
||||
* @returns `true` if the move succeeds
|
||||
*/
|
||||
apply(user: Pokemon, target: Pokemon, move: Move, args: any[]): boolean {
|
||||
apply(user: Pokemon, target: Pokemon): boolean {
|
||||
// get the last move used (excluding status based failures) as well as the corresponding moveset slot
|
||||
const lastMove = target.getLastXMoves(-1).find(m => m.move !== Moves.NONE)!;
|
||||
const movesetMove = target.getMoveset().find(m => m.moveId === lastMove.move)!;
|
||||
// TODO: How does instruct work when copying a move called via Copycat that the user itself knows?
|
||||
const lastMove = target.getLastNonVirtualMove();
|
||||
const movesetMove = target.getMoveset().find(m => m.moveId === lastMove?.move)
|
||||
|
||||
// never happens due to condition func, but makes TS compiler not sad
|
||||
if (!lastMove || !movesetMove) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// If the last move used can hit more than one target or has variable targets,
|
||||
// re-compute the targets for the attack
|
||||
// (mainly for alternating double/single battle shenanigans)
|
||||
// Rampaging moves (e.g. Outrage) are not included due to being incompatible with Instruct
|
||||
// TODO: Fix this once dragon darts gets smart targeting
|
||||
// re-compute the targets for the attack (mainly for alternating double/single battles)
|
||||
// Rampaging moves (e.g. Outrage) are not included due to being incompatible with Instruct,
|
||||
// nor is Dragon Darts (due to handling its smart targeting entirely within `MoveEffectPhase`)
|
||||
let moveTargets = movesetMove.getMove().isMultiTarget() ? getMoveTargets(target, lastMove.move).targets : lastMove.targets;
|
||||
|
||||
/** In the event the instructed move's only target is a fainted opponent, redirect it to an alive ally if possible
|
||||
Normally, all yet-unexecuted move phases would swap over when the enemy in question faints
|
||||
(see `redirectPokemonMoves` in `battle-scene.ts`),
|
||||
but since instruct adds a new move phase pre-emptively, we need to handle this interaction manually.
|
||||
*/
|
||||
// In the event the instructed move's only target is a fainted opponent, redirect it to an alive ally if possible.
|
||||
// Normally, all yet-unexecuted move phases would swap targets after any foe faints or flees (see `redirectPokemonMoves` in `battle-scene.ts`),
|
||||
// but since Instruct adds a new move phase _after_ all that occurs, we need to handle this interaction manually.
|
||||
const firstTarget = globalScene.getField()[moveTargets[0]];
|
||||
if (globalScene.currentBattle.double && moveTargets.length === 1 && firstTarget.isFainted() && firstTarget !== target.getAlly()) {
|
||||
if (
|
||||
globalScene.currentBattle.double // double battle
|
||||
&& moveTargets.length === 1
|
||||
&& firstTarget.isFainted()
|
||||
&& firstTarget !== target.getAlly()
|
||||
) {
|
||||
const ally = firstTarget.getAlly();
|
||||
if (!isNullOrUndefined(ally) && ally.isActive()) { // ally exists, is not dead and can sponge the blast
|
||||
if (!isNullOrUndefined(ally) && ally.isActive()) {
|
||||
// ally exists, is not dead and can sponge the blast
|
||||
moveTargets = [ ally.getBattlerIndex() ];
|
||||
}
|
||||
}
|
||||
@ -7042,15 +7089,15 @@ export class RepeatMoveAttr extends MoveEffectAttr {
|
||||
userPokemonName: getPokemonNameWithAffix(user),
|
||||
targetPokemonName: getPokemonNameWithAffix(target)
|
||||
}));
|
||||
target.getMoveQueue().unshift({ move: lastMove.move, targets: moveTargets, ignorePP: false });
|
||||
target.turnData.extraTurns++;
|
||||
globalScene.appendToPhase(new MovePhase(target, moveTargets, movesetMove), MoveEndPhase);
|
||||
globalScene.appendToPhase(new MovePhase(target, moveTargets, movesetMove, MoveUseType.NORMAL), MoveEndPhase);
|
||||
return true;
|
||||
}
|
||||
|
||||
getCondition(): MoveConditionFunc {
|
||||
return (user, target, move) => {
|
||||
const lastMove = target.getLastXMoves(-1).find(m => m.move !== Moves.NONE);
|
||||
return (_user, target, _move) => {
|
||||
// TODO: Check instruct behavior with struggle - ignore, fail or success
|
||||
const lastMove = target.getLastNonVirtualMove();
|
||||
const movesetMove = target.getMoveset().find(m => m.moveId === lastMove?.move);
|
||||
const uninstructableMoves = [
|
||||
// Locking/Continually Executed moves
|
||||
@ -7109,8 +7156,7 @@ export class RepeatMoveAttr extends MoveEffectAttr {
|
||||
|
||||
if (!lastMove?.move // no move to instruct
|
||||
|| !movesetMove // called move not in target's moveset (forgetting the move, etc.)
|
||||
|| movesetMove.ppUsed === movesetMove.getMovePp() // move out of pp
|
||||
|| allMoves[lastMove.move].isChargingMove() // called move is a charging/recharging move
|
||||
|| !movesetMove.isUsable(target) // Move unusable due to PP shortage or similar
|
||||
|| uninstructableMoves.includes(lastMove.move)) { // called move is in the banlist
|
||||
return false;
|
||||
}
|
||||
@ -7144,53 +7190,48 @@ export class ReducePpMoveAttr extends MoveEffectAttr {
|
||||
/**
|
||||
* Reduces the PP of the target's last-used move by an amount based on this attribute instance's {@linkcode reduction}.
|
||||
*
|
||||
* @param user {@linkcode Pokemon} that used the attack
|
||||
* @param target {@linkcode Pokemon} targeted by the attack
|
||||
* @param move N/A
|
||||
* @param args N/A
|
||||
* @returns `true`
|
||||
* @param user - N/A
|
||||
* @param target - The {@linkcode Pokemon} targeted by the attack
|
||||
* @param move - N/A
|
||||
* @param args - N/A
|
||||
* @returns always `true`
|
||||
*/
|
||||
apply(user: Pokemon, target: Pokemon, move: Move, args: any[]): boolean {
|
||||
// Null checks can be skipped due to condition function
|
||||
const lastMove = target.getLastXMoves()[0];
|
||||
const movesetMove = target.getMoveset().find(m => m.moveId === lastMove.move)!;
|
||||
/** The last move the target themselves used */
|
||||
const lastMove = target.getLastNonVirtualMove();
|
||||
const movesetMove = target.getMoveset().find(m => m.moveId === lastMove?.move)!; // bang is correct as condition prevents this from being nullish
|
||||
const lastPpUsed = movesetMove.ppUsed;
|
||||
movesetMove.ppUsed = Math.min((lastPpUsed) + this.reduction, movesetMove.getMovePp());
|
||||
movesetMove.ppUsed = Math.min(lastPpUsed + this.reduction, movesetMove.getMovePp());
|
||||
|
||||
const message = i18next.t("battle:ppReduced", { targetName: getPokemonNameWithAffix(target), moveName: movesetMove.getName(), reduction: (movesetMove.ppUsed) - lastPpUsed });
|
||||
globalScene.eventTarget.dispatchEvent(new MoveUsedEvent(target.id, movesetMove.getMove(), movesetMove.ppUsed));
|
||||
globalScene.queueMessage(message);
|
||||
globalScene.queueMessage(i18next.t("battle:ppReduced", { targetName: getPokemonNameWithAffix(target), moveName: movesetMove.getName(), reduction: (movesetMove.ppUsed) - lastPpUsed }));
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
getCondition(): MoveConditionFunc {
|
||||
return (user, target, move) => {
|
||||
const lastMove = target.getLastXMoves()[0];
|
||||
if (lastMove) {
|
||||
const movesetMove = target.getMoveset().find(m => m.moveId === lastMove.move);
|
||||
return !!movesetMove?.getPpRatio();
|
||||
}
|
||||
return false;
|
||||
const lastMove = target.getLastNonVirtualMove();
|
||||
const movesetMove = target.getMoveset().find(m => m.moveId === lastMove?.move)
|
||||
return !!movesetMove?.getPpRatio();
|
||||
};
|
||||
}
|
||||
|
||||
getTargetBenefitScore(user: Pokemon, target: Pokemon, move: Move): number {
|
||||
const lastMove = target.getLastXMoves()[0];
|
||||
if (lastMove) {
|
||||
const movesetMove = target.getMoveset().find(m => m.moveId === lastMove.move);
|
||||
if (movesetMove) {
|
||||
const maxPp = movesetMove.getMovePp();
|
||||
const ppLeft = maxPp - movesetMove.ppUsed;
|
||||
const value = -(8 - Math.ceil(Math.min(maxPp, 30) / 5));
|
||||
if (ppLeft < 4) {
|
||||
return (value / 4) * ppLeft;
|
||||
}
|
||||
return value;
|
||||
}
|
||||
const lastMove = target.getLastNonVirtualMove();
|
||||
const movesetMove = target.getMoveset().find(m => m.moveId === lastMove?.move)
|
||||
if (!movesetMove) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
return 0;
|
||||
const maxPp = movesetMove.getMovePp();
|
||||
const ppLeft = maxPp - movesetMove.ppUsed;
|
||||
const value = -(8 - Math.ceil(Math.min(maxPp, 30) / 5));
|
||||
if (ppLeft < 4) {
|
||||
return (value / 4) * ppLeft;
|
||||
}
|
||||
return value;
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@ -7206,40 +7247,36 @@ export class AttackReducePpMoveAttr extends ReducePpMoveAttr {
|
||||
/**
|
||||
* Checks if the target has used a move prior to the attack. PP-reduction is applied through the super class if so.
|
||||
*
|
||||
* @param user {@linkcode Pokemon} that used the attack
|
||||
* @param target {@linkcode Pokemon} targeted by the attack
|
||||
* @param move {@linkcode Move} being used
|
||||
* @param args N/A
|
||||
* @returns {boolean} true
|
||||
* @param user - The {@linkcode Pokemon} using the move
|
||||
* @param target -The {@linkcode Pokemon} targeted by the attack
|
||||
* @param move - The {@linkcode Move} being used
|
||||
* @param args - N/A
|
||||
* @returns - always `true`
|
||||
*/
|
||||
apply(user: Pokemon, target: Pokemon, move: Move, args: any[]): boolean {
|
||||
const lastMove = target.getLastXMoves().find(() => true);
|
||||
if (lastMove) {
|
||||
const movesetMove = target.getMoveset().find(m => m.moveId === lastMove.move);
|
||||
if (Boolean(movesetMove?.getPpRatio())) {
|
||||
super.apply(user, target, move, args);
|
||||
}
|
||||
const lastMove = target.getLastNonVirtualMove();
|
||||
const movesetMove = target.getMoveset().find(m => m.moveId === lastMove?.move);
|
||||
if (movesetMove?.getPpRatio()) {
|
||||
super.apply(user, target, move, args);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
// Override condition function to always perform damage. Instead, perform pp-reduction condition check in apply function above
|
||||
getCondition(): MoveConditionFunc {
|
||||
return (user, target, move) => true;
|
||||
/**
|
||||
* Override condition function to always perform damage.
|
||||
* Instead, perform pp-reduction condition check in {@linkcode apply}.
|
||||
* (A failed condition will prevent damage which is not what we want here)
|
||||
* @returns always `true`
|
||||
*/
|
||||
override getCondition(): MoveConditionFunc {
|
||||
return () => true;
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: Review this
|
||||
const targetMoveCopiableCondition: MoveConditionFunc = (user, target, move) => {
|
||||
const targetMoves = target.getMoveHistory().filter(m => !m.virtual);
|
||||
if (!targetMoves.length) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const copiableMove = targetMoves[0];
|
||||
|
||||
if (!copiableMove.move) {
|
||||
const copiableMove = target.getLastNonVirtualMove();
|
||||
if (!copiableMove?.move) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@ -7252,14 +7289,18 @@ const targetMoveCopiableCondition: MoveConditionFunc = (user, target, move) => {
|
||||
return true;
|
||||
};
|
||||
|
||||
/**
|
||||
* Attribute to temporarily copy the last move in the target's moveset.
|
||||
* Used by {@linkcode Moves.MIMIC}.
|
||||
*/
|
||||
export class MovesetCopyMoveAttr extends OverrideMoveEffectAttr {
|
||||
apply(user: Pokemon, target: Pokemon, move: Move, args: any[]): boolean {
|
||||
const targetMoves = target.getMoveHistory().filter(m => !m.virtual);
|
||||
if (!targetMoves.length) {
|
||||
const lastMove = target.getLastNonVirtualMove()
|
||||
if (!lastMove?.move) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const copiedMove = allMoves[targetMoves[0].move];
|
||||
const copiedMove = allMoves[lastMove.move];
|
||||
|
||||
const thisMoveIndex = user.getMoveset().findIndex(m => m.moveId === move.id);
|
||||
|
||||
@ -7267,8 +7308,9 @@ export class MovesetCopyMoveAttr extends OverrideMoveEffectAttr {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Populate summon data with a copy of the current moveset, replacing the copying move with the copied move
|
||||
user.summonData.moveset = user.getMoveset().slice(0);
|
||||
user.summonData.moveset[thisMoveIndex] = new PokemonMove(copiedMove.id, 0, 0);
|
||||
user.summonData.moveset[thisMoveIndex] = new PokemonMove(copiedMove.id);
|
||||
|
||||
globalScene.queueMessage(i18next.t("moveTriggers:copiedMove", { pokemonName: getPokemonNameWithAffix(user), moveName: copiedMove.name }));
|
||||
|
||||
@ -7292,13 +7334,14 @@ export class SketchAttr extends MoveEffectAttr {
|
||||
constructor() {
|
||||
super(true);
|
||||
}
|
||||
|
||||
/**
|
||||
* User copies the opponent's last used move, if possible
|
||||
* @param {Pokemon} user Pokemon that used the move and will replace Sketch with the copied move
|
||||
* @param {Pokemon} target Pokemon that the user wants to copy a move from
|
||||
* @param {Move} move Move being used
|
||||
* @param {any[]} args Unused
|
||||
* @returns {boolean} true if the function succeeds, otherwise false
|
||||
* User copies the opponent's last used move, if possible.
|
||||
* @param user - The {@linkcode Pokemon} using the move
|
||||
* @param target - The {@linkcode Pokemon} being targeted by the move
|
||||
* @param move - The {@linkcoed Move} being used
|
||||
* @param args - Unused
|
||||
* @returns Whether a move was successfully learnt
|
||||
*/
|
||||
|
||||
apply(user: Pokemon, target: Pokemon, move: Move, args: any[]): boolean {
|
||||
@ -7306,9 +7349,9 @@ export class SketchAttr extends MoveEffectAttr {
|
||||
return false;
|
||||
}
|
||||
|
||||
const targetMove = target.getLastXMoves(-1)
|
||||
.find(m => m.move !== Moves.NONE && m.move !== Moves.STRUGGLE && !m.virtual);
|
||||
const targetMove = target.getLastNonVirtualMove()
|
||||
if (!targetMove) {
|
||||
// failsafe for TS compiler
|
||||
return false;
|
||||
}
|
||||
|
||||
@ -7331,32 +7374,10 @@ export class SketchAttr extends MoveEffectAttr {
|
||||
return false;
|
||||
}
|
||||
|
||||
const targetMove = target.getMoveHistory().filter(m => !m.virtual).at(-1);
|
||||
if (!targetMove) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const unsketchableMoves = [
|
||||
Moves.CHATTER,
|
||||
Moves.MIRROR_MOVE,
|
||||
Moves.SLEEP_TALK,
|
||||
Moves.STRUGGLE,
|
||||
Moves.SKETCH,
|
||||
Moves.REVIVAL_BLESSING,
|
||||
Moves.TERA_STARSTORM,
|
||||
Moves.BREAKNECK_BLITZ__PHYSICAL,
|
||||
Moves.BREAKNECK_BLITZ__SPECIAL
|
||||
];
|
||||
|
||||
if (unsketchableMoves.includes(targetMove.move)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (user.getMoveset().find(m => m.moveId === targetMove.move)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
const targetMove = target.getLastNonVirtualMove();
|
||||
return !isNullOrUndefined(targetMove)
|
||||
&& !invalidSketchMoves.has(targetMove.move)
|
||||
&& user.getMoveset().every(m => m.moveId !== targetMove.move)
|
||||
};
|
||||
}
|
||||
}
|
||||
@ -7796,19 +7817,20 @@ export class LastResortAttr extends MoveAttr {
|
||||
// TODO: Verify behavior as Bulbapedia page is _extremely_ poorly documented
|
||||
getCondition(): MoveConditionFunc {
|
||||
return (user: Pokemon, _target: Pokemon, move: Move) => {
|
||||
const movesInMoveset = new Set<Moves>(user.getMoveset().map(m => m.moveId));
|
||||
if (!movesInMoveset.delete(move.id) || !movesInMoveset.size) {
|
||||
const otherMovesInMoveset = new Set<Moves>(user.getMoveset().map(m => m.moveId));
|
||||
if (!otherMovesInMoveset.delete(move.id) || !otherMovesInMoveset.size) {
|
||||
return false; // Last resort fails if used when not in user's moveset or no other moves exist
|
||||
}
|
||||
|
||||
const movesInHistory = new Set(
|
||||
const movesInHistory = new Set<Moves>(
|
||||
user.getMoveHistory()
|
||||
.filter(m => !m.virtual) // TODO: Change to (m) => m < MoveUseType.INDIRECT after Dancer PR refactors virtual into enum
|
||||
.filter(m => m.useType < MoveUseType.INDIRECT) // Last resort ignores virtual moves
|
||||
.map(m => m.move)
|
||||
);
|
||||
|
||||
// Since `Set.intersection()` is only present in ESNext, we have to coerce it to an array to check inclusion
|
||||
return [...movesInMoveset].every(m => movesInHistory.has(m))
|
||||
// Since `Set.intersection()` is only present in ESNext, we have to do this to check inclusion
|
||||
// grumble mumble grumble mumble...
|
||||
return [...otherMovesInMoveset].every(m => movesInHistory.has(m))
|
||||
};
|
||||
}
|
||||
}
|
||||
@ -7830,27 +7852,26 @@ export class VariableTargetAttr extends MoveAttr {
|
||||
}
|
||||
|
||||
/**
|
||||
* Attribute for {@linkcode Moves.AFTER_YOU}
|
||||
* Attribute to cause the target to move immediately after the user.
|
||||
*
|
||||
* [After You - Move | Bulbapedia](https://bulbapedia.bulbagarden.net/wiki/After_You_(move))
|
||||
* Used by {@linkcode Moves.AFTER_YOU}.
|
||||
*/
|
||||
export class AfterYouAttr extends MoveEffectAttr {
|
||||
/**
|
||||
* Allows the target of this move to act right after the user.
|
||||
*
|
||||
* @param user {@linkcode Pokemon} that is using the move.
|
||||
* @param target {@linkcode Pokemon} that will move right after this move is used.
|
||||
* @param move {@linkcode Move} {@linkcode Moves.AFTER_YOU}
|
||||
* @param _args N/A
|
||||
* @returns true
|
||||
* Cause the target of this move to act right after the user.
|
||||
* @param user - Unused
|
||||
* @param target - The {@linkcode Pokemon} targeted by this move
|
||||
* @param _move - Unused
|
||||
* @param _args - Unused
|
||||
* @returns `true`
|
||||
*/
|
||||
override apply(user: Pokemon, target: Pokemon, _move: Move, _args: any[]): boolean {
|
||||
globalScene.queueMessage(i18next.t("moveTriggers:afterYou", { targetName: getPokemonNameWithAffix(target) }));
|
||||
|
||||
//Will find next acting phase of the targeted pokémon, delete it and queue it next on successful delete.
|
||||
const nextAttackPhase = globalScene.findPhase<MovePhase>((phase) => phase.pokemon === target);
|
||||
if (nextAttackPhase && globalScene.tryRemovePhase((phase: MovePhase) => phase.pokemon === target)) {
|
||||
globalScene.prependToPhase(new MovePhase(target, [ ...nextAttackPhase.targets ], nextAttackPhase.move), MovePhase);
|
||||
// Will find next acting phase of the targeted pokémon, delete it and queue it right after us.
|
||||
const targetNextPhase = globalScene.findPhase<MovePhase>(phase => phase.pokemon === target);
|
||||
if (targetNextPhase && globalScene.tryRemovePhase((phase: MovePhase) => phase.pokemon === target)) {
|
||||
globalScene.prependToPhase(targetNextPhase, MovePhase);
|
||||
}
|
||||
|
||||
return true;
|
||||
@ -7860,7 +7881,6 @@ export class AfterYouAttr extends MoveEffectAttr {
|
||||
/**
|
||||
* Move effect to force the target to move last, ignoring priority.
|
||||
* If applied to multiple targets, they move in speed order after all other moves.
|
||||
* @extends MoveEffectAttr
|
||||
*/
|
||||
export class ForceLastAttr extends MoveEffectAttr {
|
||||
/**
|
||||
@ -7875,6 +7895,7 @@ export class ForceLastAttr extends MoveEffectAttr {
|
||||
override apply(user: Pokemon, target: Pokemon, _move: Move, _args: any[]): boolean {
|
||||
globalScene.queueMessage(i18next.t("moveTriggers:forceLast", { targetPokemonName: getPokemonNameWithAffix(target) }));
|
||||
|
||||
// TODO: Refactor this to be more readable
|
||||
const targetMovePhase = globalScene.findPhase<MovePhase>((phase) => phase.pokemon === target);
|
||||
if (targetMovePhase && !targetMovePhase.isForcedLast() && globalScene.tryRemovePhase((phase: MovePhase) => phase.pokemon === target)) {
|
||||
// Finding the phase to insert the move in front of -
|
||||
@ -7887,7 +7908,7 @@ export class ForceLastAttr extends MoveEffectAttr {
|
||||
globalScene.phaseQueue.splice(
|
||||
globalScene.phaseQueue.indexOf(prependPhase),
|
||||
0,
|
||||
new MovePhase(target, [ ...targetMovePhase.targets ], targetMovePhase.move, false, false, false, true)
|
||||
new MovePhase(target, [ ...targetMovePhase.targets ], targetMovePhase.move, targetMovePhase.useType, true)
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -7895,12 +7916,18 @@ export class ForceLastAttr extends MoveEffectAttr {
|
||||
}
|
||||
}
|
||||
|
||||
/** Returns whether a {@linkcode MovePhase} has been forced last and the corresponding pokemon is slower than {@linkcode target} */
|
||||
/**
|
||||
* Returns whether a {@linkcode MovePhase} has been forced last and the corresponding pokemon is slower than {@linkcode target}.
|
||||
|
||||
* TODO:
|
||||
- Make this a class method
|
||||
- Make this look at speed order from TurnStartPhase
|
||||
*/
|
||||
const phaseForcedSlower = (phase: MovePhase, target: Pokemon, trickRoom: boolean): boolean => {
|
||||
let slower: boolean;
|
||||
// quashed pokemon still have speed ties
|
||||
if (phase.pokemon.getEffectiveStat(Stat.SPD) === target.getEffectiveStat(Stat.SPD)) {
|
||||
slower = !!target.randSeedInt(2);
|
||||
slower = !target.randSeedInt(2);
|
||||
} else {
|
||||
slower = !trickRoom ? phase.pokemon.getEffectiveStat(Stat.SPD) < target.getEffectiveStat(Stat.SPD) : phase.pokemon.getEffectiveStat(Stat.SPD) > target.getEffectiveStat(Stat.SPD);
|
||||
}
|
||||
@ -8042,8 +8069,7 @@ export class UpperHandCondition extends MoveCondition {
|
||||
super((user, target, move) => {
|
||||
const targetCommand = globalScene.currentBattle.turnCommands[target.getBattlerIndex()];
|
||||
|
||||
return !!targetCommand
|
||||
&& targetCommand.command === Command.FIGHT
|
||||
return targetCommand?.command === Command.FIGHT
|
||||
&& !target.turnData.acted
|
||||
&& !!targetCommand.move?.move
|
||||
&& allMoves[targetCommand.move.move].category !== MoveCategory.STATUS
|
||||
@ -8076,6 +8102,7 @@ export class ResistLastMoveTypeAttr extends MoveEffectAttr {
|
||||
constructor() {
|
||||
super(true);
|
||||
}
|
||||
|
||||
/**
|
||||
* User changes its type to a random type that resists the target's last used move
|
||||
* @param {Pokemon} user Pokemon that used the move and will change types
|
||||
@ -8089,7 +8116,8 @@ export class ResistLastMoveTypeAttr extends MoveEffectAttr {
|
||||
return false;
|
||||
}
|
||||
|
||||
const [ targetMove ] = target.getLastXMoves(1); // target's most recent move
|
||||
// TODO: Confirm how this interacts with status-induced failures and called moves
|
||||
const targetMove = target.getLastXMoves(1)[0]; // target's most recent move
|
||||
if (!targetMove) {
|
||||
return false;
|
||||
}
|
||||
@ -8131,9 +8159,9 @@ export class ResistLastMoveTypeAttr extends MoveEffectAttr {
|
||||
}
|
||||
|
||||
getCondition(): MoveConditionFunc {
|
||||
// TODO: Does this count dancer?
|
||||
return (user, target, move) => {
|
||||
const moveHistory = target.getLastXMoves();
|
||||
return moveHistory.length !== 0;
|
||||
return target.getLastXMoves(-1).some(tm => tm.move !== Moves.NONE);
|
||||
};
|
||||
}
|
||||
}
|
||||
@ -8395,9 +8423,9 @@ export function initMoves() {
|
||||
.attr(FixedDamageAttr, 20),
|
||||
new StatusMove(Moves.DISABLE, PokemonType.NORMAL, 100, 20, -1, 0, 1)
|
||||
.attr(AddBattlerTagAttr, BattlerTagType.DISABLED, false, true)
|
||||
.condition((user, target, move) => {
|
||||
const lastRealMove = target.getLastXMoves(-1).find(m => !m.virtual);
|
||||
return !isNullOrUndefined(lastRealMove) && lastRealMove.move !== Moves.NONE && lastRealMove.move !== Moves.STRUGGLE;
|
||||
.condition((_user, target, _move) => {
|
||||
const lastNonVirtualMove = target.getLastNonVirtualMove();
|
||||
return !isNullOrUndefined(lastNonVirtualMove) && lastNonVirtualMove.move !== Moves.STRUGGLE;
|
||||
})
|
||||
.ignoresSubstitute()
|
||||
.reflectable(),
|
||||
@ -8589,7 +8617,8 @@ export function initMoves() {
|
||||
new SelfStatusMove(Moves.METRONOME, PokemonType.NORMAL, -1, 10, -1, 0, 1)
|
||||
.attr(RandomMoveAttr, invalidMetronomeMoves),
|
||||
new StatusMove(Moves.MIRROR_MOVE, PokemonType.FLYING, -1, 20, -1, 0, 1)
|
||||
.attr(CopyMoveAttr, true, invalidMirrorMoveMoves),
|
||||
.attr(CopyMoveAttr, true, invalidMirrorMoveMoves)
|
||||
.edgeCase(), // May or may not have incorrect interactions with Struggle
|
||||
new AttackMove(Moves.SELF_DESTRUCT, PokemonType.NORMAL, MoveCategory.PHYSICAL, 200, 100, 5, -1, 0, 1)
|
||||
.attr(SacrificialAttr)
|
||||
.makesContact(false)
|
||||
@ -9458,7 +9487,8 @@ export function initMoves() {
|
||||
.target(MoveTarget.NEAR_ENEMY)
|
||||
.unimplemented(),
|
||||
new SelfStatusMove(Moves.COPYCAT, PokemonType.NORMAL, -1, 20, -1, 0, 4)
|
||||
.attr(CopyMoveAttr, false, invalidCopycatMoves),
|
||||
.attr(CopyMoveAttr, false, invalidCopycatMoves)
|
||||
.edgeCase(), // May or may not have incorrect interactions with Struggle
|
||||
new StatusMove(Moves.POWER_SWAP, PokemonType.PSYCHIC, -1, 10, 100, 0, 4)
|
||||
.attr(SwapStatStagesAttr, [ Stat.ATK, Stat.SPATK ])
|
||||
.ignoresSubstitute(),
|
||||
@ -9828,7 +9858,13 @@ export function initMoves() {
|
||||
.chargeAttr(SemiInvulnerableAttr, BattlerTagType.FLYING)
|
||||
.condition(failOnGravityCondition)
|
||||
.condition((user, target, move) => !target.getTag(BattlerTagType.SUBSTITUTE))
|
||||
.partial(), // Should immobilize the target, Flying types should take no damage. cf https://bulbapedia.bulbagarden.net/wiki/Sky_Drop_(move) and https://www.smogon.com/dex/sv/moves/sky-drop/
|
||||
.partial(),
|
||||
/* Cf https://bulbapedia.bulbagarden.net/wiki/Sky_Drop_(move) and https://www.smogon.com/dex/sv/moves/sky-drop/:
|
||||
* Should immobilize and give target semi-invulnerability
|
||||
* Flying types should take no damage
|
||||
* Should fail on targets above a certain weight threshold
|
||||
* Should remove all redirection effects on successful takeoff (Rage Poweder, etc.)
|
||||
*/
|
||||
new SelfStatusMove(Moves.SHIFT_GEAR, PokemonType.STEEL, -1, 10, -1, 0, 5)
|
||||
.attr(StatStageChangeAttr, [ Stat.ATK ], 1, true)
|
||||
.attr(StatStageChangeAttr, [ Stat.SPD ], 2, true),
|
||||
@ -10399,10 +10435,10 @@ export function initMoves() {
|
||||
new StatusMove(Moves.INSTRUCT, PokemonType.PSYCHIC, -1, 15, -1, 0, 7)
|
||||
.ignoresSubstitute()
|
||||
.attr(RepeatMoveAttr)
|
||||
// incorrect interactions with Gigaton Hammer, Blood Moon & Torment
|
||||
// Also has incorrect interactions with Dancer due to the latter
|
||||
// erroneously adding copied moves to move history.
|
||||
.edgeCase(),
|
||||
// incorrect interactions with Gigaton Hammer, Blood Moon & Torment due to them making moves _fail on use_,
|
||||
// not merely unselectable.
|
||||
// Also my or may not have incorrect interactions with Struggle (needs verification).
|
||||
new AttackMove(Moves.BEAK_BLAST, PokemonType.FLYING, MoveCategory.PHYSICAL, 100, 100, 15, -1, -3, 7)
|
||||
.attr(BeakBlastHeaderAttr)
|
||||
.ballBombMove()
|
||||
@ -10459,7 +10495,11 @@ export function initMoves() {
|
||||
.bitingMove()
|
||||
.attr(RemoveScreensAttr),
|
||||
new AttackMove(Moves.STOMPING_TANTRUM, PokemonType.GROUND, MoveCategory.PHYSICAL, 75, 100, 10, -1, 0, 7)
|
||||
.attr(MovePowerMultiplierAttr, (user, target, move) => user.getLastXMoves(2)[1]?.result === MoveResult.MISS || user.getLastXMoves(2)[1]?.result === MoveResult.FAIL ? 2 : 1),
|
||||
.attr(MovePowerMultiplierAttr, (user) => {
|
||||
// TODO: Verify if Stomping Tantrum skips dancer moves and/or copied moves
|
||||
const lastNonDancerMove = user.getLastXMoves(-1).filter(m => m.useType !== MoveUseType.INDIRECT)[1] as TurnMove | undefined;
|
||||
return lastNonDancerMove && (lastNonDancerMove.result === MoveResult.MISS || lastNonDancerMove.result === MoveResult.FAIL) ? 2 : 1
|
||||
}),
|
||||
new AttackMove(Moves.SHADOW_BONE, PokemonType.GHOST, MoveCategory.PHYSICAL, 85, 100, 10, 20, 0, 7)
|
||||
.attr(StatStageChangeAttr, [ Stat.DEF ], -1)
|
||||
.makesContact(false),
|
||||
|
@ -76,13 +76,14 @@ export const normalForm: Species[] = [
|
||||
|
||||
/**
|
||||
* Gets the {@linkcode PokemonSpecies} object associated with the {@linkcode Species} enum given
|
||||
* @param species The species to fetch
|
||||
* @returns The associated {@linkcode PokemonSpecies} object
|
||||
* @param species - The {@linkcode Species} to fetch.
|
||||
* If an array of `Species` is passed (such as for named trainer spawn pools),
|
||||
* one will be selected at random.
|
||||
* @returns The {@linkcode PokemonSpecies} object associated with the given species.
|
||||
*/
|
||||
export function getPokemonSpecies(species: Species | Species[]): PokemonSpecies {
|
||||
// If a special pool (named trainers) is used here it CAN happen that they have a array as species (which means choose one of those two). So we catch that with this code block
|
||||
if (Array.isArray(species)) {
|
||||
// Pick a random species from the list
|
||||
// TODO: can't we just use normal int number gen rather than this junk
|
||||
species = species[Math.floor(Math.random() * species.length)];
|
||||
}
|
||||
if (species >= 2000) {
|
||||
|
58
src/enums/move-use-type.ts
Normal file
58
src/enums/move-use-type.ts
Normal file
@ -0,0 +1,58 @@
|
||||
import type { BattlerTagLapseType } from "#app/data/battler-tags";
|
||||
import type { PostDancingMoveAbAttr } from "#app/data/abilities/ability";
|
||||
|
||||
/**
|
||||
* Enum representing all the possible ways a given move can be executed.
|
||||
* Each one inherits the properties (or exclusions) of all types preceding it.
|
||||
* Properties newly found on a given use type will be **bolded**,
|
||||
* while oddities breaking a previous trend will be listed in _italics_.
|
||||
*/
|
||||
export enum MoveUseType {
|
||||
/**
|
||||
* This move was used normally (i.e. clicking on the button) or called via Instruct.
|
||||
* It deducts PP from the user's moveset (failing if out of PP), and interacts normally with other moves and abilities.
|
||||
*/
|
||||
NORMAL,
|
||||
|
||||
/**
|
||||
* Identical to {@linkcode MoveUseType.NORMAL}, except the move **does not consume PP** on use
|
||||
* and **will not fail** if none is left before its execution.
|
||||
* PP can still be reduced by other effects (such as Spite or Eerie Spell).
|
||||
*/
|
||||
IGNORE_PP,
|
||||
|
||||
/**
|
||||
* This move was called indirectly by another effect other than Instruct or the user's previous move.
|
||||
* Currently only used by {@linkcode PostDancingMoveAbAttr | Dancer}.
|
||||
|
||||
* Indirect moves ignore PP checks similar to {@linkcode MoveUseType.IGNORE_PP}, but additionally **cannot be copied**
|
||||
* by all move-copying effects (barring reflection).
|
||||
* They are also **"skipped over" by most moveset and move history-related effects** (PP reduction, Last Resort, etc).
|
||||
|
||||
* They still respect the user's volatile status conditions and confusion (though will uniquely _cure freeze and sleep before use_).
|
||||
*/
|
||||
INDIRECT,
|
||||
|
||||
/**
|
||||
* This move was called as part of another move's effect (such as for most {@link https://bulbapedia.bulbagarden.net/wiki/Category:Moves_that_call_other_moves | Move-calling moves}).
|
||||
|
||||
* Follow-up moves **bypass cancellation** from all **non-volatile status conditions** and **{@linkcode BattlerTagLapseType.MOVE}-type effects**
|
||||
* (having been checked already on the calling move).
|
||||
|
||||
* They are _not ignored_ by other move-calling moves and abilities (unlike {@linkcode MoveUseType.FOLLOW_UP} and {@linkcode MoveUseType.REFLECTED}),
|
||||
* but still inherit the former's disregard for moveset-related effects.
|
||||
*/
|
||||
FOLLOW_UP,
|
||||
|
||||
/**
|
||||
* This move was reflected by Magic Coat or Magic Bounce.
|
||||
|
||||
* Reflected moves ignore all the same cancellation checks as {@linkcode MoveUseType.INDIRECT}
|
||||
* and retain the same copy prevention as {@linkcode MoveUseType.FOLLOW_UP}, but additionally
|
||||
* **cannot be reflected by other reflecting effects**.
|
||||
|
||||
* Also used for the "attack" portion of Future Sight and Doom Desire
|
||||
* (in which case the reflection blockage is completely irrelevant.)
|
||||
*/
|
||||
REFLECTED
|
||||
}
|
@ -260,6 +260,7 @@ import { MoveFlags } from "#enums/MoveFlags";
|
||||
import { timedEventManager } from "#app/global-event-manager";
|
||||
import { loadMoveAnimations } from "#app/sprites/pokemon-asset-loader";
|
||||
import { ResetStatusPhase } from "#app/phases/reset-status-phase";
|
||||
import { MoveUseType } from "#enums/move-use-type";
|
||||
|
||||
export enum LearnMoveSituation {
|
||||
MISC,
|
||||
@ -406,7 +407,7 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container {
|
||||
super(globalScene, x, y);
|
||||
|
||||
if (!species.isObtainable() && this.isPlayer()) {
|
||||
throw `Cannot create a player Pokemon for species '${species.getName(formIndex)}'`;
|
||||
throw `Cannot create a player Pokemon for species "${species.getName(formIndex)}"`;
|
||||
}
|
||||
|
||||
this.species = species;
|
||||
@ -641,7 +642,7 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container {
|
||||
/**
|
||||
* Checks if a pokemon is fainted (ie: its `hp <= 0`).
|
||||
* It's usually better to call {@linkcode isAllowedInBattle()}
|
||||
* @param checkStatus `true` to also check that the pokemon's status is {@linkcode StatusEffect.FAINT}
|
||||
* @param checkStatus - Whether to also check the pokemon's status for {@linkcode StatusEffect.FAINT}; default `false`
|
||||
* @returns `true` if the pokemon is fainted
|
||||
*/
|
||||
public isFainted(checkStatus = false): boolean {
|
||||
@ -3255,8 +3256,9 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container {
|
||||
* If it rolls shiny, or if it's already shiny, also sets a random variant and give the Pokemon the associated luck.
|
||||
*
|
||||
* The base shiny odds are {@linkcode BASE_SHINY_CHANCE} / `65536`
|
||||
* @param thresholdOverride number that is divided by `2^16` (`65536`) to get the shiny chance, overrides {@linkcode shinyThreshold} if set (bypassing shiny rate modifiers such as Shiny Charm)
|
||||
* @param applyModifiersToOverride If {@linkcode thresholdOverride} is set and this is true, will apply Shiny Charm and event modifiers to {@linkcode thresholdOverride}
|
||||
* @param thresholdOverride - number that is divided by `2^16` (`65536`) to get the shiny chance, overrides {@linkcode shinyThreshold} if set (bypassing shiny rate modifiers such as Shiny Charm)
|
||||
* @param applyModifiersToOverride - Whether to apply Shiny Charm and event modifiers to {@linkcode thresholdOverride}.
|
||||
* Does nothing if {@linkcode thresholdOverride} is not set.
|
||||
* @returns `true` if the Pokemon has been set as a shiny, `false` otherwise
|
||||
*/
|
||||
public trySetShinySeed(
|
||||
@ -3711,7 +3713,7 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container {
|
||||
while (rand > stabMovePool[index][1]) {
|
||||
rand -= stabMovePool[index++][1];
|
||||
}
|
||||
this.moveset.push(new PokemonMove(stabMovePool[index][0], 0, 0));
|
||||
this.moveset.push(new PokemonMove(stabMovePool[index][0]));
|
||||
}
|
||||
} else {
|
||||
// Normal wild pokemon just force a random damaging move
|
||||
@ -3725,7 +3727,7 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container {
|
||||
while (rand > attackMovePool[index][1]) {
|
||||
rand -= attackMovePool[index++][1];
|
||||
}
|
||||
this.moveset.push(new PokemonMove(attackMovePool[index][0], 0, 0));
|
||||
this.moveset.push(new PokemonMove(attackMovePool[index][0]));
|
||||
}
|
||||
}
|
||||
|
||||
@ -3777,7 +3779,7 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container {
|
||||
while (rand > movePool[index][1]) {
|
||||
rand -= movePool[index++][1];
|
||||
}
|
||||
this.moveset.push(new PokemonMove(movePool[index][0], 0, 0));
|
||||
this.moveset.push(new PokemonMove(movePool[index][0]));
|
||||
}
|
||||
|
||||
// Trigger FormChange, except for enemy Pokemon during Mystery Encounters, to avoid crashes
|
||||
@ -3810,8 +3812,7 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container {
|
||||
ui =>
|
||||
ui instanceof BattleInfo &&
|
||||
(ui as BattleInfo) instanceof PlayerBattleInfo === this.isPlayer(),
|
||||
)
|
||||
.find(() => true);
|
||||
)[0] as Phaser.GameObjects.GameObject | undefined;
|
||||
if (!otherBattleInfo || !this.getFieldIndex()) {
|
||||
globalScene.fieldUI.sendToBack(this.battleInfo);
|
||||
globalScene.sendTextToBack(); // Push the top right text objects behind everything else
|
||||
@ -4911,7 +4912,7 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container {
|
||||
}
|
||||
|
||||
/**@overload */
|
||||
getTag(tagType: BattlerTagType.GRUDGE): GrudgeTag | nil;
|
||||
getTag(tagType: BattlerTagType.GRUDGE): GrudgeTag | undefined;
|
||||
|
||||
/** @overload */
|
||||
getTag(tagType: BattlerTagType): BattlerTag | undefined;
|
||||
@ -5139,8 +5140,8 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container {
|
||||
/**
|
||||
* Returns a list of the most recent move entries in this Pokemon's move history.
|
||||
* The retrieved move entries are sorted in order from NEWEST to OLDEST.
|
||||
* @param moveCount The number of move entries to retrieve.
|
||||
* If negative, retrieve the Pokemon's entire move history (equivalent to reversing the output of {@linkcode getMoveHistory()}).
|
||||
* @param moveCount The number of move entries to retrieve.\
|
||||
* If negative, retrieves the Pokemon's entire move history (equivalent to reversing the output of {@linkcode getMoveHistory()}).
|
||||
* Default is `1`.
|
||||
* @returns A list of {@linkcode TurnMove}, as specified above.
|
||||
*/
|
||||
@ -5154,10 +5155,42 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container {
|
||||
return moveHistory.slice(0).reverse();
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the most recently executed {@linkcode TurnMove} this {@linkcode Pokemon} has used that is:
|
||||
* - Not {@linkcode Moves.NONE}
|
||||
* - Non-virtual ({@linkcode MoveUseType | useType} < {@linkcode MoveUseType.INDIRECT})
|
||||
* @param ignoreStruggle - Whether to additionally ignore {@linkcode Moves.STRUGGLE}; default `false`
|
||||
* @param ignoreFollowUp - Whether to ignore moves with a use type of {@linkcode MoveUseType.FOLLOW_UP}
|
||||
* (Copycat, Mirror Move, etc.); default `true`
|
||||
* @returns The last move this Pokemon has used satisfying the aforementioned conditions,
|
||||
* or `undefined` if no applicable moves have been used since switching in.
|
||||
*/
|
||||
getLastNonVirtualMove(ignoreStruggle = false, ignoreFollowUp = true): TurnMove | undefined {
|
||||
return this.getLastXMoves(-1).find(m =>
|
||||
m.move !== Moves.NONE
|
||||
&& (m.useType < MoveUseType.INDIRECT ||
|
||||
(!ignoreFollowUp && m.useType === MoveUseType.FOLLOW_UP))
|
||||
&& (!ignoreStruggle || m.move !== Moves.STRUGGLE)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Return this Pokemon's move queue, consisting of all the moves it is slated to perform.
|
||||
* @returns An array of {@linkcode TurnMove}, as described above
|
||||
*/
|
||||
getMoveQueue(): TurnMove[] {
|
||||
return this.summonData.moveQueue;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a new entry to the end of this Pokemon's move queue.
|
||||
* @param queuedMove - A {@linkcode TurnMove} to push to this Pokemon's queue.
|
||||
*/
|
||||
pushMoveQueue(queuedMove: TurnMove): void {
|
||||
this.summonData.moveQueue.push(queuedMove);
|
||||
}
|
||||
|
||||
|
||||
changeForm(formChange: SpeciesFormChange): Promise<void> {
|
||||
return new Promise(resolve => {
|
||||
this.formIndex = Math.max(
|
||||
@ -5627,22 +5660,19 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container {
|
||||
|
||||
if (effect === StatusEffect.SLEEP) {
|
||||
sleepTurnsRemaining = new NumberHolder(this.randSeedIntRange(2, 4));
|
||||
|
||||
this.setFrameRate(4);
|
||||
|
||||
// If the user is invulnerable, lets remove their invulnerability when they fall asleep
|
||||
const invulnerableTags = [
|
||||
// If the user is invulnerable, remove their invulnerability when they fall asleep
|
||||
// and remove the upcoming attack from the move queue.
|
||||
const tag = [
|
||||
BattlerTagType.UNDERGROUND,
|
||||
BattlerTagType.UNDERWATER,
|
||||
BattlerTagType.HIDDEN,
|
||||
BattlerTagType.FLYING,
|
||||
];
|
||||
|
||||
const tag = invulnerableTags.find(t => this.getTag(t));
|
||||
|
||||
].find(t => this.getTag(t));
|
||||
if (tag) {
|
||||
this.removeTag(tag);
|
||||
this.getMoveQueue().pop();
|
||||
this.getMoveQueue().shift();
|
||||
}
|
||||
}
|
||||
|
||||
@ -5675,7 +5705,7 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container {
|
||||
|
||||
/**
|
||||
* Performs the action of clearing a Pokemon's status
|
||||
*
|
||||
*
|
||||
* This is a helper to {@linkcode resetStatus}, which should be called directly instead of this method
|
||||
*/
|
||||
public clearStatus(confusion: boolean, reloadAssets: boolean) {
|
||||
@ -7023,7 +7053,7 @@ export class PlayerPokemon extends Pokemon {
|
||||
copyMoveset(): PokemonMove[] {
|
||||
const newMoveset: PokemonMove[] = [];
|
||||
this.moveset.forEach(move => {
|
||||
newMoveset.push(new PokemonMove(move.moveId, 0, move.ppUp, move.virtual, move.maxPpOverride));
|
||||
newMoveset.push(new PokemonMove(move.moveId, 0, move.ppUp, move.maxPpOverride));
|
||||
});
|
||||
|
||||
return newMoveset;
|
||||
@ -7213,19 +7243,22 @@ export class EnemyPokemon extends Pokemon {
|
||||
* the Pokemon the move will target.
|
||||
* @returns this Pokemon's next move in the format {move, moveTargets}
|
||||
*/
|
||||
// TODO: Refactor this and split it up
|
||||
getNextMove(): TurnMove {
|
||||
// If this Pokemon has a move already queued, return it.
|
||||
const moveQueue = this.getMoveQueue();
|
||||
if (moveQueue.length !== 0) {
|
||||
const queuedMove = moveQueue[0];
|
||||
if (queuedMove) {
|
||||
const moveIndex = this.getMoveset().findIndex(m => m.moveId === queuedMove.move);
|
||||
if ((moveIndex > -1 && this.getMoveset()[moveIndex].isUsable(this, queuedMove.ignorePP)) || queuedMove.virtual) {
|
||||
return queuedMove;
|
||||
} else {
|
||||
this.getMoveQueue().shift();
|
||||
return this.getNextMove();
|
||||
}
|
||||
for (const queuedMove of this.getMoveQueue()) {
|
||||
const moveIndex = this.getMoveset().findIndex(m => m.moveId === queuedMove.move);
|
||||
// If the queued move was called indirectly, ignore all PP and usability checks.
|
||||
// Otherwise, ensure that the move being used is actually usable
|
||||
if (
|
||||
queuedMove.useType >= MoveUseType.INDIRECT ||
|
||||
(moveIndex > -1 &&
|
||||
this.getMoveset()[moveIndex].isUsable(
|
||||
this,
|
||||
queuedMove.useType >= MoveUseType.IGNORE_PP )
|
||||
)
|
||||
) {
|
||||
return queuedMove;
|
||||
}
|
||||
}
|
||||
|
||||
@ -7235,9 +7268,10 @@ export class EnemyPokemon extends Pokemon {
|
||||
if (movePool.length) {
|
||||
// If there's only 1 move in the move pool, use it.
|
||||
if (movePool.length === 1) {
|
||||
return { move: movePool[0].moveId, targets: this.getNextTargets(movePool[0].moveId) };
|
||||
return { move: movePool[0].moveId, targets: this.getNextTargets(movePool[0].moveId) , useType: MoveUseType.NORMAL};
|
||||
}
|
||||
// If a move is forced because of Encore, use it.
|
||||
// Said moves are executed normally
|
||||
const encoreTag = this.getTag(EncoreTag) as EncoreTag;
|
||||
if (encoreTag) {
|
||||
const encoreMove = movePool.find(m => m.moveId === encoreTag.moveId);
|
||||
@ -7245,13 +7279,14 @@ export class EnemyPokemon extends Pokemon {
|
||||
return {
|
||||
move: encoreMove.moveId,
|
||||
targets: this.getNextTargets(encoreMove.moveId),
|
||||
useType: MoveUseType.NORMAL
|
||||
};
|
||||
}
|
||||
}
|
||||
switch (this.aiType) {
|
||||
case AiType.RANDOM: // No enemy should spawn with this AI type in-game
|
||||
const moveId = movePool[globalScene.randBattleSeedInt(movePool.length)].moveId;
|
||||
return { move: moveId, targets: this.getNextTargets(moveId) };
|
||||
return { move: moveId, targets: this.getNextTargets(moveId), useType: MoveUseType.NORMAL };
|
||||
case AiType.SMART_RANDOM:
|
||||
case AiType.SMART:
|
||||
/**
|
||||
@ -7430,13 +7465,15 @@ export class EnemyPokemon extends Pokemon {
|
||||
}
|
||||
}
|
||||
console.log(movePool.map(m => m.getName()), moveScores, r, sortedMovePool.map(m => m.getName()));
|
||||
return { move: sortedMovePool[r]!.moveId, targets: moveTargets[sortedMovePool[r]!.moveId] };
|
||||
return { move: sortedMovePool[r]!.moveId, targets: moveTargets[sortedMovePool[r]!.moveId], useType: MoveUseType.NORMAL };
|
||||
}
|
||||
}
|
||||
|
||||
// No moves left means struggle
|
||||
return {
|
||||
move: Moves.STRUGGLE,
|
||||
targets: this.getNextTargets(Moves.STRUGGLE),
|
||||
useType: MoveUseType.IGNORE_PP,
|
||||
};
|
||||
}
|
||||
|
||||
@ -7796,10 +7833,9 @@ interface IllusionData {
|
||||
export interface TurnMove {
|
||||
move: Moves;
|
||||
targets: BattlerIndex[];
|
||||
useType: MoveUseType;
|
||||
result?: MoveResult;
|
||||
virtual?: boolean;
|
||||
turn?: number;
|
||||
ignorePP?: boolean;
|
||||
}
|
||||
|
||||
export interface AttackMoveResult {
|
||||
@ -7818,6 +7854,12 @@ export interface AttackMoveResult {
|
||||
export class PokemonSummonData {
|
||||
/** [Atk, Def, SpAtk, SpDef, Spd, Acc, Eva] */
|
||||
public statStages: number[] = [0, 0, 0, 0, 0, 0, 0];
|
||||
/**
|
||||
* A queue of moves yet to be executed, used by charging, recharging and frenzy moves.
|
||||
* So long as this array is nonempty, this Pokemon's corresponding `CommandPhase` will be skipped over entirely
|
||||
* in favor of using the queued move.
|
||||
* TODO: Clean up a lot of the code surrounding the move queue. It's intertwined with the
|
||||
*/
|
||||
public moveQueue: TurnMove[] = [];
|
||||
public tags: BattlerTag[] = [];
|
||||
public abilitySuppressed = false;
|
||||
@ -7938,7 +7980,6 @@ export class PokemonWaveData {
|
||||
* Resets at the start of a new turn, as well as on switch.
|
||||
*/
|
||||
export class PokemonTurnData {
|
||||
public flinched = false;
|
||||
public acted = false;
|
||||
/** How many times the current move should hit the target(s) */
|
||||
public hitCount = 0;
|
||||
@ -7960,8 +8001,9 @@ export class PokemonTurnData {
|
||||
public failedRunAway = false;
|
||||
public joinedRound = false;
|
||||
/**
|
||||
* The amount of times this Pokemon has acted again and used a move in the current turn.
|
||||
* Used to make sure multi-hits occur properly when the user is
|
||||
* forced to act again in the same turn
|
||||
* forced to act again in the same turn, and must be incremented by any effects that grant extra turns.
|
||||
*/
|
||||
public extraTurns = 0;
|
||||
/**
|
||||
@ -8038,10 +8080,9 @@ export class PokemonMove {
|
||||
public moveId: Moves;
|
||||
public ppUsed: number;
|
||||
public ppUp: number;
|
||||
public virtual: boolean;
|
||||
|
||||
/**
|
||||
* If defined and nonzero, overrides the maximum PP of the move (e.g., due to move being copied by Transform).
|
||||
* If defined and nonzero, overrides the maximum PP of the move (e.g. due to Transform).
|
||||
* This also nullifies all effects of `ppUp`.
|
||||
*/
|
||||
public maxPpOverride?: number;
|
||||
@ -8050,13 +8091,11 @@ export class PokemonMove {
|
||||
moveId: Moves,
|
||||
ppUsed = 0,
|
||||
ppUp = 0,
|
||||
virtual = false,
|
||||
maxPpOverride?: number,
|
||||
) {
|
||||
this.moveId = moveId;
|
||||
this.ppUsed = ppUsed;
|
||||
this.ppUp = ppUp;
|
||||
this.virtual = virtual;
|
||||
this.maxPpOverride = maxPpOverride;
|
||||
}
|
||||
|
||||
@ -8074,6 +8113,7 @@ export class PokemonMove {
|
||||
ignorePp = false,
|
||||
ignoreRestrictionTags = false,
|
||||
): boolean {
|
||||
// TODO: Add Sky Drop's 1 turn stall
|
||||
if (
|
||||
this.moveId &&
|
||||
!ignoreRestrictionTags &&
|
||||
@ -8096,10 +8136,10 @@ export class PokemonMove {
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets {@link ppUsed} for this move and ensures the value does not exceed {@link getMovePp}
|
||||
* @param count Amount of PP to use
|
||||
* Increments this move's {@linkcode ppUsed} variable (up to a maximum of {@link getMovePp}).
|
||||
* @param count - Amount of PP to consume; default `1`
|
||||
*/
|
||||
usePp(count: number = 1) {
|
||||
usePp(count = 1) {
|
||||
this.ppUsed = Math.min(this.ppUsed + count, this.getMovePp());
|
||||
}
|
||||
|
||||
@ -8128,7 +8168,6 @@ export class PokemonMove {
|
||||
source.moveId,
|
||||
source.ppUsed,
|
||||
source.ppUp,
|
||||
source.virtual,
|
||||
source.maxPpOverride,
|
||||
);
|
||||
}
|
||||
|
@ -23,6 +23,7 @@ import { MysteryEncounterMode } from "#enums/mystery-encounter-mode";
|
||||
import { isNullOrUndefined } from "#app/utils/common";
|
||||
import { ArenaTagSide } from "#app/data/arena-tag";
|
||||
import { ArenaTagType } from "#app/enums/arena-tag-type";
|
||||
import { MoveUseType } from "#enums/move-use-type";
|
||||
|
||||
export class CommandPhase extends FieldPhase {
|
||||
protected fieldIndex: number;
|
||||
@ -80,7 +81,7 @@ export class CommandPhase extends FieldPhase {
|
||||
) {
|
||||
globalScene.currentBattle.turnCommands[this.fieldIndex] = {
|
||||
command: Command.FIGHT,
|
||||
move: { move: Moves.NONE, targets: [] },
|
||||
move: { move: Moves.NONE, targets: [], useType: MoveUseType.NORMAL },
|
||||
skip: true,
|
||||
};
|
||||
}
|
||||
@ -103,29 +104,33 @@ export class CommandPhase extends FieldPhase {
|
||||
moveQueue.length &&
|
||||
moveQueue[0] &&
|
||||
moveQueue[0].move &&
|
||||
!moveQueue[0].virtual &&
|
||||
moveQueue[0].useType < MoveUseType.INDIRECT &&
|
||||
(!playerPokemon.getMoveset().find(m => m.moveId === moveQueue[0].move) ||
|
||||
!playerPokemon
|
||||
.getMoveset()
|
||||
[playerPokemon.getMoveset().findIndex(m => m.moveId === moveQueue[0].move)].isUsable(
|
||||
playerPokemon,
|
||||
moveQueue[0].ignorePP,
|
||||
moveQueue[0].useType >= MoveUseType.IGNORE_PP,
|
||||
))
|
||||
) {
|
||||
moveQueue.shift();
|
||||
}
|
||||
|
||||
// TODO: Refactor this. I did a few simple find/replace matches but this is just ABHORRENTLY structured
|
||||
if (moveQueue.length > 0) {
|
||||
const queuedMove = moveQueue[0];
|
||||
if (!queuedMove.move) {
|
||||
this.handleCommand(Command.FIGHT, -1);
|
||||
this.handleCommand(Command.FIGHT, -1, MoveUseType.NORMAL);
|
||||
} else {
|
||||
const moveIndex = playerPokemon.getMoveset().findIndex(m => m.moveId === queuedMove.move);
|
||||
if (
|
||||
(moveIndex > -1 && playerPokemon.getMoveset()[moveIndex].isUsable(playerPokemon, queuedMove.ignorePP)) ||
|
||||
queuedMove.virtual
|
||||
(moveIndex > -1 &&
|
||||
playerPokemon
|
||||
.getMoveset()
|
||||
[moveIndex].isUsable(playerPokemon, queuedMove.useType >= MoveUseType.IGNORE_PP)) ||
|
||||
queuedMove.useType < MoveUseType.INDIRECT
|
||||
) {
|
||||
this.handleCommand(Command.FIGHT, moveIndex, queuedMove.ignorePP, queuedMove);
|
||||
this.handleCommand(Command.FIGHT, moveIndex, queuedMove.useType, queuedMove);
|
||||
} else {
|
||||
globalScene.ui.setMode(UiMode.COMMAND, this.fieldIndex);
|
||||
}
|
||||
@ -143,18 +148,23 @@ export class CommandPhase extends FieldPhase {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* TODO: Remove `args` and clean this thing up
|
||||
* Code will need to be copied over from pkty except replacing the `virtual` and `ignorePP` args with a corresponding `MoveUseType`.
|
||||
*/
|
||||
handleCommand(command: Command, cursor: number, ...args: any[]): boolean {
|
||||
const playerPokemon = globalScene.getPlayerField()[this.fieldIndex];
|
||||
let success = false;
|
||||
|
||||
switch (command) {
|
||||
// TODO: We don't need 2 args for this - moveUseType is carried over from queuedMove
|
||||
case Command.TERA:
|
||||
case Command.FIGHT:
|
||||
let useStruggle = false;
|
||||
const turnMove: TurnMove | undefined = args.length === 2 ? (args[1] as TurnMove) : undefined;
|
||||
if (
|
||||
cursor === -1 ||
|
||||
playerPokemon.trySelectMove(cursor, args[0] as boolean) ||
|
||||
playerPokemon.trySelectMove(cursor, (args[0] as MoveUseType) >= MoveUseType.IGNORE_PP) ||
|
||||
(useStruggle = cursor > -1 && !playerPokemon.getMoveset().filter(m => m.isUsable(playerPokemon)).length)
|
||||
) {
|
||||
let moveId: Moves;
|
||||
@ -171,7 +181,7 @@ export class CommandPhase extends FieldPhase {
|
||||
const turnCommand: TurnCommand = {
|
||||
command: Command.FIGHT,
|
||||
cursor: cursor,
|
||||
move: { move: moveId, targets: [], ignorePP: args[0] },
|
||||
move: { move: moveId, targets: [], useType: args[0] },
|
||||
args: args,
|
||||
};
|
||||
const preTurnCommand: TurnCommand = {
|
||||
|
@ -1,7 +1,7 @@
|
||||
import { globalScene } from "#app/global-scene";
|
||||
import type { BattlerIndex } from "#app/battle";
|
||||
import { MoveChargeAnim } from "#app/data/battle-anims";
|
||||
import { applyMoveChargeAttrs, MoveEffectAttr, InstantChargeAttr } from "#app/data/moves/move";
|
||||
import { applyMoveChargeAttrs, MoveEffectAttr, InstantChargeAttr, type ChargingMove } from "#app/data/moves/move";
|
||||
import type { PokemonMove } from "#app/field/pokemon";
|
||||
import type Pokemon from "#app/field/pokemon";
|
||||
import { MoveResult } from "#app/field/pokemon";
|
||||
@ -10,10 +10,10 @@ import { MovePhase } from "#app/phases/move-phase";
|
||||
import { PokemonPhase } from "#app/phases/pokemon-phase";
|
||||
import { BattlerTagType } from "#enums/battler-tag-type";
|
||||
import { MoveEndPhase } from "#app/phases/move-end-phase";
|
||||
import type { MoveUseType } from "#enums/move-use-type";
|
||||
|
||||
/**
|
||||
* Phase for the "charging turn" of two-turn moves (e.g. Dig).
|
||||
* @extends {@linkcode PokemonPhase}
|
||||
*/
|
||||
export class MoveChargePhase extends PokemonPhase {
|
||||
/** The move instance that this phase applies */
|
||||
@ -21,10 +21,21 @@ export class MoveChargePhase extends PokemonPhase {
|
||||
/** The field index targeted by the move (Charging moves assume single target) */
|
||||
public targetIndex: BattlerIndex;
|
||||
|
||||
constructor(battlerIndex: BattlerIndex, targetIndex: BattlerIndex, move: PokemonMove) {
|
||||
/** The {@linkcode MoveUseType} of the move that triggered the charge; passed on from move phase */
|
||||
private useType: MoveUseType;
|
||||
|
||||
/**
|
||||
* Create a new MoveChargePhase.
|
||||
* @param battlerIndex - The {@linkcode BattlerIndex} of the user.
|
||||
* @param targetIndex - The {@linkcode BattlerIndex} of the target.
|
||||
* @param move - The {@linkcode PokemonMove} being used
|
||||
* @param useType - The move's {@linkcode MoveUseType}
|
||||
*/
|
||||
constructor(battlerIndex: BattlerIndex, targetIndex: BattlerIndex, move: PokemonMove, useType: MoveUseType) {
|
||||
super(battlerIndex);
|
||||
this.move = move;
|
||||
this.targetIndex = targetIndex;
|
||||
this.useType = useType;
|
||||
}
|
||||
|
||||
public override start() {
|
||||
@ -38,7 +49,8 @@ export class MoveChargePhase extends PokemonPhase {
|
||||
// immediately end this phase.
|
||||
if (!target || !move.isChargingMove()) {
|
||||
console.warn("Invalid parameters for MoveChargePhase");
|
||||
return super.end();
|
||||
super.end();
|
||||
return;
|
||||
}
|
||||
|
||||
new MoveChargeAnim(move.chargeAnim, move.id, user).play(false, () => {
|
||||
@ -53,30 +65,27 @@ export class MoveChargePhase extends PokemonPhase {
|
||||
/** Checks the move's instant charge conditions, then ends this phase. */
|
||||
public override end() {
|
||||
const user = this.getUserPokemon();
|
||||
const move = this.move.getMove();
|
||||
const move = this.move.getMove() as ChargingMove;
|
||||
|
||||
if (move.isChargingMove()) {
|
||||
const instantCharge = new BooleanHolder(false);
|
||||
const instantCharge = new BooleanHolder(false);
|
||||
applyMoveChargeAttrs(InstantChargeAttr, user, null, move, instantCharge);
|
||||
|
||||
applyMoveChargeAttrs(InstantChargeAttr, user, null, move, instantCharge);
|
||||
|
||||
if (instantCharge.value) {
|
||||
// this MoveEndPhase will be duplicated by the queued MovePhase if not removed
|
||||
globalScene.tryRemovePhase(phase => phase instanceof MoveEndPhase && phase.getPokemon() === user);
|
||||
// queue a new MovePhase for this move's attack phase
|
||||
globalScene.unshiftPhase(new MovePhase(user, [this.targetIndex], this.move, false));
|
||||
} else {
|
||||
user.getMoveQueue().push({ move: move.id, targets: [this.targetIndex] });
|
||||
}
|
||||
|
||||
// Add this move's charging phase to the user's move history
|
||||
user.pushMoveHistory({
|
||||
move: this.move.moveId,
|
||||
targets: [this.targetIndex],
|
||||
result: MoveResult.OTHER,
|
||||
});
|
||||
// If instantly charging, remove the pending MoveEndPhase and queue a new MovePhase for the "attack" portion of the move.
|
||||
// Otherwise, add the attack portion to the user's move queue to execute next turn.
|
||||
if (instantCharge.value) {
|
||||
globalScene.tryRemovePhase(phase => phase instanceof MoveEndPhase && phase.getPokemon() === user);
|
||||
globalScene.unshiftPhase(new MovePhase(user, [this.targetIndex], this.move, this.useType));
|
||||
} else {
|
||||
user.pushMoveQueue({ move: move.id, targets: [this.targetIndex], useType: this.useType });
|
||||
}
|
||||
super.end();
|
||||
|
||||
// Add this move's charging phase to the user's move history
|
||||
user.pushMoveHistory({
|
||||
move: this.move.moveId,
|
||||
targets: [this.targetIndex],
|
||||
result: MoveResult.OTHER,
|
||||
useType: this.useType,
|
||||
});
|
||||
}
|
||||
|
||||
public getUserPokemon(): Pokemon {
|
||||
|
@ -48,7 +48,7 @@ import { MoveTarget } from "#enums/MoveTarget";
|
||||
import { MoveCategory } from "#enums/MoveCategory";
|
||||
import { SpeciesFormChangePostMoveTrigger } from "#app/data/pokemon-forms";
|
||||
import { PokemonType } from "#enums/pokemon-type";
|
||||
import { DamageResult, PokemonMove, type TurnMove } from "#app/field/pokemon";
|
||||
import { type DamageResult, PokemonMove, type TurnMove } from "#app/field/pokemon";
|
||||
import type Pokemon from "#app/field/pokemon";
|
||||
import { HitResult, MoveResult } from "#app/field/pokemon";
|
||||
import { getPokemonNameWithAffix } from "#app/messages";
|
||||
@ -72,25 +72,30 @@ import { ShowAbilityPhase } from "./show-ability-phase";
|
||||
import { MovePhase } from "./move-phase";
|
||||
import { MoveEndPhase } from "./move-end-phase";
|
||||
import { HideAbilityPhase } from "#app/phases/hide-ability-phase";
|
||||
import { TypeDamageMultiplier } from "#app/data/type";
|
||||
import type { TypeDamageMultiplier } from "#app/data/type";
|
||||
import { HitCheckResult } from "#enums/hit-check-result";
|
||||
import type Move from "#app/data/moves/move";
|
||||
import { isFieldTargeted } from "#app/data/moves/move-utils";
|
||||
import { FaintPhase } from "./faint-phase";
|
||||
import { DamageAchv } from "#app/system/achv";
|
||||
import { MoveUseType } from "#enums/move-use-type";
|
||||
|
||||
type HitCheckEntry = [HitCheckResult, TypeDamageMultiplier];
|
||||
export type HitCheckEntry = [HitCheckResult, TypeDamageMultiplier];
|
||||
|
||||
export class MoveEffectPhase extends PokemonPhase {
|
||||
public move: Move;
|
||||
private virtual = false;
|
||||
protected targets: BattlerIndex[];
|
||||
protected reflected = false;
|
||||
protected useType: MoveUseType;
|
||||
|
||||
/** The result of the hit check against each target */
|
||||
private hitChecks: HitCheckEntry[];
|
||||
|
||||
/** The move history entry for the move */
|
||||
/**
|
||||
* Log to be entered into the user's move history once the move result is resolved.
|
||||
|
||||
* Note that `result` logs whether the move was successfully
|
||||
* used in the sense of "Does it have an effect on the user?".
|
||||
*/
|
||||
private moveHistoryEntry: TurnMove;
|
||||
|
||||
/** Is this the first strike of a move? */
|
||||
@ -98,19 +103,20 @@ export class MoveEffectPhase extends PokemonPhase {
|
||||
/** Is this the last strike of a move? */
|
||||
private lastHit: boolean;
|
||||
|
||||
/** Phases queued during moves */
|
||||
/**
|
||||
* Phases queued during moves; used to add a new MovePhase for reflected moves after triggering.
|
||||
* TODO: Remove this and move the reflection logic to ability-side
|
||||
*/
|
||||
private queuedPhases: Phase[] = [];
|
||||
|
||||
/**
|
||||
* @param reflected Indicates that the move was reflected by the user due to magic coat or magic bounce
|
||||
* @param virtual Indicates that the move is a virtual move (i.e. called by metronome)
|
||||
* @param useType - The {@linkcode MoveUseType} corresponding to how this move was used.
|
||||
*/
|
||||
constructor(battlerIndex: BattlerIndex, targets: BattlerIndex[], move: Move, reflected = false, virtual = false) {
|
||||
constructor(battlerIndex: BattlerIndex, targets: BattlerIndex[], move: Move, useType: MoveUseType) {
|
||||
super(battlerIndex);
|
||||
this.move = move;
|
||||
this.virtual = virtual;
|
||||
this.useType = useType;
|
||||
|
||||
this.reflected = reflected;
|
||||
/**
|
||||
* In double battles, if the right Pokemon selects a spread move and the left Pokemon dies
|
||||
* with no party members available to switch in, then the right Pokemon takes the index
|
||||
@ -129,11 +135,11 @@ export class MoveEffectPhase extends PokemonPhase {
|
||||
* Compute targets and the results of hit checks of the invoked move against all targets,
|
||||
* organized by battler index.
|
||||
*
|
||||
* **This is *not* a pure function**; it has the following side effects
|
||||
* - `this.hitChecks` - The results of the hit checks against each target
|
||||
* - `this.moveHistoryEntry` - Sets success or failure based on the hit check results
|
||||
* - user.turnData.hitCount and user.turnData.hitsLeft - Both set to 1 if the
|
||||
* move was unsuccessful against all targets
|
||||
* **This is *not* a pure function** and has the following side effects:
|
||||
* - Sets `this.hitChecks` to the results of the hit checks against each target
|
||||
* - Sets success/failure of `this.moveHistoryEntry` based on the hit check results
|
||||
* - Sets `user.turnData.hitCount` and `user.turnData.hitsLeft` to 1 if the move
|
||||
* was unsuccessful against all targets (effectively canceling it)
|
||||
*
|
||||
* @returns The targets of the invoked move
|
||||
* @see {@linkcode hitCheck}
|
||||
@ -181,7 +187,7 @@ export class MoveEffectPhase extends PokemonPhase {
|
||||
* Queue the phaes that should occur when the target reflects the move back to the user
|
||||
* @param user - The {@linkcode Pokemon} using this phase's invoked move
|
||||
* @param target - The {@linkcode Pokemon} that is reflecting the move
|
||||
*
|
||||
* TODO: Rework this to use `onApply` of Magic Coat
|
||||
*/
|
||||
private queueReflectedMove(user: Pokemon, target: Pokemon): void {
|
||||
const newTargets = this.move.isMultiTarget()
|
||||
@ -195,15 +201,13 @@ export class MoveEffectPhase extends PokemonPhase {
|
||||
this.queuedPhases.push(new HideAbilityPhase());
|
||||
}
|
||||
|
||||
this.queuedPhases.push(
|
||||
new MovePhase(target, newTargets, new PokemonMove(this.move.id, 0, 0, true), true, true, true),
|
||||
);
|
||||
this.queuedPhases.push(new MovePhase(target, newTargets, new PokemonMove(this.move.id), MoveUseType.REFLECTED));
|
||||
}
|
||||
|
||||
/**
|
||||
* Apply the move to each of the resolved targets.
|
||||
* Apply the move to each of its resolved targets.
|
||||
* @param targets - The resolved set of targets of the move
|
||||
* @throws Error if there was an unexpected hit check result
|
||||
* @throws - Error if there was an unexpected hit check result
|
||||
*/
|
||||
private applyToTargets(user: Pokemon, targets: Pokemon[]): void {
|
||||
for (const [i, target] of targets.entries()) {
|
||||
@ -225,6 +229,7 @@ export class MoveEffectPhase extends PokemonPhase {
|
||||
case HitCheckResult.NO_EFFECT_NO_MESSAGE:
|
||||
case HitCheckResult.PROTECTED:
|
||||
case HitCheckResult.TARGET_NOT_ON_FIELD:
|
||||
// Apply effects for ineffective moves (e.g. High Jump Kick crash dmg)
|
||||
applyMoveAttrs(NoEffectAttr, user, target, this.move);
|
||||
break;
|
||||
case HitCheckResult.MISS:
|
||||
@ -286,8 +291,18 @@ export class MoveEffectPhase extends PokemonPhase {
|
||||
const overridden = new BooleanHolder(false);
|
||||
const move = this.move;
|
||||
|
||||
// Assume single target for override
|
||||
applyMoveAttrs(OverrideMoveEffectAttr, user, this.getFirstTarget() ?? null, move, overridden, this.virtual);
|
||||
// Apply effects to override a move effect.
|
||||
// Assuming single target here works as this is (currently)
|
||||
// only used for Future Sight and Pledge moves.
|
||||
// TODO: change if any other move effect overrides are introduced
|
||||
applyMoveAttrs(
|
||||
OverrideMoveEffectAttr,
|
||||
user,
|
||||
this.getFirstTarget() ?? null,
|
||||
move,
|
||||
overridden,
|
||||
this.useType >= MoveUseType.INDIRECT,
|
||||
);
|
||||
|
||||
// If other effects were overriden, stop this phase before they can be applied
|
||||
if (overridden.value) {
|
||||
@ -297,8 +312,8 @@ export class MoveEffectPhase extends PokemonPhase {
|
||||
// Lapse `MOVE_EFFECT` effects (i.e. semi-invulnerability) when applicable
|
||||
user.lapseTags(BattlerTagLapseType.MOVE_EFFECT);
|
||||
|
||||
// If the user is acting again (such as due to Instruct), reset hitsLeft/hitCount so that
|
||||
// the move executes correctly (ensures all hits of a multi-hit are properly calculated)
|
||||
// If the user is acting again (such as due to Instruct or Dancer), reset hitsLeft/hitCount and
|
||||
// recalculate hit count for multi-hit moves.
|
||||
if (user.turnData.hitsLeft === 0 && user.turnData.hitCount > 0 && user.turnData.extraTurns > 0) {
|
||||
user.turnData.hitsLeft = -1;
|
||||
user.turnData.hitCount = 0;
|
||||
@ -323,16 +338,11 @@ export class MoveEffectPhase extends PokemonPhase {
|
||||
user.turnData.hitsLeft = hitCount.value;
|
||||
}
|
||||
|
||||
/*
|
||||
* Log to be entered into the user's move history once the move result is resolved.
|
||||
* Note that `result` logs whether the move was successfully
|
||||
* used in the sense of "Does it have an effect on the user?".
|
||||
*/
|
||||
this.moveHistoryEntry = {
|
||||
move: this.move.id,
|
||||
targets: this.targets,
|
||||
result: MoveResult.PENDING,
|
||||
virtual: this.virtual,
|
||||
useType: this.useType,
|
||||
};
|
||||
|
||||
const fieldMove = isFieldTargeted(move);
|
||||
@ -396,29 +406,35 @@ export class MoveEffectPhase extends PokemonPhase {
|
||||
|
||||
public override end(): void {
|
||||
const user = this.getUserPokemon();
|
||||
/**
|
||||
* If this phase isn't for the invoked move's last strike,
|
||||
* unshift another MoveEffectPhase for the next strike.
|
||||
* Otherwise, queue a message indicating the number of times the move has struck
|
||||
* (if the move has struck more than once), then apply the heal from Shell Bell
|
||||
* to the user.
|
||||
*/
|
||||
if (user) {
|
||||
if (user.turnData.hitsLeft && --user.turnData.hitsLeft >= 1 && this.getFirstTarget()?.isActive()) {
|
||||
globalScene.unshiftPhase(this.getNewHitPhase());
|
||||
} else {
|
||||
// Queue message for number of hits made by multi-move
|
||||
// If multi-hit attack only hits once, still want to render a message
|
||||
const hitsTotal = user.turnData.hitCount - Math.max(user.turnData.hitsLeft, 0);
|
||||
if (hitsTotal > 1 || (user.turnData.hitsLeft && user.turnData.hitsLeft > 0)) {
|
||||
// If there are multiple hits, or if there are hits of the multi-hit move left
|
||||
globalScene.queueMessage(i18next.t("battle:attackHitsCount", { count: hitsTotal }));
|
||||
}
|
||||
globalScene.applyModifiers(HitHealModifier, this.player, user);
|
||||
this.getTargets().forEach(target => (target.turnData.moveEffectiveness = null));
|
||||
}
|
||||
if (!user) {
|
||||
super.end();
|
||||
return;
|
||||
}
|
||||
|
||||
/**
|
||||
* If this phase isn't for the invoked move's last strike (and we still have something to hit),
|
||||
* unshift another MoveEffectPhase for the next strike before ending this phase.
|
||||
*/
|
||||
if (--user.turnData.hitsLeft >= 1 && this.getFirstTarget()) {
|
||||
globalScene.unshiftPhase(this.getNewHitPhase());
|
||||
super.end();
|
||||
return;
|
||||
}
|
||||
|
||||
/**
|
||||
* All hits of the move have resolved by now.
|
||||
* Queue message for multi-strike moves before applying Shell Bell heals & proccing Dancer-like effects.
|
||||
*/
|
||||
const hitsTotal = user.turnData.hitCount - Math.max(user.turnData.hitsLeft, 0);
|
||||
if (hitsTotal > 1 || user.turnData.hitsLeft > 0) {
|
||||
// Queue message if multiple hits occurred or were slated to occur (such as a Triple Axel miss)
|
||||
globalScene.queueMessage(i18next.t("battle:attackHitsCount", { count: hitsTotal }));
|
||||
}
|
||||
|
||||
globalScene.applyModifiers(HitHealModifier, this.player, user);
|
||||
this.getTargets().forEach(target => {
|
||||
target.turnData.moveEffectiveness = null;
|
||||
});
|
||||
super.end();
|
||||
}
|
||||
|
||||
@ -428,7 +444,6 @@ export class MoveEffectPhase extends PokemonPhase {
|
||||
* @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): void {
|
||||
applyPostDefendAbAttrs(PostDefendAbAttr, target, user, this.move, hitResult);
|
||||
@ -440,7 +455,6 @@ export class MoveEffectPhase extends PokemonPhase {
|
||||
* @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 {
|
||||
if (this.move.hasAttr(FlinchAttr)) {
|
||||
@ -460,8 +474,9 @@ export class MoveEffectPhase extends PokemonPhase {
|
||||
* @param user - The {@linkcode Pokemon} using this phase's invoked move
|
||||
* @param target - {@linkcode Pokemon} the target to check for protection
|
||||
* @param move - The {@linkcode Move} being used
|
||||
* @returns Whether the pokemon was protected
|
||||
*/
|
||||
private protectedCheck(user: Pokemon, target: Pokemon) {
|
||||
private protectedCheck(user: Pokemon, target: Pokemon): boolean {
|
||||
/** 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)? */
|
||||
@ -482,14 +497,15 @@ export class MoveEffectPhase extends PokemonPhase {
|
||||
);
|
||||
}
|
||||
|
||||
// TODO: Break up this chunky boolean to make it more palatable
|
||||
return (
|
||||
![MoveTarget.ENEMY_SIDE, MoveTarget.BOTH_SIDES].includes(this.move.moveTarget) &&
|
||||
(bypassIgnoreProtect.value || !this.move.doesFlagEffectApply({ flag: 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))) ||
|
||||
target.findTags(t => t instanceof ProtectedTag).some(t => target.lapseTag(t.tagType))) ||
|
||||
(this.move.category !== MoveCategory.STATUS &&
|
||||
target.findTags(t => t instanceof DamageProtectedTag).find(t => target.lapseTag(t.tagType))))
|
||||
target.findTags(t => t instanceof DamageProtectedTag).some(t => target.lapseTag(t.tagType))))
|
||||
);
|
||||
}
|
||||
|
||||
@ -549,7 +565,11 @@ export class MoveEffectPhase extends PokemonPhase {
|
||||
return [HitCheckResult.PROTECTED, 0];
|
||||
}
|
||||
|
||||
if (!this.reflected && move.doesFlagEffectApply({ flag: MoveFlags.REFLECTABLE, user, target })) {
|
||||
// Reflected moves cannot be reflected again
|
||||
if (
|
||||
this.useType < MoveUseType.REFLECTED &&
|
||||
move.doesFlagEffectApply({ flag: MoveFlags.REFLECTABLE, user, target })
|
||||
) {
|
||||
return [HitCheckResult.REFLECTED, 0];
|
||||
}
|
||||
|
||||
@ -623,22 +643,19 @@ export class MoveEffectPhase extends PokemonPhase {
|
||||
if (!user) {
|
||||
return false;
|
||||
}
|
||||
if (user.hasAbilityWithAttr(AlwaysHitAbAttr) || target.hasAbilityWithAttr(AlwaysHitAbAttr)) {
|
||||
return true;
|
||||
}
|
||||
if (this.move.hasAttr(ToxicAccuracyAttr) && user.isOfType(PokemonType.POISON)) {
|
||||
return true;
|
||||
}
|
||||
// TODO: Fix lock on / mind reader check.
|
||||
if (
|
||||
user.getTag(BattlerTagType.IGNORE_ACCURACY) &&
|
||||
(user.getLastXMoves().find(() => true)?.targets || []).indexOf(target.getBattlerIndex()) !== -1
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
if (isFieldTargeted(this.move)) {
|
||||
return true;
|
||||
|
||||
switch (true) {
|
||||
// No Guard
|
||||
case user.hasAbilityWithAttr(AlwaysHitAbAttr) || target.hasAbilityWithAttr(AlwaysHitAbAttr):
|
||||
// Toxic as poison type
|
||||
case this.move.hasAttr(ToxicAccuracyAttr) && user.isOfType(PokemonType.POISON):
|
||||
// Lock On/Mind Reader
|
||||
case !!user.getTag(BattlerTagType.IGNORE_ACCURACY):
|
||||
// Spikes and company
|
||||
case isFieldTargeted(this.move):
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
@ -662,12 +679,17 @@ export class MoveEffectPhase extends PokemonPhase {
|
||||
return (this.player ? globalScene.getPlayerField() : globalScene.getEnemyField())[this.fieldIndex];
|
||||
}
|
||||
|
||||
/** @returns An array of all {@linkcode Pokemon} targeted by this phase's invoked move */
|
||||
/**
|
||||
* @returns An array of {@linkcode Pokemon} that are:
|
||||
* - On-field and active
|
||||
* - Non-fainted
|
||||
* - Targeted by this phase's invoked move
|
||||
*/
|
||||
public getTargets(): Pokemon[] {
|
||||
return globalScene.getField(true).filter(p => this.targets.indexOf(p.getBattlerIndex()) > -1);
|
||||
}
|
||||
|
||||
/** @returns The first target of this phase's invoked move */
|
||||
/** @returns The first active, non-fainted target of this phase's invoked move. */
|
||||
public getFirstTarget(): Pokemon | undefined {
|
||||
return this.getTargets()[0];
|
||||
}
|
||||
@ -709,7 +731,7 @@ export class MoveEffectPhase extends PokemonPhase {
|
||||
|
||||
/** @returns A new `MoveEffectPhase` with the same properties as this phase */
|
||||
protected getNewHitPhase(): MoveEffectPhase {
|
||||
return new MoveEffectPhase(this.battlerIndex, this.targets, this.move, this.reflected, this.virtual);
|
||||
return new MoveEffectPhase(this.battlerIndex, this.targets, this.move, this.useType);
|
||||
}
|
||||
|
||||
/** Removes all substitutes that were broken by this phase's invoked move */
|
||||
@ -731,7 +753,6 @@ export class MoveEffectPhase extends PokemonPhase {
|
||||
* @param firstTarget Whether the target is the first to be hit by the current strike
|
||||
* @param selfTarget If defined, limits the effects triggered to either self-targeted
|
||||
* effects (if set to `true`) or targeted effects (if set to `false`).
|
||||
* @returns a `Promise` applying the relevant move effects.
|
||||
*/
|
||||
protected triggerMoveEffects(
|
||||
triggerType: MoveEffectTrigger,
|
||||
@ -780,6 +801,7 @@ export class MoveEffectPhase extends PokemonPhase {
|
||||
|
||||
const hitResult = this.applyMove(user, target, effectiveness);
|
||||
|
||||
// Apply effects to the user (always) and the target (if not blocked by substitute).
|
||||
this.triggerMoveEffects(MoveEffectTrigger.POST_APPLY, user, target, firstTarget, true);
|
||||
if (!this.move.hitsSubstitute(user, target)) {
|
||||
this.applyOnTargetEffects(user, target, hitResult, firstTarget);
|
||||
@ -810,7 +832,7 @@ export class MoveEffectPhase extends PokemonPhase {
|
||||
*/
|
||||
applyMoveAttrs(StatChangeBeforeDmgCalcAttr, user, target, this.move);
|
||||
|
||||
const { result: result, damage: dmg } = target.getAttackDamage({
|
||||
const { result, damage: dmg } = target.getAttackDamage({
|
||||
source: user,
|
||||
move: this.move,
|
||||
ignoreAbility: false,
|
||||
|
@ -24,9 +24,9 @@ export class MoveEndPhase extends PokemonPhase {
|
||||
if (!this.wasFollowUp && pokemon?.isActive(true)) {
|
||||
pokemon.lapseTags(BattlerTagLapseType.AFTER_MOVE);
|
||||
}
|
||||
globalScene.arena.setIgnoreAbilities(false);
|
||||
|
||||
// Remove effects which were set on a Pokemon which removes them on summon (i.e. via Mold Breaker)
|
||||
globalScene.arena.setIgnoreAbilities(false);
|
||||
for (const target of this.targets) {
|
||||
if (target) {
|
||||
applyPostSummonAbAttrs(PostSummonRemoveEffectAbAttr, target);
|
||||
|
@ -50,17 +50,19 @@ import { BattlerTagType } from "#enums/battler-tag-type";
|
||||
import { Moves } from "#enums/moves";
|
||||
import { StatusEffect } from "#enums/status-effect";
|
||||
import i18next from "i18next";
|
||||
import { MoveUseType } from "#enums/move-use-type";
|
||||
|
||||
export class MovePhase extends BattlePhase {
|
||||
protected _pokemon: Pokemon;
|
||||
protected _move: PokemonMove;
|
||||
protected _targets: BattlerIndex[];
|
||||
protected followUp: boolean;
|
||||
protected ignorePp: boolean;
|
||||
protected _useType: MoveUseType;
|
||||
protected forcedLast: boolean;
|
||||
|
||||
/** Whether the current move should fail but still use PP */
|
||||
protected failed = false;
|
||||
/** Whether the current move should cancel and retain PP */
|
||||
protected cancelled = false;
|
||||
protected reflected = false;
|
||||
|
||||
public get pokemon(): Pokemon {
|
||||
return this._pokemon;
|
||||
@ -78,6 +80,14 @@ export class MovePhase extends BattlePhase {
|
||||
this._move = move;
|
||||
}
|
||||
|
||||
public get useType(): MoveUseType {
|
||||
return this._useType;
|
||||
}
|
||||
|
||||
protected set useType(useType: MoveUseType) {
|
||||
this._useType = useType;
|
||||
}
|
||||
|
||||
public get targets(): BattlerIndex[] {
|
||||
return this._targets;
|
||||
}
|
||||
@ -87,51 +97,42 @@ export class MovePhase extends BattlePhase {
|
||||
}
|
||||
|
||||
/**
|
||||
* @param followUp Indicates that the move being used is a "follow-up" - for example, a move being used by Metronome or Dancer.
|
||||
* Follow-ups bypass a few failure conditions, including flinches, sleep/paralysis/freeze and volatile status checks, etc.
|
||||
* @param reflected Indicates that the move was reflected by Magic Coat or Magic Bounce.
|
||||
* Reflected moves cannot be reflected again and will not trigger Dancer.
|
||||
* Create a new MovePhase for using moves.
|
||||
* @param pokemon - The {@linkcode Pokemon} using the move
|
||||
* @param move - The {@linkcode PokemonMove} to use
|
||||
* @param useType - The {@linkcode MoveUseType} corresponding to this move's means of execution (usually `MoveUseType.NORMAL`).
|
||||
* Not marked optional to ensure callers correctly pass on `useTypes`.
|
||||
* @param forcedLast - Whether to force this phase to occur last in order (for {@linkcode Moves.QUASH}); default `false`
|
||||
*/
|
||||
|
||||
constructor(
|
||||
pokemon: Pokemon,
|
||||
targets: BattlerIndex[],
|
||||
move: PokemonMove,
|
||||
followUp = false,
|
||||
ignorePp = false,
|
||||
reflected = false,
|
||||
forcedLast = false,
|
||||
) {
|
||||
constructor(pokemon: Pokemon, targets: BattlerIndex[], move: PokemonMove, useType: MoveUseType, forcedLast = false) {
|
||||
super();
|
||||
|
||||
this.pokemon = pokemon;
|
||||
this.targets = targets;
|
||||
this.move = move;
|
||||
this.followUp = followUp;
|
||||
this.ignorePp = ignorePp;
|
||||
this.reflected = reflected;
|
||||
this.useType = useType;
|
||||
this.forcedLast = forcedLast;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the pokemon is active, if the move is usable, and that the move is targetting something.
|
||||
* Checks if the pokemon is active, if the move is usable, and that the move is targeting something.
|
||||
* @param ignoreDisableTags `true` to not check if the move is disabled
|
||||
* @returns `true` if all the checks pass
|
||||
*/
|
||||
public canMove(ignoreDisableTags = false): boolean {
|
||||
return (
|
||||
this.pokemon.isActive(true) &&
|
||||
this.move.isUsable(this.pokemon, this.ignorePp, ignoreDisableTags) &&
|
||||
this.move.isUsable(this.pokemon, this.useType >= MoveUseType.IGNORE_PP, ignoreDisableTags) &&
|
||||
!!this.targets.length
|
||||
);
|
||||
}
|
||||
|
||||
/**Signifies the current move should fail but still use PP */
|
||||
/** Signifies the current move should fail but still use PP */
|
||||
public fail(): void {
|
||||
this.failed = true;
|
||||
}
|
||||
|
||||
/**Signifies the current move should cancel and retain PP */
|
||||
/** Signifies the current move should cancel and retain PP */
|
||||
public cancel(): void {
|
||||
this.cancelled = true;
|
||||
}
|
||||
@ -139,7 +140,7 @@ export class MovePhase extends BattlePhase {
|
||||
/**
|
||||
* Shows whether the current move has been forced to the end of the turn
|
||||
* Needed for speed order, see {@linkcode Moves.QUASH}
|
||||
* */
|
||||
*/
|
||||
public isForcedLast(): boolean {
|
||||
return this.forcedLast;
|
||||
}
|
||||
@ -147,7 +148,7 @@ export class MovePhase extends BattlePhase {
|
||||
public start(): void {
|
||||
super.start();
|
||||
|
||||
console.log(Moves[this.move.moveId]);
|
||||
console.log(Moves[this.move.moveId], MoveUseType[this.useType]);
|
||||
|
||||
// Check if move is unusable (e.g. because it's out of PP due to a mid-turn Spite).
|
||||
if (!this.canMove(true)) {
|
||||
@ -156,26 +157,27 @@ export class MovePhase extends BattlePhase {
|
||||
this.showMoveText();
|
||||
this.showFailedText();
|
||||
}
|
||||
return this.end();
|
||||
this.end();
|
||||
return;
|
||||
}
|
||||
|
||||
this.pokemon.turnData.acted = true;
|
||||
|
||||
// Reset hit-related turn data when starting follow-up moves (e.g. Metronomed moves, Dancer repeats)
|
||||
if (this.followUp) {
|
||||
if (this.useType >= MoveUseType.INDIRECT) {
|
||||
this.pokemon.turnData.hitsLeft = -1;
|
||||
this.pokemon.turnData.hitCount = 0;
|
||||
}
|
||||
|
||||
// Check move to see if arena.ignoreAbilities should be true.
|
||||
if (!this.followUp || this.reflected) {
|
||||
if (
|
||||
this.move
|
||||
.getMove()
|
||||
.doesFlagEffectApply({ flag: MoveFlags.IGNORE_ABILITIES, user: this.pokemon, isFollowUp: this.followUp })
|
||||
) {
|
||||
globalScene.arena.setIgnoreAbilities(true, this.pokemon.getBattlerIndex());
|
||||
}
|
||||
if (
|
||||
this.move.getMove().doesFlagEffectApply({
|
||||
flag: MoveFlags.IGNORE_ABILITIES,
|
||||
user: this.pokemon,
|
||||
isFollowUp: this.useType >= MoveUseType.INDIRECT, // Sunsteel strike and co. don't work when called indirectly
|
||||
})
|
||||
) {
|
||||
globalScene.arena.setIgnoreAbilities(true, this.pokemon.getBattlerIndex());
|
||||
}
|
||||
|
||||
this.resolveRedirectTarget();
|
||||
@ -190,6 +192,7 @@ export class MovePhase extends BattlePhase {
|
||||
this.resolveFinalPreMoveCancellationChecks();
|
||||
}
|
||||
|
||||
// Cancel, charge or use the move as applicable.
|
||||
if (this.cancelled || this.failed) {
|
||||
this.handlePreMoveFailures();
|
||||
} else if (this.move.getMove().isChargingMove() && !this.pokemon.getTag(BattlerTagType.CHARGING)) {
|
||||
@ -208,7 +211,7 @@ export class MovePhase extends BattlePhase {
|
||||
|
||||
if (
|
||||
(targets.length === 0 && !this.move.getMove().hasAttr(AddArenaTrapTagAttr)) ||
|
||||
(moveQueue.length && moveQueue[0].move === Moves.NONE)
|
||||
(moveQueue.length > 0 && moveQueue[0].move === Moves.NONE)
|
||||
) {
|
||||
this.showMoveText();
|
||||
this.showFailedText();
|
||||
@ -221,81 +224,97 @@ export class MovePhase extends BattlePhase {
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles {@link StatusEffect.SLEEP Sleep}/{@link StatusEffect.PARALYSIS Paralysis}/{@link StatusEffect.FREEZE Freeze} rolls and side effects.
|
||||
* Handles {@link StatusEffect.SLEEP | Sleep}/{@link StatusEffect.PARALYSIS | Paralysis}/{@link StatusEffect.FREEZE | Freeze} rolls and side effects.
|
||||
*/
|
||||
protected resolvePreMoveStatusEffects(): void {
|
||||
if (!this.followUp && this.pokemon.status && !this.pokemon.status.isPostTurn()) {
|
||||
this.pokemon.status.incrementTurn();
|
||||
let activated = false;
|
||||
let healed = false;
|
||||
// Skip for follow ups/reflected moves, no status condition or post turn statuses (e.g. Poison/Toxic)
|
||||
if (!this.pokemon.status || this.pokemon.status?.isPostTurn() || this.useType >= MoveUseType.FOLLOW_UP) {
|
||||
return;
|
||||
}
|
||||
|
||||
switch (this.pokemon.status.effect) {
|
||||
case StatusEffect.PARALYSIS:
|
||||
activated =
|
||||
(!this.pokemon.randSeedInt(4) || Overrides.STATUS_ACTIVATION_OVERRIDE === true) &&
|
||||
Overrides.STATUS_ACTIVATION_OVERRIDE !== false;
|
||||
break;
|
||||
case StatusEffect.SLEEP: {
|
||||
applyMoveAttrs(BypassSleepAttr, this.pokemon, null, this.move.getMove());
|
||||
const turnsRemaining = new NumberHolder(this.pokemon.status.sleepTurnsRemaining ?? 0);
|
||||
applyAbAttrs(
|
||||
ReduceStatusEffectDurationAbAttr,
|
||||
this.pokemon,
|
||||
null,
|
||||
false,
|
||||
this.pokemon.status.effect,
|
||||
turnsRemaining,
|
||||
);
|
||||
this.pokemon.status.sleepTurnsRemaining = turnsRemaining.value;
|
||||
healed = this.pokemon.status.sleepTurnsRemaining <= 0;
|
||||
activated = !healed && !this.pokemon.getTag(BattlerTagType.BYPASS_SLEEP);
|
||||
break;
|
||||
}
|
||||
case StatusEffect.FREEZE:
|
||||
healed =
|
||||
!!this.move
|
||||
.getMove()
|
||||
.findAttr(
|
||||
attr => attr instanceof HealStatusEffectAttr && attr.selfTarget && attr.isOfEffect(StatusEffect.FREEZE),
|
||||
) ||
|
||||
(!this.pokemon.randSeedInt(5) && Overrides.STATUS_ACTIVATION_OVERRIDE !== true) ||
|
||||
Overrides.STATUS_ACTIVATION_OVERRIDE === false;
|
||||
if (
|
||||
this.useType === MoveUseType.INDIRECT &&
|
||||
[StatusEffect.SLEEP, StatusEffect.FREEZE].includes(this.pokemon.status.effect)
|
||||
) {
|
||||
// Dancer thaws out or wakes up a frozen/sleeping user prior to use
|
||||
this.pokemon.resetStatus(false);
|
||||
return;
|
||||
}
|
||||
|
||||
activated = !healed;
|
||||
break;
|
||||
/** Whether to prevent us from using the move */
|
||||
let activated = false;
|
||||
/** Whether to cure the status */
|
||||
let healed = false;
|
||||
|
||||
switch (this.pokemon.status.effect) {
|
||||
case StatusEffect.PARALYSIS:
|
||||
activated =
|
||||
(!this.pokemon.randSeedInt(4) || Overrides.STATUS_ACTIVATION_OVERRIDE === true) &&
|
||||
Overrides.STATUS_ACTIVATION_OVERRIDE !== false;
|
||||
break;
|
||||
case StatusEffect.SLEEP: {
|
||||
applyMoveAttrs(BypassSleepAttr, this.pokemon, null, this.move.getMove());
|
||||
const turnsRemaining = new NumberHolder(this.pokemon.status.sleepTurnsRemaining ?? 0);
|
||||
applyAbAttrs(
|
||||
ReduceStatusEffectDurationAbAttr,
|
||||
this.pokemon,
|
||||
null,
|
||||
false,
|
||||
this.pokemon.status.effect,
|
||||
turnsRemaining,
|
||||
);
|
||||
this.pokemon.status.sleepTurnsRemaining = turnsRemaining.value;
|
||||
healed = this.pokemon.status.sleepTurnsRemaining <= 0;
|
||||
activated = !healed && !this.pokemon.getTag(BattlerTagType.BYPASS_SLEEP);
|
||||
break;
|
||||
}
|
||||
case StatusEffect.FREEZE:
|
||||
healed =
|
||||
!!this.move
|
||||
.getMove()
|
||||
.findAttr(
|
||||
attr => attr instanceof HealStatusEffectAttr && attr.selfTarget && attr.isOfEffect(StatusEffect.FREEZE),
|
||||
) ||
|
||||
(!this.pokemon.randSeedInt(5) && Overrides.STATUS_ACTIVATION_OVERRIDE !== true) ||
|
||||
Overrides.STATUS_ACTIVATION_OVERRIDE === false;
|
||||
|
||||
if (activated) {
|
||||
this.cancel();
|
||||
globalScene.queueMessage(
|
||||
getStatusEffectActivationText(this.pokemon.status.effect, getPokemonNameWithAffix(this.pokemon)),
|
||||
);
|
||||
globalScene.unshiftPhase(
|
||||
new CommonAnimPhase(
|
||||
this.pokemon.getBattlerIndex(),
|
||||
undefined,
|
||||
CommonAnim.POISON + (this.pokemon.status.effect - 1),
|
||||
),
|
||||
);
|
||||
} else if (healed) {
|
||||
globalScene.queueMessage(
|
||||
getStatusEffectHealText(this.pokemon.status.effect, getPokemonNameWithAffix(this.pokemon)),
|
||||
);
|
||||
this.pokemon.resetStatus();
|
||||
this.pokemon.updateInfo();
|
||||
}
|
||||
activated = !healed;
|
||||
break;
|
||||
}
|
||||
|
||||
if (activated) {
|
||||
// Cancel move activation and play effect
|
||||
this.cancel();
|
||||
globalScene.queueMessage(
|
||||
getStatusEffectActivationText(this.pokemon.status.effect, getPokemonNameWithAffix(this.pokemon)),
|
||||
);
|
||||
globalScene.unshiftPhase(
|
||||
new CommonAnimPhase(
|
||||
this.pokemon.getBattlerIndex(),
|
||||
undefined,
|
||||
CommonAnim.POISON + (this.pokemon.status.effect - 1), // offset anim # by effect #
|
||||
),
|
||||
);
|
||||
} else if (healed) {
|
||||
// cure status and play effect
|
||||
globalScene.queueMessage(
|
||||
getStatusEffectHealText(this.pokemon.status.effect, getPokemonNameWithAffix(this.pokemon)),
|
||||
);
|
||||
this.pokemon.resetStatus();
|
||||
this.pokemon.updateInfo();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Lapse {@linkcode BattlerTagLapseType.PRE_MOVE PRE_MOVE} tags that trigger before a move is used, regardless of whether or not it failed.
|
||||
* Also lapse {@linkcode BattlerTagLapseType.MOVE MOVE} tags if the move should be successful.
|
||||
* Lapse {@linkcode BattlerTagLapseType.PRE_MOVE | PRE_MOVE} tags that trigger before a move is used, regardless of whether or not it failed.
|
||||
* Also lapse {@linkcode BattlerTagLapseType.MOVE | MOVE} tags if the move is successful and not called indirectly.
|
||||
*/
|
||||
protected lapsePreMoveAndMoveTags(): void {
|
||||
this.pokemon.lapseTags(BattlerTagLapseType.PRE_MOVE);
|
||||
|
||||
// TODO: does this intentionally happen before the no targets/Moves.NONE on queue cancellation case is checked?
|
||||
if (!this.followUp && this.canMove() && !this.cancelled) {
|
||||
// (In other words, check if truant can proc on a move w/o targets)
|
||||
if (this.useType < MoveUseType.FOLLOW_UP && this.canMove() && !this.cancelled) {
|
||||
this.pokemon.lapseTags(BattlerTagLapseType.MOVE);
|
||||
}
|
||||
}
|
||||
@ -303,64 +322,46 @@ export class MovePhase extends BattlePhase {
|
||||
protected useMove(): void {
|
||||
const targets = this.getActiveTargetPokemon();
|
||||
const moveQueue = this.pokemon.getMoveQueue();
|
||||
const move = this.move.getMove();
|
||||
|
||||
// form changes happen even before we know that the move wll execute.
|
||||
globalScene.triggerPokemonFormChange(this.pokemon, SpeciesFormChangePreMoveTrigger);
|
||||
|
||||
const isDelayedAttack = this.move.getMove().hasAttr(DelayedAttackAttr);
|
||||
const isDelayedAttack = move.hasAttr(DelayedAttackAttr);
|
||||
if (isDelayedAttack) {
|
||||
// Check the player side arena if future sight is active
|
||||
const futureSightTags = globalScene.arena.findTags(t => t.tagType === ArenaTagType.FUTURE_SIGHT);
|
||||
const doomDesireTags = globalScene.arena.findTags(t => t.tagType === ArenaTagType.DOOM_DESIRE);
|
||||
let fail = false;
|
||||
// Check the player side arena if another delayed attack is active and hitting the same slot.
|
||||
const delayedAttackTags = globalScene.arena.findTags(t =>
|
||||
[ArenaTagType.FUTURE_SIGHT, ArenaTagType.DOOM_DESIRE].includes(t.tagType),
|
||||
) as DelayedAttackTag[];
|
||||
const currentTargetIndex = targets[0].getBattlerIndex();
|
||||
for (const tag of futureSightTags) {
|
||||
if ((tag as DelayedAttackTag).targetIndex === currentTargetIndex) {
|
||||
fail = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
for (const tag of doomDesireTags) {
|
||||
if ((tag as DelayedAttackTag).targetIndex === currentTargetIndex) {
|
||||
fail = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (fail) {
|
||||
|
||||
if (delayedAttackTags.some(tag => tag.targetIndex === currentTargetIndex)) {
|
||||
this.showMoveText();
|
||||
this.showFailedText();
|
||||
return this.end();
|
||||
}
|
||||
}
|
||||
|
||||
let success = true;
|
||||
// Check if there are any attributes that can interrupt the move, overriding the fail message.
|
||||
for (const move of this.move.getMove().getAttrs(PreUseInterruptAttr)) {
|
||||
if (move.apply(this.pokemon, targets[0], this.move.getMove())) {
|
||||
success = false;
|
||||
break;
|
||||
this.failMove();
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Check if the move has any attributes that can interrupt its own use **before** displaying text.
|
||||
let success = !move.getAttrs(PreUseInterruptAttr).some(attr => attr.apply(this.pokemon, targets[0], move));
|
||||
if (success) {
|
||||
this.showMoveText();
|
||||
}
|
||||
|
||||
if (moveQueue.length > 0) {
|
||||
// Using .shift here clears out two turn moves once they've been used
|
||||
this.ignorePp = moveQueue.shift()?.ignorePP ?? false;
|
||||
}
|
||||
|
||||
// Clear out any two turn moves once they've been used.
|
||||
// TODO: Refactor move queues and remove this assignment;
|
||||
// Move queues should be handled by the calling `CommandPhase` or a manager for it
|
||||
this.useType = moveQueue.shift()?.useType ?? this.useType;
|
||||
if (this.pokemon.getTag(BattlerTagType.CHARGING)?.sourceMove === this.move.moveId) {
|
||||
this.pokemon.lapseTag(BattlerTagType.CHARGING);
|
||||
}
|
||||
|
||||
// "commit" to using the move, deducting PP.
|
||||
if (!this.ignorePp) {
|
||||
if (this.useType < MoveUseType.IGNORE_PP) {
|
||||
// "commit" to using the move, deducting PP.
|
||||
const ppUsed = 1 + this.getPpIncreaseFromPressure(targets);
|
||||
|
||||
this.move.usePp(ppUsed);
|
||||
globalScene.eventTarget.dispatchEvent(new MoveUsedEvent(this.pokemon?.id, this.move.getMove(), this.move.ppUsed));
|
||||
globalScene.eventTarget.dispatchEvent(new MoveUsedEvent(this.pokemon?.id, move, this.move.ppUsed));
|
||||
}
|
||||
|
||||
/**
|
||||
@ -370,123 +371,125 @@ export class MovePhase extends BattlePhase {
|
||||
* - The target's `ForceSwitchOutImmunityAbAttr` is not triggered (see {@linkcode Move.prototype.applyConditions})
|
||||
* - Weather does not block the move
|
||||
* - Terrain does not block the move
|
||||
*
|
||||
* TODO: These steps are straightforward, but the implementation below is extremely convoluted.
|
||||
*/
|
||||
|
||||
const move = this.move.getMove();
|
||||
|
||||
/**
|
||||
* Move conditions assume the move has a single target
|
||||
* TODO: is this sustainable?
|
||||
*/
|
||||
let failedDueToTerrain = false;
|
||||
let failedDueToWeather = false;
|
||||
if (success) {
|
||||
const passesConditions = move.applyConditions(this.pokemon, targets[0], move);
|
||||
failedDueToWeather = globalScene.arena.isMoveWeatherCancelled(this.pokemon, move);
|
||||
failedDueToTerrain = globalScene.arena.isMoveTerrainCancelled(this.pokemon, this.targets, move);
|
||||
success = passesConditions && !failedDueToWeather && !failedDueToTerrain;
|
||||
const passesConditions = move.applyConditions(this.pokemon, targets[0], move);
|
||||
const failedDueToWeather = globalScene.arena.isMoveWeatherCancelled(this.pokemon, move);
|
||||
const failedDueToTerrain = globalScene.arena.isMoveTerrainCancelled(this.pokemon, this.targets, move);
|
||||
success &&= passesConditions && !failedDueToWeather && !failedDueToTerrain;
|
||||
|
||||
if (!success) {
|
||||
this.failMove(failedDueToWeather, failedDueToTerrain);
|
||||
return;
|
||||
}
|
||||
|
||||
// Update the battle's "last move" pointer, unless we're currently mimicking a move.
|
||||
if (!allMoves[this.move.moveId].hasAttr(CopyMoveAttr)) {
|
||||
// The last move used is unaffected by moves that fail
|
||||
if (success) {
|
||||
globalScene.currentBattle.lastMove = this.move.moveId;
|
||||
}
|
||||
if (!allMoves[this.move.moveId].hasAttr(CopyMoveAttr) && this.useType !== MoveUseType.INDIRECT) {
|
||||
// Update the battle's "last move" pointer unless we're currently mimicking a move or triggering Dancer.
|
||||
// TODO: Research how Copycat interacts with the final attacking turn of Future Sight and co.
|
||||
globalScene.currentBattle.lastMove = this.move.moveId;
|
||||
}
|
||||
|
||||
/**
|
||||
* If the move has not failed, trigger ability-based user type changes and then execute it.
|
||||
*
|
||||
* Notably, Roar, Whirlwind, Trick-or-Treat, and Forest's Curse will trigger these type changes even
|
||||
* if the move fails.
|
||||
*/
|
||||
if (success) {
|
||||
const move = this.move.getMove();
|
||||
applyPreAttackAbAttrs(PokemonTypeChangeAbAttr, this.pokemon, null, move);
|
||||
globalScene.unshiftPhase(
|
||||
new MoveEffectPhase(this.pokemon.getBattlerIndex(), this.targets, move, this.reflected, this.move.virtual),
|
||||
);
|
||||
} else {
|
||||
if ([Moves.ROAR, Moves.WHIRLWIND, Moves.TRICK_OR_TREAT, Moves.FORESTS_CURSE].includes(this.move.moveId)) {
|
||||
applyPreAttackAbAttrs(PokemonTypeChangeAbAttr, this.pokemon, null, this.move.getMove());
|
||||
}
|
||||
|
||||
this.pokemon.pushMoveHistory({
|
||||
move: this.move.moveId,
|
||||
targets: this.targets,
|
||||
result: MoveResult.FAIL,
|
||||
virtual: this.move.virtual,
|
||||
});
|
||||
|
||||
const failureMessage = move.getFailedText(this.pokemon, targets[0], move);
|
||||
let failedText: string | undefined;
|
||||
if (failureMessage) {
|
||||
failedText = failureMessage;
|
||||
} else if (failedDueToTerrain) {
|
||||
failedText = getTerrainBlockMessage(targets[0], globalScene.arena.getTerrainType());
|
||||
} else if (failedDueToWeather) {
|
||||
failedText = getWeatherBlockMessage(globalScene.arena.getWeatherType());
|
||||
}
|
||||
|
||||
this.showFailedText(failedText);
|
||||
|
||||
// Remove the user from its semi-invulnerable state (if applicable)
|
||||
this.pokemon.lapseTags(BattlerTagLapseType.MOVE_EFFECT);
|
||||
}
|
||||
// trigger ability-based user type changes and then execute move effects.
|
||||
applyPreAttackAbAttrs(PokemonTypeChangeAbAttr, this.pokemon, null, move);
|
||||
globalScene.unshiftPhase(new MoveEffectPhase(this.pokemon.getBattlerIndex(), this.targets, move, this.useType));
|
||||
|
||||
// Handle Dancer, which triggers immediately after a move is used (rather than waiting on `this.end()`).
|
||||
// Note that the `!this.followUp` check here prevents an infinite Dancer loop.
|
||||
if (this.move.getMove().hasFlag(MoveFlags.DANCE_MOVE) && !this.followUp) {
|
||||
// Note the MoveUseType check here prevents an infinite Dancer loop.
|
||||
if (
|
||||
this.move.getMove().hasFlag(MoveFlags.DANCE_MOVE) &&
|
||||
![MoveUseType.INDIRECT, MoveUseType.REFLECTED].includes(this.useType)
|
||||
) {
|
||||
// TODO: Fix in dancer PR to move to MEP for hit checks
|
||||
globalScene.getField(true).forEach(pokemon => {
|
||||
applyPostMoveUsedAbAttrs(PostMoveUsedAbAttr, pokemon, this.move, this.pokemon, this.targets);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/** Queues a {@linkcode MoveChargePhase} for this phase's invoked move. */
|
||||
/**
|
||||
* Fail the move currently being used.
|
||||
* Handles failure messages, pushing to move history, etc.
|
||||
* Notably, Roar, Whirlwind, Trick-or-Treat, and Forest's Curse will trigger type changes even on failure.
|
||||
* @param failedDueToWeather - Whether the move failed due to weather (default `false`)
|
||||
* @param failedDueToTerrain - Whether the move failed due to terrain (default `false`)
|
||||
*/
|
||||
protected failMove(failedDueToWeather = false, failedDueToTerrain = false) {
|
||||
const move = this.move.getMove();
|
||||
const targets = this.getActiveTargetPokemon();
|
||||
|
||||
if ([Moves.ROAR, Moves.WHIRLWIND, Moves.TRICK_OR_TREAT, Moves.FORESTS_CURSE].includes(this.move.moveId)) {
|
||||
applyPreAttackAbAttrs(PokemonTypeChangeAbAttr, this.pokemon, null, move);
|
||||
}
|
||||
|
||||
this.pokemon.pushMoveHistory({
|
||||
move: this.move.moveId,
|
||||
targets: this.targets,
|
||||
result: MoveResult.FAIL,
|
||||
useType: this.useType,
|
||||
});
|
||||
|
||||
// Use move-specific failure messages if present before checking terrain/weather blockage
|
||||
// and falling back to the classic "But it failed!".
|
||||
const failureMessage =
|
||||
move.getFailedText(this.pokemon, targets[0], move) ??
|
||||
(failedDueToTerrain
|
||||
? getTerrainBlockMessage(targets[0], globalScene.arena.getTerrainType())
|
||||
: failedDueToWeather
|
||||
? getWeatherBlockMessage(globalScene.arena.getWeatherType())
|
||||
: i18next.t("battle:attackFailed"));
|
||||
|
||||
this.showFailedText(failureMessage);
|
||||
|
||||
// Remove the user from its semi-invulnerable state (if applicable)
|
||||
this.pokemon.lapseTags(BattlerTagLapseType.MOVE_EFFECT);
|
||||
}
|
||||
|
||||
/**
|
||||
* Queue a {@linkcode MoveChargePhase} for this phase's invoked move.
|
||||
* Does NOT consume PP (occurs on the 2nd strike of the move)
|
||||
*/
|
||||
protected chargeMove() {
|
||||
const move = this.move.getMove();
|
||||
const targets = this.getActiveTargetPokemon();
|
||||
|
||||
if (move.applyConditions(this.pokemon, targets[0], move)) {
|
||||
// Protean and Libero apply on the charging turn of charge moves
|
||||
applyPreAttackAbAttrs(PokemonTypeChangeAbAttr, this.pokemon, null, this.move.getMove());
|
||||
this.showMoveText();
|
||||
|
||||
this.showMoveText();
|
||||
globalScene.unshiftPhase(new MoveChargePhase(this.pokemon.getBattlerIndex(), this.targets[0], this.move));
|
||||
} else {
|
||||
this.pokemon.pushMoveHistory({
|
||||
move: this.move.moveId,
|
||||
targets: this.targets,
|
||||
result: MoveResult.FAIL,
|
||||
virtual: this.move.virtual,
|
||||
});
|
||||
|
||||
const failureMessage = move.getFailedText(this.pokemon, targets[0], move);
|
||||
this.showMoveText();
|
||||
this.showFailedText(failureMessage ?? undefined);
|
||||
|
||||
// Remove the user from its semi-invulnerable state (if applicable)
|
||||
this.pokemon.lapseTags(BattlerTagLapseType.MOVE_EFFECT);
|
||||
// Conditions currently assume single target
|
||||
// TODO: Is this sustainable?
|
||||
if (!move.applyConditions(this.pokemon, targets[0], move)) {
|
||||
this.failMove();
|
||||
return;
|
||||
}
|
||||
|
||||
// Protean and Libero apply on the charging turn of charge moves
|
||||
applyPreAttackAbAttrs(PokemonTypeChangeAbAttr, this.pokemon, null, this.move.getMove());
|
||||
|
||||
globalScene.unshiftPhase(
|
||||
new MoveChargePhase(this.pokemon.getBattlerIndex(), this.targets[0], this.move, this.useType),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Queues a {@linkcode MoveEndPhase} and then ends the phase
|
||||
* Queue a {@linkcode MoveEndPhase} and then end this phase.
|
||||
*/
|
||||
public end(): void {
|
||||
globalScene.unshiftPhase(
|
||||
new MoveEndPhase(this.pokemon.getBattlerIndex(), this.getActiveTargetPokemon(), this.followUp),
|
||||
new MoveEndPhase(
|
||||
this.pokemon.getBattlerIndex(),
|
||||
this.getActiveTargetPokemon(),
|
||||
this.useType >= MoveUseType.INDIRECT,
|
||||
),
|
||||
);
|
||||
|
||||
super.end();
|
||||
}
|
||||
|
||||
/**
|
||||
* Applies PP increasing abilities (currently only {@link Abilities.PRESSURE Pressure}) if they exist on the target pokemon.
|
||||
* Applies PP increasing abilities (currently only {@linkcode Abilities.PRESSURE | Pressure}) if they exist on the target pokemon.
|
||||
* Note that targets must include only active pokemon.
|
||||
*
|
||||
* TODO: This hardcodes the PP increase at 1 per opponent, rather than deferring to the ability.
|
||||
@ -562,40 +565,43 @@ export class MovePhase extends BattlePhase {
|
||||
}
|
||||
|
||||
/**
|
||||
* Counter-attacking moves pass in `[`{@linkcode BattlerIndex.ATTACKER}`]` into the constructor's `targets` param.
|
||||
* This function modifies `this.targets` to reflect the actual battler index of the user's last
|
||||
* attacker.
|
||||
* Update the targets of any counter-attacking moves with `[`{@linkcode BattlerIndex.ATTACKER}`]` set
|
||||
* to reflect the actual battler index of the user's last attacker.
|
||||
*
|
||||
* If there is no last attacker, or they are no longer on the field, a message is displayed and the
|
||||
* If there is no last attacker or they are no longer on the field, a message is displayed and the
|
||||
* move is marked for failure.
|
||||
*/
|
||||
protected resolveCounterAttackTarget(): void {
|
||||
if (this.targets.length === 1 && this.targets[0] === BattlerIndex.ATTACKER) {
|
||||
if (this.pokemon.turnData.attacksReceived.length) {
|
||||
this.targets[0] = this.pokemon.turnData.attacksReceived[0].sourceBattlerIndex;
|
||||
if (this.targets.length !== 1 || this.targets[0] !== BattlerIndex.ATTACKER) {
|
||||
return;
|
||||
}
|
||||
|
||||
// account for metal burst and comeuppance hitting remaining targets in double battles
|
||||
// counterattack will redirect to remaining ally if original attacker faints
|
||||
if (globalScene.currentBattle.double && this.move.getMove().hasFlag(MoveFlags.REDIRECT_COUNTER)) {
|
||||
if (globalScene.getField()[this.targets[0]].hp === 0) {
|
||||
const opposingField = this.pokemon.isPlayer() ? globalScene.getEnemyField() : globalScene.getPlayerField();
|
||||
this.targets[0] = opposingField.find(p => p.hp > 0)?.getBattlerIndex() ?? BattlerIndex.ATTACKER;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (this.pokemon.turnData.attacksReceived.length) {
|
||||
this.targets[0] = this.pokemon.turnData.attacksReceived[0].sourceBattlerIndex;
|
||||
|
||||
if (this.targets[0] === BattlerIndex.ATTACKER) {
|
||||
this.fail();
|
||||
this.showMoveText();
|
||||
this.showFailedText();
|
||||
// account for metal burst and comeuppance hitting remaining targets in double battles
|
||||
// counterattack will redirect to remaining ally if original attacker faints
|
||||
if (
|
||||
globalScene.currentBattle.double &&
|
||||
this.move.getMove().hasFlag(MoveFlags.REDIRECT_COUNTER) &&
|
||||
globalScene.getField()[this.targets[0]].hp === 0
|
||||
) {
|
||||
const opposingField = this.pokemon.isPlayer() ? globalScene.getEnemyField() : globalScene.getPlayerField();
|
||||
this.targets[0] = opposingField.find(p => p.hp > 0)?.getBattlerIndex() ?? BattlerIndex.ATTACKER;
|
||||
}
|
||||
}
|
||||
|
||||
if (this.targets[0] === BattlerIndex.ATTACKER) {
|
||||
this.fail();
|
||||
this.showMoveText();
|
||||
this.showFailedText();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles the case where the move was cancelled or failed:
|
||||
* - Uses PP if the move failed (not cancelled) and should use PP (failed moves are not affected by {@link Abilities.PRESSURE Pressure})
|
||||
* - Records a cancelled OR failed move in move history, so abilities like {@link Abilities.TRUANT Truant} don't trigger on the
|
||||
* - Uses PP if the move failed (not cancelled) and should use PP (failed moves are not affected by {@link Abilities.PRESSURE | Pressure})
|
||||
* - Records a cancelled OR failed move in move history, so abilities like {@link Abilities.TRUANT | Truant} don't trigger on the
|
||||
* next turn and soft-lock.
|
||||
* - Lapses `MOVE_EFFECT` tags:
|
||||
* - Semi-invulnerable battler tags (Fly/Dive/etc.) are intended to lapse on move effects, but also need
|
||||
@ -603,53 +609,57 @@ export class MovePhase extends BattlePhase {
|
||||
*
|
||||
* TODO: ...this seems weird.
|
||||
* - Lapses `AFTER_MOVE` tags:
|
||||
* - This handles the effects of {@link Moves.SUBSTITUTE Substitute}
|
||||
* - This handles the effects of {@link Moves.SUBSTITUTE | Substitute}
|
||||
* - Removes the second turn of charge moves
|
||||
*/
|
||||
protected handlePreMoveFailures(): void {
|
||||
if (this.cancelled || this.failed) {
|
||||
if (this.failed) {
|
||||
const ppUsed = this.ignorePp ? 0 : 1;
|
||||
|
||||
if (ppUsed) {
|
||||
this.move.usePp();
|
||||
}
|
||||
|
||||
globalScene.eventTarget.dispatchEvent(new MoveUsedEvent(this.pokemon?.id, this.move.getMove(), ppUsed));
|
||||
}
|
||||
|
||||
if (this.cancelled && this.pokemon.summonData.tags?.find(t => t.tagType === BattlerTagType.FRENZY)) {
|
||||
frenzyMissFunc(this.pokemon, this.move.getMove());
|
||||
}
|
||||
|
||||
this.pokemon.pushMoveHistory({
|
||||
move: Moves.NONE,
|
||||
result: MoveResult.FAIL,
|
||||
targets: this.targets,
|
||||
});
|
||||
|
||||
this.pokemon.lapseTags(BattlerTagLapseType.MOVE_EFFECT);
|
||||
this.pokemon.lapseTags(BattlerTagLapseType.AFTER_MOVE);
|
||||
|
||||
this.pokemon.getMoveQueue().shift();
|
||||
if (!this.cancelled && !this.failed) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.failed) {
|
||||
const ppUsed = this.useType >= MoveUseType.IGNORE_PP ? 0 : 1;
|
||||
this.move.usePp(ppUsed);
|
||||
|
||||
globalScene.eventTarget.dispatchEvent(new MoveUsedEvent(this.pokemon?.id, this.move.getMove(), ppUsed));
|
||||
}
|
||||
|
||||
if (this.cancelled && this.pokemon.summonData.tags?.find(t => t.tagType === BattlerTagType.FRENZY)) {
|
||||
frenzyMissFunc(this.pokemon, this.move.getMove());
|
||||
}
|
||||
|
||||
this.pokemon.pushMoveHistory({
|
||||
move: Moves.NONE,
|
||||
result: MoveResult.FAIL,
|
||||
targets: this.targets,
|
||||
useType: this.useType,
|
||||
});
|
||||
|
||||
this.pokemon.lapseTags(BattlerTagLapseType.MOVE_EFFECT);
|
||||
this.pokemon.lapseTags(BattlerTagLapseType.AFTER_MOVE);
|
||||
|
||||
// This clears out 2 turn moves after they've been used
|
||||
// TODO: Remove post move queue refactor
|
||||
this.pokemon.getMoveQueue().shift();
|
||||
}
|
||||
|
||||
/**
|
||||
* Displays the move's usage text to the player, unless it's a charge turn (ie: {@link Moves.SOLAR_BEAM Solar Beam}),
|
||||
* the pokemon is on a recharge turn (ie: {@link Moves.HYPER_BEAM Hyper Beam}), or a 2-turn move was interrupted (ie: {@link Moves.FLY Fly}).
|
||||
* Displays the move's usage text to the player as applicable for the move being used.
|
||||
*/
|
||||
public showMoveText(): void {
|
||||
if (this.move.moveId === Moves.NONE) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.pokemon.getTag(BattlerTagType.RECHARGING) || this.pokemon.getTag(BattlerTagType.INTERRUPTED)) {
|
||||
// No text for Moves.NONE, recharging/2-turn moves or interrupted moves
|
||||
if (
|
||||
this.move.moveId === Moves.NONE ||
|
||||
this.pokemon.getTag(BattlerTagType.RECHARGING) ||
|
||||
this.pokemon.getTag(BattlerTagType.INTERRUPTED)
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Play message for magic coat reflection
|
||||
// TODO: This should be done by the move...
|
||||
globalScene.queueMessage(
|
||||
i18next.t(this.reflected ? "battle:magicCoatActivated" : "battle:useMove", {
|
||||
i18next.t(this.useType === MoveUseType.REFLECTED ? "battle:magicCoatActivated" : "battle:useMove", {
|
||||
pokemonNameWithAffix: getPokemonNameWithAffix(this.pokemon),
|
||||
moveName: this.move.getName(),
|
||||
}),
|
||||
@ -658,6 +668,11 @@ export class MovePhase extends BattlePhase {
|
||||
applyMoveAttrs(PreMoveMessageAttr, this.pokemon, this.pokemon.getOpponents(false)[0], this.move.getMove());
|
||||
}
|
||||
|
||||
/**
|
||||
* Display the text for a move failing to execute.
|
||||
* @param failedText - The failure text to display; defaults to `"battle:attackFailed"` locale key
|
||||
* ("But it failed!" in english)
|
||||
*/
|
||||
public showFailedText(failedText: string = i18next.t("battle:attackFailed")): void {
|
||||
globalScene.queueMessage(failedText);
|
||||
}
|
||||
|
@ -4,21 +4,23 @@ import type Pokemon from "#app/field/pokemon";
|
||||
import { FieldPhase } from "./field-phase";
|
||||
|
||||
export abstract class PokemonPhase extends FieldPhase {
|
||||
protected battlerIndex: BattlerIndex | number;
|
||||
protected battlerIndex: BattlerIndex;
|
||||
public player: boolean;
|
||||
public fieldIndex: number;
|
||||
|
||||
constructor(battlerIndex?: BattlerIndex | number) {
|
||||
constructor(battlerIndex?: BattlerIndex) {
|
||||
super();
|
||||
|
||||
battlerIndex =
|
||||
battlerIndex ??
|
||||
globalScene
|
||||
.getField()
|
||||
.find(p => p?.isActive())! // TODO: is the bang correct here?
|
||||
.getBattlerIndex();
|
||||
.find(p => p?.isActive())
|
||||
?.getBattlerIndex();
|
||||
if (battlerIndex === undefined) {
|
||||
console.warn("There are no Pokemon on the field!"); // TODO: figure out a suitable fallback behavior
|
||||
// TODO: figure out a suitable fallback behavior
|
||||
console.warn("There are no Pokemon on the field!");
|
||||
battlerIndex = BattlerIndex.PLAYER;
|
||||
}
|
||||
|
||||
this.battlerIndex = battlerIndex;
|
||||
|
@ -51,7 +51,7 @@ export class PokemonTransformPhase extends PokemonPhase {
|
||||
user.summonData.moveset = target.getMoveset().map(m => {
|
||||
if (m) {
|
||||
// If PP value is less than 5, do nothing. If greater, we need to reduce the value to 5.
|
||||
return new PokemonMove(m.moveId, 0, 0, false, Math.min(m.getMove().pp, 5));
|
||||
return new PokemonMove(m.moveId, 0, 0, Math.min(m.getMove().pp, 5));
|
||||
}
|
||||
console.warn(`Transform: somehow iterating over a ${m} value when copying moveset!`);
|
||||
return new PokemonMove(Moves.NONE);
|
||||
|
@ -27,16 +27,16 @@ export class TurnStartPhase extends FieldPhase {
|
||||
/**
|
||||
* This orders the active Pokemon on the field by speed into an BattlerIndex array and returns that array.
|
||||
* It also checks for Trick Room and reverses the array if it is present.
|
||||
* @returns {@linkcode BattlerIndex[]} the battle indices of all pokemon on the field ordered by speed
|
||||
* @returns An array of {@linkcode BattlerIndex}es containing all on-field pokemon sorted in speed order.
|
||||
*/
|
||||
getSpeedOrder(): BattlerIndex[] {
|
||||
const playerField = globalScene.getPlayerField().filter(p => p.isActive()) as Pokemon[];
|
||||
const enemyField = globalScene.getEnemyField().filter(p => p.isActive()) as Pokemon[];
|
||||
|
||||
// We shuffle the list before sorting so speed ties produce random results
|
||||
// Shuffle the list before sorting so speed ties produce random results
|
||||
// This is seeded with the current turn to prevent an inconsistency with variable turn order
|
||||
// based on how long since you last reloaded
|
||||
let orderedTargets: Pokemon[] = playerField.concat(enemyField);
|
||||
// We seed it with the current turn to prevent an inconsistency where it
|
||||
// was varying based on how long since you last reloaded
|
||||
globalScene.executeWithSeedOffset(
|
||||
() => {
|
||||
orderedTargets = randSeedShuffle(orderedTargets);
|
||||
@ -45,11 +45,11 @@ export class TurnStartPhase extends FieldPhase {
|
||||
globalScene.waveSeed,
|
||||
);
|
||||
|
||||
// Next, a check for Trick Room is applied to determine sort order.
|
||||
// Check for Trick Room and reverse sort order if active.
|
||||
// Notably, Pokerogue does NOT have the "outspeed trick room" glitch at >1809 spd.
|
||||
const speedReversed = new BooleanHolder(false);
|
||||
globalScene.arena.applyTags(TrickRoomTag, false, speedReversed);
|
||||
|
||||
// Adjust the sort function based on whether Trick Room is active.
|
||||
orderedTargets.sort((a: Pokemon, b: Pokemon) => {
|
||||
const aSpeed = a?.getEffectiveStat(Stat.SPD) ?? 0;
|
||||
const bSpeed = b?.getEffectiveStat(Stat.SPD) ?? 0;
|
||||
@ -72,7 +72,7 @@ export class TurnStartPhase extends FieldPhase {
|
||||
// This occurs before the main loop because of battles with more than two Pokemon
|
||||
const battlerBypassSpeed = {};
|
||||
|
||||
globalScene.getField(true).map(p => {
|
||||
globalScene.getField(true).forEach(p => {
|
||||
const bypassSpeed = new BooleanHolder(false);
|
||||
const canCheckHeldItems = new BooleanHolder(true);
|
||||
applyAbAttrs(BypassSpeedChanceAbAttr, p, null, false, bypassSpeed);
|
||||
@ -120,7 +120,8 @@ export class TurnStartPhase extends FieldPhase {
|
||||
}
|
||||
}
|
||||
|
||||
// If there is no difference between the move's calculated priorities, the game checks for differences in battlerBypassSpeed and returns the result.
|
||||
// If there is no difference between the move's calculated priorities,
|
||||
// check for differences in battlerBypassSpeed and returns the result.
|
||||
if (battlerBypassSpeed[a].value !== battlerBypassSpeed[b].value) {
|
||||
return battlerBypassSpeed[a].value ? -1 : 1;
|
||||
}
|
||||
@ -133,6 +134,8 @@ export class TurnStartPhase extends FieldPhase {
|
||||
return moveOrder;
|
||||
}
|
||||
|
||||
// TODO: Refactor this alongside `CommandPhase.handleCommand` to use SEPARATE METHODS
|
||||
// Also need a clearer distinction between "turn command" and queued moves
|
||||
start() {
|
||||
super.start();
|
||||
|
||||
@ -171,34 +174,19 @@ export class TurnStartPhase extends FieldPhase {
|
||||
continue;
|
||||
}
|
||||
const move =
|
||||
pokemon.getMoveset().find(m => m.moveId === queuedMove.move && m.ppUsed < m.getMovePp()) ||
|
||||
pokemon.getMoveset().find(m => m.moveId === queuedMove.move && m.ppUsed < m.getMovePp()) ??
|
||||
new PokemonMove(queuedMove.move);
|
||||
if (move.getMove().hasAttr(MoveHeaderAttr)) {
|
||||
globalScene.unshiftPhase(new MoveHeaderPhase(pokemon, move));
|
||||
}
|
||||
if (pokemon.isPlayer()) {
|
||||
if (turnCommand.cursor === -1) {
|
||||
globalScene.pushPhase(new MovePhase(pokemon, turnCommand.targets || turnCommand.move!.targets, move)); //TODO: is the bang correct here?
|
||||
} else {
|
||||
const playerPhase = new MovePhase(
|
||||
pokemon,
|
||||
turnCommand.targets || turnCommand.move!.targets,
|
||||
move,
|
||||
false,
|
||||
queuedMove.ignorePP,
|
||||
); //TODO: is the bang correct here?
|
||||
globalScene.pushPhase(playerPhase);
|
||||
}
|
||||
if (pokemon.isPlayer() && turnCommand.cursor === -1) {
|
||||
globalScene.pushPhase(
|
||||
new MovePhase(pokemon, turnCommand.targets || turnCommand.move!.targets, move, turnCommand.move!.useType),
|
||||
); //TODO: is the bang correct here?
|
||||
} else {
|
||||
globalScene.pushPhase(
|
||||
new MovePhase(
|
||||
pokemon,
|
||||
turnCommand.targets || turnCommand.move!.targets,
|
||||
move,
|
||||
false,
|
||||
queuedMove.ignorePP,
|
||||
),
|
||||
); //TODO: is the bang correct here?
|
||||
new MovePhase(pokemon, turnCommand.targets || turnCommand.move!.targets, move, queuedMove.useType),
|
||||
); // TODO: is the bang correct here?
|
||||
}
|
||||
break;
|
||||
case Command.BALL:
|
||||
|
@ -15,6 +15,7 @@ import type Pokemon from "#app/field/pokemon";
|
||||
import type { CommandPhase } from "#app/phases/command-phase";
|
||||
import MoveInfoOverlay from "./move-info-overlay";
|
||||
import { BattleType } from "#enums/battle-type";
|
||||
import { MoveUseType } from "#enums/move-use-type";
|
||||
|
||||
export default class FightUiHandler extends UiHandler implements InfoToggle {
|
||||
public static readonly MOVES_CONTAINER_NAME = "moves";
|
||||
@ -138,51 +139,58 @@ export default class FightUiHandler extends UiHandler implements InfoToggle {
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Process the player inputting the selected {@linkcode Button}.
|
||||
* @param button - The {@linkcode Button} being pressed
|
||||
* @returns Whether the input was successful (ie did anything).
|
||||
*/
|
||||
processInput(button: Button): boolean {
|
||||
const ui = this.getUi();
|
||||
|
||||
const cursor = this.getCursor();
|
||||
let success = false;
|
||||
|
||||
const cursor = this.getCursor();
|
||||
|
||||
if (button === Button.CANCEL || button === Button.ACTION) {
|
||||
if (button === Button.ACTION) {
|
||||
if ((globalScene.getCurrentPhase() as CommandPhase).handleCommand(this.fromCommand, cursor, false)) {
|
||||
switch (button) {
|
||||
case Button.CANCEL:
|
||||
{
|
||||
// Attempts to back out of the move selection pane are blocked in certain MEs
|
||||
const { battleType, mysteryEncounter } = globalScene.currentBattle;
|
||||
if (battleType === BattleType.MYSTERY_ENCOUNTER || !mysteryEncounter?.skipToFightInput) {
|
||||
ui.setMode(UiMode.COMMAND, this.fieldIndex);
|
||||
success = true;
|
||||
}
|
||||
}
|
||||
break;
|
||||
case Button.ACTION:
|
||||
if (
|
||||
(globalScene.getCurrentPhase() as CommandPhase).handleCommand(this.fromCommand, cursor, MoveUseType.NORMAL)
|
||||
) {
|
||||
success = true;
|
||||
} else {
|
||||
ui.playError();
|
||||
}
|
||||
} else {
|
||||
// Cannot back out of fight menu if skipToFightInput is enabled
|
||||
const { battleType, mysteryEncounter } = globalScene.currentBattle;
|
||||
if (battleType !== BattleType.MYSTERY_ENCOUNTER || !mysteryEncounter?.skipToFightInput) {
|
||||
ui.setMode(UiMode.COMMAND, this.fieldIndex);
|
||||
success = true;
|
||||
break;
|
||||
case Button.UP:
|
||||
if (cursor >= 2) {
|
||||
success = this.setCursor(cursor - 2);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
switch (button) {
|
||||
case Button.UP:
|
||||
if (cursor >= 2) {
|
||||
success = this.setCursor(cursor - 2);
|
||||
}
|
||||
break;
|
||||
case Button.DOWN:
|
||||
if (cursor < 2) {
|
||||
success = this.setCursor(cursor + 2);
|
||||
}
|
||||
break;
|
||||
case Button.LEFT:
|
||||
if (cursor % 2 === 1) {
|
||||
success = this.setCursor(cursor - 1);
|
||||
}
|
||||
break;
|
||||
case Button.RIGHT:
|
||||
if (cursor % 2 === 0) {
|
||||
success = this.setCursor(cursor + 1);
|
||||
}
|
||||
break;
|
||||
}
|
||||
break;
|
||||
case Button.DOWN:
|
||||
if (cursor < 2) {
|
||||
success = this.setCursor(cursor + 2);
|
||||
}
|
||||
break;
|
||||
case Button.LEFT:
|
||||
if (cursor % 2 === 1) {
|
||||
success = this.setCursor(cursor - 1);
|
||||
}
|
||||
break;
|
||||
case Button.RIGHT:
|
||||
if (cursor % 2 === 0) {
|
||||
success = this.setCursor(cursor + 1);
|
||||
}
|
||||
break;
|
||||
default:
|
||||
// other inputs do nothing while in fight menu
|
||||
}
|
||||
|
||||
if (success) {
|
||||
|
@ -1,11 +1,11 @@
|
||||
import type { PlayerPokemon, PokemonMove } from "#app/field/pokemon";
|
||||
import type { PlayerPokemon, PokemonMove, TurnMove } from "#app/field/pokemon";
|
||||
import type Pokemon from "#app/field/pokemon";
|
||||
import { MoveResult } from "#app/field/pokemon";
|
||||
import { addBBCodeTextObject, addTextObject, getTextColor, TextStyle } from "#app/ui/text";
|
||||
import { Command } from "#app/ui/command-ui-handler";
|
||||
import MessageUiHandler from "#app/ui/message-ui-handler";
|
||||
import { UiMode } from "#enums/ui-mode";
|
||||
import { BooleanHolder, toReadableString, randInt, getLocalizedSpriteKey } from "#app/utils/common";
|
||||
import { BooleanHolder, toReadableString, randInt, getLocalizedSpriteKey, isNullOrUndefined } from "#app/utils/common";
|
||||
import {
|
||||
PokemonFormChangeItemModifier,
|
||||
PokemonHeldItemModifier,
|
||||
@ -1027,12 +1027,12 @@ export default class PartyUiHandler extends MessageUiHandler {
|
||||
(m as SwitchEffectTransferModifier).pokemonId === globalScene.getPlayerField()[this.fieldIndex].id,
|
||||
);
|
||||
|
||||
const moveHistory = globalScene.getPlayerField()[this.fieldIndex].getMoveHistory();
|
||||
const lastMove: TurnMove | undefined = globalScene.getPlayerField()[this.fieldIndex].getLastXMoves()[0];
|
||||
const isBatonPassMove =
|
||||
this.partyUiMode === PartyUiMode.FAINT_SWITCH &&
|
||||
moveHistory.length &&
|
||||
allMoves[moveHistory[moveHistory.length - 1].move].getAttrs(ForceSwitchOutAttr)[0]?.isBatonPass() &&
|
||||
moveHistory[moveHistory.length - 1].result === MoveResult.SUCCESS;
|
||||
!isNullOrUndefined(lastMove) &&
|
||||
allMoves[lastMove.move].getAttrs(ForceSwitchOutAttr)[0]?.isBatonPass() &&
|
||||
lastMove.result === MoveResult.SUCCESS;
|
||||
|
||||
// isBatonPassMove and allowBatonModifierSwitch shouldn't ever be true
|
||||
// at the same time, because they both explicitly check for a mutually
|
||||
|
@ -42,8 +42,8 @@ describe("Moves - Dig", () => {
|
||||
const enemyPokemon = game.scene.getEnemyPokemon()!;
|
||||
|
||||
game.move.select(Moves.DIG);
|
||||
|
||||
await game.phaseInterceptor.to("TurnEndPhase");
|
||||
|
||||
expect(playerPokemon.getTag(BattlerTagType.UNDERGROUND)).toBeDefined();
|
||||
expect(enemyPokemon.getLastXMoves(1)[0].result).toBe(MoveResult.MISS);
|
||||
expect(playerPokemon.hp).toBe(playerPokemon.getMaxHp());
|
||||
@ -53,9 +53,22 @@ describe("Moves - Dig", () => {
|
||||
await game.phaseInterceptor.to("TurnEndPhase");
|
||||
expect(playerPokemon.getTag(BattlerTagType.UNDERGROUND)).toBeUndefined();
|
||||
expect(enemyPokemon.hp).toBeLessThan(enemyPokemon.getMaxHp());
|
||||
expect(playerPokemon.getMoveQueue()).toHaveLength(0);
|
||||
expect(playerPokemon.getMoveHistory()).toHaveLength(2);
|
||||
});
|
||||
|
||||
const playerDig = playerPokemon.getMoveset().find(mv => mv && mv.moveId === Moves.DIG);
|
||||
it("should deduct PP only on the 2nd turn of the move", async () => {
|
||||
await game.classicMode.startBattle([Species.MAGIKARP]);
|
||||
|
||||
const playerPokemon = game.scene.getPlayerPokemon()!;
|
||||
|
||||
game.move.select(Moves.DIG);
|
||||
await game.phaseInterceptor.to("TurnEndPhase");
|
||||
|
||||
const playerDig = playerPokemon.getMoveset().find(mv => mv?.moveId === Moves.DIG);
|
||||
expect(playerDig?.ppUsed).toBe(0);
|
||||
|
||||
await game.phaseInterceptor.to("TurnEndPhase");
|
||||
expect(playerDig?.ppUsed).toBe(1);
|
||||
});
|
||||
|
||||
|
@ -1,8 +1,10 @@
|
||||
import { BattlerIndex } from "#app/battle";
|
||||
import { MoveResult } from "#app/field/pokemon";
|
||||
import { Abilities } from "#enums/abilities";
|
||||
import { MoveUseType } from "#enums/move-use-type";
|
||||
import { Moves } from "#enums/moves";
|
||||
import { Species } from "#enums/species";
|
||||
import { Stat } from "#enums/stat";
|
||||
import GameManager from "#test/testUtils/gameManager";
|
||||
import { afterEach, beforeAll, beforeEach, describe, expect, it } from "vitest";
|
||||
|
||||
@ -28,25 +30,31 @@ describe("Moves - Disable", () => {
|
||||
.enemyAbility(Abilities.BALL_FETCH)
|
||||
.moveset([Moves.DISABLE, Moves.SPLASH])
|
||||
.enemyMoveset(Moves.SPLASH)
|
||||
.starterSpecies(Species.PIKACHU)
|
||||
.enemySpecies(Species.SHUCKLE);
|
||||
});
|
||||
|
||||
it("restricts moves", async () => {
|
||||
await game.classicMode.startBattle();
|
||||
it("should restrict the last move used", async () => {
|
||||
game.override.enemyMoveset([Moves.GROWL, Moves.SPLASH]);
|
||||
await game.classicMode.startBattle([Species.PIKACHU]);
|
||||
|
||||
const enemyMon = game.scene.getEnemyPokemon()!;
|
||||
|
||||
game.move.select(Moves.SPLASH);
|
||||
await game.forceEnemyMove(Moves.GROWL);
|
||||
await game.toNextTurn();
|
||||
|
||||
game.move.select(Moves.DISABLE);
|
||||
await game.forceEnemyMove(Moves.SPLASH);
|
||||
await game.setTurnOrder([BattlerIndex.ENEMY, BattlerIndex.PLAYER]);
|
||||
await game.toNextTurn();
|
||||
|
||||
expect(enemyMon.getMoveHistory()).toHaveLength(1);
|
||||
expect(enemyMon.getLastXMoves(-1)).toHaveLength(1);
|
||||
expect(enemyMon.isMoveRestricted(Moves.SPLASH)).toBe(true);
|
||||
expect(enemyMon.isMoveRestricted(Moves.GROWL)).toBe(false);
|
||||
});
|
||||
|
||||
it("fails if enemy has no move history", async () => {
|
||||
await game.classicMode.startBattle();
|
||||
it("should fail if enemy has no move history", async () => {
|
||||
await game.classicMode.startBattle([Species.PIKACHU]);
|
||||
|
||||
const playerMon = game.scene.getPlayerPokemon()!;
|
||||
const enemyMon = game.scene.getEnemyPokemon()!;
|
||||
@ -55,15 +63,15 @@ describe("Moves - Disable", () => {
|
||||
await game.setTurnOrder([BattlerIndex.PLAYER, BattlerIndex.ENEMY]);
|
||||
await game.toNextTurn();
|
||||
|
||||
expect(playerMon.getMoveHistory()[0]).toMatchObject({
|
||||
expect(playerMon.getLastXMoves()[0]).toMatchObject({
|
||||
move: Moves.DISABLE,
|
||||
result: MoveResult.FAIL,
|
||||
});
|
||||
expect(enemyMon.isMoveRestricted(Moves.SPLASH)).toBe(false);
|
||||
}, 20000);
|
||||
});
|
||||
|
||||
it("causes STRUGGLE if all usable moves are disabled", async () => {
|
||||
await game.classicMode.startBattle();
|
||||
await game.classicMode.startBattle([Species.PIKACHU]);
|
||||
|
||||
const enemyMon = game.scene.getEnemyPokemon()!;
|
||||
|
||||
@ -74,15 +82,14 @@ describe("Moves - Disable", () => {
|
||||
game.move.select(Moves.SPLASH);
|
||||
await game.toNextTurn();
|
||||
|
||||
const enemyHistory = enemyMon.getMoveHistory();
|
||||
const enemyHistory = enemyMon.getLastXMoves(-1);
|
||||
expect(enemyHistory).toHaveLength(2);
|
||||
expect(enemyHistory[0].move).toBe(Moves.SPLASH);
|
||||
expect(enemyHistory[1].move).toBe(Moves.STRUGGLE);
|
||||
}, 20000);
|
||||
expect(enemyHistory.map(m => m.move)).toEqual([Moves.STRUGGLE, Moves.SPLASH]);
|
||||
});
|
||||
|
||||
it("cannot disable STRUGGLE", async () => {
|
||||
game.override.enemyMoveset([Moves.STRUGGLE]);
|
||||
await game.classicMode.startBattle();
|
||||
await game.classicMode.startBattle([Species.PIKACHU]);
|
||||
|
||||
const playerMon = game.scene.getPlayerPokemon()!;
|
||||
const enemyMon = game.scene.getEnemyPokemon()!;
|
||||
@ -94,33 +101,39 @@ describe("Moves - Disable", () => {
|
||||
expect(playerMon.getLastXMoves()[0].result).toBe(MoveResult.FAIL);
|
||||
expect(enemyMon.getLastXMoves()[0].move).toBe(Moves.STRUGGLE);
|
||||
expect(enemyMon.isMoveRestricted(Moves.STRUGGLE)).toBe(false);
|
||||
}, 20000);
|
||||
});
|
||||
|
||||
it("interrupts target's move when target moves after", async () => {
|
||||
await game.classicMode.startBattle();
|
||||
it("should interrupt target's move if used first", async () => {
|
||||
await game.classicMode.startBattle([Species.PIKACHU]);
|
||||
|
||||
const enemyMon = game.scene.getEnemyPokemon()!;
|
||||
// add splash to enemy move history
|
||||
enemyMon.pushMoveHistory({
|
||||
move: Moves.SPLASH,
|
||||
targets: [BattlerIndex.ENEMY],
|
||||
useType: MoveUseType.NORMAL,
|
||||
});
|
||||
|
||||
game.move.select(Moves.SPLASH);
|
||||
await game.toNextTurn();
|
||||
|
||||
// Both mons just used Splash last turn; now have player use Disable.
|
||||
game.move.select(Moves.DISABLE);
|
||||
await game.setTurnOrder([BattlerIndex.PLAYER, BattlerIndex.ENEMY]);
|
||||
await game.toNextTurn();
|
||||
|
||||
const enemyHistory = enemyMon.getMoveHistory();
|
||||
const enemyHistory = enemyMon.getLastXMoves(-1);
|
||||
expect(enemyHistory).toHaveLength(2);
|
||||
expect(enemyHistory[0]).toMatchObject({
|
||||
move: Moves.SPLASH,
|
||||
result: MoveResult.SUCCESS,
|
||||
result: MoveResult.FAIL,
|
||||
});
|
||||
expect(enemyHistory[1].result).toBe(MoveResult.FAIL);
|
||||
}, 20000);
|
||||
});
|
||||
|
||||
it("disables NATURE POWER, not the move invoked by it", async () => {
|
||||
game.override.enemyMoveset([Moves.NATURE_POWER]);
|
||||
await game.classicMode.startBattle();
|
||||
it.each([
|
||||
{ name: "Nature Power", moveId: Moves.NATURE_POWER },
|
||||
{ name: "Mirror Move", moveId: Moves.MIRROR_MOVE },
|
||||
{ name: "Copycat", moveId: Moves.COPYCAT },
|
||||
{ name: "Copycat", moveId: Moves.COPYCAT },
|
||||
])("should ignore virtual moves called by $name", async ({ moveId }) => {
|
||||
game.override.enemyMoveset(moveId);
|
||||
await game.classicMode.startBattle([Species.PIKACHU]);
|
||||
|
||||
const enemyMon = game.scene.getEnemyPokemon()!;
|
||||
|
||||
@ -128,27 +141,38 @@ describe("Moves - Disable", () => {
|
||||
await game.setTurnOrder([BattlerIndex.ENEMY, BattlerIndex.PLAYER]);
|
||||
await game.toNextTurn();
|
||||
|
||||
expect(enemyMon.isMoveRestricted(Moves.NATURE_POWER)).toBe(true);
|
||||
expect(enemyMon.isMoveRestricted(enemyMon.getLastXMoves(2)[0].move)).toBe(false);
|
||||
}, 20000);
|
||||
expect.soft(enemyMon.isMoveRestricted(moveId), `calling move ${Moves[moveId]} was not disabled`).toBe(true);
|
||||
const calledMove = enemyMon.getLastXMoves()[0].move;
|
||||
expect(
|
||||
enemyMon.isMoveRestricted(calledMove),
|
||||
`called move ${Moves[calledMove]} (from ${Moves[moveId]}) was incorrectly disabled`,
|
||||
).toBe(false);
|
||||
});
|
||||
|
||||
it("disables most recent move", async () => {
|
||||
game.override.enemyMoveset([Moves.SPLASH, Moves.TACKLE]);
|
||||
await game.classicMode.startBattle();
|
||||
it("should ignore dancer copied moves, even if also in moveset", async () => {
|
||||
game.override
|
||||
.enemyAbility(Abilities.DANCER)
|
||||
.moveset([Moves.DISABLE, Moves.SWORDS_DANCE])
|
||||
.enemyMoveset([Moves.SPLASH, Moves.SWORDS_DANCE]);
|
||||
|
||||
const enemyMon = game.scene.getEnemyPokemon()!;
|
||||
await game.classicMode.startBattle([Species.PIKACHU]);
|
||||
|
||||
game.move.select(Moves.SPLASH);
|
||||
await game.forceEnemyMove(Moves.SPLASH, BattlerIndex.PLAYER);
|
||||
game.move.select(Moves.SWORDS_DANCE);
|
||||
await game.forceEnemyMove(Moves.SPLASH);
|
||||
await game.setTurnOrder([BattlerIndex.ENEMY, BattlerIndex.PLAYER]);
|
||||
await game.toNextTurn();
|
||||
|
||||
game.move.select(Moves.DISABLE);
|
||||
await game.forceEnemyMove(Moves.TACKLE, BattlerIndex.PLAYER);
|
||||
await game.setTurnOrder([BattlerIndex.ENEMY, BattlerIndex.PLAYER]);
|
||||
await game.toNextTurn();
|
||||
await game.forceEnemyMove(Moves.SWORDS_DANCE);
|
||||
await game.setTurnOrder([BattlerIndex.PLAYER, BattlerIndex.ENEMY]);
|
||||
await game.phaseInterceptor.to("TurnEndPhase");
|
||||
|
||||
expect(enemyMon.isMoveRestricted(Moves.TACKLE)).toBe(true);
|
||||
expect(enemyMon.isMoveRestricted(Moves.SPLASH)).toBe(false);
|
||||
}, 20000);
|
||||
// Dancer-induced Swords Dance was ignored in favor of splash,
|
||||
// leaving the subsequent _normal_ swords dance free to work as normal
|
||||
const shuckle = game.scene.getEnemyPokemon()!;
|
||||
expect.soft(shuckle.isMoveRestricted(Moves.SPLASH)).toBe(true);
|
||||
expect.soft(shuckle.isMoveRestricted(Moves.SWORDS_DANCE)).toBe(false);
|
||||
expect(shuckle.getLastXMoves()[0]).toMatchObject({ move: Moves.SWORDS_DANCE, result: MoveResult.SUCCESS });
|
||||
expect(shuckle.getStatStage(Stat.ATK)).toBe(2);
|
||||
});
|
||||
});
|
||||
|
@ -80,7 +80,7 @@ describe("Moves - Electro Shot", () => {
|
||||
expect(playerPokemon.getTag(BattlerTagType.CHARGING)).toBeUndefined();
|
||||
expect(enemyPokemon.hp).toBeLessThan(enemyPokemon.getMaxHp());
|
||||
expect(playerPokemon.getMoveHistory()).toHaveLength(2);
|
||||
expect(playerPokemon.getLastXMoves(1)[0].result).toBe(MoveResult.SUCCESS);
|
||||
expect(playerPokemon.getLastXMoves()[0].result).toBe(MoveResult.SUCCESS);
|
||||
|
||||
const playerElectroShot = playerPokemon.getMoveset().find(mv => mv && mv.moveId === Moves.ELECTRO_SHOT);
|
||||
expect(playerElectroShot?.ppUsed).toBe(1);
|
||||
|
@ -3,6 +3,7 @@ import type Pokemon from "#app/field/pokemon";
|
||||
import { MoveResult } from "#app/field/pokemon";
|
||||
import type { MovePhase } from "#app/phases/move-phase";
|
||||
import { Abilities } from "#enums/abilities";
|
||||
import { MoveUseType } from "#enums/move-use-type";
|
||||
import { Moves } from "#enums/moves";
|
||||
import { Species } from "#enums/species";
|
||||
import GameManager from "#test/testUtils/gameManager";
|
||||
@ -149,7 +150,7 @@ describe("Moves - Instruct", () => {
|
||||
game.move.select(Moves.INSTRUCT);
|
||||
await game.setTurnOrder([BattlerIndex.PLAYER, BattlerIndex.ENEMY]);
|
||||
await game.phaseInterceptor.to("MovePhase");
|
||||
// force enemy's instructed move to bork and then immediately thaw out
|
||||
// force enemy's instructed move (and only the instructed move) to bork
|
||||
await game.move.forceStatusActivation(true);
|
||||
await game.move.forceStatusActivation(false);
|
||||
await game.phaseInterceptor.to("TurnEndPhase", false);
|
||||
@ -200,26 +201,6 @@ describe("Moves - Instruct", () => {
|
||||
expect(karp1.isFainted()).toBe(true);
|
||||
expect(karp2.isFainted()).toBe(true);
|
||||
});
|
||||
it("should allow for dancer copying of instructed dance move", async () => {
|
||||
game.override.battleStyle("double").enemyMoveset([Moves.INSTRUCT, Moves.SPLASH]).enemyLevel(1000);
|
||||
await game.classicMode.startBattle([Species.ORICORIO, Species.VOLCARONA]);
|
||||
|
||||
const [oricorio, volcarona] = game.scene.getPlayerField();
|
||||
game.move.changeMoveset(oricorio, Moves.SPLASH);
|
||||
game.move.changeMoveset(volcarona, Moves.FIERY_DANCE);
|
||||
|
||||
game.move.select(Moves.SPLASH, BattlerIndex.PLAYER);
|
||||
game.move.select(Moves.FIERY_DANCE, BattlerIndex.PLAYER_2, BattlerIndex.ENEMY);
|
||||
await game.forceEnemyMove(Moves.INSTRUCT, BattlerIndex.PLAYER_2);
|
||||
await game.forceEnemyMove(Moves.SPLASH);
|
||||
await game.setTurnOrder([BattlerIndex.PLAYER, BattlerIndex.PLAYER_2, BattlerIndex.ENEMY, BattlerIndex.ENEMY_2]);
|
||||
await game.phaseInterceptor.to("BerryPhase");
|
||||
|
||||
// fiery dance triggered dancer successfully for a total of 4 hits
|
||||
// Enemy level is set to a high value so that it does not faint even after all 4 hits
|
||||
instructSuccess(volcarona, Moves.FIERY_DANCE);
|
||||
expect(game.scene.getEnemyField()[0].turnData.attacksReceived.length).toBe(4);
|
||||
});
|
||||
|
||||
it("should not repeat move when switching out", async () => {
|
||||
game.override.enemyMoveset(Moves.INSTRUCT).enemySpecies(Species.UNOWN);
|
||||
@ -228,19 +209,18 @@ describe("Moves - Instruct", () => {
|
||||
const amoonguss = game.scene.getPlayerPokemon()!;
|
||||
game.move.changeMoveset(amoonguss, Moves.SEED_BOMB);
|
||||
|
||||
amoonguss.summonData.moveHistory = [
|
||||
{
|
||||
move: Moves.SEED_BOMB,
|
||||
targets: [BattlerIndex.ENEMY],
|
||||
result: MoveResult.SUCCESS,
|
||||
},
|
||||
];
|
||||
amoonguss.pushMoveHistory({
|
||||
move: Moves.SEED_BOMB,
|
||||
targets: [BattlerIndex.ENEMY],
|
||||
result: MoveResult.SUCCESS,
|
||||
useType: MoveUseType.NORMAL,
|
||||
});
|
||||
|
||||
game.doSwitchPokemon(1);
|
||||
await game.phaseInterceptor.to("TurnEndPhase", false);
|
||||
|
||||
const enemyMoves = game.scene.getEnemyPokemon()!.getLastXMoves(-1)!;
|
||||
expect(enemyMoves[0].result).toBe(MoveResult.FAIL);
|
||||
const enemyMoves = game.scene.getEnemyPokemon()?.getLastXMoves(-1)!;
|
||||
expect(enemyMoves?.[0]?.result).toBe(MoveResult.FAIL);
|
||||
});
|
||||
|
||||
it("should fail if no move has yet been used by target", async () => {
|
||||
@ -301,14 +281,12 @@ describe("Moves - Instruct", () => {
|
||||
|
||||
const player = game.scene.getPlayerPokemon()!;
|
||||
const enemy = game.scene.getEnemyPokemon()!;
|
||||
enemy.summonData.moveHistory = [
|
||||
{
|
||||
move: Moves.SONIC_BOOM,
|
||||
targets: [BattlerIndex.PLAYER],
|
||||
result: MoveResult.SUCCESS,
|
||||
virtual: false,
|
||||
},
|
||||
];
|
||||
enemy.pushMoveHistory({
|
||||
move: Moves.SONIC_BOOM,
|
||||
targets: [BattlerIndex.PLAYER],
|
||||
result: MoveResult.SUCCESS,
|
||||
useType: MoveUseType.NORMAL,
|
||||
});
|
||||
|
||||
game.move.select(Moves.INSTRUCT);
|
||||
await game.forceEnemyMove(Moves.HYPER_BEAM);
|
||||
@ -350,14 +328,12 @@ describe("Moves - Instruct", () => {
|
||||
await game.classicMode.startBattle([Species.LUCARIO, Species.BANETTE]);
|
||||
|
||||
const enemyPokemon = game.scene.getEnemyPokemon()!;
|
||||
enemyPokemon.summonData.moveHistory = [
|
||||
{
|
||||
move: Moves.WHIRLWIND,
|
||||
targets: [BattlerIndex.PLAYER],
|
||||
result: MoveResult.SUCCESS,
|
||||
virtual: false,
|
||||
},
|
||||
];
|
||||
enemyPokemon.pushMoveHistory({
|
||||
move: Moves.WHIRLWIND,
|
||||
targets: [BattlerIndex.PLAYER],
|
||||
result: MoveResult.SUCCESS,
|
||||
useType: MoveUseType.NORMAL,
|
||||
});
|
||||
|
||||
game.move.select(Moves.INSTRUCT);
|
||||
await game.forceEnemyMove(Moves.SPLASH);
|
||||
@ -377,11 +353,20 @@ describe("Moves - Instruct", () => {
|
||||
.enemyMoveset([Moves.SPLASH, Moves.PSYCHIC_TERRAIN]);
|
||||
await game.classicMode.startBattle([Species.BANETTE, Species.KLEFKI]);
|
||||
|
||||
const banette = game.scene.getPlayerPokemon()!;
|
||||
|
||||
game.move.select(Moves.QUICK_ATTACK, BattlerIndex.PLAYER, BattlerIndex.ENEMY); // succeeds due to terrain no
|
||||
game.move.select(Moves.SPLASH, BattlerIndex.PLAYER_2);
|
||||
await game.forceEnemyMove(Moves.SPLASH);
|
||||
await game.forceEnemyMove(Moves.PSYCHIC_TERRAIN);
|
||||
await game.toNextTurn();
|
||||
expect(banette.getLastXMoves(-1)[0]).toEqual(
|
||||
expect.objectContaining({
|
||||
move: Moves.QUICK_ATTACK,
|
||||
targets: [BattlerIndex.ENEMY],
|
||||
result: MoveResult.SUCCESS,
|
||||
}),
|
||||
);
|
||||
|
||||
game.move.select(Moves.SPLASH, BattlerIndex.PLAYER);
|
||||
game.move.select(Moves.INSTRUCT, BattlerIndex.PLAYER_2, BattlerIndex.PLAYER);
|
||||
@ -389,32 +374,76 @@ describe("Moves - Instruct", () => {
|
||||
await game.phaseInterceptor.to("TurnEndPhase", false);
|
||||
|
||||
// quick attack failed when instructed
|
||||
const banette = game.scene.getPlayerPokemon()!;
|
||||
expect(banette.getLastXMoves(-1)[1].move).toBe(Moves.QUICK_ATTACK);
|
||||
expect(banette.getLastXMoves(-1)[1].result).toBe(MoveResult.FAIL);
|
||||
});
|
||||
|
||||
it("should still work w/ prankster in psychic terrain", async () => {
|
||||
game.override.battleStyle("double").enemyMoveset([Moves.SPLASH, Moves.PSYCHIC_TERRAIN]);
|
||||
// TODO: Enable once Sky Drop is fully implemented
|
||||
it.todo("should not work against Sky Dropped targets, even if user/target have No Guard", async () => {
|
||||
game.override.battleStyle("double").ability(Abilities.NO_GUARD).enemyMoveset([Moves.ASTONISH, Moves.SKY_DROP]);
|
||||
await game.classicMode.startBattle([Species.BANETTE, Species.KLEFKI]);
|
||||
|
||||
const [banette, klefki] = game.scene.getPlayerField()!;
|
||||
game.move.changeMoveset(banette, [Moves.VINE_WHIP, Moves.SPLASH]);
|
||||
game.move.changeMoveset(klefki, [Moves.INSTRUCT, Moves.SPLASH]);
|
||||
const [banette, klefki] = game.scene.getPlayerField();
|
||||
game.move.changeMoveset(banette, Moves.VINE_WHIP);
|
||||
game.move.changeMoveset(klefki, Moves.INSTRUCT);
|
||||
banette.pushMoveHistory({
|
||||
move: Moves.VINE_WHIP,
|
||||
targets: [BattlerIndex.ENEMY],
|
||||
result: MoveResult.SUCCESS,
|
||||
useType: MoveUseType.NORMAL,
|
||||
});
|
||||
|
||||
game.move.select(Moves.VINE_WHIP, BattlerIndex.PLAYER, BattlerIndex.ENEMY);
|
||||
game.move.select(Moves.SPLASH, BattlerIndex.PLAYER_2);
|
||||
await game.forceEnemyMove(Moves.SPLASH);
|
||||
await game.forceEnemyMove(Moves.PSYCHIC_TERRAIN);
|
||||
await game.toNextTurn();
|
||||
// Attempt to instruct banette after having been sent airborne
|
||||
game.move.select(Moves.VINE_WHIP, BattlerIndex.PLAYER);
|
||||
game.move.select(Moves.INSTRUCT, BattlerIndex.PLAYER_2, BattlerIndex.PLAYER);
|
||||
await game.forceEnemyMove(Moves.SKY_DROP, BattlerIndex.PLAYER);
|
||||
await game.forceEnemyMove(Moves.ASTONISH, BattlerIndex.PLAYER);
|
||||
await game.setTurnOrder([BattlerIndex.ENEMY, BattlerIndex.PLAYER_2, BattlerIndex.PLAYER, BattlerIndex.ENEMY_2]);
|
||||
await game.phaseInterceptor.to("TurnEndPhase", false);
|
||||
|
||||
game.move.select(Moves.SPLASH, BattlerIndex.PLAYER);
|
||||
// Klefki instruct fails due to banette being airborne, even though it got hit prior
|
||||
expect(banette.visible).toBe(false);
|
||||
expect(banette.isFullHp()).toBe(false);
|
||||
expect(klefki.getLastXMoves(-1)[0]).toMatchObject({
|
||||
move: Moves.INSTRUCT,
|
||||
targets: [BattlerIndex.PLAYER],
|
||||
result: MoveResult.FAIL,
|
||||
});
|
||||
});
|
||||
|
||||
it("should still work with prankster in psychic terrain", async () => {
|
||||
game.override
|
||||
.battleStyle("double")
|
||||
.ability(Abilities.PRANKSTER)
|
||||
.enemyMoveset(Moves.SPLASH)
|
||||
.enemyAbility(Abilities.PSYCHIC_SURGE);
|
||||
await game.classicMode.startBattle([Species.BANETTE, Species.KLEFKI]);
|
||||
|
||||
const [banette, klefki] = game.scene.getPlayerField();
|
||||
game.move.changeMoveset(banette, [Moves.VINE_WHIP]);
|
||||
game.move.changeMoveset(klefki, Moves.INSTRUCT);
|
||||
banette.pushMoveHistory({
|
||||
move: Moves.VINE_WHIP,
|
||||
targets: [BattlerIndex.ENEMY],
|
||||
result: MoveResult.SUCCESS,
|
||||
useType: MoveUseType.NORMAL,
|
||||
});
|
||||
|
||||
game.move.select(Moves.VINE_WHIP, BattlerIndex.PLAYER);
|
||||
game.move.select(Moves.INSTRUCT, BattlerIndex.PLAYER_2, BattlerIndex.PLAYER); // copies vine whip
|
||||
await game.setTurnOrder([BattlerIndex.PLAYER_2, BattlerIndex.PLAYER, BattlerIndex.ENEMY, BattlerIndex.ENEMY_2]);
|
||||
await game.phaseInterceptor.to("TurnEndPhase", false);
|
||||
|
||||
// Klefki instructing a non-priority move succeeds, ignoring the priority of Instruct itself
|
||||
expect(banette.getLastXMoves(-1)[1].move).toBe(Moves.VINE_WHIP);
|
||||
expect(banette.getLastXMoves(-1)[2].move).toBe(Moves.VINE_WHIP);
|
||||
expect(banette.getMoveset().find(m => m?.moveId === Moves.VINE_WHIP)?.ppUsed).toBe(2);
|
||||
expect(klefki.getLastXMoves(-1)[0]).toEqual(
|
||||
expect.objectContaining({
|
||||
move: Moves.INSTRUCT,
|
||||
targets: [BattlerIndex.PLAYER],
|
||||
result: MoveResult.SUCCESS,
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it("should cause spread moves to correctly hit targets in doubles after singles", async () => {
|
||||
@ -423,14 +452,15 @@ describe("Moves - Instruct", () => {
|
||||
.moveset([Moves.BREAKING_SWIPE, Moves.INSTRUCT, Moves.SPLASH])
|
||||
.enemyMoveset(Moves.SONIC_BOOM)
|
||||
.enemySpecies(Species.AXEW)
|
||||
.startingLevel(500);
|
||||
.startingLevel(500)
|
||||
.enemyLevel(1);
|
||||
await game.classicMode.startBattle([Species.KORAIDON, Species.KLEFKI]);
|
||||
|
||||
const koraidon = game.scene.getPlayerField()[0]!;
|
||||
|
||||
game.move.select(Moves.BREAKING_SWIPE);
|
||||
await game.phaseInterceptor.to("TurnEndPhase", false);
|
||||
expect(koraidon.getInverseHp()).toBe(0);
|
||||
expect(koraidon.hp).toBe(koraidon.getMaxHp());
|
||||
expect(koraidon.getLastXMoves(-1)[0].targets).toEqual([BattlerIndex.ENEMY]);
|
||||
await game.toNextWave();
|
||||
|
||||
@ -438,9 +468,10 @@ describe("Moves - Instruct", () => {
|
||||
game.move.select(Moves.INSTRUCT, BattlerIndex.PLAYER_2, BattlerIndex.PLAYER);
|
||||
await game.setTurnOrder([BattlerIndex.PLAYER_2, BattlerIndex.PLAYER, BattlerIndex.ENEMY, BattlerIndex.ENEMY_2]);
|
||||
await game.phaseInterceptor.to("TurnEndPhase", false);
|
||||
|
||||
// did not take damage since enemies died beforehand;
|
||||
// last move used hit both enemies
|
||||
expect(koraidon.getInverseHp()).toBe(0);
|
||||
expect(koraidon.hp).toBe(koraidon.getMaxHp());
|
||||
expect(koraidon.getLastXMoves(-1)[1].targets?.sort()).toEqual([BattlerIndex.ENEMY, BattlerIndex.ENEMY_2]);
|
||||
});
|
||||
|
||||
@ -450,7 +481,8 @@ describe("Moves - Instruct", () => {
|
||||
.moveset([Moves.BRUTAL_SWING, Moves.INSTRUCT, Moves.SPLASH])
|
||||
.enemySpecies(Species.AXEW)
|
||||
.enemyMoveset(Moves.SONIC_BOOM)
|
||||
.startingLevel(500);
|
||||
.startingLevel(500)
|
||||
.enemyLevel(1);
|
||||
await game.classicMode.startBattle([Species.KORAIDON, Species.KLEFKI]);
|
||||
|
||||
const koraidon = game.scene.getPlayerField()[0]!;
|
||||
@ -458,22 +490,24 @@ describe("Moves - Instruct", () => {
|
||||
game.move.select(Moves.BRUTAL_SWING);
|
||||
await game.setTurnOrder([BattlerIndex.PLAYER, BattlerIndex.ENEMY]);
|
||||
await game.phaseInterceptor.to("TurnEndPhase", false);
|
||||
expect(koraidon.getInverseHp()).toBe(0);
|
||||
|
||||
expect(koraidon.hp).toBe(koraidon.getMaxHp());
|
||||
expect(koraidon.getLastXMoves(-1)[0].targets).toEqual([BattlerIndex.ENEMY]);
|
||||
|
||||
await game.toNextWave();
|
||||
|
||||
game.move.select(Moves.SPLASH, BattlerIndex.PLAYER);
|
||||
game.move.select(Moves.INSTRUCT, BattlerIndex.PLAYER_2, BattlerIndex.PLAYER);
|
||||
await game.setTurnOrder([BattlerIndex.PLAYER_2, BattlerIndex.PLAYER, BattlerIndex.ENEMY, BattlerIndex.ENEMY_2]);
|
||||
await game.phaseInterceptor.to("TurnEndPhase", false);
|
||||
|
||||
// did not take damage since enemies died beforehand;
|
||||
// last move used hit everything around it
|
||||
expect(koraidon.getInverseHp()).toBe(0);
|
||||
expect(koraidon.getLastXMoves(-1)[1].targets?.sort()).toEqual([
|
||||
BattlerIndex.PLAYER_2,
|
||||
BattlerIndex.ENEMY,
|
||||
BattlerIndex.ENEMY_2,
|
||||
]);
|
||||
expect(koraidon.hp).toBe(koraidon.getMaxHp());
|
||||
expect(koraidon.getLastXMoves(-1)[1].targets).toHaveLength(3);
|
||||
expect(koraidon.getLastXMoves(-1)[1].targets).toEqual(
|
||||
expect.arrayContaining([BattlerIndex.PLAYER_2, BattlerIndex.ENEMY, BattlerIndex.ENEMY_2]),
|
||||
);
|
||||
});
|
||||
|
||||
it("should cause multi-hit moves to hit the appropriate number of times in singles", async () => {
|
||||
|
@ -1,6 +1,7 @@
|
||||
import { BattlerIndex } from "#app/battle";
|
||||
import { MoveResult } from "#app/field/pokemon";
|
||||
import { Abilities } from "#enums/abilities";
|
||||
import { MoveUseType } from "#enums/move-use-type";
|
||||
import { Moves } from "#enums/moves";
|
||||
import { Species } from "#enums/species";
|
||||
import GameManager from "#test/testUtils/gameManager";
|
||||
@ -53,19 +54,19 @@ describe("Moves - Last Resort", () => {
|
||||
expectLastResortFail();
|
||||
|
||||
// Splash (1/3)
|
||||
blissey.pushMoveHistory({ move: Moves.SPLASH, targets: [BattlerIndex.PLAYER] });
|
||||
blissey.pushMoveHistory({ move: Moves.SPLASH, targets: [BattlerIndex.PLAYER], useType: MoveUseType.NORMAL });
|
||||
game.move.select(Moves.LAST_RESORT);
|
||||
await game.phaseInterceptor.to("TurnEndPhase");
|
||||
expectLastResortFail();
|
||||
|
||||
// Growl (2/3)
|
||||
blissey.pushMoveHistory({ move: Moves.GROWL, targets: [BattlerIndex.ENEMY] });
|
||||
blissey.pushMoveHistory({ move: Moves.GROWL, targets: [BattlerIndex.ENEMY], useType: MoveUseType.NORMAL });
|
||||
game.move.select(Moves.LAST_RESORT);
|
||||
await game.phaseInterceptor.to("TurnEndPhase");
|
||||
expectLastResortFail(); // Were last resort itself counted, it would error here
|
||||
|
||||
// Growth (3/3)
|
||||
blissey.pushMoveHistory({ move: Moves.GROWTH, targets: [BattlerIndex.PLAYER] });
|
||||
blissey.pushMoveHistory({ move: Moves.GROWTH, targets: [BattlerIndex.PLAYER], useType: MoveUseType.NORMAL });
|
||||
game.move.select(Moves.LAST_RESORT);
|
||||
await game.phaseInterceptor.to("TurnEndPhase");
|
||||
expect(game.scene.getPlayerPokemon()?.getLastXMoves()[0]).toEqual(
|
||||
@ -117,11 +118,12 @@ describe("Moves - Last Resort", () => {
|
||||
expect.objectContaining({
|
||||
move: Moves.LAST_RESORT,
|
||||
result: MoveResult.SUCCESS,
|
||||
virtual: true,
|
||||
useType: MoveUseType.FOLLOW_UP,
|
||||
}),
|
||||
expect.objectContaining({
|
||||
move: Moves.SLEEP_TALK,
|
||||
result: MoveResult.SUCCESS,
|
||||
useType: MoveUseType.NORMAL,
|
||||
}),
|
||||
]);
|
||||
});
|
||||
|
@ -1,8 +1,10 @@
|
||||
import { BattlerIndex } from "#app/battle";
|
||||
import { RechargingTag, SemiInvulnerableTag } from "#app/data/battler-tags";
|
||||
import { allMoves, RandomMoveAttr } from "#app/data/moves/move";
|
||||
import { Abilities } from "#app/enums/abilities";
|
||||
import { Stat } from "#app/enums/stat";
|
||||
import { CommandPhase } from "#app/phases/command-phase";
|
||||
import { BattlerTagType } from "#enums/battler-tag-type";
|
||||
import { Moves } from "#enums/moves";
|
||||
import { Species } from "#enums/species";
|
||||
import GameManager from "#test/testUtils/gameManager";
|
||||
@ -32,7 +34,6 @@ describe("Moves - Metronome", () => {
|
||||
.moveset([Moves.METRONOME, Moves.SPLASH])
|
||||
.battleStyle("single")
|
||||
.startingLevel(100)
|
||||
.starterSpecies(Species.REGIELEKI)
|
||||
.enemyLevel(100)
|
||||
.enemySpecies(Species.SHUCKLE)
|
||||
.enemyMoveset(Moves.SPLASH)
|
||||
@ -40,7 +41,7 @@ describe("Moves - Metronome", () => {
|
||||
});
|
||||
|
||||
it("should have one semi-invulnerable turn and deal damage on the second turn when a semi-invulnerable move is called", async () => {
|
||||
await game.classicMode.startBattle();
|
||||
await game.classicMode.startBattle([Species.REGIELEKI]);
|
||||
const player = game.scene.getPlayerPokemon()!;
|
||||
const enemy = game.scene.getEnemyPokemon()!;
|
||||
vi.spyOn(randomMoveAttr, "getMoveOverride").mockReturnValue(Moves.DIVE);
|
||||
@ -56,7 +57,7 @@ describe("Moves - Metronome", () => {
|
||||
});
|
||||
|
||||
it("should apply secondary effects of a move", async () => {
|
||||
await game.classicMode.startBattle();
|
||||
await game.classicMode.startBattle([Species.REGIELEKI]);
|
||||
const player = game.scene.getPlayerPokemon()!;
|
||||
vi.spyOn(randomMoveAttr, "getMoveOverride").mockReturnValue(Moves.WOOD_HAMMER);
|
||||
|
||||
@ -67,7 +68,7 @@ describe("Moves - Metronome", () => {
|
||||
});
|
||||
|
||||
it("should recharge after using recharge move", async () => {
|
||||
await game.classicMode.startBattle();
|
||||
await game.classicMode.startBattle([Species.REGIELEKI]);
|
||||
const player = game.scene.getPlayerPokemon()!;
|
||||
vi.spyOn(randomMoveAttr, "getMoveOverride").mockReturnValue(Moves.HYPER_BEAM);
|
||||
vi.spyOn(allMoves[Moves.HYPER_BEAM], "accuracy", "get").mockReturnValue(100);
|
||||
@ -78,6 +79,36 @@ describe("Moves - Metronome", () => {
|
||||
expect(player.getTag(RechargingTag)).toBeTruthy();
|
||||
});
|
||||
|
||||
it("should charge when calling charging moves while still maintaining follow-up status", async () => {
|
||||
game.override.moveset([]).enemyMoveset(Moves.SPITE);
|
||||
vi.spyOn(randomMoveAttr, "getMoveOverride").mockReturnValue(Moves.SOLAR_BEAM);
|
||||
await game.classicMode.startBattle([Species.REGIELEKI]);
|
||||
|
||||
const player = game.scene.getPlayerPokemon()!;
|
||||
game.move.changeMoveset(player, [Moves.METRONOME, Moves.SOLAR_BEAM]);
|
||||
|
||||
const [metronomeMove, solarBeamMove] = player.getMoveset();
|
||||
expect(metronomeMove).toBeDefined();
|
||||
expect(solarBeamMove).toBeDefined();
|
||||
|
||||
game.move.select(Moves.METRONOME);
|
||||
await game.setTurnOrder([BattlerIndex.PLAYER, BattlerIndex.ENEMY]);
|
||||
await game.phaseInterceptor.to("TurnEndPhase");
|
||||
|
||||
expect(player.getTag(BattlerTagType.CHARGING)).toBeTruthy();
|
||||
const turn1PpUsed = metronomeMove.ppUsed;
|
||||
expect.soft(turn1PpUsed).toBeGreaterThan(1);
|
||||
expect(solarBeamMove.ppUsed).toBe(0);
|
||||
|
||||
await game.setTurnOrder([BattlerIndex.PLAYER, BattlerIndex.ENEMY]);
|
||||
await game.toNextTurn();
|
||||
|
||||
expect(player.getTag(BattlerTagType.CHARGING)).toBeFalsy();
|
||||
const turn2PpUsed = metronomeMove.ppUsed - turn1PpUsed;
|
||||
expect(turn2PpUsed).toBeGreaterThan(1);
|
||||
expect(solarBeamMove.ppUsed).toBe(0);
|
||||
});
|
||||
|
||||
it("should only target ally for Aromatic Mist", async () => {
|
||||
game.override.battleStyle("double");
|
||||
await game.classicMode.startBattle([Species.REGIELEKI, Species.RATTATA]);
|
||||
@ -97,7 +128,7 @@ describe("Moves - Metronome", () => {
|
||||
});
|
||||
|
||||
it("should cause opponent to flee, and not crash for Roar", async () => {
|
||||
await game.classicMode.startBattle();
|
||||
await game.classicMode.startBattle([Species.REGIELEKI]);
|
||||
vi.spyOn(randomMoveAttr, "getMoveOverride").mockReturnValue(Moves.ROAR);
|
||||
|
||||
const enemyPokemon = game.scene.getEnemyPokemon()!;
|
||||
|
@ -7,7 +7,6 @@ import { Species } from "#enums/species";
|
||||
import { BerryPhase } from "#app/phases/berry-phase";
|
||||
import { MoveResult, PokemonMove } from "#app/field/pokemon";
|
||||
import { PokemonType } from "#enums/pokemon-type";
|
||||
import { MoveEffectPhase } from "#app/phases/move-effect-phase";
|
||||
import { StatusEffect } from "#enums/status-effect";
|
||||
import { BattlerIndex } from "#app/battle";
|
||||
|
||||
@ -168,10 +167,10 @@ describe("Moves - Powder", () => {
|
||||
game.move.select(Moves.FIERY_DANCE, 0, BattlerIndex.ENEMY);
|
||||
game.move.select(Moves.POWDER, 1, BattlerIndex.ENEMY);
|
||||
|
||||
await game.phaseInterceptor.to(MoveEffectPhase);
|
||||
await game.phaseInterceptor.to("MoveEffectPhase");
|
||||
const enemyStartingHp = enemyPokemon.hp;
|
||||
|
||||
await game.phaseInterceptor.to(BerryPhase, false);
|
||||
await game.phaseInterceptor.to("BerryPhase");
|
||||
|
||||
// player should not take damage
|
||||
expect(enemyPokemon.getLastXMoves()[0].result).toBe(MoveResult.FAIL);
|
||||
@ -182,7 +181,7 @@ describe("Moves - Powder", () => {
|
||||
);
|
||||
});
|
||||
|
||||
it("should cancel Fiery Dance, then prevent it from triggering Dancer", async () => {
|
||||
it("should cancel Fiery Dance and prevent it from triggering Dancer", async () => {
|
||||
game.override.ability(Abilities.DANCER).enemyMoveset(Moves.FIERY_DANCE);
|
||||
|
||||
await game.classicMode.startBattle([Species.CHARIZARD]);
|
||||
|
@ -2,7 +2,6 @@ import { Stat } from "#enums/stat";
|
||||
import { StockpilingTag } from "#app/data/battler-tags";
|
||||
import { allMoves } from "#app/data/moves/move";
|
||||
import { BattlerTagType } from "#app/enums/battler-tag-type";
|
||||
import type { TurnMove } from "#app/field/pokemon";
|
||||
import { MoveResult } from "#app/field/pokemon";
|
||||
import GameManager from "#test/testUtils/gameManager";
|
||||
import { Abilities } from "#enums/abilities";
|
||||
@ -127,7 +126,7 @@ describe("Moves - Spit Up", () => {
|
||||
game.move.select(Moves.SPIT_UP);
|
||||
await game.phaseInterceptor.to(TurnInitPhase);
|
||||
|
||||
expect(pokemon.getMoveHistory().at(-1)).toMatchObject<TurnMove>({
|
||||
expect(pokemon.getMoveHistory().at(-1)).toMatchObject({
|
||||
move: Moves.SPIT_UP,
|
||||
result: MoveResult.FAIL,
|
||||
targets: [game.scene.getEnemyPokemon()!.getBattlerIndex()],
|
||||
@ -154,7 +153,7 @@ describe("Moves - Spit Up", () => {
|
||||
|
||||
await game.phaseInterceptor.to(TurnInitPhase);
|
||||
|
||||
expect(pokemon.getMoveHistory().at(-1)).toMatchObject<TurnMove>({
|
||||
expect(pokemon.getMoveHistory().at(-1)).toMatchObject({
|
||||
move: Moves.SPIT_UP,
|
||||
result: MoveResult.SUCCESS,
|
||||
targets: [game.scene.getEnemyPokemon()!.getBattlerIndex()],
|
||||
@ -186,7 +185,7 @@ describe("Moves - Spit Up", () => {
|
||||
game.move.select(Moves.SPIT_UP);
|
||||
await game.phaseInterceptor.to(TurnInitPhase);
|
||||
|
||||
expect(pokemon.getMoveHistory().at(-1)).toMatchObject<TurnMove>({
|
||||
expect(pokemon.getMoveHistory().at(-1)).toMatchObject({
|
||||
move: Moves.SPIT_UP,
|
||||
result: MoveResult.SUCCESS,
|
||||
targets: [game.scene.getEnemyPokemon()!.getBattlerIndex()],
|
||||
|
@ -1,6 +1,5 @@
|
||||
import { Stat } from "#enums/stat";
|
||||
import { StockpilingTag } from "#app/data/battler-tags";
|
||||
import type { TurnMove } from "#app/field/pokemon";
|
||||
import { MoveResult } from "#app/field/pokemon";
|
||||
import { CommandPhase } from "#app/phases/command-phase";
|
||||
import { TurnInitPhase } from "#app/phases/turn-init-phase";
|
||||
@ -73,7 +72,7 @@ describe("Moves - Stockpile", () => {
|
||||
expect(user.getStatStage(Stat.SPDEF)).toBe(3);
|
||||
expect(stockpilingTag).toBeDefined();
|
||||
expect(stockpilingTag.stockpiledCount).toBe(3);
|
||||
expect(user.getMoveHistory().at(-1)).toMatchObject<TurnMove>({
|
||||
expect(user.getMoveHistory().at(-1)).toMatchObject({
|
||||
result: MoveResult.FAIL,
|
||||
move: Moves.STOCKPILE,
|
||||
targets: [user.getBattlerIndex()],
|
||||
|
@ -1,7 +1,6 @@
|
||||
import { Stat } from "#enums/stat";
|
||||
import { StockpilingTag } from "#app/data/battler-tags";
|
||||
import { BattlerTagType } from "#app/enums/battler-tag-type";
|
||||
import type { TurnMove } from "#app/field/pokemon";
|
||||
import { MoveResult } from "#app/field/pokemon";
|
||||
import { MovePhase } from "#app/phases/move-phase";
|
||||
import { TurnInitPhase } from "#app/phases/turn-init-phase";
|
||||
@ -135,7 +134,7 @@ describe("Moves - Swallow", () => {
|
||||
game.move.select(Moves.SWALLOW);
|
||||
await game.phaseInterceptor.to(TurnInitPhase);
|
||||
|
||||
expect(pokemon.getMoveHistory().at(-1)).toMatchObject<TurnMove>({
|
||||
expect(pokemon.getMoveHistory().at(-1)).toMatchObject({
|
||||
move: Moves.SWALLOW,
|
||||
result: MoveResult.FAIL,
|
||||
targets: [pokemon.getBattlerIndex()],
|
||||
@ -160,7 +159,7 @@ describe("Moves - Swallow", () => {
|
||||
|
||||
await game.phaseInterceptor.to(TurnInitPhase);
|
||||
|
||||
expect(pokemon.getMoveHistory().at(-1)).toMatchObject<TurnMove>({
|
||||
expect(pokemon.getMoveHistory().at(-1)).toMatchObject({
|
||||
move: Moves.SWALLOW,
|
||||
result: MoveResult.SUCCESS,
|
||||
targets: [pokemon.getBattlerIndex()],
|
||||
@ -191,7 +190,7 @@ describe("Moves - Swallow", () => {
|
||||
|
||||
await game.phaseInterceptor.to(TurnInitPhase);
|
||||
|
||||
expect(pokemon.getMoveHistory().at(-1)).toMatchObject<TurnMove>({
|
||||
expect(pokemon.getMoveHistory().at(-1)).toMatchObject({
|
||||
move: Moves.SWALLOW,
|
||||
result: MoveResult.SUCCESS,
|
||||
targets: [pokemon.getBattlerIndex()],
|
||||
|
@ -116,20 +116,14 @@ const POOL_3_POKEMON: { species: Species; formIndex?: number }[] = [
|
||||
|
||||
const POOL_4_POKEMON = [Species.GENESECT, Species.SLITHER_WING, Species.BUZZWOLE, Species.PHEROMOSA];
|
||||
|
||||
const PHYSICAL_TUTOR_MOVES = [
|
||||
Moves.MEGAHORN,
|
||||
Moves.ATTACK_ORDER,
|
||||
Moves.BUG_BITE,
|
||||
Moves.FIRST_IMPRESSION,
|
||||
Moves.LUNGE
|
||||
];
|
||||
const PHYSICAL_TUTOR_MOVES = [Moves.MEGAHORN, Moves.ATTACK_ORDER, Moves.BUG_BITE, Moves.FIRST_IMPRESSION, Moves.LUNGE];
|
||||
|
||||
const SPECIAL_TUTOR_MOVES = [
|
||||
Moves.SILVER_WIND,
|
||||
Moves.SIGNAL_BEAM,
|
||||
Moves.BUG_BUZZ,
|
||||
Moves.POLLEN_PUFF,
|
||||
Moves.STRUGGLE_BUG
|
||||
Moves.STRUGGLE_BUG,
|
||||
];
|
||||
|
||||
const STATUS_TUTOR_MOVES = [
|
||||
@ -137,16 +131,10 @@ const STATUS_TUTOR_MOVES = [
|
||||
Moves.DEFEND_ORDER,
|
||||
Moves.RAGE_POWDER,
|
||||
Moves.STICKY_WEB,
|
||||
Moves.SILK_TRAP
|
||||
Moves.SILK_TRAP,
|
||||
];
|
||||
|
||||
const MISC_TUTOR_MOVES = [
|
||||
Moves.LEECH_LIFE,
|
||||
Moves.U_TURN,
|
||||
Moves.HEAL_ORDER,
|
||||
Moves.QUIVER_DANCE,
|
||||
Moves.INFESTATION,
|
||||
];
|
||||
const MISC_TUTOR_MOVES = [Moves.LEECH_LIFE, Moves.U_TURN, Moves.HEAL_ORDER, Moves.QUIVER_DANCE, Moves.INFESTATION];
|
||||
|
||||
describe("Bug-Type Superfan - Mystery Encounter", () => {
|
||||
let phaserGame: Phaser.Game;
|
||||
|
@ -24,6 +24,7 @@ import { FunAndGamesEncounter } from "#app/data/mystery-encounters/encounters/fu
|
||||
import { Moves } from "#enums/moves";
|
||||
import { Command } from "#app/ui/command-ui-handler";
|
||||
import * as EncounterPhaseUtils from "#app/data/mystery-encounters/utils/encounter-phase-utils";
|
||||
import { MoveUseType } from "#enums/move-use-type";
|
||||
|
||||
const namespace = "mysteryEncounters/funAndGames";
|
||||
const defaultParty = [Species.LAPRAS, Species.GENGAR, Species.ABRA];
|
||||
@ -152,15 +153,15 @@ describe("Fun And Games! - Mystery Encounter", () => {
|
||||
});
|
||||
|
||||
// Turn 1
|
||||
(game.scene.getCurrentPhase() as CommandPhase).handleCommand(Command.FIGHT, 0, false);
|
||||
(game.scene.getCurrentPhase() as CommandPhase).handleCommand(Command.FIGHT, 0, MoveUseType.NORMAL);
|
||||
await game.phaseInterceptor.to(CommandPhase);
|
||||
|
||||
// Turn 2
|
||||
(game.scene.getCurrentPhase() as CommandPhase).handleCommand(Command.FIGHT, 0, false);
|
||||
(game.scene.getCurrentPhase() as CommandPhase).handleCommand(Command.FIGHT, 0, MoveUseType.NORMAL);
|
||||
await game.phaseInterceptor.to(CommandPhase);
|
||||
|
||||
// Turn 3
|
||||
(game.scene.getCurrentPhase() as CommandPhase).handleCommand(Command.FIGHT, 0, false);
|
||||
(game.scene.getCurrentPhase() as CommandPhase).handleCommand(Command.FIGHT, 0, MoveUseType.NORMAL);
|
||||
await game.phaseInterceptor.to(SelectModifierPhase, false);
|
||||
|
||||
// Rewards
|
||||
@ -179,7 +180,7 @@ describe("Fun And Games! - Mystery Encounter", () => {
|
||||
|
||||
// Skip minigame
|
||||
scene.currentBattle.mysteryEncounter!.misc.turnsRemaining = 0;
|
||||
(game.scene.getCurrentPhase() as CommandPhase).handleCommand(Command.FIGHT, 0, false);
|
||||
(game.scene.getCurrentPhase() as CommandPhase).handleCommand(Command.FIGHT, 0, MoveUseType.NORMAL);
|
||||
await game.phaseInterceptor.to(SelectModifierPhase, false);
|
||||
|
||||
// Rewards
|
||||
@ -208,7 +209,7 @@ describe("Fun And Games! - Mystery Encounter", () => {
|
||||
const wobbuffet = scene.getEnemyPokemon()!;
|
||||
wobbuffet.hp = Math.floor(0.2 * wobbuffet.getMaxHp());
|
||||
scene.currentBattle.mysteryEncounter!.misc.turnsRemaining = 0;
|
||||
(game.scene.getCurrentPhase() as CommandPhase).handleCommand(Command.FIGHT, 0, false);
|
||||
(game.scene.getCurrentPhase() as CommandPhase).handleCommand(Command.FIGHT, 0, MoveUseType.NORMAL);
|
||||
await game.phaseInterceptor.to(SelectModifierPhase, false);
|
||||
|
||||
// Rewards
|
||||
@ -238,7 +239,7 @@ describe("Fun And Games! - Mystery Encounter", () => {
|
||||
const wobbuffet = scene.getEnemyPokemon()!;
|
||||
wobbuffet.hp = Math.floor(0.1 * wobbuffet.getMaxHp());
|
||||
scene.currentBattle.mysteryEncounter!.misc.turnsRemaining = 0;
|
||||
(game.scene.getCurrentPhase() as CommandPhase).handleCommand(Command.FIGHT, 0, false);
|
||||
(game.scene.getCurrentPhase() as CommandPhase).handleCommand(Command.FIGHT, 0, MoveUseType.NORMAL);
|
||||
await game.phaseInterceptor.to(SelectModifierPhase, false);
|
||||
|
||||
// Rewards
|
||||
@ -268,7 +269,7 @@ describe("Fun And Games! - Mystery Encounter", () => {
|
||||
const wobbuffet = scene.getEnemyPokemon()!;
|
||||
wobbuffet.hp = 1;
|
||||
scene.currentBattle.mysteryEncounter!.misc.turnsRemaining = 0;
|
||||
(game.scene.getCurrentPhase() as CommandPhase).handleCommand(Command.FIGHT, 0, false);
|
||||
(game.scene.getCurrentPhase() as CommandPhase).handleCommand(Command.FIGHT, 0, MoveUseType.NORMAL);
|
||||
await game.phaseInterceptor.to(SelectModifierPhase, false);
|
||||
|
||||
// Rewards
|
||||
|
@ -58,6 +58,7 @@ import { expect, vi } from "vitest";
|
||||
import { globalScene } from "#app/global-scene";
|
||||
import type StarterSelectUiHandler from "#app/ui/starter-select-ui-handler";
|
||||
import { MockFetch } from "#test/testUtils/mocks/mockFetch";
|
||||
import { MoveUseType } from "#enums/move-use-type";
|
||||
|
||||
/**
|
||||
* Class to manage the game state and transitions between phases.
|
||||
@ -396,6 +397,7 @@ export default class GameManager {
|
||||
target !== undefined && !legalTargets.multiple && legalTargets.targets.includes(target)
|
||||
? [target]
|
||||
: enemy.getNextTargets(moveId),
|
||||
useType: MoveUseType.NORMAL,
|
||||
});
|
||||
|
||||
/**
|
||||
@ -417,9 +419,17 @@ export default class GameManager {
|
||||
};
|
||||
}
|
||||
|
||||
/** Transition to the next upcoming {@linkcode CommandPhase} */
|
||||
/**
|
||||
* Transition to the next upcoming {@linkcode CommandPhase}.
|
||||
* @returns A promise that resolves once the next {@linkcode CommandPhase} has been reached.
|
||||
|
||||
* @remarks
|
||||
* If all active player Pokemon are using a rampaging, charging, recharging or other move that
|
||||
* disables user input, this **will not resolve** until at least 1 player pokemon becomes actionable.
|
||||
*/
|
||||
async toNextTurn() {
|
||||
await this.phaseInterceptor.to(CommandPhase);
|
||||
console.log("==================[New Turn]==================");
|
||||
}
|
||||
|
||||
/**
|
||||
@ -429,6 +439,7 @@ export default class GameManager {
|
||||
async toNextWave() {
|
||||
this.doSelectModifier();
|
||||
|
||||
// forcibly end the message box for switching pokemon
|
||||
this.onNextPrompt(
|
||||
"CheckSwitchPhase",
|
||||
UiMode.CONFIRM,
|
||||
@ -439,7 +450,8 @@ export default class GameManager {
|
||||
() => this.isCurrentPhase(TurnInitPhase),
|
||||
);
|
||||
|
||||
await this.toNextTurn();
|
||||
await this.phaseInterceptor.to(CommandPhase);
|
||||
console.log("==================[New Wave]==================");
|
||||
}
|
||||
|
||||
/**
|
||||
@ -507,9 +519,9 @@ export default class GameManager {
|
||||
* @returns A promise that resolves once the fainted pokemon's FaintPhase finishes running.
|
||||
*/
|
||||
async killPokemon(pokemon: PlayerPokemon | EnemyPokemon) {
|
||||
pokemon.hp = 0;
|
||||
this.scene.unshiftPhase(new FaintPhase(pokemon.getBattlerIndex(), true));
|
||||
return new Promise<void>(async (resolve, reject) => {
|
||||
pokemon.hp = 0;
|
||||
this.scene.pushPhase(new FaintPhase(pokemon.getBattlerIndex(), true));
|
||||
await this.phaseInterceptor.to(FaintPhase).catch(e => reject(e));
|
||||
resolve();
|
||||
});
|
||||
@ -541,8 +553,8 @@ export default class GameManager {
|
||||
}
|
||||
|
||||
/**
|
||||
* Select a pokemon from the party menu during the given phase.
|
||||
* Only really handles the basic case of "navigate to party slot and press Action twice" -
|
||||
* Select a pokemon from the party menu during the given phase.
|
||||
* Only really handles the basic case of "navigate to party slot and press Action twice" -
|
||||
* any menus that come up afterwards are ignored and must be handled separately by the caller.
|
||||
* @param slot - The 0-indexed position of the pokemon in your party to switch to
|
||||
* @param inPhase - Which phase to expect the selection to occur in. Defaults to `SwitchPhase`
|
||||
|
@ -12,6 +12,7 @@ import { Moves } from "#enums/moves";
|
||||
import { getMovePosition } from "#test/testUtils/gameManagerUtils";
|
||||
import { GameManagerHelper } from "#test/testUtils/helpers/gameManagerHelper";
|
||||
import { vi } from "vitest";
|
||||
import { MoveUseType } from "#enums/move-use-type";
|
||||
|
||||
/**
|
||||
* Helper to handle a Pokemon's move
|
||||
@ -57,7 +58,11 @@ export class MoveHelper extends GameManagerHelper {
|
||||
this.game.scene.ui.setMode(UiMode.FIGHT, (this.game.scene.getCurrentPhase() as CommandPhase).getFieldIndex());
|
||||
});
|
||||
this.game.onNextPrompt("CommandPhase", UiMode.FIGHT, () => {
|
||||
(this.game.scene.getCurrentPhase() as CommandPhase).handleCommand(Command.FIGHT, movePosition, false);
|
||||
(this.game.scene.getCurrentPhase() as CommandPhase).handleCommand(
|
||||
Command.FIGHT,
|
||||
movePosition,
|
||||
MoveUseType.NORMAL,
|
||||
);
|
||||
});
|
||||
|
||||
if (targetIndex !== null) {
|
||||
@ -84,7 +89,7 @@ export class MoveHelper extends GameManagerHelper {
|
||||
);
|
||||
});
|
||||
this.game.onNextPrompt("CommandPhase", UiMode.FIGHT, () => {
|
||||
(this.game.scene.getCurrentPhase() as CommandPhase).handleCommand(Command.TERA, movePosition, false);
|
||||
(this.game.scene.getCurrentPhase() as CommandPhase).handleCommand(Command.TERA, movePosition, MoveUseType.NORMAL);
|
||||
});
|
||||
|
||||
if (targetIndex !== null) {
|
||||
|
Loading…
Reference in New Issue
Block a user