Refactor mostly complete, need to recheck tests

This commit is contained in:
Sirz Benjie 2025-08-18 19:24:22 -05:00
parent a77e3c911f
commit c0827230cb
No known key found for this signature in database
GPG Key ID: 4A524B4D196C759E
11 changed files with 464 additions and 218 deletions

View File

@ -1168,7 +1168,6 @@ export class PowderTag extends BattlerTag {
} }
// Disable the target's fire type move and damage it (subject to Magic Guard) // Disable the target's fire type move and damage it (subject to Magic Guard)
currentPhase.showMoveText();
currentPhase.fail(); currentPhase.fail();
const idx = pokemon.getBattlerIndex(); const idx = pokemon.getBattlerIndex();

View File

@ -1,13 +1,16 @@
// biome-ignore lint/correctness/noUnusedImports: Used in a tsdoc comment
import type { GameMode } from "#app/game-mode";
import { globalScene } from "#app/global-scene"; import { globalScene } from "#app/global-scene";
import { getPokemonNameWithAffix } from "#app/messages"; import { getPokemonNameWithAffix } from "#app/messages";
import { TrappedTag } from "#data/battler-tags";
import { allMoves } from "#data/data-lists"; import { allMoves } from "#data/data-lists";
import { ArenaTagType } from "#enums/arena-tag-type"; import { ArenaTagType } from "#enums/arena-tag-type";
import { Command } from "#enums/command"; import { Command } from "#enums/command";
import { MoveCategory } from "#enums/move-category"; import { MoveCategory, type MoveDamageCategory } from "#enums/move-category";
import type { MoveId } from "#enums/move-id";
import { isVirtual } from "#enums/move-use-mode";
import { PokemonType } from "#enums/pokemon-type";
import type { Pokemon } from "#field/pokemon"; import type { Pokemon } from "#field/pokemon";
import type { Move, MoveConditionFunc, UserMoveConditionFunc } from "#moves/move"; import type { Move, MoveConditionFunc, UserMoveConditionFunc } from "#moves/move";
import { getCounterAttackTarget } from "#moves/move-utils";
import i18next from "i18next"; import i18next from "i18next";
/** /**
@ -54,6 +57,81 @@ export class FirstMoveCondition extends MoveCondition {
} }
} }
/**
* Condition that fails the move if the user has less than 1/x of their max HP.
* @remarks
* Used by Clangorous Soul and Fillet Away
*/
export class FailIfInsufficientHpCondition extends MoveCondition {
/**
* Condition that fails the move if the user has less than 1/x of their max HP.
* @param ratio - The required HP ratio (the `x` in `1/x`)
*/
constructor(cutRatio: number) {
super(user => user.getHpRatio() > 1 / cutRatio);
}
}
/**
* Teleport condition checks
*
* @remarks
* For trainer pokemon, just checks if there are any benched pokemon allowed in battle
*
* Wild pokemon cannot teleport if either:
* - The current battle is a double battle
* - They are under the effects of a *move-based* trapping effect like and are neither a ghost type nor have an active run away ability
*/
export const failTeleportCondition = new MoveCondition(user => {
if (user.hasTrainer()) {
const party = user.isPlayer() ? globalScene.getPlayerParty() : globalScene.getEnemyParty();
for (const pokemon of party) {
if (!pokemon.isOnField() && pokemon.isAllowedInBattle()) {
return true;
}
}
return false;
}
// Wild pokemon
// Cannot teleport in double battles (even if last remaining)
if (globalScene.currentBattle.double) {
return false;
}
// If smoke ball / shed tail items are ever added, checks for them should be placed here
// If a conditional "run away" ability is ever added, then we should use the apply method instead of the `hasAbility`
if (user.isOfType(PokemonType.GHOST, true, true) || user.hasAbilityWithAttr("RunSuccessAbAttr")) {
return true;
}
// Wild pokemon are prevented from fleeing if they are trapped *specifically*
if (globalScene.arena.hasTag(ArenaTagType.FAIRY_LOCK) || user.getTag(TrappedTag) !== undefined) {
// Fairy Lock prevents teleporting
return false;
}
return true;
});
/**
* Condition that forces moves to fail if the target's selected move is not an attacking move
*
* @remarks
* Used by Sucker Punch and Thunderclap
*/
export const failIfTargetNotAttackingCondition = new MoveCondition((_user, target) => {
const turnCommand = globalScene.currentBattle.turnCommands[target.getBattlerIndex()];
if (!turnCommand || !turnCommand.move) {
return false;
}
return (
turnCommand.command === Command.FIGHT &&
!target.turnData.acted &&
allMoves[turnCommand.move.move].category !== MoveCategory.STATUS
);
});
/** /**
* Condition that forces moves to fail against the final boss in classic and the major boss in endless * Condition that forces moves to fail against the final boss in classic and the major boss in endless
* @remarks * @remarks
@ -86,6 +164,54 @@ export const UpperHandCondition = new MoveCondition((_user, target) => {
); );
}); });
/**
* Condition used by the move {@link https://bulbapedia.bulbagarden.net/wiki/Last_Resort_(move) | Last Resort}
*
* @remarks
* Last resort fails if
* - It is not in the user's moveset
* - The user does not know at least one other move
* - The user has not directly used each other move in its moveset since it was sent into battle
* - A move is considered *used* for this purpose if it passed the first failure check sequence in the move phase
*/
export const lastResortCondition = new MoveCondition((user, _target, move) => {
const otherMovesInMoveset = new Set<MoveId>(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<MoveId>(
user
.getMoveHistory()
.filter(m => !isVirtual(m.useMode)) // Last resort ignores virtual moves
.map(m => m.move),
);
// Since `Set.intersection()` is only present in ESNext, we have to do this to check inclusion
return [...otherMovesInMoveset].every(m => movesInHistory.has(m));
});
/**
* Condition used by counter-like moves if the user was hit by at least one qualifying attack this turn.
* Qualifying attacks are those that match the specified category (physical, special or either)
* that did not come from an ally.
*/
class CounterAttackConditon extends MoveCondition {
/**
* @param damageCategory - The category of move to counter (physical or special), or `undefined` to counter both
*/
constructor(damageCategory?: MoveDamageCategory) {
super(user => getCounterAttackTarget(user, damageCategory) !== null);
}
}
/** Condition check for counterattacks that proc againt physical moves */
export const counterAttackCondition_Physical = new CounterAttackConditon(MoveCategory.PHYSICAL);
/** Condition check for counterattacks that proc against special moves*/
export const counterAttackCondition_Special = new CounterAttackConditon(MoveCategory.SPECIAL);
/** Condition check for counterattacks that proc against moves regardless of damage type */
export const counterAttackCondition_Both = new CounterAttackConditon();
/** /**
* A restriction that prevents a move from being selected * A restriction that prevents a move from being selected
* *

View File

@ -1,6 +1,7 @@
import { allMoves } from "#data/data-lists"; import { allMoves } from "#data/data-lists";
import type { BattlerIndex } from "#enums/battler-index"; import type { BattlerIndex } from "#enums/battler-index";
import { BattlerTagType } from "#enums/battler-tag-type"; import { BattlerTagType } from "#enums/battler-tag-type";
import { MoveCategory, type MoveDamageCategory } from "#enums/move-category";
import type { MoveId } from "#enums/move-id"; import type { MoveId } from "#enums/move-id";
import { MoveTarget } from "#enums/move-target"; import { MoveTarget } from "#enums/move-target";
import { PokemonType } from "#enums/pokemon-type"; import { PokemonType } from "#enums/pokemon-type";
@ -8,6 +9,7 @@ import type { Pokemon } from "#field/pokemon";
import { applyMoveAttrs } from "#moves/apply-attrs"; import { applyMoveAttrs } from "#moves/apply-attrs";
import type { Move, MoveTargetSet, UserMoveConditionFunc } from "#moves/move"; import type { Move, MoveTargetSet, UserMoveConditionFunc } from "#moves/move";
import { isNullOrUndefined, NumberHolder } from "#utils/common"; import { isNullOrUndefined, NumberHolder } from "#utils/common";
import { areAllies } from "#utils/pokemon-utils";
/** /**
* Return whether the move targets the field * Return whether the move targets the field
@ -133,3 +135,25 @@ export const frenzyMissFunc: UserMoveConditionFunc = (user: Pokemon, move: Move)
return true; return true;
}; };
/**
* Determine the target for the `user`'s counter-attack move
* @param user - The pokemon using the counter-like move
* @param damageCategory - The category of move to counter (physical or special), or `undefined` to counter both
* @returns - The battler index of the most recent, non-ally attacker using a move that matches the specified category, or `null` if no such attacker exists
*/
export function getCounterAttackTarget(user: Pokemon, damageCategory?: MoveDamageCategory): BattlerIndex | null {
for (const attackRecord of user.turnData.attacksReceived) {
// check if the attacker was an ally
const moveCategory = allMoves[attackRecord.move].category;
const sourceBattlerIndex = attackRecord.sourceBattlerIndex;
if (
moveCategory !== MoveCategory.STATUS &&
!areAllies(sourceBattlerIndex, user.getBattlerIndex()) &&
(damageCategory === undefined || moveCategory === damageCategory)
) {
return sourceBattlerIndex;
}
}
return null;
}

View File

@ -36,7 +36,7 @@ import { AbilityId } from "#enums/ability-id";
import { ArenaTagSide } from "#enums/arena-tag-side"; import { ArenaTagSide } from "#enums/arena-tag-side";
import { ArenaTagType } from "#enums/arena-tag-type"; import { ArenaTagType } from "#enums/arena-tag-type";
import { BattleType } from "#enums/battle-type"; import { BattleType } from "#enums/battle-type";
import type { BattlerIndex } from "#enums/battler-index"; import { BattlerIndex } from "#enums/battler-index";
import { BattlerTagType } from "#enums/battler-tag-type"; import { BattlerTagType } from "#enums/battler-tag-type";
import { BiomeId } from "#enums/biome-id"; import { BiomeId } from "#enums/biome-id";
import { ChallengeType } from "#enums/challenge-type"; import { ChallengeType } from "#enums/challenge-type";
@ -48,7 +48,7 @@ import { ChargeAnim } from "#enums/move-anims-common";
import { MoveId } from "#enums/move-id"; import { MoveId } from "#enums/move-id";
import { MoveResult } from "#enums/move-result"; import { MoveResult } from "#enums/move-result";
import { isVirtual, MoveUseMode } from "#enums/move-use-mode"; import { isVirtual, MoveUseMode } from "#enums/move-use-mode";
import { MoveCategory } from "#enums/move-category"; import { MoveCategory, MoveDamageCategory } from "#enums/move-category";
import { MoveEffectTrigger } from "#enums/move-effect-trigger"; import { MoveEffectTrigger } from "#enums/move-effect-trigger";
import { MoveFlags } from "#enums/move-flags"; import { MoveFlags } from "#enums/move-flags";
import { MoveTarget } from "#enums/move-target"; import { MoveTarget } from "#enums/move-target";
@ -78,13 +78,12 @@ import {
} from "#modifiers/modifier"; } from "#modifiers/modifier";
import { applyMoveAttrs } from "#moves/apply-attrs"; import { applyMoveAttrs } from "#moves/apply-attrs";
import { invalidAssistMoves, invalidCopycatMoves, invalidMetronomeMoves, invalidMirrorMoveMoves, invalidSketchMoves, invalidSleepTalkMoves } from "#moves/invalid-moves"; import { invalidAssistMoves, invalidCopycatMoves, invalidMetronomeMoves, invalidMirrorMoveMoves, invalidSketchMoves, invalidSleepTalkMoves } from "#moves/invalid-moves";
import { frenzyMissFunc, getMoveTargets } from "#moves/move-utils"; import { frenzyMissFunc, getCounterAttackTarget, getMoveTargets } from "#moves/move-utils";
import { PokemonMove } from "#moves/pokemon-move"; import { PokemonMove } from "#moves/pokemon-move";
import { MoveEndPhase } from "#phases/move-end-phase"; import { MoveEndPhase } from "#phases/move-end-phase";
import { MovePhase } from "#phases/move-phase"; import { MovePhase } from "#phases/move-phase";
import { PokemonHealPhase } from "#phases/pokemon-heal-phase"; import { PokemonHealPhase } from "#phases/pokemon-heal-phase";
import { SwitchSummonPhase } from "#phases/switch-summon-phase"; import { SwitchSummonPhase } from "#phases/switch-summon-phase";
import type { AttackMoveResult } from "#types/attack-move-result";
import type { Localizable } from "#types/locales"; import type { Localizable } from "#types/locales";
import type { ChargingMove, MoveAttrMap, MoveAttrString, MoveClassMap, MoveKindString, MoveMessageFunc } from "#types/move-types"; import type { ChargingMove, MoveAttrMap, MoveAttrString, MoveClassMap, MoveKindString, MoveMessageFunc } from "#types/move-types";
import type { TurnMove } from "#types/turn-move"; import type { TurnMove } from "#types/turn-move";
@ -93,7 +92,7 @@ import { getEnumValues } from "#utils/enums";
import { toCamelCase, toTitleCase } from "#utils/strings"; import { toCamelCase, toTitleCase } from "#utils/strings";
import i18next from "i18next"; import i18next from "i18next";
import { applyChallenges } from "#utils/challenge-utils"; import { applyChallenges } from "#utils/challenge-utils";
import { ConsecutiveUseRestriction, failAgainstFinalBossCondition, FirstMoveCondition, GravityUseRestriction, MoveCondition, MoveRestriction, UpperHandCondition } from "#moves/move-condition"; import { ConsecutiveUseRestriction, counterAttackCondition_Both, counterAttackCondition_Physical, counterAttackCondition_Special, failAgainstFinalBossCondition, FailIfInsufficientHpCondition, failIfTargetNotAttackingCondition, failTeleportCondition, FirstMoveCondition, GravityUseRestriction, lastResortCondition, MoveCondition, MoveRestriction, UpperHandCondition } from "#moves/move-condition";
/** /**
* A function used to conditionally determine execution of a given {@linkcode MoveAttr}. * A function used to conditionally determine execution of a given {@linkcode MoveAttr}.
@ -117,15 +116,56 @@ export abstract class Move implements Localizable {
public priority: number; public priority: number;
public generation: number; public generation: number;
public attrs: MoveAttr[] = []; public attrs: MoveAttr[] = [];
/** Conditions that must be met for the move to succeed when it is used. /**
* * Conditions that must be met for the move to succeed when it is used.
*
* @remarks
* These are the default conditions checked during the move effect phase (aka sequence 4).
* When adding a new condition, if unsure of where it occurs in the failure checks, it should go here.
* @remarks Different from {@linkcode restrictions}, which is checked when the move is selected * @remarks Different from {@linkcode restrictions}, which is checked when the move is selected
*/ */
private conditions: MoveCondition[] = []; private conditions: MoveCondition[] = [];
/** /**
* Move failure conditions that occur during the second check (after move message and before ) * Move failure conditions that occur during the second sequence (after move message and before )
*/ */
private conditionsSeq2: MoveCondition[] = []; private conditionsSeq2: MoveCondition[] = [];
/**
* Move failure conditions that occur during the third sequence (after accuracy and before move effects).
*
* @remarks
* List of *move-based* conditions that occur in this sequence:
* - Battle mechanics research conducted by Smogon and manual checks from SirzBenjie
* - Steel Roller with no terrain
* - Follow Me / Rage Powder failing due to being used in single battle
* - Stockpile with >= 3 stacks already
* - Spit up / swallow with 0 stockpiles
* - Counter / Mirror Coat / Metal Burst with no damage taken from enemies this turn
* - Last resort's bespoke failure conditions
* - Snore while not asleep
* - Sucker punch failling due to the target having already used their selected move or not having selected a damaging move
* - Magic Coat failing due to being used as the last move in the turn
* - Protect-like moves failing due to consecutive use or being the last move in the turn
* - First turn moves (e.g. mat block, fake out) failing due to not being used on first turn
* - Rest failing due, in this order, to already being asleep (including from comatose), under the effect of heal block, being full hp, having insomnia / vital spirit
* - Teleport failing when used by a wild pokemon that can't use it to flee (because it is trapped, in a double battle, etc) or used by a trainer pokemon with no pokemon to switch to
* - Fling with no usable item (not done as Pokerogue does not yet implement fling)
* - Magnet rise failing while under the effect of ingrain, smack down, or gravity (gravity check not done as redundant with sequence 1)
* - Splash failing when gravity is in effect (Not done in Pokerogue as this is redundant with the check that occurs during sequence 1)
* - Stuff cheeks with no berry
* - Destiny bond failing on consecutive use
* - Quick Guard / Wide Guard failing due to being used as the last move in the turn
* - Ally switch's chance to fail on consecutive use
* - Species specific moves (like hyperspace fury, aura wheel, dark void) being used by a pokemon that is not hoopa (Not applicable to Pokerogue, which permits this)
* - Poltergeist against a target with no item
* - Shell Trap failing due to not being hit by a physical move
* - Aurora veil failing due to no hail
* - Clangorous soul and Fillet Awayfailing due to insufficient HP
* - Upper hand failing due to the target not selecting a priority move
* - (Various moves that fail when used against titans / raid bosses, not listed as pokerogue does not yet implement each)
*
* @see {@link https://www.smogon.com/forums/threads/sword-shield-battle-mechanics-research.3655528/page-54#post-8548957}
*/
private conditionsSeq3: MoveCondition[] = [];
/** Conditions that must be false for a move to be able to be selected. /** Conditions that must be false for a move to be able to be selected.
* *
* @remarks Different from {@linkcode conditions}, which is checked when the move is invoked * @remarks Different from {@linkcode conditions}, which is checked when the move is invoked
@ -387,7 +427,7 @@ export abstract class Move implements Localizable {
* @param checkSequence - The sequence number where the failure check occurs * @param checkSequence - The sequence number where the failure check occurs
* @returns `this` for method chaining * @returns `this` for method chaining
*/ */
condition(condition: MoveCondition | MoveConditionFunc, checkSequence: 2 | 3 = 3): this { condition(condition: MoveCondition | MoveConditionFunc, checkSequence: 2 | 3 | 4 = 4): this {
const conditionsArray = checkSequence === 2 ? this.conditionsSeq2 : this.conditions; const conditionsArray = checkSequence === 2 ? this.conditionsSeq2 : this.conditions;
if (typeof condition === "function") { if (typeof condition === "function") {
condition = new MoveCondition(condition); condition = new MoveCondition(condition);
@ -412,10 +452,10 @@ export abstract class Move implements Localizable {
* @param i18nkey - The i18n key for the restriction text * @param i18nkey - The i18n key for the restriction text
* @param alsoCondition - If `true`, also adds a {@linkcode MoveCondition} that checks the same condition when the * @param alsoCondition - If `true`, also adds a {@linkcode MoveCondition} that checks the same condition when the
* move is used; default `false` * move is used; default `false`
* @param conditionSeq - The sequence number where the failure check occurs; default `4`
* @returns `this` for method chaining * @returns `this` for method chaining
*/ */
public restriction(restriction: UserMoveConditionFunc, i18nkey: string, alsoCondition?: boolean, conditionSeq?: number): this;
public restriction(restriction: UserMoveConditionFunc, i18nkey: string, alsoCondition?: boolean): this;
/** /**
* Adds a restriction condition to this move. * Adds a restriction condition to this move.
* The move will not be selectable if at least 1 of its restrictions is met. * The move will not be selectable if at least 1 of its restrictions is met.
@ -424,13 +464,27 @@ export abstract class Move implements Localizable {
* @param i18nkey - The i18n key for the restriction text, ignored if `restriction` is a `MoveRestriction` * @param i18nkey - The i18n key for the restriction text, ignored if `restriction` is a `MoveRestriction`
* @param alsoCondition - If `true`, also adds a {@linkcode MoveCondition} that checks the same condition when the * @param alsoCondition - If `true`, also adds a {@linkcode MoveCondition} that checks the same condition when the
* move is used; default `false`. Ignored if `restriction` is a `MoveRestriction`. * move is used; default `false`. Ignored if `restriction` is a `MoveRestriction`.
* @param conditionSeq - The sequence number where the failure check occurs; default `4`. Ignored if `alsoCondition`
* is false
* @returns `this` for method chaining * @returns `this` for method chaining
*/ */
public restriction<T extends UserMoveConditionFunc | MoveRestriction>(restriction: T, i18nkey?: string, alsoCondition: typeof restriction extends MoveRestriction ? false : boolean = false): this { public restriction<T extends UserMoveConditionFunc | MoveRestriction>(restriction: T, i18nkey?: string, alsoCondition: typeof restriction extends MoveRestriction ? false : boolean = false, conditionSeq = 4): this {
if (typeof restriction === "function") { if (typeof restriction === "function") {
this.restrictions.push(new MoveRestriction(restriction)); this.restrictions.push(new MoveRestriction(restriction));
if (alsoCondition) { if (alsoCondition) {
this.conditions.push(new MoveCondition((user, _, move) => restriction(user, move))); let conditionArray: MoveCondition[];
switch (conditionSeq) {
case 2:
conditionArray = this.conditionsSeq2;
break;
case 3:
conditionArray = this.conditionsSeq3;
break;
default:
conditionArray = this.conditions;
}
conditionArray.push(new MoveCondition((user, _, move) => !restriction(user, move)));
} }
} else { } else {
this.restrictions.push(restriction); this.restrictions.push(restriction);
@ -640,9 +694,14 @@ export abstract class Move implements Localizable {
/** /**
* Sets the {@linkcode MoveFlags.GRAVITY} flag for the calling Move and adds {@linkcode GravityUseRestriction} to the * Sets the {@linkcode MoveFlags.GRAVITY} flag for the calling Move and adds {@linkcode GravityUseRestriction} to the
* move's restrictions. * move's restrictions.
*
* @returns The {@linkcode Move} that called this function
*
* @remarks
* No {@linkcode condition} is added, as gravity's condition is already checked
* during the first sequence of a move's failure check, and this would be redundant.
* *
* @see {@linkcode MoveId.GRAVITY} * @see {@linkcode MoveId.GRAVITY}
* @returns The {@linkcode Move} that called this function
*/ */
affectedByGravity(): this { affectedByGravity(): this {
this.setFlag(MoveFlags.GRAVITY, true); this.setFlag(MoveFlags.GRAVITY, true);
@ -680,16 +739,6 @@ export abstract class Move implements Localizable {
return this; return this;
} }
/**
* Sets the {@linkcode MoveFlags.REDIRECT_COUNTER} flag for the calling Move
* @see {@linkcode MoveId.METAL_BURST}
* @returns The {@linkcode Move} that called this function
*/
redirectCounter(): this {
this.setFlag(MoveFlags.REDIRECT_COUNTER, true);
return this;
}
/** /**
* Sets the {@linkcode MoveFlags.REFLECTABLE} flag for the calling Move * Sets the {@linkcode MoveFlags.REFLECTABLE} flag for the calling Move
* @see {@linkcode MoveId.ATTRACT} * @see {@linkcode MoveId.ATTRACT}
@ -771,11 +820,24 @@ export abstract class Move implements Localizable {
* @param user - {@linkcode Pokemon} to apply conditions to * @param user - {@linkcode Pokemon} to apply conditions to
* @param target - {@linkcode Pokemon} to apply conditions to * @param target - {@linkcode Pokemon} to apply conditions to
* @param move - {@linkcode Move} to apply conditions to * @param move - {@linkcode Move} to apply conditions to
* @param sequence - The sequence number where the condition check occurs, defaults to 3 * @param sequence - The sequence number where the condition check occurs, or `-1` to check all; defaults to 3. Pass -1 to check all
* @returns boolean: false if any of the apply()'s return false, else true * @returns boolean: false if any of the apply()'s return false, else true
*/ */
applyConditions(user: Pokemon, target: Pokemon, sequence: number = 3): boolean { applyConditions(user: Pokemon, target: Pokemon, sequence: -1 | 2 | 3 | 4 = 4): boolean {
const conditionsArray = sequence === 2 ? this.conditionsSeq2 : this.conditions; let conditionsArray: MoveCondition[];
switch (sequence) {
case -1:
conditionsArray = [...this.conditionsSeq2, ...this.conditionsSeq3, ...this.conditions];
case 2:
conditionsArray = this.conditionsSeq2;
break;
case 3:
conditionsArray = this.conditionsSeq3;
break;
case 4:
default:
conditionsArray = this.conditions;
}
return conditionsArray.every(cond => cond.apply(user, target, this)); return conditionsArray.every(cond => cond.apply(user, target, this));
} }
@ -1731,28 +1793,77 @@ export class MatchHpAttr extends FixedDamageAttr {
}*/ }*/
} }
type MoveFilter = (move: Move) => boolean;
export class CounterDamageAttr extends FixedDamageAttr { export class CounterDamageAttr extends FixedDamageAttr {
private moveFilter: MoveFilter; /** The damage category of counter attacks to process, or `undefined` for either */
private moveFilter?: MoveDamageCategory;
private multiplier: number; private multiplier: number;
constructor(moveFilter: MoveFilter, multiplier: number) { /**
* @param multiplier - The damage multiplier to apply to the total damage received
* @param moveFilter - If set, only damage from moves of this category will be counted, otherwise all damage is counted
*/
constructor(multiplier: number, moveFilter?: MoveDamageCategory) {
super(0); super(0);
this.moveFilter = moveFilter; this.moveFilter = moveFilter;
this.multiplier = multiplier; this.multiplier = multiplier;
} }
apply(user: Pokemon, target: Pokemon, move: Move, args: any[]): boolean { apply(user: Pokemon, target: Pokemon, move: Move, args: any[]): boolean {
const damage = user.turnData.attacksReceived.filter(ar => this.moveFilter(allMoves[ar.move])).reduce((total: number, ar: AttackMoveResult) => total + ar.damage, 0); let damage = 0;
for (const ar of user.turnData.attacksReceived) {
// TODO: Adjust this for moves with variable damage categories
const category = allMoves[ar.move].category;
if (category === MoveCategory.STATUS || (this.moveFilter && category !== this.moveFilter)) {
continue;
}
damage += ar.damage;
}
(args[0] as NumberHolder).value = toDmgValue(damage * this.multiplier); (args[0] as NumberHolder).value = toDmgValue(damage * this.multiplier);
return true; return true;
} }
}
getCondition(): MoveConditionFunc { /**
return (user, target, move) => !!user.turnData.attacksReceived.filter(ar => this.moveFilter(allMoves[ar.move])).length; * Attribute for counter-like moves to redirect the move to a different target
*/
export class CounterRedirectAttr extends MoveAttr {
declare private moveFilter?: MoveDamageCategory;
constructor(moveFilter? : MoveDamageCategory) {
super();
if (moveFilter !== undefined) {
this.moveFilter = moveFilter;
}
}
/**
* Applies the counter redirect attribute to the move
* @param user - The user of the counter move
* @param target - The target of the move (unused)
* @param move - The move being used
* @param args - args[0] holds the battler index of the target that the move will be redirected to
*/
override apply(user: Pokemon, target: Pokemon | null, move: Move, args: [NumberHolder, ...any[]]): boolean {
const desiredTarget = getCounterAttackTarget(user, this.moveFilter);
if (desiredTarget !== null && desiredTarget !== BattlerIndex.ATTACKER) {
// check if the target is still alive
if (
globalScene.currentBattle.double &&
!globalScene.getField()[desiredTarget]?.isActive
) {
const targetField = desiredTarget >= BattlerIndex.ENEMY ? globalScene.getEnemyField() : globalScene.getPlayerField();
args[0].value = targetField.find(p => p.hp > 0)?.getBattlerIndex() ?? BattlerIndex.ATTACKER;
} else {
args[0].value = desiredTarget;
}
return true;
}
return false;
}
override canApply(user: Pokemon, target: Pokemon, move: Move, args: [NumberHolder, ...any[]]): boolean {
return args[0].value === BattlerIndex.ATTACKER;
} }
} }
@ -3744,7 +3855,7 @@ export class CutHpStatStageBoostAttr extends StatStageChangeAttr {
} }
getCondition(): MoveConditionFunc { getCondition(): MoveConditionFunc {
return (user, _target, _move) => user.getHpRatio() > 1 / this.cutRatio && this.stats.some(s => user.getStatStage(s) < 6); return user => user.getHpRatio() > 1 / this.cutRatio && this.stats.some(s => user.getStatStage(s) < 6);
} }
} }
@ -8017,31 +8128,6 @@ export class StatusIfBoostedAttr extends MoveEffectAttr {
} }
} }
/**
* Attribute to fail move usage unless all of the user's other moves have been used at least once.
* Used by {@linkcode MoveId.LAST_RESORT}.
*/
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 otherMovesInMoveset = new Set<MoveId>(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<MoveId>(
user.getMoveHistory()
.filter(m => !isVirtual(m.useMode)) // Last resort ignores virtual moves
.map(m => m.move)
);
// Since `Set.intersection()` is only present in ESNext, we have to do this to check inclusion
return [...otherMovesInMoveset].every(m => movesInHistory.has(m))
};
}
}
export class VariableTargetAttr extends MoveAttr { export class VariableTargetAttr extends MoveAttr {
private targetChangeFunc: (user: Pokemon, target: Pokemon, move: Move) => number; private targetChangeFunc: (user: Pokemon, target: Pokemon, move: Move) => number;
@ -8330,6 +8416,7 @@ const MoveAttrs = Object.freeze({
TargetHalfHpDamageAttr, TargetHalfHpDamageAttr,
MatchHpAttr, MatchHpAttr,
CounterDamageAttr, CounterDamageAttr,
CounterRedirectAttr,
LevelDamageAttr, LevelDamageAttr,
RandomLevelDamageAttr, RandomLevelDamageAttr,
ModifiedDamageAttr, ModifiedDamageAttr,
@ -8520,7 +8607,6 @@ const MoveAttrs = Object.freeze({
DestinyBondAttr, DestinyBondAttr,
AddBattlerTagIfBoostedAttr, AddBattlerTagIfBoostedAttr,
StatusIfBoostedAttr, StatusIfBoostedAttr,
LastResortAttr,
VariableTargetAttr, VariableTargetAttr,
AfterYouAttr, AfterYouAttr,
ForceLastAttr, ForceLastAttr,
@ -8723,7 +8809,8 @@ export function initMoves() {
new AttackMove(MoveId.LOW_KICK, PokemonType.FIGHTING, MoveCategory.PHYSICAL, -1, 100, 20, -1, 0, 1) new AttackMove(MoveId.LOW_KICK, PokemonType.FIGHTING, MoveCategory.PHYSICAL, -1, 100, 20, -1, 0, 1)
.attr(WeightPowerAttr), .attr(WeightPowerAttr),
new AttackMove(MoveId.COUNTER, PokemonType.FIGHTING, MoveCategory.PHYSICAL, -1, 100, 20, -1, -5, 1) new AttackMove(MoveId.COUNTER, PokemonType.FIGHTING, MoveCategory.PHYSICAL, -1, 100, 20, -1, -5, 1)
.attr(CounterDamageAttr, (move: Move) => move.category === MoveCategory.PHYSICAL, 2) .attr(CounterDamageAttr, 2, MoveCategory.PHYSICAL)
.condition(counterAttackCondition_Physical, 3)
.target(MoveTarget.ATTACKER), .target(MoveTarget.ATTACKER),
new AttackMove(MoveId.SEISMIC_TOSS, PokemonType.FIGHTING, MoveCategory.PHYSICAL, -1, 100, 20, -1, 0, 1) new AttackMove(MoveId.SEISMIC_TOSS, PokemonType.FIGHTING, MoveCategory.PHYSICAL, -1, 100, 20, -1, 0, 1)
.attr(LevelDamageAttr), .attr(LevelDamageAttr),
@ -8823,7 +8910,8 @@ export function initMoves() {
.partial(), // No effect implemented .partial(), // No effect implemented
new SelfStatusMove(MoveId.TELEPORT, PokemonType.PSYCHIC, -1, 20, -1, -6, 1) new SelfStatusMove(MoveId.TELEPORT, PokemonType.PSYCHIC, -1, 20, -1, -6, 1)
.attr(ForceSwitchOutAttr, true) .attr(ForceSwitchOutAttr, true)
.hidesUser(), .hidesUser()
.condition(failTeleportCondition, 3),
new AttackMove(MoveId.NIGHT_SHADE, PokemonType.GHOST, MoveCategory.SPECIAL, -1, 100, 15, -1, 0, 1) new AttackMove(MoveId.NIGHT_SHADE, PokemonType.GHOST, MoveCategory.SPECIAL, -1, 100, 15, -1, 0, 1)
.attr(LevelDamageAttr), .attr(LevelDamageAttr),
new StatusMove(MoveId.MIMIC, PokemonType.NORMAL, -1, 10, -1, 0, 1) new StatusMove(MoveId.MIMIC, PokemonType.NORMAL, -1, 10, -1, 0, 1)
@ -8876,7 +8964,7 @@ export function initMoves() {
new AttackMove(MoveId.SELF_DESTRUCT, PokemonType.NORMAL, MoveCategory.PHYSICAL, 200, 100, 5, -1, 0, 1) new AttackMove(MoveId.SELF_DESTRUCT, PokemonType.NORMAL, MoveCategory.PHYSICAL, 200, 100, 5, -1, 0, 1)
.attr(SacrificialAttr) .attr(SacrificialAttr)
.makesContact(false) .makesContact(false)
.condition(failIfDampCondition) .condition(failIfDampCondition, 3)
.target(MoveTarget.ALL_NEAR_OTHERS), .target(MoveTarget.ALL_NEAR_OTHERS),
new AttackMove(MoveId.EGG_BOMB, PokemonType.NORMAL, MoveCategory.PHYSICAL, 100, 75, 10, -1, 0, 1) new AttackMove(MoveId.EGG_BOMB, PokemonType.NORMAL, MoveCategory.PHYSICAL, 100, 75, 10, -1, 0, 1)
.makesContact(false) .makesContact(false)
@ -8977,7 +9065,7 @@ export function initMoves() {
new AttackMove(MoveId.CRABHAMMER, PokemonType.WATER, MoveCategory.PHYSICAL, 100, 90, 10, -1, 0, 1) new AttackMove(MoveId.CRABHAMMER, PokemonType.WATER, MoveCategory.PHYSICAL, 100, 90, 10, -1, 0, 1)
.attr(HighCritAttr), .attr(HighCritAttr),
new AttackMove(MoveId.EXPLOSION, PokemonType.NORMAL, MoveCategory.PHYSICAL, 250, 100, 5, -1, 0, 1) new AttackMove(MoveId.EXPLOSION, PokemonType.NORMAL, MoveCategory.PHYSICAL, 250, 100, 5, -1, 0, 1)
.condition(failIfDampCondition) .condition(failIfDampCondition, 3)
.attr(SacrificialAttr) .attr(SacrificialAttr)
.makesContact(false) .makesContact(false)
.target(MoveTarget.ALL_NEAR_OTHERS), .target(MoveTarget.ALL_NEAR_OTHERS),
@ -8989,7 +9077,7 @@ export function initMoves() {
new SelfStatusMove(MoveId.REST, PokemonType.PSYCHIC, -1, 5, -1, 0, 1) new SelfStatusMove(MoveId.REST, PokemonType.PSYCHIC, -1, 5, -1, 0, 1)
.attr(StatusEffectAttr, StatusEffect.SLEEP, true, 3, true) .attr(StatusEffectAttr, StatusEffect.SLEEP, true, 3, true)
.attr(HealAttr, 1, true) .attr(HealAttr, 1, true)
.condition((user, target, move) => !user.isFullHp() && user.canSetStatus(StatusEffect.SLEEP, true, true, user)) .condition((user, target, move) => !user.isFullHp() && user.canSetStatus(StatusEffect.SLEEP, true, true, user), 3)
.triageMove(), .triageMove(),
new AttackMove(MoveId.ROCK_SLIDE, PokemonType.ROCK, MoveCategory.PHYSICAL, 75, 90, 10, 30, 0, 1) new AttackMove(MoveId.ROCK_SLIDE, PokemonType.ROCK, MoveCategory.PHYSICAL, 75, 90, 10, 30, 0, 1)
.attr(FlinchAttr) .attr(FlinchAttr)
@ -9045,7 +9133,7 @@ export function initMoves() {
new AttackMove(MoveId.SNORE, PokemonType.NORMAL, MoveCategory.SPECIAL, 50, 100, 15, 30, 0, 2) new AttackMove(MoveId.SNORE, PokemonType.NORMAL, MoveCategory.SPECIAL, 50, 100, 15, 30, 0, 2)
.attr(BypassSleepAttr) .attr(BypassSleepAttr)
.attr(FlinchAttr) .attr(FlinchAttr)
.condition(userSleptOrComatoseCondition) .condition(userSleptOrComatoseCondition, 3)
.soundBased(), .soundBased(),
new StatusMove(MoveId.CURSE, PokemonType.GHOST, -1, 10, -1, 0, 2) new StatusMove(MoveId.CURSE, PokemonType.GHOST, -1, 10, -1, 0, 2)
.attr(CurseAttr) .attr(CurseAttr)
@ -9077,7 +9165,7 @@ export function initMoves() {
.target(MoveTarget.ALL_NEAR_ENEMIES), .target(MoveTarget.ALL_NEAR_ENEMIES),
new SelfStatusMove(MoveId.PROTECT, PokemonType.NORMAL, -1, 10, -1, 4, 2) new SelfStatusMove(MoveId.PROTECT, PokemonType.NORMAL, -1, 10, -1, 4, 2)
.attr(ProtectAttr) .attr(ProtectAttr)
.condition(failIfLastCondition), .condition(failIfLastCondition, 3),
new AttackMove(MoveId.MACH_PUNCH, PokemonType.FIGHTING, MoveCategory.PHYSICAL, 40, 100, 30, -1, 1, 2) new AttackMove(MoveId.MACH_PUNCH, PokemonType.FIGHTING, MoveCategory.PHYSICAL, 40, 100, 30, -1, 1, 2)
.punchingMove(), .punchingMove(),
new StatusMove(MoveId.SCARY_FACE, PokemonType.NORMAL, 100, 10, -1, 0, 2) new StatusMove(MoveId.SCARY_FACE, PokemonType.NORMAL, 100, 10, -1, 0, 2)
@ -9137,7 +9225,7 @@ export function initMoves() {
.target(MoveTarget.ALL_NEAR_ENEMIES), .target(MoveTarget.ALL_NEAR_ENEMIES),
new SelfStatusMove(MoveId.DETECT, PokemonType.FIGHTING, -1, 5, -1, 4, 2) new SelfStatusMove(MoveId.DETECT, PokemonType.FIGHTING, -1, 5, -1, 4, 2)
.attr(ProtectAttr) .attr(ProtectAttr)
.condition(failIfLastCondition), .condition(failIfLastCondition, 3),
new AttackMove(MoveId.BONE_RUSH, PokemonType.GROUND, MoveCategory.PHYSICAL, 25, 90, 10, -1, 0, 2) new AttackMove(MoveId.BONE_RUSH, PokemonType.GROUND, MoveCategory.PHYSICAL, 25, 90, 10, -1, 0, 2)
.attr(MultiHitAttr) .attr(MultiHitAttr)
.makesContact(false), .makesContact(false),
@ -9159,7 +9247,7 @@ export function initMoves() {
.triageMove(), .triageMove(),
new SelfStatusMove(MoveId.ENDURE, PokemonType.NORMAL, -1, 10, -1, 4, 2) new SelfStatusMove(MoveId.ENDURE, PokemonType.NORMAL, -1, 10, -1, 4, 2)
.attr(ProtectAttr, BattlerTagType.ENDURING) .attr(ProtectAttr, BattlerTagType.ENDURING)
.condition(failIfLastCondition), .condition(failIfLastCondition, 3),
new StatusMove(MoveId.CHARM, PokemonType.FAIRY, 100, 20, -1, 0, 2) new StatusMove(MoveId.CHARM, PokemonType.FAIRY, 100, 20, -1, 0, 2)
.attr(StatStageChangeAttr, [ Stat.ATK ], -2) .attr(StatStageChangeAttr, [ Stat.ATK ], -2)
.reflectable(), .reflectable(),
@ -9194,7 +9282,7 @@ export function initMoves() {
new SelfStatusMove(MoveId.SLEEP_TALK, PokemonType.NORMAL, -1, 10, -1, 0, 2) new SelfStatusMove(MoveId.SLEEP_TALK, PokemonType.NORMAL, -1, 10, -1, 0, 2)
.attr(BypassSleepAttr) .attr(BypassSleepAttr)
.attr(RandomMovesetMoveAttr, invalidSleepTalkMoves, false) .attr(RandomMovesetMoveAttr, invalidSleepTalkMoves, false)
.condition(userSleptOrComatoseCondition) .condition(userSleptOrComatoseCondition, 3)
.target(MoveTarget.NEAR_ENEMY), .target(MoveTarget.NEAR_ENEMY),
new StatusMove(MoveId.HEAL_BELL, PokemonType.NORMAL, -1, 5, -1, 0, 2) new StatusMove(MoveId.HEAL_BELL, PokemonType.NORMAL, -1, 5, -1, 0, 2)
.attr(PartyStatusCureAttr, i18next.t("moveTriggers:bellChimed"), AbilityId.SOUNDPROOF) .attr(PartyStatusCureAttr, i18next.t("moveTriggers:bellChimed"), AbilityId.SOUNDPROOF)
@ -9297,7 +9385,8 @@ export function initMoves() {
.attr(StatStageChangeAttr, [ Stat.DEF ], -1) .attr(StatStageChangeAttr, [ Stat.DEF ], -1)
.bitingMove(), .bitingMove(),
new AttackMove(MoveId.MIRROR_COAT, PokemonType.PSYCHIC, MoveCategory.SPECIAL, -1, 100, 20, -1, -5, 2) new AttackMove(MoveId.MIRROR_COAT, PokemonType.PSYCHIC, MoveCategory.SPECIAL, -1, 100, 20, -1, -5, 2)
.attr(CounterDamageAttr, (move: Move) => move.category === MoveCategory.SPECIAL, 2) .attr(CounterDamageAttr, 2, MoveCategory.SPECIAL)
.condition(counterAttackCondition_Special, 3)
.target(MoveTarget.ATTACKER), .target(MoveTarget.ATTACKER),
new StatusMove(MoveId.PSYCH_UP, PokemonType.NORMAL, -1, 10, -1, 0, 2) new StatusMove(MoveId.PSYCH_UP, PokemonType.NORMAL, -1, 10, -1, 0, 2)
.ignoresSubstitute() .ignoresSubstitute()
@ -9326,21 +9415,21 @@ export function initMoves() {
.makesContact(false), .makesContact(false),
new AttackMove(MoveId.FAKE_OUT, PokemonType.NORMAL, MoveCategory.PHYSICAL, 40, 100, 10, 100, 3, 3) new AttackMove(MoveId.FAKE_OUT, PokemonType.NORMAL, MoveCategory.PHYSICAL, 40, 100, 10, 100, 3, 3)
.attr(FlinchAttr) .attr(FlinchAttr)
.condition(new FirstMoveCondition()), .condition(new FirstMoveCondition(), 3),
new AttackMove(MoveId.UPROAR, PokemonType.NORMAL, MoveCategory.SPECIAL, 90, 100, 10, -1, 0, 3) new AttackMove(MoveId.UPROAR, PokemonType.NORMAL, MoveCategory.SPECIAL, 90, 100, 10, -1, 0, 3)
.soundBased() .soundBased()
.target(MoveTarget.RANDOM_NEAR_ENEMY) .target(MoveTarget.RANDOM_NEAR_ENEMY)
.partial(), // Does not lock the user, does not stop Pokemon from sleeping .partial(), // Does not lock the user, does not stop Pokemon from sleeping
// Likely can make use of FrenzyAttr and an ArenaTag (just without the FrenzyMissFunc) // Likely can make use of FrenzyAttr and an ArenaTag (just without the FrenzyMissFunc)
new SelfStatusMove(MoveId.STOCKPILE, PokemonType.NORMAL, -1, 20, -1, 0, 3) new SelfStatusMove(MoveId.STOCKPILE, PokemonType.NORMAL, -1, 20, -1, 0, 3)
.condition(user => (user.getTag(StockpilingTag)?.stockpiledCount ?? 0) < 3) .condition(user => (user.getTag(StockpilingTag)?.stockpiledCount ?? 0) < 3, 3)
.attr(AddBattlerTagAttr, BattlerTagType.STOCKPILING, true), .attr(AddBattlerTagAttr, BattlerTagType.STOCKPILING, true),
new AttackMove(MoveId.SPIT_UP, PokemonType.NORMAL, MoveCategory.SPECIAL, -1, 100, 10, -1, 0, 3) new AttackMove(MoveId.SPIT_UP, PokemonType.NORMAL, MoveCategory.SPECIAL, -1, 100, 10, -1, 0, 3)
.condition(hasStockpileStacksCondition) .condition(hasStockpileStacksCondition, 3)
.attr(SpitUpPowerAttr, 100) .attr(SpitUpPowerAttr, 100)
.attr(RemoveBattlerTagAttr, [ BattlerTagType.STOCKPILING ], true), .attr(RemoveBattlerTagAttr, [ BattlerTagType.STOCKPILING ], true),
new SelfStatusMove(MoveId.SWALLOW, PokemonType.NORMAL, -1, 10, -1, 0, 3) new SelfStatusMove(MoveId.SWALLOW, PokemonType.NORMAL, -1, 10, -1, 0, 3)
.condition(hasStockpileStacksCondition) .condition(hasStockpileStacksCondition, 3)
.attr(SwallowHealAttr) .attr(SwallowHealAttr)
.attr(RemoveBattlerTagAttr, [ BattlerTagType.STOCKPILING ], true) .attr(RemoveBattlerTagAttr, [ BattlerTagType.STOCKPILING ], true)
.triageMove(), .triageMove(),
@ -9379,7 +9468,8 @@ export function initMoves() {
.attr(MovePowerMultiplierAttr, (user, target, move) => target.status?.effect === StatusEffect.PARALYSIS ? 2 : 1) .attr(MovePowerMultiplierAttr, (user, target, move) => target.status?.effect === StatusEffect.PARALYSIS ? 2 : 1)
.attr(HealStatusEffectAttr, true, StatusEffect.PARALYSIS), .attr(HealStatusEffectAttr, true, StatusEffect.PARALYSIS),
new SelfStatusMove(MoveId.FOLLOW_ME, PokemonType.NORMAL, -1, 20, -1, 2, 3) new SelfStatusMove(MoveId.FOLLOW_ME, PokemonType.NORMAL, -1, 20, -1, 2, 3)
.attr(AddBattlerTagAttr, BattlerTagType.CENTER_OF_ATTENTION, true), .attr(AddBattlerTagAttr, BattlerTagType.CENTER_OF_ATTENTION, true)
.condition(failIfSingleBattle, 3),
new StatusMove(MoveId.NATURE_POWER, PokemonType.NORMAL, -1, 20, -1, 0, 3) new StatusMove(MoveId.NATURE_POWER, PokemonType.NORMAL, -1, 20, -1, 0, 3)
.attr(NaturePowerAttr), .attr(NaturePowerAttr),
new SelfStatusMove(MoveId.CHARGE, PokemonType.ELECTRIC, -1, 20, -1, 0, 3) new SelfStatusMove(MoveId.CHARGE, PokemonType.ELECTRIC, -1, 20, -1, 0, 3)
@ -9414,7 +9504,7 @@ export function initMoves() {
.attr(StatStageChangeAttr, [ Stat.ATK, Stat.DEF ], -1, true), .attr(StatStageChangeAttr, [ Stat.ATK, Stat.DEF ], -1, true),
new SelfStatusMove(MoveId.MAGIC_COAT, PokemonType.PSYCHIC, -1, 15, -1, 4, 3) new SelfStatusMove(MoveId.MAGIC_COAT, PokemonType.PSYCHIC, -1, 15, -1, 4, 3)
.attr(AddBattlerTagAttr, BattlerTagType.MAGIC_COAT, true, true, 0) .attr(AddBattlerTagAttr, BattlerTagType.MAGIC_COAT, true, true, 0)
.condition(failIfLastCondition) .condition(failIfLastCondition, 3)
// Interactions with stomping tantrum, instruct, and other moves that // Interactions with stomping tantrum, instruct, and other moves that
// rely on move history // rely on move history
// Also will not reflect roar / whirlwind if the target has ForceSwitchOutImmunityAbAttr // Also will not reflect roar / whirlwind if the target has ForceSwitchOutImmunityAbAttr
@ -9713,8 +9803,8 @@ export function initMoves() {
.attr(AcupressureStatStageChangeAttr) .attr(AcupressureStatStageChangeAttr)
.target(MoveTarget.USER_OR_NEAR_ALLY), .target(MoveTarget.USER_OR_NEAR_ALLY),
new AttackMove(MoveId.METAL_BURST, PokemonType.STEEL, MoveCategory.PHYSICAL, -1, 100, 10, -1, 0, 4) new AttackMove(MoveId.METAL_BURST, PokemonType.STEEL, MoveCategory.PHYSICAL, -1, 100, 10, -1, 0, 4)
.attr(CounterDamageAttr, (move: Move) => (move.category === MoveCategory.PHYSICAL || move.category === MoveCategory.SPECIAL), 1.5) .attr(CounterDamageAttr, 1.5)
.redirectCounter() .condition(counterAttackCondition_Both, 3)
.makesContact(false) .makesContact(false)
.target(MoveTarget.ATTACKER), .target(MoveTarget.ATTACKER),
new AttackMove(MoveId.U_TURN, PokemonType.BUG, MoveCategory.PHYSICAL, 70, 100, 20, -1, 0, 4) new AttackMove(MoveId.U_TURN, PokemonType.BUG, MoveCategory.PHYSICAL, 70, 100, 20, -1, 0, 4)
@ -9776,21 +9866,15 @@ export function initMoves() {
.makesContact(true) .makesContact(true)
.attr(PunishmentPowerAttr), .attr(PunishmentPowerAttr),
new AttackMove(MoveId.LAST_RESORT, PokemonType.NORMAL, MoveCategory.PHYSICAL, 140, 100, 5, -1, 0, 4) new AttackMove(MoveId.LAST_RESORT, PokemonType.NORMAL, MoveCategory.PHYSICAL, 140, 100, 5, -1, 0, 4)
.attr(LastResortAttr) .condition(lastResortCondition, 3)
.edgeCase(), // May or may not need to ignore remotely called moves depending on how it works .edgeCase(), // When a move is overwritten and later relearned, Last Resort's tracking of it should be reset
new StatusMove(MoveId.WORRY_SEED, PokemonType.GRASS, 100, 10, -1, 0, 4) new StatusMove(MoveId.WORRY_SEED, PokemonType.GRASS, 100, 10, -1, 0, 4)
.attr(AbilityChangeAttr, AbilityId.INSOMNIA) .attr(AbilityChangeAttr, AbilityId.INSOMNIA)
// TODO: Enable / remove once balance reaches a consensus on ability overrides during boss fights // TODO: Enable / remove once balance reaches a consensus on ability overrides during boss fights
// .condition(failAgainstFinalBossCondition, 3) // .condition(failAgainstFinalBossCondition, 3)
.reflectable(), .reflectable(),
new AttackMove(MoveId.SUCKER_PUNCH, PokemonType.DARK, MoveCategory.PHYSICAL, 70, 100, 5, -1, 1, 4) new AttackMove(MoveId.SUCKER_PUNCH, PokemonType.DARK, MoveCategory.PHYSICAL, 70, 100, 5, -1, 1, 4)
.condition((user, target, move) => { .condition(failIfTargetNotAttackingCondition, 3),
const turnCommand = globalScene.currentBattle.turnCommands[target.getBattlerIndex()];
if (!turnCommand || !turnCommand.move) {
return false;
}
return (turnCommand.command === Command.FIGHT && !target.turnData.acted && allMoves[turnCommand.move.move].category !== MoveCategory.STATUS);
}),
new StatusMove(MoveId.TOXIC_SPIKES, PokemonType.POISON, -1, 20, -1, 0, 4) new StatusMove(MoveId.TOXIC_SPIKES, PokemonType.POISON, -1, 20, -1, 0, 4)
.attr(AddArenaTrapTagAttr, ArenaTagType.TOXIC_SPIKES) .attr(AddArenaTrapTagAttr, ArenaTagType.TOXIC_SPIKES)
.target(MoveTarget.ENEMY_SIDE) .target(MoveTarget.ENEMY_SIDE)
@ -9802,7 +9886,7 @@ export function initMoves() {
.attr(AddBattlerTagAttr, BattlerTagType.AQUA_RING, true, true), .attr(AddBattlerTagAttr, BattlerTagType.AQUA_RING, true, true),
new SelfStatusMove(MoveId.MAGNET_RISE, PokemonType.ELECTRIC, -1, 10, -1, 0, 4) new SelfStatusMove(MoveId.MAGNET_RISE, PokemonType.ELECTRIC, -1, 10, -1, 0, 4)
.attr(AddBattlerTagAttr, BattlerTagType.FLOATING, true, true, 5) .attr(AddBattlerTagAttr, BattlerTagType.FLOATING, true, true, 5)
.condition(user => [ BattlerTagType.FLOATING, BattlerTagType.IGNORE_FLYING, BattlerTagType.INGRAIN ].every((tag) => !user.getTag(tag))) .condition(user => [ BattlerTagType.FLOATING, BattlerTagType.IGNORE_FLYING, BattlerTagType.INGRAIN ].every((tag) => !user.getTag(tag)), 3)
.affectedByGravity(), .affectedByGravity(),
new AttackMove(MoveId.FLARE_BLITZ, PokemonType.FIRE, MoveCategory.PHYSICAL, 120, 100, 15, 10, 0, 4) new AttackMove(MoveId.FLARE_BLITZ, PokemonType.FIRE, MoveCategory.PHYSICAL, 120, 100, 15, 10, 0, 4)
.attr(RecoilAttr, false, 0.33) .attr(RecoilAttr, false, 0.33)
@ -10012,7 +10096,7 @@ export function initMoves() {
new StatusMove(MoveId.WIDE_GUARD, PokemonType.ROCK, -1, 10, -1, 3, 5) new StatusMove(MoveId.WIDE_GUARD, PokemonType.ROCK, -1, 10, -1, 3, 5)
.target(MoveTarget.USER_SIDE) .target(MoveTarget.USER_SIDE)
.attr(AddArenaTagAttr, ArenaTagType.WIDE_GUARD, 1, true, true) .attr(AddArenaTagAttr, ArenaTagType.WIDE_GUARD, 1, true, true)
.condition(failIfLastCondition), .condition(failIfLastCondition, 3),
new StatusMove(MoveId.GUARD_SPLIT, PokemonType.PSYCHIC, -1, 10, -1, 0, 5) new StatusMove(MoveId.GUARD_SPLIT, PokemonType.PSYCHIC, -1, 10, -1, 0, 5)
// TODO: Enable / remove once balance reaches a consensus on imprison interaction during the final boss fight // TODO: Enable / remove once balance reaches a consensus on imprison interaction during the final boss fight
// .condition(failAgainstFinalBossCondition, 2) // .condition(failAgainstFinalBossCondition, 2)
@ -10042,6 +10126,7 @@ export function initMoves() {
.condition((_user, target, _move) => isNullOrUndefined(target.getTag(BattlerTagType.INGRAIN)) && isNullOrUndefined(target.getTag(BattlerTagType.IGNORE_FLYING))) .condition((_user, target, _move) => isNullOrUndefined(target.getTag(BattlerTagType.INGRAIN)) && isNullOrUndefined(target.getTag(BattlerTagType.IGNORE_FLYING)))
.attr(AddBattlerTagAttr, BattlerTagType.TELEKINESIS, false, true, 3) .attr(AddBattlerTagAttr, BattlerTagType.TELEKINESIS, false, true, 3)
.attr(AddBattlerTagAttr, BattlerTagType.FLOATING, false, true, 3) .attr(AddBattlerTagAttr, BattlerTagType.FLOATING, false, true, 3)
.affectedByGravity()
.reflectable(), .reflectable(),
new StatusMove(MoveId.MAGIC_ROOM, PokemonType.PSYCHIC, -1, 10, -1, 0, 5) new StatusMove(MoveId.MAGIC_ROOM, PokemonType.PSYCHIC, -1, 10, -1, 0, 5)
.ignoresProtect() .ignoresProtect()
@ -10121,7 +10206,7 @@ export function initMoves() {
new StatusMove(MoveId.QUICK_GUARD, PokemonType.FIGHTING, -1, 15, -1, 3, 5) new StatusMove(MoveId.QUICK_GUARD, PokemonType.FIGHTING, -1, 15, -1, 3, 5)
.target(MoveTarget.USER_SIDE) .target(MoveTarget.USER_SIDE)
.attr(AddArenaTagAttr, ArenaTagType.QUICK_GUARD, 1, true, true) .attr(AddArenaTagAttr, ArenaTagType.QUICK_GUARD, 1, true, true)
.condition(failIfLastCondition), .condition(failIfLastCondition, 3),
new SelfStatusMove(MoveId.ALLY_SWITCH, PokemonType.PSYCHIC, -1, 15, -1, 2, 5) new SelfStatusMove(MoveId.ALLY_SWITCH, PokemonType.PSYCHIC, -1, 15, -1, 2, 5)
.ignoresProtect() .ignoresProtect()
.unimplemented(), .unimplemented(),
@ -10336,8 +10421,8 @@ export function initMoves() {
new StatusMove(MoveId.MAT_BLOCK, PokemonType.FIGHTING, -1, 10, -1, 0, 6) new StatusMove(MoveId.MAT_BLOCK, PokemonType.FIGHTING, -1, 10, -1, 0, 6)
.target(MoveTarget.USER_SIDE) .target(MoveTarget.USER_SIDE)
.attr(AddArenaTagAttr, ArenaTagType.MAT_BLOCK, 1, true, true) .attr(AddArenaTagAttr, ArenaTagType.MAT_BLOCK, 1, true, true)
.condition(new FirstMoveCondition()) .condition(new FirstMoveCondition(), 3)
.condition(failIfLastCondition), .condition(failIfLastCondition, 3),
new AttackMove(MoveId.BELCH, PokemonType.POISON, MoveCategory.SPECIAL, 120, 90, 10, -1, 0, 6) new AttackMove(MoveId.BELCH, PokemonType.POISON, MoveCategory.SPECIAL, 120, 90, 10, -1, 0, 6)
.restriction(user => !user.battleData.hasEatenBerry, "battle:moveDisabledBelch", true), .restriction(user => !user.battleData.hasEatenBerry, "battle:moveDisabledBelch", true),
new StatusMove(MoveId.ROTOTILLER, PokemonType.GROUND, -1, 10, -1, 0, 6) new StatusMove(MoveId.ROTOTILLER, PokemonType.GROUND, -1, 10, -1, 0, 6)
@ -10399,7 +10484,7 @@ export function initMoves() {
new StatusMove(MoveId.CRAFTY_SHIELD, PokemonType.FAIRY, -1, 10, -1, 3, 6) new StatusMove(MoveId.CRAFTY_SHIELD, PokemonType.FAIRY, -1, 10, -1, 3, 6)
.target(MoveTarget.USER_SIDE) .target(MoveTarget.USER_SIDE)
.attr(AddArenaTagAttr, ArenaTagType.CRAFTY_SHIELD, 1, true, true) .attr(AddArenaTagAttr, ArenaTagType.CRAFTY_SHIELD, 1, true, true)
.condition(failIfLastCondition), .condition(failIfLastCondition, 3),
new StatusMove(MoveId.FLOWER_SHIELD, PokemonType.FAIRY, -1, 10, -1, 0, 6) new StatusMove(MoveId.FLOWER_SHIELD, PokemonType.FAIRY, -1, 10, -1, 0, 6)
.target(MoveTarget.ALL) .target(MoveTarget.ALL)
.attr(StatStageChangeAttr, [ Stat.DEF ], 1, false, { condition: (user, target, move) => target.getTypes().includes(PokemonType.GRASS) && !target.getTag(SemiInvulnerableTag) }), .attr(StatStageChangeAttr, [ Stat.DEF ], 1, false, { condition: (user, target, move) => target.getTypes().includes(PokemonType.GRASS) && !target.getTag(SemiInvulnerableTag) }),
@ -10427,7 +10512,7 @@ export function initMoves() {
.attr(AddArenaTagAttr, ArenaTagType.FAIRY_LOCK, 2, true), .attr(AddArenaTagAttr, ArenaTagType.FAIRY_LOCK, 2, true),
new SelfStatusMove(MoveId.KINGS_SHIELD, PokemonType.STEEL, -1, 10, -1, 4, 6) new SelfStatusMove(MoveId.KINGS_SHIELD, PokemonType.STEEL, -1, 10, -1, 4, 6)
.attr(ProtectAttr, BattlerTagType.KINGS_SHIELD) .attr(ProtectAttr, BattlerTagType.KINGS_SHIELD)
.condition(failIfLastCondition), .condition(failIfLastCondition, 3),
new StatusMove(MoveId.PLAY_NICE, PokemonType.NORMAL, -1, 20, -1, 0, 6) new StatusMove(MoveId.PLAY_NICE, PokemonType.NORMAL, -1, 20, -1, 0, 6)
.attr(StatStageChangeAttr, [ Stat.ATK ], -1) .attr(StatStageChangeAttr, [ Stat.ATK ], -1)
.ignoresSubstitute() .ignoresSubstitute()
@ -10455,7 +10540,7 @@ export function initMoves() {
.attr(StatStageChangeAttr, [ Stat.SPATK ], -1), .attr(StatStageChangeAttr, [ Stat.SPATK ], -1),
new SelfStatusMove(MoveId.SPIKY_SHIELD, PokemonType.GRASS, -1, 10, -1, 4, 6) new SelfStatusMove(MoveId.SPIKY_SHIELD, PokemonType.GRASS, -1, 10, -1, 4, 6)
.attr(ProtectAttr, BattlerTagType.SPIKY_SHIELD) .attr(ProtectAttr, BattlerTagType.SPIKY_SHIELD)
.condition(failIfLastCondition), .condition(failIfLastCondition, 3),
new StatusMove(MoveId.AROMATIC_MIST, PokemonType.FAIRY, -1, 20, -1, 0, 6) new StatusMove(MoveId.AROMATIC_MIST, PokemonType.FAIRY, -1, 20, -1, 0, 6)
.attr(StatStageChangeAttr, [ Stat.SPDEF ], 1) .attr(StatStageChangeAttr, [ Stat.SPDEF ], 1)
.ignoresSubstitute() .ignoresSubstitute()
@ -10623,10 +10708,10 @@ export function initMoves() {
.attr(SandHealAttr) .attr(SandHealAttr)
.triageMove(), .triageMove(),
new AttackMove(MoveId.FIRST_IMPRESSION, PokemonType.BUG, MoveCategory.PHYSICAL, 90, 100, 10, -1, 2, 7) new AttackMove(MoveId.FIRST_IMPRESSION, PokemonType.BUG, MoveCategory.PHYSICAL, 90, 100, 10, -1, 2, 7)
.condition(new FirstMoveCondition()), .condition(new FirstMoveCondition(), 3),
new SelfStatusMove(MoveId.BANEFUL_BUNKER, PokemonType.POISON, -1, 10, -1, 4, 7) new SelfStatusMove(MoveId.BANEFUL_BUNKER, PokemonType.POISON, -1, 10, -1, 4, 7)
.attr(ProtectAttr, BattlerTagType.BANEFUL_BUNKER) .attr(ProtectAttr, BattlerTagType.BANEFUL_BUNKER)
.condition(failIfLastCondition), .condition(failIfLastCondition, 3),
new AttackMove(MoveId.SPIRIT_SHACKLE, PokemonType.GHOST, MoveCategory.PHYSICAL, 80, 100, 10, 100, 0, 7) new AttackMove(MoveId.SPIRIT_SHACKLE, PokemonType.GHOST, MoveCategory.PHYSICAL, 80, 100, 10, 100, 0, 7)
.attr(AddBattlerTagAttr, BattlerTagType.TRAPPED, false, false, 1, 1, true) .attr(AddBattlerTagAttr, BattlerTagType.TRAPPED, false, false, 1, 1, true)
.makesContact(false), .makesContact(false),
@ -10759,7 +10844,16 @@ export function initMoves() {
new AttackMove(MoveId.BRUTAL_SWING, PokemonType.DARK, MoveCategory.PHYSICAL, 60, 100, 20, -1, 0, 7) new AttackMove(MoveId.BRUTAL_SWING, PokemonType.DARK, MoveCategory.PHYSICAL, 60, 100, 20, -1, 0, 7)
.target(MoveTarget.ALL_NEAR_OTHERS), .target(MoveTarget.ALL_NEAR_OTHERS),
new StatusMove(MoveId.AURORA_VEIL, PokemonType.ICE, -1, 20, -1, 0, 7) new StatusMove(MoveId.AURORA_VEIL, PokemonType.ICE, -1, 20, -1, 0, 7)
.condition((user, target, move) => (globalScene.arena.weather?.weatherType === WeatherType.HAIL || globalScene.arena.weather?.weatherType === WeatherType.SNOW) && !globalScene.arena.weather?.isEffectSuppressed()) .condition(
() => {
const weather = globalScene.arena.weather;
if (isNullOrUndefined(weather) || weather.isEffectSuppressed()) {
return false;
}
return weather.weatherType === WeatherType.HAIL || weather.weatherType === WeatherType.SNOW;
},
3
)
.attr(AddArenaTagAttr, ArenaTagType.AURORA_VEIL, 5, true) .attr(AddArenaTagAttr, ArenaTagType.AURORA_VEIL, 5, true)
.target(MoveTarget.USER_SIDE), .target(MoveTarget.USER_SIDE),
/* Unused */ /* Unused */
@ -10796,7 +10890,10 @@ export function initMoves() {
.attr(AddBattlerTagHeaderAttr, BattlerTagType.SHELL_TRAP) .attr(AddBattlerTagHeaderAttr, BattlerTagType.SHELL_TRAP)
.target(MoveTarget.ALL_NEAR_ENEMIES) .target(MoveTarget.ALL_NEAR_ENEMIES)
// Fails if the user was not hit by a physical attack during the turn // Fails if the user was not hit by a physical attack during the turn
.condition((user, target, move) => user.getTag(ShellTrapTag)?.activated === true), .condition(
user => user.getTag(ShellTrapTag)?.activated === true,
3
),
new AttackMove(MoveId.FLEUR_CANNON, PokemonType.FAIRY, MoveCategory.SPECIAL, 130, 90, 5, -1, 0, 7) new AttackMove(MoveId.FLEUR_CANNON, PokemonType.FAIRY, MoveCategory.SPECIAL, 130, 90, 5, -1, 0, 7)
.attr(StatStageChangeAttr, [ Stat.SPATK ], -2, true), .attr(StatStageChangeAttr, [ Stat.SPATK ], -2, true),
new AttackMove(MoveId.PSYCHIC_FANGS, PokemonType.PSYCHIC, MoveCategory.PHYSICAL, 85, 100, 10, -1, 0, 7) new AttackMove(MoveId.PSYCHIC_FANGS, PokemonType.PSYCHIC, MoveCategory.PHYSICAL, 85, 100, 10, -1, 0, 7)
@ -10840,7 +10937,7 @@ export function initMoves() {
.edgeCase(), // I assume it's because it needs thunderbolt and pikachu in a cap .edgeCase(), // I assume it's because it needs thunderbolt and pikachu in a cap
/* End Unused */ /* End Unused */
new AttackMove(MoveId.MIND_BLOWN, PokemonType.FIRE, MoveCategory.SPECIAL, 150, 100, 5, -1, 0, 7) new AttackMove(MoveId.MIND_BLOWN, PokemonType.FIRE, MoveCategory.SPECIAL, 150, 100, 5, -1, 0, 7)
.condition(failIfDampCondition) .condition(failIfDampCondition, 3)
.attr(HalfSacrificialAttr) .attr(HalfSacrificialAttr)
.target(MoveTarget.ALL_NEAR_OTHERS), .target(MoveTarget.ALL_NEAR_OTHERS),
new AttackMove(MoveId.PLASMA_FISTS, PokemonType.ELECTRIC, MoveCategory.PHYSICAL, 100, 100, 15, -1, 0, 7) new AttackMove(MoveId.PLASMA_FISTS, PokemonType.ELECTRIC, MoveCategory.PHYSICAL, 100, 100, 15, -1, 0, 7)
@ -11031,7 +11128,8 @@ export function initMoves() {
new SelfStatusMove(MoveId.CLANGOROUS_SOUL, PokemonType.DRAGON, 100, 5, -1, 0, 8) new SelfStatusMove(MoveId.CLANGOROUS_SOUL, PokemonType.DRAGON, 100, 5, -1, 0, 8)
.attr(CutHpStatStageBoostAttr, [ Stat.ATK, Stat.DEF, Stat.SPATK, Stat.SPDEF, Stat.SPD ], 1, 3) .attr(CutHpStatStageBoostAttr, [ Stat.ATK, Stat.DEF, Stat.SPATK, Stat.SPDEF, Stat.SPD ], 1, 3)
.soundBased() .soundBased()
.danceMove(), .danceMove()
.condition(new FailIfInsufficientHpCondition(3), 3),
new AttackMove(MoveId.BODY_PRESS, PokemonType.FIGHTING, MoveCategory.PHYSICAL, 80, 100, 10, -1, 0, 8) new AttackMove(MoveId.BODY_PRESS, PokemonType.FIGHTING, MoveCategory.PHYSICAL, 80, 100, 10, -1, 0, 8)
.attr(DefAtkAttr), .attr(DefAtkAttr),
new StatusMove(MoveId.DECORATE, PokemonType.FAIRY, -1, 15, -1, 0, 8) new StatusMove(MoveId.DECORATE, PokemonType.FAIRY, -1, 15, -1, 0, 8)
@ -11078,7 +11176,7 @@ export function initMoves() {
.triageMove(), .triageMove(),
new SelfStatusMove(MoveId.OBSTRUCT, PokemonType.DARK, 100, 10, -1, 4, 8) new SelfStatusMove(MoveId.OBSTRUCT, PokemonType.DARK, 100, 10, -1, 4, 8)
.attr(ProtectAttr, BattlerTagType.OBSTRUCT) .attr(ProtectAttr, BattlerTagType.OBSTRUCT)
.condition(failIfLastCondition), .condition(failIfLastCondition, 3),
new AttackMove(MoveId.FALSE_SURRENDER, PokemonType.DARK, MoveCategory.PHYSICAL, 80, -1, 10, -1, 0, 8), new AttackMove(MoveId.FALSE_SURRENDER, PokemonType.DARK, MoveCategory.PHYSICAL, 80, -1, 10, -1, 0, 8),
new AttackMove(MoveId.METEOR_ASSAULT, PokemonType.FIGHTING, MoveCategory.PHYSICAL, 150, 100, 5, -1, 0, 8) new AttackMove(MoveId.METEOR_ASSAULT, PokemonType.FIGHTING, MoveCategory.PHYSICAL, 150, 100, 5, -1, 0, 8)
.attr(RechargeAttr) .attr(RechargeAttr)
@ -11092,7 +11190,7 @@ export function initMoves() {
.attr(VariableTargetAttr, (user, target, move) => globalScene.arena.getTerrainType() === TerrainType.PSYCHIC && user.isGrounded() ? MoveTarget.ALL_NEAR_ENEMIES : MoveTarget.NEAR_OTHER), .attr(VariableTargetAttr, (user, target, move) => globalScene.arena.getTerrainType() === TerrainType.PSYCHIC && user.isGrounded() ? MoveTarget.ALL_NEAR_ENEMIES : MoveTarget.NEAR_OTHER),
new AttackMove(MoveId.STEEL_ROLLER, PokemonType.STEEL, MoveCategory.PHYSICAL, 130, 100, 5, -1, 0, 8) new AttackMove(MoveId.STEEL_ROLLER, PokemonType.STEEL, MoveCategory.PHYSICAL, 130, 100, 5, -1, 0, 8)
.attr(ClearTerrainAttr) .attr(ClearTerrainAttr)
.condition((user, target, move) => !!globalScene.arena.terrain), .condition(() => !!globalScene.arena.terrain, 3),
new AttackMove(MoveId.SCALE_SHOT, PokemonType.DRAGON, MoveCategory.PHYSICAL, 25, 90, 20, -1, 0, 8) new AttackMove(MoveId.SCALE_SHOT, PokemonType.DRAGON, MoveCategory.PHYSICAL, 25, 90, 20, -1, 0, 8)
.attr(StatStageChangeAttr, [ Stat.SPD ], 1, true, { lastHitOnly: true }) .attr(StatStageChangeAttr, [ Stat.SPD ], 1, true, { lastHitOnly: true })
.attr(StatStageChangeAttr, [ Stat.DEF ], -1, true, { lastHitOnly: true }) .attr(StatStageChangeAttr, [ Stat.DEF ], -1, true, { lastHitOnly: true })
@ -11109,7 +11207,7 @@ export function initMoves() {
.attr(SacrificialAttr) .attr(SacrificialAttr)
.target(MoveTarget.ALL_NEAR_OTHERS) .target(MoveTarget.ALL_NEAR_OTHERS)
.attr(MovePowerMultiplierAttr, (user, target, move) => globalScene.arena.getTerrainType() === TerrainType.MISTY && user.isGrounded() ? 1.5 : 1) .attr(MovePowerMultiplierAttr, (user, target, move) => globalScene.arena.getTerrainType() === TerrainType.MISTY && user.isGrounded() ? 1.5 : 1)
.condition(failIfDampCondition) .condition(failIfDampCondition, 3)
.makesContact(false), .makesContact(false),
new AttackMove(MoveId.GRASSY_GLIDE, PokemonType.GRASS, MoveCategory.PHYSICAL, 55, 100, 20, -1, 0, 8) new AttackMove(MoveId.GRASSY_GLIDE, PokemonType.GRASS, MoveCategory.PHYSICAL, 55, 100, 20, -1, 0, 8)
.attr(IncrementMovePriorityAttr, (user, target, move) => globalScene.arena.getTerrainType() === TerrainType.GRASSY && user.isGrounded()), .attr(IncrementMovePriorityAttr, (user, target, move) => globalScene.arena.getTerrainType() === TerrainType.GRASSY && user.isGrounded()),
@ -11127,7 +11225,7 @@ export function initMoves() {
new AttackMove(MoveId.LASH_OUT, PokemonType.DARK, MoveCategory.PHYSICAL, 75, 100, 5, -1, 0, 8) new AttackMove(MoveId.LASH_OUT, PokemonType.DARK, MoveCategory.PHYSICAL, 75, 100, 5, -1, 0, 8)
.attr(MovePowerMultiplierAttr, (user, _target, _move) => user.turnData.statStagesDecreased ? 2 : 1), .attr(MovePowerMultiplierAttr, (user, _target, _move) => user.turnData.statStagesDecreased ? 2 : 1),
new AttackMove(MoveId.POLTERGEIST, PokemonType.GHOST, MoveCategory.PHYSICAL, 110, 90, 5, -1, 0, 8) new AttackMove(MoveId.POLTERGEIST, PokemonType.GHOST, MoveCategory.PHYSICAL, 110, 90, 5, -1, 0, 8)
.condition(failIfNoTargetHeldItemsCondition) .condition(failIfNoTargetHeldItemsCondition, 3)
.attr(PreMoveMessageAttr, attackedByItemMessageFunc) .attr(PreMoveMessageAttr, attackedByItemMessageFunc)
.makesContact(false), .makesContact(false),
new StatusMove(MoveId.CORROSIVE_GAS, PokemonType.POISON, 100, 40, -1, 0, 8) new StatusMove(MoveId.CORROSIVE_GAS, PokemonType.POISON, 100, 40, -1, 0, 8)
@ -11371,7 +11469,7 @@ export function initMoves() {
.attr(StatStageChangeAttr, [ Stat.ATK, Stat.SPATK ], -1, true, { condition: (user, target, move) => user.isTerastallized && user.isOfType(PokemonType.STELLAR) }), .attr(StatStageChangeAttr, [ Stat.ATK, Stat.SPATK ], -1, true, { condition: (user, target, move) => user.isTerastallized && user.isOfType(PokemonType.STELLAR) }),
new SelfStatusMove(MoveId.SILK_TRAP, PokemonType.BUG, -1, 10, -1, 4, 9) new SelfStatusMove(MoveId.SILK_TRAP, PokemonType.BUG, -1, 10, -1, 4, 9)
.attr(ProtectAttr, BattlerTagType.SILK_TRAP) .attr(ProtectAttr, BattlerTagType.SILK_TRAP)
.condition(failIfLastCondition), .condition(failIfLastCondition, 3),
new AttackMove(MoveId.AXE_KICK, PokemonType.FIGHTING, MoveCategory.PHYSICAL, 120, 90, 10, 30, 0, 9) new AttackMove(MoveId.AXE_KICK, PokemonType.FIGHTING, MoveCategory.PHYSICAL, 120, 90, 10, 30, 0, 9)
.attr(MissEffectAttr, crashDamageFunc) .attr(MissEffectAttr, crashDamageFunc)
.attr(NoEffectAttr, crashDamageFunc) .attr(NoEffectAttr, crashDamageFunc)
@ -11400,10 +11498,7 @@ export function initMoves() {
.attr(ClearTerrainAttr), .attr(ClearTerrainAttr),
new AttackMove(MoveId.GLAIVE_RUSH, PokemonType.DRAGON, MoveCategory.PHYSICAL, 120, 100, 5, -1, 0, 9) new AttackMove(MoveId.GLAIVE_RUSH, PokemonType.DRAGON, MoveCategory.PHYSICAL, 120, 100, 5, -1, 0, 9)
.attr(AddBattlerTagAttr, BattlerTagType.ALWAYS_GET_HIT, true, false, 0, 0, true) .attr(AddBattlerTagAttr, BattlerTagType.ALWAYS_GET_HIT, true, false, 0, 0, true)
.attr(AddBattlerTagAttr, BattlerTagType.RECEIVE_DOUBLE_DAMAGE, true, false, 0, 0, true) .attr(AddBattlerTagAttr, BattlerTagType.RECEIVE_DOUBLE_DAMAGE, true, false, 0, 0, true),
.condition((user, target, move) => {
return !(target.getTag(BattlerTagType.PROTECTED)?.tagType === "PROTECTED" || globalScene.arena.getTag(ArenaTagType.MAT_BLOCK)?.tagType === "MAT_BLOCK");
}),
new StatusMove(MoveId.REVIVAL_BLESSING, PokemonType.NORMAL, -1, 1, -1, 0, 9) new StatusMove(MoveId.REVIVAL_BLESSING, PokemonType.NORMAL, -1, 1, -1, 0, 9)
.triageMove() .triageMove()
.attr(RevivalBlessingAttr) .attr(RevivalBlessingAttr)
@ -11433,7 +11528,8 @@ export function initMoves() {
new StatusMove(MoveId.DOODLE, PokemonType.NORMAL, 100, 10, -1, 0, 9) new StatusMove(MoveId.DOODLE, PokemonType.NORMAL, 100, 10, -1, 0, 9)
.attr(AbilityCopyAttr, true), .attr(AbilityCopyAttr, true),
new SelfStatusMove(MoveId.FILLET_AWAY, PokemonType.NORMAL, -1, 10, -1, 0, 9) new SelfStatusMove(MoveId.FILLET_AWAY, PokemonType.NORMAL, -1, 10, -1, 0, 9)
.attr(CutHpStatStageBoostAttr, [ Stat.ATK, Stat.SPATK, Stat.SPD ], 2, 2), .attr(CutHpStatStageBoostAttr, [ Stat.ATK, Stat.SPATK, Stat.SPD ], 2, 2)
.condition(new FailIfInsufficientHpCondition(2), 3),
new AttackMove(MoveId.KOWTOW_CLEAVE, PokemonType.DARK, MoveCategory.PHYSICAL, 85, -1, 10, -1, 0, 9) new AttackMove(MoveId.KOWTOW_CLEAVE, PokemonType.DARK, MoveCategory.PHYSICAL, 85, -1, 10, -1, 0, 9)
.slicingMove(), .slicingMove(),
new AttackMove(MoveId.FLOWER_TRICK, PokemonType.GRASS, MoveCategory.PHYSICAL, 70, -1, 10, -1, 0, 9) new AttackMove(MoveId.FLOWER_TRICK, PokemonType.GRASS, MoveCategory.PHYSICAL, 70, -1, 10, -1, 0, 9)
@ -11522,8 +11618,8 @@ export function initMoves() {
.makesContact(false) .makesContact(false)
.restriction(ConsecutiveUseRestriction), .restriction(ConsecutiveUseRestriction),
new AttackMove(MoveId.COMEUPPANCE, PokemonType.DARK, MoveCategory.PHYSICAL, -1, 100, 10, -1, 0, 9) new AttackMove(MoveId.COMEUPPANCE, PokemonType.DARK, MoveCategory.PHYSICAL, -1, 100, 10, -1, 0, 9)
.attr(CounterDamageAttr, (move: Move) => (move.category === MoveCategory.PHYSICAL || move.category === MoveCategory.SPECIAL), 1.5) .attr(CounterDamageAttr, 1.5)
.redirectCounter() .condition(counterAttackCondition_Both, 3)
.target(MoveTarget.ATTACKER), .target(MoveTarget.ATTACKER),
new AttackMove(MoveId.AQUA_CUTTER, PokemonType.WATER, MoveCategory.PHYSICAL, 70, 100, 20, -1, 0, 9) new AttackMove(MoveId.AQUA_CUTTER, PokemonType.WATER, MoveCategory.PHYSICAL, 70, 100, 20, -1, 0, 9)
.attr(HighCritAttr) .attr(HighCritAttr)
@ -11575,15 +11671,9 @@ export function initMoves() {
.edgeCase(), // Should not interact with Sheer Force .edgeCase(), // Should not interact with Sheer Force
new SelfStatusMove(MoveId.BURNING_BULWARK, PokemonType.FIRE, -1, 10, -1, 4, 9) new SelfStatusMove(MoveId.BURNING_BULWARK, PokemonType.FIRE, -1, 10, -1, 4, 9)
.attr(ProtectAttr, BattlerTagType.BURNING_BULWARK) .attr(ProtectAttr, BattlerTagType.BURNING_BULWARK)
.condition(failIfLastCondition), .condition(failIfLastCondition, 3),
new AttackMove(MoveId.THUNDERCLAP, PokemonType.ELECTRIC, MoveCategory.SPECIAL, 70, 100, 5, -1, 1, 9) new AttackMove(MoveId.THUNDERCLAP, PokemonType.ELECTRIC, MoveCategory.SPECIAL, 70, 100, 5, -1, 1, 9)
.condition((user, target, move) => { .condition(failIfTargetNotAttackingCondition, 3),
const turnCommand = globalScene.currentBattle.turnCommands[target.getBattlerIndex()];
if (!turnCommand || !turnCommand.move) {
return false;
}
return (turnCommand.command === Command.FIGHT && !target.turnData.acted && allMoves[turnCommand.move.move].category !== MoveCategory.STATUS);
}),
new AttackMove(MoveId.MIGHTY_CLEAVE, PokemonType.ROCK, MoveCategory.PHYSICAL, 95, 100, 5, -1, 0, 9) new AttackMove(MoveId.MIGHTY_CLEAVE, PokemonType.ROCK, MoveCategory.PHYSICAL, 95, 100, 5, -1, 0, 9)
.slicingMove() .slicingMove()
.ignoresProtect(), .ignoresProtect(),
@ -11611,7 +11701,7 @@ export function initMoves() {
.attr(AddBattlerTagAttr, BattlerTagType.HEAL_BLOCK, false, false, 2), .attr(AddBattlerTagAttr, BattlerTagType.HEAL_BLOCK, false, false, 2),
new AttackMove(MoveId.UPPER_HAND, PokemonType.FIGHTING, MoveCategory.PHYSICAL, 65, 100, 15, 100, 3, 9) new AttackMove(MoveId.UPPER_HAND, PokemonType.FIGHTING, MoveCategory.PHYSICAL, 65, 100, 15, 100, 3, 9)
.attr(FlinchAttr) .attr(FlinchAttr)
.condition(UpperHandCondition), .condition(UpperHandCondition, 3),
new AttackMove(MoveId.MALIGNANT_CHAIN, PokemonType.POISON, MoveCategory.SPECIAL, 100, 100, 5, 50, 0, 9) new AttackMove(MoveId.MALIGNANT_CHAIN, PokemonType.POISON, MoveCategory.SPECIAL, 100, 100, 5, 50, 0, 9)
.attr(StatusEffectAttr, StatusEffect.TOXIC) .attr(StatusEffectAttr, StatusEffect.TOXIC)
); );

View File

@ -3,3 +3,6 @@ export enum MoveCategory {
SPECIAL, SPECIAL,
STATUS STATUS
} }
/** Type of damage categories */
export type MoveDamageCategory = Exclude<MoveCategory, MoveCategory.STATUS>;

View File

@ -47,10 +47,8 @@ export enum MoveFlags {
CHECK_ALL_HITS = 1 << 16, CHECK_ALL_HITS = 1 << 16,
/** Indicates a move is able to bypass its target's Substitute (if the target has one) */ /** Indicates a move is able to bypass its target's Substitute (if the target has one) */
IGNORE_SUBSTITUTE = 1 << 17, IGNORE_SUBSTITUTE = 1 << 17,
/** Indicates a move is able to be redirected to allies in a double battle if the attacker faints */
REDIRECT_COUNTER = 1 << 18,
/** Indicates a move is able to be reflected by {@linkcode AbilityId.MAGIC_BOUNCE} and {@linkcode MoveId.MAGIC_COAT} */ /** Indicates a move is able to be reflected by {@linkcode AbilityId.MAGIC_BOUNCE} and {@linkcode MoveId.MAGIC_COAT} */
REFLECTABLE = 1 << 19, REFLECTABLE = 1 << 18,
/** Indicates a move that fails when {@link https://bulbapedia.bulbagarden.net/wiki/Gravity_(move) | Gravity} is in effect */ /** Indicates a move that fails when {@link https://bulbapedia.bulbagarden.net/wiki/Gravity_(move) | Gravity} is in effect */
GRAVITY = 1 << 20, GRAVITY = 1 << 19,
} }

View File

@ -724,7 +724,7 @@ export abstract class Pokemon extends Phaser.GameObjects.Container {
abstract getFieldIndex(): number; abstract getFieldIndex(): number;
abstract getBattlerIndex(): BattlerIndex; abstract getBattlerIndex(): Exclude<BattlerIndex, BattlerIndex.ATTACKER>;
/** /**
* Load all assets needed for this Pokemon's use in battle * Load all assets needed for this Pokemon's use in battle
@ -3305,7 +3305,7 @@ export abstract class Pokemon extends Phaser.GameObjects.Container {
*/ */
public trySelectMove(moveIndex: number, ignorePp?: boolean): [boolean, string] { public trySelectMove(moveIndex: number, ignorePp?: boolean): [boolean, string] {
const move = this.getMoveset().length > moveIndex ? this.getMoveset()[moveIndex] : null; const move = this.getMoveset().length > moveIndex ? this.getMoveset()[moveIndex] : null;
return move?.isUsable(this, ignorePp) ?? [false, ""]; return move?.isUsable(this, ignorePp, true) ?? [false, ""];
} }
showInfo(): void { showInfo(): void {
@ -5787,7 +5787,7 @@ export class PlayerPokemon extends Pokemon {
return globalScene.getPlayerField().indexOf(this); return globalScene.getPlayerField().indexOf(this);
} }
getBattlerIndex(): BattlerIndex { getBattlerIndex(): Exclude<BattlerIndex, BattlerIndex.ATTACKER> {
return this.getFieldIndex(); return this.getFieldIndex();
} }
@ -6903,7 +6903,7 @@ export class EnemyPokemon extends Pokemon {
return globalScene.getEnemyField().indexOf(this); return globalScene.getEnemyField().indexOf(this);
} }
getBattlerIndex(): BattlerIndex { getBattlerIndex(): Exclude<BattlerIndex, BattlerIndex.ATTACKER> {
return BattlerIndex.ENEMY + this.getFieldIndex(); return BattlerIndex.ENEMY + this.getFieldIndex();
} }

View File

@ -254,7 +254,7 @@ export class CommandPhase extends FieldPhase {
// Ternary here ensures we don't compute struggle conditions unless necessary // Ternary here ensures we don't compute struggle conditions unless necessary
const useStruggle = canUse const useStruggle = canUse
? false ? false
: cursor > -1 && !playerPokemon.getMoveset().some(m => m.isUsable(playerPokemon)[0]); : cursor > -1 && !playerPokemon.getMoveset().some(m => m.isUsable(playerPokemon, ignorePP, true)[0]);
if (!canUse && !useStruggle) { if (!canUse && !useStruggle) {
console.error("Cannot use move:", reason); console.error("Cannot use move:", reason);

View File

@ -27,7 +27,7 @@ import { frenzyMissFunc } from "#moves/move-utils";
import type { PokemonMove } from "#moves/pokemon-move"; import type { PokemonMove } from "#moves/pokemon-move";
import { BattlePhase } from "#phases/battle-phase"; import { BattlePhase } from "#phases/battle-phase";
// biome-ignore lint/correctness/noUnusedImports: Used in a tsdoc comment // biome-ignore lint/correctness/noUnusedImports: Used in a tsdoc comment
import type { PreUseInterruptAttr } from "#types/move-types"; import type { Move, PreUseInterruptAttr } from "#types/move-types";
import { applyChallenges } from "#utils/challenge-utils"; import { applyChallenges } from "#utils/challenge-utils";
import { BooleanHolder, NumberHolder } from "#utils/common"; import { BooleanHolder, NumberHolder } from "#utils/common";
import { enumValueToKey } from "#utils/enums"; import { enumValueToKey } from "#utils/enums";
@ -201,6 +201,7 @@ export class MovePhase extends BattlePhase {
* - Pollen puff used on an ally that is under effect of heal block * - Pollen puff used on an ally that is under effect of heal block
* - Burn up / Double shock when the user does not have the required type * - Burn up / Double shock when the user does not have the required type
* - No Retreat while already under its effects * - No Retreat while already under its effects
* - Failure due to primal weather
* - (on cart, not applicable to Pokerogue) Moves that fail if used ON a raid / special boss: selfdestruct/explosion/imprision/power split / guard split * - (on cart, not applicable to Pokerogue) Moves that fail if used ON a raid / special boss: selfdestruct/explosion/imprision/power split / guard split
* - (on cart, not applicable to Pokerogue) Moves that fail during a "co-op" battle (like when Arven helps during raid boss): ally switch / teatime * - (on cart, not applicable to Pokerogue) Moves that fail during a "co-op" battle (like when Arven helps during raid boss): ally switch / teatime
* *
@ -209,17 +210,26 @@ export class MovePhase extends BattlePhase {
protected secondFailureCheck(): boolean { protected secondFailureCheck(): boolean {
const move = this.move.getMove(); const move = this.move.getMove();
const user = this.pokemon; const user = this.pokemon;
let failedText: string | undefined;
const arena = globalScene.arena;
if (!move.applyConditions(user, this.getActiveTargetPokemon()[0], 2)) { if (!move.applyConditions(user, this.getActiveTargetPokemon()[0], 2)) {
// TODO: Make pollen puff failing from heal block use its own message
this.failed = true; this.failed = true;
// Note: If any of the moves have custom failure messages, this needs to be changed } else if (arena.isMoveWeatherCancelled(user, move)) {
// As of Gen 9, none do. (Except maybe pollen puff? Need to check) failedText = getWeatherBlockMessage(globalScene.arena.getWeatherType());
return true; this.failed = true;
} else {
// Powder *always* happens last
// Note: Powder's lapse method handles everything: messages, damage, animation, primal weather interaction,
// determining type of type changing moves, etc.
// It will set this phase's `failed` flag to true if it procs
user.lapseTag(BattlerTagType.POWDER, BattlerTagLapseType.PRE_MOVE);
return this.failed;
}
if (this.failed) {
this.showFailedText(failedText);
} }
// Powder *always* happens last
// Note: Powder's lapse method handles everything: messages, damage, animation, primal weather interaction,
// determining type of type changing moves, etc.
// It will set this phase's `failed` flag to true if it procs
user.lapseTag(BattlerTagType.POWDER, BattlerTagLapseType.PRE_MOVE);
return this.failed; return this.failed;
} }
@ -229,10 +239,13 @@ export class MovePhase extends BattlePhase {
* @returns Whether the move failed * @returns Whether the move failed
* *
* @remarks * @remarks
* - Conditional attributes of the move * - Anything in {@linkcode Move.conditionsSeq3}
* - Weather blocking the move * - Weather blocking the move
* - Terrain blocking the move * - Terrain blocking the move
* - Queenly Majesty / Dazzling * - Queenly Majesty / Dazzling
* - Damp (which is handled by move conditions in pokerogue rather than the ability, like queenly majesty / dazzling)
*
* The rest of the failure conditions are marked as sequence 4 and happen in the move effect phase.
*/ */
protected thirdFailureCheck(): boolean { protected thirdFailureCheck(): boolean {
/** /**
@ -244,9 +257,8 @@ export class MovePhase extends BattlePhase {
const arena = globalScene.arena; const arena = globalScene.arena;
const user = this.pokemon; const user = this.pokemon;
const failsConditions = !move.applyConditions(user, targets[0]); const failsConditions = !move.applyConditions(user, targets[0]);
const failedDueToWeather = arena.isMoveWeatherCancelled(user, move);
const failedDueToTerrain = arena.isMoveTerrainCancelled(user, this.targets, move); const failedDueToTerrain = arena.isMoveTerrainCancelled(user, this.targets, move);
let failed = failsConditions || failedDueToWeather || failedDueToTerrain; let failed = failsConditions || failedDueToTerrain;
// Apply queenly majesty / dazzling // Apply queenly majesty / dazzling
if (!failed) { if (!failed) {
@ -264,7 +276,7 @@ export class MovePhase extends BattlePhase {
} }
if (failed) { if (failed) {
this.failMove(true, failedDueToWeather, failedDueToTerrain); this.failMove(failedDueToTerrain);
return true; return true;
} }
@ -308,17 +320,10 @@ export class MovePhase extends BattlePhase {
return; return;
} }
// Now, issue the second failure checks
// If the user was asleep but is using a move anyway, it should STILL display the "user is sleeping" message!
// At this point, cure the user's freeze
// At this point, called moves should be decided. // At this point, called moves should be decided.
// For now, this is a placeholder until we rework how called moves are handled // For now, this comment works as a placeholder until we rework how called moves are handled
// For correct alignment with mainline, this SHOULD go here, and it SHOULD rewrite its own move // For correct alignment with mainline, this SHOULD go here, and this phase SHOULD rewrite its own move
// Though, this is not the case in pokerogue.
// At this point...
// If the first failure check passes, then thaw the user if its move will thaw it. // If the first failure check passes, then thaw the user if its move will thaw it.
// The sleep message and animation are also played if the user is asleep but using a move anyway (snore, sleep talk, etc) // The sleep message and animation are also played if the user is asleep but using a move anyway (snore, sleep talk, etc)
this.post1stFailSleepOrThaw(); this.post1stFailSleepOrThaw();
@ -349,29 +354,36 @@ export class MovePhase extends BattlePhase {
// Move is announced // Move is announced
this.showMoveText(); this.showMoveText();
// Stance change happens // Stance change happens
const charging = this.move.getMove().isChargingMove() && !this.pokemon.getTag(BattlerTagType.CHARGING); const charging = this.move.getMove().isChargingMove() && !this.pokemon.getTag(BattlerTagType.CHARGING);
// Stance change happens now if the move is about to be executed const move = this.move.getMove();
// Update the battle's "last move" pointer unless we're currently mimicking a move or triggering Dancer.
if (!move.hasAttr("CopyMoveAttr") && !isReflected(this.useMode)) {
globalScene.currentBattle.lastMove = move.id;
}
// Stance change happens now if the move is about to be executed and is not a charging move
if (!charging) { if (!charging) {
this.usePP();
globalScene.triggerPokemonFormChange(this.pokemon, SpeciesFormChangePreMoveTrigger); globalScene.triggerPokemonFormChange(this.pokemon, SpeciesFormChangePreMoveTrigger);
} }
if (this.secondFailureCheck()) { // At this point, if the target index has not moved on from attacker, the move must fail
this.showFailedText(); if (this.targets[0] === BattlerIndex.ATTACKER) {
this.fail();
}
if (this.targets[0] === BattlerIndex.ATTACKER || this.secondFailureCheck()) {
this.handlePreMoveFailures(); this.handlePreMoveFailures();
this.end(); this.end();
return; return;
} }
if (!(this.failed || this.cancelled)) { if (this.resolveFinalPreMoveCancellationChecks()) {
this.resolveFinalPreMoveCancellationChecks(); this.end();
} }
// Cancel, charge or use the move as applicable. if (charging) {
if (this.cancelled || this.failed) {
this.handlePreMoveFailures();
} else if (charging) {
this.chargeMove(); this.chargeMove();
} else { } else {
this.useMove(); this.useMove();
@ -380,20 +392,25 @@ export class MovePhase extends BattlePhase {
this.end(); this.end();
} }
/** Check for cancellation edge cases - no targets remaining */ /**
protected resolveFinalPreMoveCancellationChecks(): void { * Check for cancellation edge cases - no targets remaining or the battler index being targeted is still the attacker
* @returns Whether the move fails
*/
protected resolveFinalPreMoveCancellationChecks(): boolean {
const targets = this.getActiveTargetPokemon(); const targets = this.getActiveTargetPokemon();
const moveQueue = this.pokemon.getMoveQueue(); const moveQueue = this.pokemon.getMoveQueue();
if ( if (
(targets.length === 0 && !this.move.getMove().hasAttr("AddArenaTrapTagAttr")) || (targets.length === 0 && !this.move.getMove().hasAttr("AddArenaTrapTagAttr")) ||
(moveQueue.length > 0 && moveQueue[0].move === MoveId.NONE) (moveQueue.length > 0 && moveQueue[0].move === MoveId.NONE) ||
this.targets[0] === BattlerIndex.ATTACKER
) { ) {
this.showFailedText(); this.showFailedText();
this.cancel(); this.fail();
} else { return true;
this.pokemon.lapseTags(BattlerTagLapseType.MOVE);
} }
this.pokemon.lapseTags(BattlerTagLapseType.MOVE);
return false;
} }
public getActiveTargetPokemon(): Pokemon[] { public getActiveTargetPokemon(): Pokemon[] {
@ -672,25 +689,23 @@ export class MovePhase extends BattlePhase {
/** Execute the current move and apply its effects. */ /** Execute the current move and apply its effects. */
private executeMove() { private executeMove() {
const pokemon = this.pokemon; const user = this.pokemon;
const move = this.move.getMove(); const move = this.move.getMove();
const opponent = this.getActiveTargetPokemon()[0]; const opponent = this.getActiveTargetPokemon()[0];
const targets = this.targets; const targets = this.targets;
// Trigger ability-based user type changes, display move text and then execute move effects. // Trigger ability-based user type changes, display move text and then execute move effects.
// TODO: Investigate whether PokemonTypeChangeAbAttr can drop the "opponent" parameter // TODO: Investigate whether PokemonTypeChangeAbAttr can drop the "opponent" parameter
applyAbAttrs("PokemonTypeChangeAbAttr", { pokemon, move, opponent }); applyAbAttrs("PokemonTypeChangeAbAttr", { pokemon: user, move, opponent });
this.showMoveText(); globalScene.phaseManager.unshiftNew("MoveEffectPhase", user.getBattlerIndex(), targets, move, this.useMode);
globalScene.phaseManager.unshiftNew("MoveEffectPhase", pokemon.getBattlerIndex(), targets, move, this.useMode);
// Handle Dancer, which triggers immediately after a move is used (rather than waiting on `this.end()`). // Handle Dancer, which triggers immediately after a move is used (rather than waiting on `this.end()`).
// Note the MoveUseMode check here prevents an infinite Dancer loop. // Note the MoveUseMode check here prevents an infinite Dancer loop.
// TODO: This needs to go at the end of `MoveEffectPhase` to check move results // TODO: This needs to go at the end of `MoveEffectPhase` to check move results
const dancerModes: MoveUseMode[] = [MoveUseMode.INDIRECT, MoveUseMode.REFLECTED] as const; const dancerModes: MoveUseMode[] = [MoveUseMode.INDIRECT, MoveUseMode.REFLECTED] as const;
if (this.move.getMove().hasFlag(MoveFlags.DANCE_MOVE) && !dancerModes.includes(this.useMode)) { if (this.move.getMove().hasFlag(MoveFlags.DANCE_MOVE) && !dancerModes.includes(this.useMode)) {
// biome-ignore lint/nursery/noShadow: We don't need to access `pokemon` from the outer scope
globalScene.getField(true).forEach(pokemon => { globalScene.getField(true).forEach(pokemon => {
applyAbAttrs("PostMoveUsedAbAttr", { pokemon, move: this.move, source: pokemon, targets: targets }); applyAbAttrs("PostMoveUsedAbAttr", { pokemon, move: this.move, source: user, targets: targets });
}); });
} }
} }
@ -698,11 +713,9 @@ export class MovePhase extends BattlePhase {
/** /**
* Fail the move currently being used. * Fail the move currently being used.
* Handles failure messages, pushing to move history, etc. * Handles failure messages, pushing to move history, etc.
* @param showText - Whether to show move text when failing the move.
* @param failedDueToWeather - Whether the move failed due to weather (default `false`)
* @param failedDueToTerrain - Whether the move failed due to terrain (default `false`) * @param failedDueToTerrain - Whether the move failed due to terrain (default `false`)
*/ */
protected failMove(showText: boolean, failedDueToWeather = false, failedDueToTerrain = false) { protected failMove(failedDueToTerrain = false) {
const move = this.move.getMove(); const move = this.move.getMove();
const targets = this.getActiveTargetPokemon(); const targets = this.getActiveTargetPokemon();
@ -724,10 +737,6 @@ export class MovePhase extends BattlePhase {
}); });
} }
if (showText) {
this.showMoveText();
}
this.pokemon.pushMoveHistory({ this.pokemon.pushMoveHistory({
move: this.move.moveId, move: this.move.moveId,
targets: this.targets, targets: this.targets,
@ -741,9 +750,7 @@ export class MovePhase extends BattlePhase {
move.getFailedText(this.pokemon, targets[0], move) || move.getFailedText(this.pokemon, targets[0], move) ||
(failedDueToTerrain (failedDueToTerrain
? getTerrainBlockMessage(targets[0], globalScene.arena.getTerrainType()) ? getTerrainBlockMessage(targets[0], globalScene.arena.getTerrainType())
: failedDueToWeather : i18next.t("battle:attackFailed"));
? getWeatherBlockMessage(globalScene.arena.getWeatherType())
: i18next.t("battle:attackFailed"));
this.showFailedText(failureMessage); this.showFailedText(failureMessage);
@ -760,7 +767,7 @@ export class MovePhase extends BattlePhase {
const targets = this.getActiveTargetPokemon(); const targets = this.getActiveTargetPokemon();
if (!move.applyConditions(this.pokemon, targets[0])) { if (!move.applyConditions(this.pokemon, targets[0])) {
this.failMove(true); this.failMove();
return; return;
} }
@ -888,34 +895,17 @@ export class MovePhase extends BattlePhase {
* to reflect the actual battler index of the user's last attacker. * 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. * move is marked for failure
* @todo Make this a feature of the move rather than basing logic on {@linkcode BattlerIndex.ATTACKER}
*/ */
protected resolveCounterAttackTarget(): void { protected resolveCounterAttackTarget(): void {
if (this.targets.length !== 1 || this.targets[0] !== BattlerIndex.ATTACKER) { if (this.targets.length !== 1 || this.targets[0] !== BattlerIndex.ATTACKER) {
return; return;
} }
// TODO: This should be covered in move conditions const targetHolder = new NumberHolder(BattlerIndex.ATTACKER);
if (this.pokemon.turnData.attacksReceived.length === 0) {
this.fail();
this.showMoveText();
this.showFailedText();
return;
}
this.targets[0] = this.pokemon.turnData.attacksReceived[0].sourceBattlerIndex; applyMoveAttrs("CounterRedirectAttr", this.pokemon, null, this.move.getMove(), targetHolder);
this.targets[0] = targetHolder.value;
// 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;
}
} }
/** /**
@ -937,10 +927,6 @@ export class MovePhase extends BattlePhase {
return; return;
} }
if (this.failed) {
this.usePP();
}
if (this.cancelled && this.pokemon.summonData.tags.some(t => t.tagType === BattlerTagType.FRENZY)) { if (this.cancelled && this.pokemon.summonData.tags.some(t => t.tagType === BattlerTagType.FRENZY)) {
frenzyMissFunc(this.pokemon, this.move.getMove()); frenzyMissFunc(this.pokemon, this.move.getMove());
} }

View File

@ -2,6 +2,7 @@ import { globalScene } from "#app/global-scene";
import { POKERUS_STARTER_COUNT, speciesStarterCosts } from "#balance/starters"; import { POKERUS_STARTER_COUNT, speciesStarterCosts } from "#balance/starters";
import { allSpecies } from "#data/data-lists"; import { allSpecies } from "#data/data-lists";
import type { PokemonSpecies, PokemonSpeciesForm } from "#data/pokemon-species"; import type { PokemonSpecies, PokemonSpeciesForm } from "#data/pokemon-species";
import { BattlerIndex } from "#enums/battler-index";
import type { SpeciesId } from "#enums/species-id"; import type { SpeciesId } from "#enums/species-id";
import { randSeedItem } from "./common"; import { randSeedItem } from "./common";
@ -123,3 +124,19 @@ export function getPokemonSpeciesForm(species: SpeciesId, formIndex: number): Po
} }
return retSpecies; return retSpecies;
} }
/**
* Return whether two battler indices are allies
* @param a - First battler index
* @param b - Second battler index
* @returns Whether the two battler indices are allies. Always `false` if either index is `ATTACKER`.
*/
export function areAllies(a: BattlerIndex, b: BattlerIndex): boolean {
if (a === BattlerIndex.ATTACKER || b === BattlerIndex.ATTACKER) {
return false;
}
return (
(a === BattlerIndex.PLAYER || a === BattlerIndex.PLAYER_2) ===
(b === BattlerIndex.PLAYER || b === BattlerIndex.PLAYER_2)
);
}

View File

@ -35,6 +35,7 @@ describe("Moves - Throat Chop", () => {
await game.classicMode.startBattle([SpeciesId.MAGIKARP]); await game.classicMode.startBattle([SpeciesId.MAGIKARP]);
const enemy = game.field.getEnemyPokemon(); const enemy = game.field.getEnemyPokemon();
const player = game.field.getPlayerPokemon();
game.move.select(MoveId.GROWL); game.move.select(MoveId.GROWL);
await game.setTurnOrder([BattlerIndex.ENEMY, BattlerIndex.PLAYER]); await game.setTurnOrder([BattlerIndex.ENEMY, BattlerIndex.PLAYER]);
@ -46,6 +47,8 @@ describe("Moves - Throat Chop", () => {
// Second turn, struggle if no valid moves // Second turn, struggle if no valid moves
await game.toNextTurn(); await game.toNextTurn();
expect(player.trySelectMove(MoveId.GROWL)[0]).toBe(false);
game.move.select(MoveId.GROWL); game.move.select(MoveId.GROWL);
await game.setTurnOrder([BattlerIndex.PLAYER, BattlerIndex.ENEMY]); await game.setTurnOrder([BattlerIndex.PLAYER, BattlerIndex.ENEMY]);