mirror of
https://github.com/pagefaultgames/pokerogue.git
synced 2025-08-19 13:59:27 +02:00
Refactor mostly complete, need to recheck tests
This commit is contained in:
parent
a77e3c911f
commit
c0827230cb
@ -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();
|
||||||
|
@ -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
|
||||||
*
|
*
|
||||||
|
@ -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;
|
||||||
|
}
|
||||||
|
@ -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)
|
||||||
);
|
);
|
||||||
|
@ -3,3 +3,6 @@ export enum MoveCategory {
|
|||||||
SPECIAL,
|
SPECIAL,
|
||||||
STATUS
|
STATUS
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Type of damage categories */
|
||||||
|
export type MoveDamageCategory = Exclude<MoveCategory, MoveCategory.STATUS>;
|
@ -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,
|
||||||
}
|
}
|
||||||
|
@ -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();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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);
|
||||||
|
@ -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());
|
||||||
}
|
}
|
||||||
|
@ -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)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
@ -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]);
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user