[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:
Zach Day 2024-09-05 04:44:22 -04:00 committed by GitHub
parent 1434a3edaf
commit 13f38dce8d
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
12 changed files with 315 additions and 105 deletions

13
src/data/ability.ts Normal file → Executable file
View File

@ -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 {

View File

@ -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:

View File

@ -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)

View File

@ -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",

View File

@ -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 {

View File

@ -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!",

View File

@ -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."
} }

View File

@ -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, () => {

View File

@ -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();

View File

@ -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()) {

View File

@ -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;

View File

@ -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);
});