[Move] Use BattlerTag for move-disabling effects (#2051)
* Use BattlerTag for move-disabling effects * Fix RUN command causing freeze * Improve documentation * Clean up and document PokemonMove.isUsable * Fix isMoveDisabled missing return * Tags define the message shown when disabling interrupts a move * Fix -1 duration on Disable effect * Add tests for Disable * En loc and fix message functions * Fix Disable test * Fix broken imports * Fix test * All disable tests passing * Localize remaining strings * Move cancellation logic out of lapse; use use TURN_END for lapse type * Prevent disabling STRUGGLE * Inline struggle check function * Restore RechargingTag docs * Move cancellation logic back to tag Wanted to increase similarity to the existing code base to avoid that stupid hyper beam error but it's still happening here * Fix hyper beam test * Remove erroneous shit * Fill movesets with SPLASH for disable test * More robust condition for disable checking * Remove DisabledTag lapse * Simplify DisablingBattlerTag lapse * Cancel disable-interrupted moves instead of failing them * Avoid disabling virtual moves * Consistent access modifiers across Disable tags * Add abstract function for message when player tries to select the disabled move * Fix syntax mistake * Always disable last-used non-virtual move * Overhaul tests + add tests * Implement loadTag for DisabledTag * Update translations * Update translations * Reimplement phase changes * fix battlertag strings * Fix disable test not running * Update name of base class * Rename "disabling" to "restriction" * Fix sneaky string fuckup * Fix test failure * fix merge problems * fix merge problems * Update tests * rerun RNG test * Properly mock stats in test * Document everything in battlertag * More docs + typo fix * Update tests --------- Co-authored-by: NightKev <34855794+DayKev@users.noreply.github.com>
This commit is contained in:
parent
1434a3edaf
commit
13f38dce8d
|
@ -1085,7 +1085,7 @@ export class PostDefendMoveDisableAbAttr extends PostDefendAbAttr {
|
||||||
}
|
}
|
||||||
|
|
||||||
applyPostDefend(pokemon: Pokemon, passive: boolean, simulated: boolean, attacker: Pokemon, move: Move, hitResult: HitResult, args: any[]): boolean {
|
applyPostDefend(pokemon: Pokemon, passive: boolean, simulated: boolean, attacker: Pokemon, move: Move, hitResult: HitResult, args: any[]): boolean {
|
||||||
if (!attacker.summonData.disabledMove) {
|
if (attacker.getTag(BattlerTagType.DISABLED) === null) {
|
||||||
if (move.checkFlag(MoveFlags.MAKES_CONTACT, attacker, pokemon) && (this.chance === -1 || pokemon.randSeedInt(100) < this.chance) && !attacker.isMax()) {
|
if (move.checkFlag(MoveFlags.MAKES_CONTACT, attacker, pokemon) && (this.chance === -1 || pokemon.randSeedInt(100) < this.chance) && !attacker.isMax()) {
|
||||||
if (simulated) {
|
if (simulated) {
|
||||||
return true;
|
return true;
|
||||||
|
@ -1093,21 +1093,12 @@ export class PostDefendMoveDisableAbAttr extends PostDefendAbAttr {
|
||||||
|
|
||||||
this.attacker = attacker;
|
this.attacker = attacker;
|
||||||
this.move = move;
|
this.move = move;
|
||||||
|
this.attacker.addTag(BattlerTagType.DISABLED, 4, 0, pokemon.id);
|
||||||
attacker.summonData.disabledMove = move.id;
|
|
||||||
attacker.summonData.disabledTurns = 4;
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
getTriggerMessage(pokemon: Pokemon, abilityName: string, ...args: any[]): string {
|
|
||||||
return i18next.t("abilityTriggers:postDefendMoveDisable", {
|
|
||||||
pokemonNameWithAffix: getPokemonNameWithAffix(this.attacker),
|
|
||||||
moveName: this.move.name,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export class PostStatStageChangeStatStageChangeAbAttr extends PostStatStageChangeAbAttr {
|
export class PostStatStageChangeStatStageChangeAbAttr extends PostStatStageChangeAbAttr {
|
||||||
|
|
|
@ -98,6 +98,127 @@ export interface TerrainBattlerTag {
|
||||||
terrainTypes: TerrainType[];
|
terrainTypes: TerrainType[];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Base class for tags that restrict the usage of moves. This effect is generally referred to as "disabling" a move
|
||||||
|
* in-game. This is not to be confused with {@linkcode Moves.DISABLE}.
|
||||||
|
*
|
||||||
|
* Descendants can override {@linkcode isMoveRestricted} to restrict moves that
|
||||||
|
* match a condition. A restricted move gets cancelled before it is used. Players and enemies should not be allowed
|
||||||
|
* to select restricted moves.
|
||||||
|
*/
|
||||||
|
export abstract class MoveRestrictionBattlerTag extends BattlerTag {
|
||||||
|
constructor(tagType: BattlerTagType, turnCount: integer, sourceMove?: Moves, sourceId?: integer) {
|
||||||
|
super(tagType, [ BattlerTagLapseType.PRE_MOVE, BattlerTagLapseType.TURN_END ], turnCount, sourceMove, sourceId);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @override */
|
||||||
|
override lapse(pokemon: Pokemon, lapseType: BattlerTagLapseType): boolean {
|
||||||
|
if (lapseType === BattlerTagLapseType.PRE_MOVE) {
|
||||||
|
// Cancel the affected pokemon's selected move
|
||||||
|
const phase = pokemon.scene.getCurrentPhase() as MovePhase;
|
||||||
|
const move = phase.move;
|
||||||
|
|
||||||
|
if (this.isMoveRestricted(move.moveId)) {
|
||||||
|
pokemon.scene.queueMessage(this.interruptedText(pokemon, move.moveId));
|
||||||
|
phase.cancel();
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return super.lapse(pokemon, lapseType);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets whether this tag is restricting a move.
|
||||||
|
*
|
||||||
|
* @param {Moves} move {@linkcode Moves} ID to check restriction for.
|
||||||
|
* @returns {boolean} `true` if the move is restricted by this tag, otherwise `false`.
|
||||||
|
*/
|
||||||
|
abstract isMoveRestricted(move: Moves): boolean;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets the text to display when the player attempts to select a move that is restricted by this tag.
|
||||||
|
*
|
||||||
|
* @param {Pokemon} pokemon {@linkcode Pokemon} for which the player is attempting to select the restricted move
|
||||||
|
* @param {Moves} move {@linkcode Moves} ID of the move that is having its selection denied
|
||||||
|
* @returns {string} text to display when the player attempts to select the restricted move
|
||||||
|
*/
|
||||||
|
abstract selectionDeniedText(pokemon: Pokemon, move: Moves): string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets the text to display when a move's execution is prevented as a result of the restriction.
|
||||||
|
* Because restriction effects also prevent selection of the move, this situation can only arise if a
|
||||||
|
* pokemon first selects a move, then gets outsped by a pokemon using a move that restricts the selected move.
|
||||||
|
*
|
||||||
|
* @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
|
||||||
|
*/
|
||||||
|
abstract interruptedText(pokemon: Pokemon, move: Moves): string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Tag representing the "disabling" effect performed by {@linkcode Moves.DISABLE} and {@linkcode Abilities.CURSED_BODY}.
|
||||||
|
* When the tag is added, the last-used move of the tag holder is set as the disabled move.
|
||||||
|
*/
|
||||||
|
export class DisabledTag extends MoveRestrictionBattlerTag {
|
||||||
|
/** The move being disabled. Gets set when {@linkcode onAdd} is called for this tag. */
|
||||||
|
private moveId: Moves = Moves.NONE;
|
||||||
|
|
||||||
|
constructor(sourceId: number) {
|
||||||
|
super(BattlerTagType.DISABLED, 4, Moves.DISABLE, sourceId);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @override */
|
||||||
|
override isMoveRestricted(move: Moves): boolean {
|
||||||
|
return move === this.moveId;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @override
|
||||||
|
*
|
||||||
|
* Ensures that move history exists on `pokemon` and has a valid move. If so, sets the {@link moveId} and shows a message.
|
||||||
|
* Otherwise the move ID will not get assigned and this tag will get removed next turn.
|
||||||
|
*/
|
||||||
|
override onAdd(pokemon: Pokemon): void {
|
||||||
|
super.onAdd(pokemon);
|
||||||
|
|
||||||
|
const move = pokemon.getLastXMoves()
|
||||||
|
.find(m => m.move !== Moves.NONE && m.move !== Moves.STRUGGLE && !m.virtual);
|
||||||
|
if (move === undefined) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.moveId = move.move;
|
||||||
|
|
||||||
|
pokemon.scene.queueMessage(i18next.t("battlerTags:disabledOnAdd", { pokemonNameWithAffix: getPokemonNameWithAffix(pokemon), moveName: allMoves[this.moveId].name }));
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @override */
|
||||||
|
override onRemove(pokemon: Pokemon): void {
|
||||||
|
super.onRemove(pokemon);
|
||||||
|
|
||||||
|
pokemon.scene.queueMessage(i18next.t("battlerTags:disabledLapse", { pokemonNameWithAffix: getPokemonNameWithAffix(pokemon), moveName: allMoves[this.moveId].name }));
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @override */
|
||||||
|
override selectionDeniedText(pokemon: Pokemon, move: Moves): string {
|
||||||
|
return i18next.t("battle:moveDisabled", { moveName: allMoves[move].name });
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @override */
|
||||||
|
override interruptedText(pokemon: Pokemon, move: Moves): string {
|
||||||
|
return i18next.t("battle:disableInterruptedMove", { pokemonNameWithAffix: getPokemonNameWithAffix(pokemon), moveName: allMoves[move].name });
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @override */
|
||||||
|
override loadTag(source: BattlerTag | any): void {
|
||||||
|
super.loadTag(source);
|
||||||
|
this.moveId = source.moveId;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* BattlerTag that represents the "recharge" effects of moves like Hyper Beam.
|
* BattlerTag that represents the "recharge" effects of moves like Hyper Beam.
|
||||||
*/
|
*/
|
||||||
|
@ -1995,6 +2116,8 @@ export function getBattlerTag(tagType: BattlerTagType, turnCount: number, source
|
||||||
return new StockpilingTag(sourceMove);
|
return new StockpilingTag(sourceMove);
|
||||||
case BattlerTagType.OCTOLOCK:
|
case BattlerTagType.OCTOLOCK:
|
||||||
return new OctolockTag(sourceId);
|
return new OctolockTag(sourceId);
|
||||||
|
case BattlerTagType.DISABLED:
|
||||||
|
return new DisabledTag(sourceId);
|
||||||
case BattlerTagType.IGNORE_GHOST:
|
case BattlerTagType.IGNORE_GHOST:
|
||||||
return new ExposedTag(tagType, sourceMove, Type.GHOST, [Type.NORMAL, Type.FIGHTING]);
|
return new ExposedTag(tagType, sourceMove, Type.GHOST, [Type.NORMAL, Type.FIGHTING]);
|
||||||
case BattlerTagType.IGNORE_DARK:
|
case BattlerTagType.IGNORE_DARK:
|
||||||
|
|
|
@ -4332,72 +4332,6 @@ export class TypelessAttr extends MoveAttr { }
|
||||||
*/
|
*/
|
||||||
export class BypassRedirectAttr extends MoveAttr { }
|
export class BypassRedirectAttr extends MoveAttr { }
|
||||||
|
|
||||||
export class DisableMoveAttr extends MoveEffectAttr {
|
|
||||||
constructor() {
|
|
||||||
super(false);
|
|
||||||
}
|
|
||||||
|
|
||||||
apply(user: Pokemon, target: Pokemon, move: Move, args: any[]): boolean {
|
|
||||||
if (!super.apply(user, target, move, args)) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
const moveQueue = target.getLastXMoves();
|
|
||||||
let turnMove: TurnMove | undefined;
|
|
||||||
while (moveQueue.length) {
|
|
||||||
turnMove = moveQueue.shift();
|
|
||||||
if (turnMove?.virtual) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
const moveIndex = target.getMoveset().findIndex(m => m?.moveId === turnMove?.move);
|
|
||||||
if (moveIndex === -1) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
const disabledMove = target.getMoveset()[moveIndex];
|
|
||||||
target.summonData.disabledMove = disabledMove?.moveId!; // TODO: is this bang correct?
|
|
||||||
target.summonData.disabledTurns = 4;
|
|
||||||
|
|
||||||
user.scene.queueMessage(i18next.t("abilityTriggers:postDefendMoveDisable", { pokemonNameWithAffix: getPokemonNameWithAffix(target), moveName: disabledMove?.getName()}));
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
getCondition(): MoveConditionFunc {
|
|
||||||
return (user, target, move): boolean => { // TODO: Not sure what to do here
|
|
||||||
if (target.summonData.disabledMove || target.isMax()) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
const moveQueue = target.getLastXMoves();
|
|
||||||
let turnMove: TurnMove | undefined;
|
|
||||||
while (moveQueue.length) {
|
|
||||||
turnMove = moveQueue.shift();
|
|
||||||
if (turnMove?.virtual) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
const move = target.getMoveset().find(m => m?.moveId === turnMove?.move);
|
|
||||||
if (!move) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
return false;
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
getTargetBenefitScore(user: Pokemon, target: Pokemon, move: Move): integer {
|
|
||||||
return -5;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export class FrenzyAttr extends MoveEffectAttr {
|
export class FrenzyAttr extends MoveEffectAttr {
|
||||||
constructor() {
|
constructor() {
|
||||||
super(true, MoveEffectTrigger.HIT, false, true);
|
super(true, MoveEffectTrigger.HIT, false, true);
|
||||||
|
@ -4488,6 +4422,7 @@ export class AddBattlerTagAttr extends MoveEffectAttr {
|
||||||
case BattlerTagType.INFATUATED:
|
case BattlerTagType.INFATUATED:
|
||||||
case BattlerTagType.NIGHTMARE:
|
case BattlerTagType.NIGHTMARE:
|
||||||
case BattlerTagType.DROWSY:
|
case BattlerTagType.DROWSY:
|
||||||
|
case BattlerTagType.DISABLED:
|
||||||
return -5;
|
return -5;
|
||||||
case BattlerTagType.SEEDED:
|
case BattlerTagType.SEEDED:
|
||||||
case BattlerTagType.SALT_CURED:
|
case BattlerTagType.SALT_CURED:
|
||||||
|
@ -6673,7 +6608,8 @@ export function initMoves() {
|
||||||
new AttackMove(Moves.SONIC_BOOM, Type.NORMAL, MoveCategory.SPECIAL, -1, 90, 20, -1, 0, 1)
|
new AttackMove(Moves.SONIC_BOOM, Type.NORMAL, MoveCategory.SPECIAL, -1, 90, 20, -1, 0, 1)
|
||||||
.attr(FixedDamageAttr, 20),
|
.attr(FixedDamageAttr, 20),
|
||||||
new StatusMove(Moves.DISABLE, Type.NORMAL, 100, 20, -1, 0, 1)
|
new StatusMove(Moves.DISABLE, Type.NORMAL, 100, 20, -1, 0, 1)
|
||||||
.attr(DisableMoveAttr)
|
.attr(AddBattlerTagAttr, BattlerTagType.DISABLED, false, true)
|
||||||
|
.condition((user, target, move) => target.getMoveHistory().reverse().find(m => m.move !== Moves.NONE && m.move !== Moves.STRUGGLE && !m.virtual) !== undefined)
|
||||||
.condition(failOnMaxCondition),
|
.condition(failOnMaxCondition),
|
||||||
new AttackMove(Moves.ACID, Type.POISON, MoveCategory.SPECIAL, 40, 100, 30, 10, 0, 1)
|
new AttackMove(Moves.ACID, Type.POISON, MoveCategory.SPECIAL, 40, 100, 30, 10, 0, 1)
|
||||||
.attr(StatStageChangeAttr, [ Stat.SPDEF ], -1)
|
.attr(StatStageChangeAttr, [ Stat.SPDEF ], -1)
|
||||||
|
|
|
@ -64,6 +64,7 @@ export enum BattlerTagType {
|
||||||
STOCKPILING = "STOCKPILING",
|
STOCKPILING = "STOCKPILING",
|
||||||
RECEIVE_DOUBLE_DAMAGE = "RECEIVE_DOUBLE_DAMAGE",
|
RECEIVE_DOUBLE_DAMAGE = "RECEIVE_DOUBLE_DAMAGE",
|
||||||
ALWAYS_GET_HIT = "ALWAYS_GET_HIT",
|
ALWAYS_GET_HIT = "ALWAYS_GET_HIT",
|
||||||
|
DISABLED = "DISABLED",
|
||||||
IGNORE_GHOST = "IGNORE_GHOST",
|
IGNORE_GHOST = "IGNORE_GHOST",
|
||||||
IGNORE_DARK = "IGNORE_DARK",
|
IGNORE_DARK = "IGNORE_DARK",
|
||||||
GULP_MISSILE_ARROKUDA = "GULP_MISSILE_ARROKUDA",
|
GULP_MISSILE_ARROKUDA = "GULP_MISSILE_ARROKUDA",
|
||||||
|
|
|
@ -17,7 +17,7 @@ import { initMoveAnim, loadMoveAnimAssets } from "../data/battle-anims";
|
||||||
import { Status, StatusEffect, getRandomStatus } from "../data/status-effect";
|
import { Status, StatusEffect, getRandomStatus } from "../data/status-effect";
|
||||||
import { pokemonEvolutions, pokemonPrevolutions, SpeciesFormEvolution, SpeciesEvolutionCondition, FusionSpeciesFormEvolution } from "../data/pokemon-evolutions";
|
import { pokemonEvolutions, pokemonPrevolutions, SpeciesFormEvolution, SpeciesEvolutionCondition, FusionSpeciesFormEvolution } from "../data/pokemon-evolutions";
|
||||||
import { reverseCompatibleTms, tmSpecies, tmPoolTiers } from "../data/tms";
|
import { reverseCompatibleTms, tmSpecies, tmPoolTiers } from "../data/tms";
|
||||||
import { BattlerTag, BattlerTagLapseType, EncoreTag, GroundedTag, HighestStatBoostTag, TypeImmuneTag, getBattlerTag, SemiInvulnerableTag, TypeBoostTag, ExposedTag, DragonCheerTag, CritBoostTag, TrappedTag } from "../data/battler-tags";
|
import { BattlerTag, BattlerTagLapseType, EncoreTag, GroundedTag, HighestStatBoostTag, TypeImmuneTag, getBattlerTag, SemiInvulnerableTag, TypeBoostTag, MoveRestrictionBattlerTag, ExposedTag, DragonCheerTag, CritBoostTag, TrappedTag } from "../data/battler-tags";
|
||||||
import { WeatherType } from "../data/weather";
|
import { WeatherType } from "../data/weather";
|
||||||
import { ArenaTagSide, NoCritTag, WeakenMoveScreenTag } from "../data/arena-tag";
|
import { ArenaTagSide, NoCritTag, WeakenMoveScreenTag } from "../data/arena-tag";
|
||||||
import { Ability, AbAttr, StatMultiplierAbAttr, BlockCritAbAttr, BonusCritAbAttr, BypassBurnDamageReductionAbAttr, FieldPriorityMoveImmunityAbAttr, IgnoreOpponentStatStagesAbAttr, MoveImmunityAbAttr, PreDefendFullHpEndureAbAttr, ReceivedMoveDamageMultiplierAbAttr, ReduceStatusEffectDurationAbAttr, StabBoostAbAttr, StatusEffectImmunityAbAttr, TypeImmunityAbAttr, WeightMultiplierAbAttr, allAbilities, applyAbAttrs, applyStatMultiplierAbAttrs, applyPreApplyBattlerTagAbAttrs, applyPreAttackAbAttrs, applyPreDefendAbAttrs, applyPreSetStatusAbAttrs, UnsuppressableAbilityAbAttr, SuppressFieldAbilitiesAbAttr, NoFusionAbilityAbAttr, MultCritAbAttr, IgnoreTypeImmunityAbAttr, DamageBoostAbAttr, IgnoreTypeStatusEffectImmunityAbAttr, ConditionalCritAbAttr, applyFieldStatMultiplierAbAttrs, FieldMultiplyStatAbAttr, AddSecondStrikeAbAttr, UserFieldStatusEffectImmunityAbAttr, UserFieldBattlerTagImmunityAbAttr, BattlerTagImmunityAbAttr, MoveTypeChangeAbAttr, FullHpResistTypeAbAttr, applyCheckTrappedAbAttrs, CheckTrappedAbAttr } from "../data/ability";
|
import { Ability, AbAttr, StatMultiplierAbAttr, BlockCritAbAttr, BonusCritAbAttr, BypassBurnDamageReductionAbAttr, FieldPriorityMoveImmunityAbAttr, IgnoreOpponentStatStagesAbAttr, MoveImmunityAbAttr, PreDefendFullHpEndureAbAttr, ReceivedMoveDamageMultiplierAbAttr, ReduceStatusEffectDurationAbAttr, StabBoostAbAttr, StatusEffectImmunityAbAttr, TypeImmunityAbAttr, WeightMultiplierAbAttr, allAbilities, applyAbAttrs, applyStatMultiplierAbAttrs, applyPreApplyBattlerTagAbAttrs, applyPreAttackAbAttrs, applyPreDefendAbAttrs, applyPreSetStatusAbAttrs, UnsuppressableAbilityAbAttr, SuppressFieldAbilitiesAbAttr, NoFusionAbilityAbAttr, MultCritAbAttr, IgnoreTypeImmunityAbAttr, DamageBoostAbAttr, IgnoreTypeStatusEffectImmunityAbAttr, ConditionalCritAbAttr, applyFieldStatMultiplierAbAttrs, FieldMultiplyStatAbAttr, AddSecondStrikeAbAttr, UserFieldStatusEffectImmunityAbAttr, UserFieldBattlerTagImmunityAbAttr, BattlerTagImmunityAbAttr, MoveTypeChangeAbAttr, FullHpResistTypeAbAttr, applyCheckTrappedAbAttrs, CheckTrappedAbAttr } from "../data/ability";
|
||||||
|
@ -2670,6 +2670,33 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container {
|
||||||
this.updateInfo();
|
this.updateInfo();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets whether the given move is currently disabled for this Pokemon.
|
||||||
|
*
|
||||||
|
* @param {Moves} moveId {@linkcode Moves} ID of the move to check
|
||||||
|
* @returns {boolean} `true` if the move is disabled for this Pokemon, otherwise `false`
|
||||||
|
*
|
||||||
|
* @see {@linkcode MoveRestrictionBattlerTag}
|
||||||
|
*/
|
||||||
|
isMoveRestricted(moveId: Moves): boolean {
|
||||||
|
return this.getRestrictingTag(moveId) !== null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets the {@link MoveRestrictionBattlerTag} that is restricting a move, if it exists.
|
||||||
|
*
|
||||||
|
* @param {Moves} moveId {@linkcode Moves} ID of the move to check
|
||||||
|
* @returns {MoveRestrictionBattlerTag | null} the first tag on this Pokemon that restricts the move, or `null` if the move is not restricted.
|
||||||
|
*/
|
||||||
|
getRestrictingTag(moveId: Moves): MoveRestrictionBattlerTag | null {
|
||||||
|
for (const tag of this.findTags(t => t instanceof MoveRestrictionBattlerTag)) {
|
||||||
|
if ((tag as MoveRestrictionBattlerTag).isMoveRestricted(moveId)) {
|
||||||
|
return tag as MoveRestrictionBattlerTag;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
getMoveHistory(): TurnMove[] {
|
getMoveHistory(): TurnMove[] {
|
||||||
return this.battleSummonData.moveHistory;
|
return this.battleSummonData.moveHistory;
|
||||||
}
|
}
|
||||||
|
@ -4458,8 +4485,6 @@ export interface AttackMoveResult {
|
||||||
export class PokemonSummonData {
|
export class PokemonSummonData {
|
||||||
public statStages: number[] = [ 0, 0, 0, 0, 0, 0, 0 ];
|
public statStages: number[] = [ 0, 0, 0, 0, 0, 0, 0 ];
|
||||||
public moveQueue: QueuedMove[] = [];
|
public moveQueue: QueuedMove[] = [];
|
||||||
public disabledMove: Moves = Moves.NONE;
|
|
||||||
public disabledTurns: number = 0;
|
|
||||||
public tags: BattlerTag[] = [];
|
public tags: BattlerTag[] = [];
|
||||||
public abilitySuppressed: boolean = false;
|
public abilitySuppressed: boolean = false;
|
||||||
public abilitiesApplied: Abilities[] = [];
|
public abilitiesApplied: Abilities[] = [];
|
||||||
|
@ -4540,7 +4565,7 @@ export type DamageResult = HitResult.EFFECTIVE | HitResult.SUPER_EFFECTIVE | Hit
|
||||||
* It links to {@linkcode Move} class via the move ID.
|
* It links to {@linkcode Move} class via the move ID.
|
||||||
* Compared to {@linkcode Move}, this class also tracks if a move has received.
|
* Compared to {@linkcode Move}, this class also tracks if a move has received.
|
||||||
* PP Ups, amount of PP used, and things like that.
|
* PP Ups, amount of PP used, and things like that.
|
||||||
* @see {@linkcode isUsable} - checks if move is disabled, out of PP, or not implemented.
|
* @see {@linkcode isUsable} - checks if move is restricted, out of PP, or not implemented.
|
||||||
* @see {@linkcode getMove} - returns {@linkcode Move} object by looking it up via ID.
|
* @see {@linkcode getMove} - returns {@linkcode Move} object by looking it up via ID.
|
||||||
* @see {@linkcode usePp} - removes a point of PP from the move.
|
* @see {@linkcode usePp} - removes a point of PP from the move.
|
||||||
* @see {@linkcode getMovePp} - returns amount of PP a move currently has.
|
* @see {@linkcode getMovePp} - returns amount of PP a move currently has.
|
||||||
|
@ -4560,11 +4585,25 @@ export class PokemonMove {
|
||||||
this.virtual = !!virtual;
|
this.virtual = !!virtual;
|
||||||
}
|
}
|
||||||
|
|
||||||
isUsable(pokemon: Pokemon, ignorePp?: boolean): boolean {
|
/**
|
||||||
if (this.moveId && pokemon.summonData?.disabledMove === this.moveId) {
|
* Checks whether the move can be selected or performed by a Pokemon, without consideration for the move's targets.
|
||||||
|
* The move is unusable if it is out of PP, restricted by an effect, or unimplemented.
|
||||||
|
*
|
||||||
|
* @param {Pokemon} pokemon {@linkcode Pokemon} that would be using this move
|
||||||
|
* @param {boolean} ignorePp If `true`, skips the PP check
|
||||||
|
* @param {boolean} ignoreRestrictionTags If `true`, skips the check for move restriction tags (see {@link MoveRestrictionBattlerTag})
|
||||||
|
* @returns `true` if the move can be selected and used by the Pokemon, otherwise `false`.
|
||||||
|
*/
|
||||||
|
isUsable(pokemon: Pokemon, ignorePp?: boolean, ignoreRestrictionTags?: boolean): boolean {
|
||||||
|
if (this.moveId && !ignoreRestrictionTags && pokemon.isMoveRestricted(this.moveId)) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
return (ignorePp || this.ppUsed < this.getMovePp() || this.getMove().pp === -1) && !this.getMove().name.endsWith(" (N)");
|
|
||||||
|
if (this.getMove().name.endsWith(" (N)")) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (ignorePp || this.ppUsed < this.getMovePp() || this.getMove().pp === -1);
|
||||||
}
|
}
|
||||||
|
|
||||||
getMove(): Move {
|
getMove(): Move {
|
||||||
|
|
|
@ -44,6 +44,7 @@
|
||||||
"moveNotImplemented": "{{moveName}} is not yet implemented and cannot be selected.",
|
"moveNotImplemented": "{{moveName}} is not yet implemented and cannot be selected.",
|
||||||
"moveNoPP": "There's no PP left for\nthis move!",
|
"moveNoPP": "There's no PP left for\nthis move!",
|
||||||
"moveDisabled": "{{moveName}} is disabled!",
|
"moveDisabled": "{{moveName}} is disabled!",
|
||||||
|
"disableInterruptedMove": "{{pokemonNameWithAffix}}'s {{moveName}}\nis disabled!",
|
||||||
"noPokeballForce": "An unseen force\nprevents using Poké Balls.",
|
"noPokeballForce": "An unseen force\nprevents using Poké Balls.",
|
||||||
"noPokeballTrainer": "You can't catch\nanother trainer's Pokémon!",
|
"noPokeballTrainer": "You can't catch\nanother trainer's Pokémon!",
|
||||||
"noPokeballMulti": "You can only throw a Poké Ball\nwhen there is one Pokémon remaining!",
|
"noPokeballMulti": "You can only throw a Poké Ball\nwhen there is one Pokémon remaining!",
|
||||||
|
|
|
@ -67,5 +67,7 @@
|
||||||
"saltCuredLapse": "{{pokemonNameWithAffix}} is hurt by {{moveName}}!",
|
"saltCuredLapse": "{{pokemonNameWithAffix}} is hurt by {{moveName}}!",
|
||||||
"cursedOnAdd": "{{pokemonNameWithAffix}} cut its own HP and put a curse on the {{pokemonName}}!",
|
"cursedOnAdd": "{{pokemonNameWithAffix}} cut its own HP and put a curse on the {{pokemonName}}!",
|
||||||
"cursedLapse": "{{pokemonNameWithAffix}} is afflicted by the Curse!",
|
"cursedLapse": "{{pokemonNameWithAffix}} is afflicted by the Curse!",
|
||||||
"stockpilingOnAdd": "{{pokemonNameWithAffix}} stockpiled {{stockpiledCount}}!"
|
"stockpilingOnAdd": "{{pokemonNameWithAffix}} stockpiled {{stockpiledCount}}!",
|
||||||
}
|
"disabledOnAdd": "{{pokemonNameWithAffix}}'s {{moveName}}\nwas disabled!",
|
||||||
|
"disabledLapse": "{{pokemonNameWithAffix}}'s {{moveName}}\nis no longer disabled."
|
||||||
|
}
|
||||||
|
|
|
@ -107,8 +107,9 @@ export class CommandPhase extends FieldPhase {
|
||||||
|
|
||||||
// Decides between a Disabled, Not Implemented, or No PP translation message
|
// Decides between a Disabled, Not Implemented, or No PP translation message
|
||||||
const errorMessage =
|
const errorMessage =
|
||||||
playerPokemon.summonData.disabledMove === move.moveId ? "battle:moveDisabled" :
|
playerPokemon.isMoveRestricted(move.moveId)
|
||||||
move.getName().endsWith(" (N)") ? "battle:moveNotImplemented" : "battle:moveNoPP";
|
? playerPokemon.getRestrictingTag(move.moveId)!.selectionDeniedText(playerPokemon, move.moveId)
|
||||||
|
: move.getName().endsWith(" (N)") ? "battle:moveNotImplemented" : "battle:moveNoPP";
|
||||||
const moveName = move.getName().replace(" (N)", ""); // Trims off the indicator
|
const moveName = move.getName().replace(" (N)", ""); // Trims off the indicator
|
||||||
|
|
||||||
this.scene.ui.showText(i18next.t(errorMessage, { moveName: moveName }), null, () => {
|
this.scene.ui.showText(i18next.t(errorMessage, { moveName: moveName }), null, () => {
|
||||||
|
|
|
@ -44,8 +44,8 @@ export class MovePhase extends BattlePhase {
|
||||||
this.cancelled = false;
|
this.cancelled = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
canMove(): boolean {
|
canMove(ignoreDisableTags?: boolean): boolean {
|
||||||
return this.pokemon.isActive(true) && this.move.isUsable(this.pokemon, this.ignorePp) && !!this.targets.length;
|
return this.pokemon.isActive(true) && this.move.isUsable(this.pokemon, this.ignorePp, ignoreDisableTags) && !!this.targets.length;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**Signifies the current move should fail but still use PP */
|
/**Signifies the current move should fail but still use PP */
|
||||||
|
@ -63,10 +63,7 @@ export class MovePhase extends BattlePhase {
|
||||||
|
|
||||||
console.log(Moves[this.move.moveId]);
|
console.log(Moves[this.move.moveId]);
|
||||||
|
|
||||||
if (!this.canMove()) {
|
if (!this.canMove(true)) {
|
||||||
if (this.move.moveId && this.pokemon.summonData?.disabledMove === this.move.moveId) {
|
|
||||||
this.scene.queueMessage(i18next.t("battle:moveDisabled", { moveName: this.move.getName() }));
|
|
||||||
}
|
|
||||||
if (this.pokemon.isActive(true) && this.move.ppUsed >= this.move.getMovePp()) { // if the move PP was reduced from Spite or otherwise, the move fails
|
if (this.pokemon.isActive(true) && this.move.ppUsed >= this.move.getMovePp()) { // if the move PP was reduced from Spite or otherwise, the move fails
|
||||||
this.fail();
|
this.fail();
|
||||||
this.showMoveText();
|
this.showMoveText();
|
||||||
|
|
|
@ -1,9 +1,7 @@
|
||||||
import BattleScene from "#app/battle-scene.js";
|
import BattleScene from "#app/battle-scene.js";
|
||||||
import { applyPostTurnAbAttrs, PostTurnAbAttr } from "#app/data/ability.js";
|
import { applyPostTurnAbAttrs, PostTurnAbAttr } from "#app/data/ability.js";
|
||||||
import { BattlerTagLapseType } from "#app/data/battler-tags.js";
|
import { BattlerTagLapseType } from "#app/data/battler-tags.js";
|
||||||
import { allMoves } from "#app/data/move.js";
|
|
||||||
import { TerrainType } from "#app/data/terrain.js";
|
import { TerrainType } from "#app/data/terrain.js";
|
||||||
import { Moves } from "#app/enums/moves.js";
|
|
||||||
import { WeatherType } from "#app/enums/weather-type.js";
|
import { WeatherType } from "#app/enums/weather-type.js";
|
||||||
import { TurnEndEvent } from "#app/events/battle-scene.js";
|
import { TurnEndEvent } from "#app/events/battle-scene.js";
|
||||||
import Pokemon from "#app/field/pokemon.js";
|
import Pokemon from "#app/field/pokemon.js";
|
||||||
|
@ -11,7 +9,6 @@ import { getPokemonNameWithAffix } from "#app/messages.js";
|
||||||
import { TurnHealModifier, EnemyTurnHealModifier, EnemyStatusEffectHealChanceModifier, TurnStatusEffectModifier, TurnHeldItemTransferModifier } from "#app/modifier/modifier.js";
|
import { TurnHealModifier, EnemyTurnHealModifier, EnemyStatusEffectHealChanceModifier, TurnStatusEffectModifier, TurnHeldItemTransferModifier } from "#app/modifier/modifier.js";
|
||||||
import i18next from "i18next";
|
import i18next from "i18next";
|
||||||
import { FieldPhase } from "./field-phase";
|
import { FieldPhase } from "./field-phase";
|
||||||
import { MessagePhase } from "./message-phase";
|
|
||||||
import { PokemonHealPhase } from "./pokemon-heal-phase";
|
import { PokemonHealPhase } from "./pokemon-heal-phase";
|
||||||
|
|
||||||
export class TurnEndPhase extends FieldPhase {
|
export class TurnEndPhase extends FieldPhase {
|
||||||
|
@ -28,11 +25,6 @@ export class TurnEndPhase extends FieldPhase {
|
||||||
const handlePokemon = (pokemon: Pokemon) => {
|
const handlePokemon = (pokemon: Pokemon) => {
|
||||||
pokemon.lapseTags(BattlerTagLapseType.TURN_END);
|
pokemon.lapseTags(BattlerTagLapseType.TURN_END);
|
||||||
|
|
||||||
if (pokemon.summonData.disabledMove && !--pokemon.summonData.disabledTurns) {
|
|
||||||
this.scene.pushPhase(new MessagePhase(this.scene, i18next.t("battle:notDisabled", { pokemonName: getPokemonNameWithAffix(pokemon), moveName: allMoves[pokemon.summonData.disabledMove].name })));
|
|
||||||
pokemon.summonData.disabledMove = Moves.NONE;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.scene.applyModifiers(TurnHealModifier, pokemon.isPlayer(), pokemon);
|
this.scene.applyModifiers(TurnHealModifier, pokemon.isPlayer(), pokemon);
|
||||||
|
|
||||||
if (this.scene.arena.terrain?.terrainType === TerrainType.GRASSY && pokemon.isGrounded()) {
|
if (this.scene.arena.terrain?.terrainType === TerrainType.GRASSY && pokemon.isGrounded()) {
|
||||||
|
|
|
@ -127,8 +127,6 @@ export default class PokemonData {
|
||||||
this.summonData.stats = source.summonData.stats;
|
this.summonData.stats = source.summonData.stats;
|
||||||
this.summonData.statStages = source.summonData.statStages;
|
this.summonData.statStages = source.summonData.statStages;
|
||||||
this.summonData.moveQueue = source.summonData.moveQueue;
|
this.summonData.moveQueue = source.summonData.moveQueue;
|
||||||
this.summonData.disabledMove = source.summonData.disabledMove;
|
|
||||||
this.summonData.disabledTurns = source.summonData.disabledTurns;
|
|
||||||
this.summonData.abilitySuppressed = source.summonData.abilitySuppressed;
|
this.summonData.abilitySuppressed = source.summonData.abilitySuppressed;
|
||||||
this.summonData.abilitiesApplied = source.summonData.abilitiesApplied;
|
this.summonData.abilitiesApplied = source.summonData.abilitiesApplied;
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,129 @@
|
||||||
|
import { BattlerIndex } from "#app/battle";
|
||||||
|
import { MoveResult } from "#app/field/pokemon";
|
||||||
|
import { Abilities } from "#enums/abilities";
|
||||||
|
import { Moves } from "#enums/moves";
|
||||||
|
import { Species } from "#enums/species";
|
||||||
|
import GameManager from "#test/utils/gameManager";
|
||||||
|
import { SPLASH_ONLY } from "#test/utils/testUtils";
|
||||||
|
import { afterEach, beforeAll, beforeEach, describe, expect, it } from "vitest";
|
||||||
|
|
||||||
|
describe("Moves - Disable", () => {
|
||||||
|
let phaserGame: Phaser.Game;
|
||||||
|
let game: GameManager;
|
||||||
|
|
||||||
|
beforeAll(() => {
|
||||||
|
phaserGame = new Phaser.Game({
|
||||||
|
type: Phaser.HEADLESS
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
game.phaseInterceptor.restoreOg();
|
||||||
|
});
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
game = new GameManager(phaserGame);
|
||||||
|
game.override
|
||||||
|
.battleType("single")
|
||||||
|
.ability(Abilities.BALL_FETCH)
|
||||||
|
.enemyAbility(Abilities.BALL_FETCH)
|
||||||
|
.moveset([Moves.DISABLE, Moves.SPLASH])
|
||||||
|
.enemyMoveset(SPLASH_ONLY)
|
||||||
|
.starterSpecies(Species.PIKACHU)
|
||||||
|
.enemySpecies(Species.SHUCKLE);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("restricts moves", async () => {
|
||||||
|
await game.classicMode.startBattle();
|
||||||
|
|
||||||
|
const enemyMon = game.scene.getEnemyPokemon()!;
|
||||||
|
|
||||||
|
game.move.select(Moves.DISABLE);
|
||||||
|
await game.setTurnOrder([BattlerIndex.ENEMY, BattlerIndex.PLAYER]);
|
||||||
|
await game.toNextTurn();
|
||||||
|
|
||||||
|
expect(enemyMon.getMoveHistory()).toHaveLength(1);
|
||||||
|
expect(enemyMon.isMoveRestricted(Moves.SPLASH)).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("fails if enemy has no move history", async() => {
|
||||||
|
await game.classicMode.startBattle();
|
||||||
|
|
||||||
|
const playerMon = game.scene.getPlayerPokemon()!;
|
||||||
|
const enemyMon = game.scene.getEnemyPokemon()!;
|
||||||
|
|
||||||
|
game.move.select(Moves.DISABLE);
|
||||||
|
await game.setTurnOrder([BattlerIndex.PLAYER, BattlerIndex.ENEMY]);
|
||||||
|
await game.toNextTurn();
|
||||||
|
|
||||||
|
expect(playerMon.getMoveHistory()[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();
|
||||||
|
|
||||||
|
const enemyMon = game.scene.getEnemyPokemon()!;
|
||||||
|
|
||||||
|
game.move.select(Moves.DISABLE);
|
||||||
|
await game.setTurnOrder([BattlerIndex.ENEMY, BattlerIndex.PLAYER]);
|
||||||
|
await game.toNextTurn();
|
||||||
|
|
||||||
|
game.move.select(Moves.SPLASH);
|
||||||
|
await game.toNextTurn();
|
||||||
|
|
||||||
|
const enemyHistory = enemyMon.getMoveHistory();
|
||||||
|
expect(enemyHistory).toHaveLength(2);
|
||||||
|
expect(enemyHistory[0].move).toBe(Moves.SPLASH);
|
||||||
|
expect(enemyHistory[1].move).toBe(Moves.STRUGGLE);
|
||||||
|
}, 20000);
|
||||||
|
|
||||||
|
it("cannot disable STRUGGLE", async() => {
|
||||||
|
game.override.enemyMoveset(Array(4).fill(Moves.STRUGGLE));
|
||||||
|
await game.classicMode.startBattle();
|
||||||
|
|
||||||
|
const playerMon = game.scene.getPlayerPokemon()!;
|
||||||
|
const enemyMon = game.scene.getEnemyPokemon()!;
|
||||||
|
|
||||||
|
game.move.select(Moves.DISABLE);
|
||||||
|
await game.setTurnOrder([BattlerIndex.ENEMY, BattlerIndex.PLAYER]);
|
||||||
|
await game.toNextTurn();
|
||||||
|
|
||||||
|
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();
|
||||||
|
|
||||||
|
const enemyMon = game.scene.getEnemyPokemon()!;
|
||||||
|
|
||||||
|
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();
|
||||||
|
expect(enemyHistory).toHaveLength(2);
|
||||||
|
expect(enemyHistory[0]).toMatchObject({ move: Moves.SPLASH, result: MoveResult.SUCCESS });
|
||||||
|
expect(enemyHistory[1].result).toBe(MoveResult.FAIL);
|
||||||
|
}, 20000);
|
||||||
|
|
||||||
|
it("disables NATURE POWER, not the move invoked by it", async() => {
|
||||||
|
game.override.enemyMoveset(Array(4).fill(Moves.NATURE_POWER));
|
||||||
|
await game.classicMode.startBattle();
|
||||||
|
|
||||||
|
const enemyMon = game.scene.getEnemyPokemon()!;
|
||||||
|
|
||||||
|
game.move.select(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)[1].move)).toBe(false);
|
||||||
|
}, 20000);
|
||||||
|
});
|
Loading…
Reference in New Issue