Reverted a couple doc changes

This commit is contained in:
Bertie690 2025-05-10 16:28:22 -04:00
parent c2e7c95620
commit da6443562b
7 changed files with 332 additions and 373 deletions

View File

@ -5557,7 +5557,7 @@ class ForceSwitchOutHelper {
* - If the Pokémon is still alive (hp > 0), and if so, it leaves the field and a new SwitchPhase is initiated.
*/
if (switchOutTarget instanceof PlayerPokemon) {
if (globalScene.getPlayerParty().every(p => !p.isActive(true))) {
if (globalScene.getPlayerParty().filter((p) => p.isAllowedInBattle() && !p.isOnField()).length < 1) {
return false;
}
@ -6901,8 +6901,8 @@ export function initAbilities() {
new Ability(Abilities.ANALYTIC, 5)
.attr(MovePowerBoostAbAttr, (user, target, move) => {
// Boost power if all other Pokemon have already moved (no other moves are slated to execute)
const laterMovePhase = globalScene.findPhase((phase) => phase instanceof MovePhase && phase.pokemon.id !== user?.id);
return isNullOrUndefined(laterMovePhase);
const movePhase = globalScene.findPhase((phase) => phase instanceof MovePhase && phase.pokemon.id !== user?.id);
return isNullOrUndefined(movePhase);
}, 1.3),
new Ability(Abilities.ILLUSION, 5)
// The Pokemon generate an illusion if it's available

View File

@ -51,10 +51,6 @@ export enum BattlerTagLapseType {
MOVE,
PRE_MOVE,
AFTER_MOVE,
/**
* TODO: Stop treating this like a catch-all "semi invulnerability" tag;
* we may want to use this for other stuff later
*/
MOVE_EFFECT,
TURN_END,
HIT,
@ -65,7 +61,7 @@ export enum BattlerTagLapseType {
export class BattlerTag {
public tagType: BattlerTagType;
public lapseTypes: BattlerTagLapseType[]; // TODO: Make this a set
public lapseTypes: BattlerTagLapseType[];
public turnCount: number;
public sourceMove: Moves;
public sourceId?: number;
@ -98,7 +94,7 @@ export class BattlerTag {
onOverlap(_pokemon: Pokemon): void {}
/**
* Trigger and tick down this {@linkcode BattlerTag}'s duration.
* Tick down this {@linkcode BattlerTag}'s duration.
* @returns `true` if the tag should be kept (`turnCount` > 0`)
*/
lapse(_pokemon: Pokemon, _lapseType: BattlerTagLapseType): boolean {
@ -186,21 +182,21 @@ export abstract class MoveRestrictionBattlerTag extends BattlerTag {
}
/**
* Check if this tag is currently restricting a move's use.
* Gets whether this tag is restricting a move.
*
* @param move - The {@linkcode Moves | move ID} whose usability is being checked.
* @param user - The {@linkcode Pokemon} using the move.
* @returns Whether the given move is restricted by this tag.
* @param move - {@linkcode Moves} ID to check restriction for.
* @param user - The {@linkcode Pokemon} involved
* @returns `true` if the move is restricted by this tag, otherwise `false`.
*/
public abstract isMoveRestricted(move: Moves, user?: Pokemon): boolean;
/**
* Check if this tag is restricting a move during target selection.
* Returns `false` by default unless overridden by a child class.
* @param _move - The {@linkcode Moves | move ID} whose selectability is being checked
* @param _user - The {@linkcode Pokemon} using the move.
* @param _target - The {@linkcode Pokemon} being targeted
* @returns Whether the given move should be unselectable when choosing targets.
* Checks if this tag is restricting a move based on a user's decisions during the target selection phase
*
* @param {Moves} _move {@linkcode Moves} move ID to check restriction for
* @param {Pokemon} _user {@linkcode Pokemon} the user of the above move
* @param {Pokemon} _target {@linkcode Pokemon} the target of the above move
* @returns {boolean} `false` unless overridden by the child tag
*/
isMoveTargetRestricted(_move: Moves, _user: Pokemon, _target: Pokemon): boolean {
return false;
@ -346,10 +342,9 @@ export class DisabledTag extends MoveRestrictionBattlerTag {
/**
* @override
* Display the text that occurs when a move is interrupted via Disable.
* @param pokemon - The {@linkcode Pokemon} attempting to use the restricted move
* @param move - The {@linkcode Moves | move ID} of the move being interrupted
* @returns The text to display when the given move is interrupted
* @param {Pokemon} pokemon {@linkcode Pokemon} attempting to use the restricted move
* @param {Moves} move {@linkcode Moves} ID of the move being interrupted
* @returns {string} text to display when the move is interrupted
*/
override interruptedText(pokemon: Pokemon, move: Moves): string {
return i18next.t("battle:disableInterruptedMove", {
@ -367,6 +362,7 @@ export class DisabledTag extends MoveRestrictionBattlerTag {
/**
* Tag used by Gorilla Tactics to restrict the user to using only one move.
* @extends MoveRestrictionBattlerTag
*/
export class GorillaTacticsTag extends MoveRestrictionBattlerTag {
private moveId = Moves.NONE;
@ -416,10 +412,11 @@ export class GorillaTacticsTag extends MoveRestrictionBattlerTag {
}
/**
*
* @override
* @param pokemon - The {@linkcode Pokemon} attempting to select a move
* @param _move - Unused
* @returns The text to display when the move is rendered unselectable
* @param {Pokemon} pokemon n/a
* @param {Moves} _move {@linkcode Moves} ID of the move being denied
* @returns {string} text to display when the move is denied
*/
override selectionDeniedText(pokemon: Pokemon, _move: Moves): string {
return i18next.t("battle:canOnlyUseMove", {
@ -640,7 +637,7 @@ class NoRetreatTag extends TrappedTag {
*/
export class FlinchedTag extends BattlerTag {
constructor(sourceMove: Moves) {
super(BattlerTagType.FLINCHED, [BattlerTagLapseType.PRE_MOVE, BattlerTagLapseType.TURN_END], 1, sourceMove);
super(BattlerTagType.FLINCHED, [BattlerTagLapseType.PRE_MOVE, BattlerTagLapseType.TURN_END], 0, sourceMove);
}
onAdd(pokemon: Pokemon): void {
@ -1481,6 +1478,10 @@ export class WrapTag extends DamagingTrapTag {
}
export abstract class VortexTrapTag extends DamagingTrapTag {
constructor(tagType: BattlerTagType, commonAnim: CommonAnim, turnCount: number, sourceMove: Moves, sourceId: number) {
super(tagType, commonAnim, turnCount, sourceMove, sourceId);
}
getTrapMessage(pokemon: Pokemon): string {
return i18next.t("battlerTags:vortexOnTrap", {
pokemonNameWithAffix: getPokemonNameWithAffix(pokemon),
@ -2748,7 +2749,10 @@ export class HealBlockTag extends MoveRestrictionBattlerTag {
* @returns `true` if the move has a TRIAGE_MOVE flag and is a status move
*/
override isMoveRestricted(move: Moves): boolean {
return allMoves[move].hasFlag(MoveFlags.TRIAGE_MOVE) && allMoves[move].category === MoveCategory.STATUS;
if (allMoves[move].hasFlag(MoveFlags.TRIAGE_MOVE) && allMoves[move].category === MoveCategory.STATUS) {
return true;
}
return false;
}
/**
@ -3422,8 +3426,7 @@ export class PsychoShiftTag extends BattlerTag {
}
/**
* Tag associated with {@linkcode Moves.MAGIC_COAT | Magic Coat} that reflects certain status moves directed at the user.
* TODO: Move Reflection code out of `move-effect-phase` and into here
* Tag associated with the move Magic Coat.
*/
export class MagicCoatTag extends BattlerTag {
constructor() {

View File

@ -702,10 +702,10 @@ export default class Move implements Localizable {
/**
* Sees if a move has a custom failure text (by looking at each {@linkcode MoveAttr} of this move)
* @param user - The {@linkcode Pokemon} using this move
* @param target - The {@linkcode Pokemon} targeted by this move
* @param move - The {@linkcode Move} being used
* @returns A string containing the custom failure text, or `undefined` if no custom text exists.
* @param user {@linkcode Pokemon} using the move
* @param target {@linkcode Pokemon} target of the move
* @param move {@linkcode Move} with this attribute
* @returns string of the custom failure text, or `null` if it uses the default text ("But it failed!")
*/
getFailedText(user: Pokemon, target: Pokemon, move: Move): string | undefined {
for (const attr of this.attrs) {
@ -1373,8 +1373,8 @@ export class PreMoveMessageAttr extends MoveAttr {
/**
* Attribute for moves that can be conditionally interrupted to be considered to
* have failed before their "useMove" message is displayed.
* Currently used by {@linkcode Moves.FOCUS_PUNCH}.
* have failed before their "useMove" message is displayed. Currently used by
* Focus Punch.
* @extends MoveAttr
*/
export class PreUseInterruptAttr extends MoveAttr {
@ -1383,40 +1383,38 @@ export class PreUseInterruptAttr extends MoveAttr {
protected conditionFunc: MoveConditionFunc;
/**
* Create a new PreUseInterruptAttr.
* @param message - Custom failure text to display when the move is interrupted, either as a string or a function producing one.
* If ommitted, will display the default failure text upon cancellation.
* @param conditionFunc - A {@linkcode MoveConditionFunc} that returns `true` if the move should be canceled.
* Create a new MoveInterruptedMessageAttr.
* @param message The message to display when the move is interrupted, or a function that formats the message based on the user, target, and move.
*/
constructor(message: string | undefined | ((user: Pokemon, target: Pokemon, move: Move) => string), conditionFunc: MoveConditionFunc) {
constructor(message?: string | ((user: Pokemon, target: Pokemon, move: Move) => string), conditionFunc?: MoveConditionFunc) {
super();
this.message = message;
this.conditionFunc = conditionFunc;
this.conditionFunc = conditionFunc ?? (() => true);
}
/**
* Conditionally cancel this pokemon's current move.
* @param user - The {@linkcode Pokemon} using this move
* @param target - The {@linkcode Pokemon} targeted by this move
* @param move - The {@linkcode Move} being used
* @returns `true` if the move should be cancelled.
* Message to display when a move is interrupted.
* @param user {@linkcode Pokemon} using the move
* @param target {@linkcode Pokemon} target of the move
* @param move {@linkcode Move} with this attribute
*/
override apply(user: Pokemon, target: Pokemon, move: Move): boolean {
return this.conditionFunc(user, target, move);
}
/**
* Obtain the text displayed upon this move's interruption.
* @param user - The {@linkcode Pokemon} using this move
* @param target - The {@linkcode Pokemon} targeted by this move
* @param move - The {@linkcode Move} being used
* @returns A string containing the custom failure text, or `undefined` if no custom text exists.
* Message to display when a move is interrupted.
* @param user {@linkcode Pokemon} using the move
* @param target {@linkcode Pokemon} target of the move
* @param move {@linkcode Move} with this attribute
*/
override getFailedText(user: Pokemon, target: Pokemon, move: Move): string | undefined {
if (this.conditionFunc(user, target, move)) {
return typeof this.message !== "function"
? this.message
if (this.message && this.conditionFunc(user, target, move)) {
const message =
typeof this.message === "string"
? (this.message as string)
: this.message(user, target, move);
return message;
}
}
}
@ -3059,17 +3057,7 @@ export class DelayedAttackAttr extends OverrideMoveEffectAttr {
this.chargeText = chargeText;
}
/**
* Apply the delayed attack, either setting it up or triggering the attack.
* @param user - The {@linkcode Pokemon} using the move
* @param target - The {@linkcode Pokemon} being targeted
* @param move - The {@linkcode Move} being used
* @param args -
* `[0]` - {@linkcode BooleanHolder} containing whether the move was overriden
* `[1]` - Whether the move is supposed to set up a delayed attack (`true`) or activate (`false`)
* @returns always `true`
*/
apply(user: Pokemon, target: Pokemon, move: Move, args: [BooleanHolder, boolean]): boolean {
apply(user: Pokemon, target: Pokemon, move: Move, args: any[]): boolean {
// Edge case for the move applied on a pokemon that has fainted
if (!target) {
return true;
@ -3105,9 +3093,9 @@ export class AwaitCombinedPledgeAttr extends OverrideMoveEffectAttr {
/**
* If the user's ally is set to use a different move with this attribute,
* defer this move's effects for a combined move on the ally's turn.
* @param user - The {@linkcode Pokemon} using the move
* @param target - Unused
* @param move - The {@linkcode Move} being used
* @param user the {@linkcode Pokemon} using this move
* @param target n/a
* @param move the {@linkcode Move} being used
* @param args
* - [0] a {@linkcode BooleanHolder} indicating whether the move's base
* effects should be overridden this turn.
@ -3814,31 +3802,27 @@ export class DoublePowerChanceAttr extends VariablePowerAttr {
export abstract class ConsecutiveUsePowerMultiplierAttr extends MovePowerMultiplierAttr {
constructor(limit: number, resetOnFail: boolean, resetOnLimit?: boolean, ...comboMoves: Moves[]) {
super((user: Pokemon, _target: Pokemon, move: Move): number => {
const moveHistory = user.getLastXMoves(-1).slice(1, limit+1); // don't count the first history entry (ie the current move)
super((user: Pokemon, target: Pokemon, move: Move): number => {
const moveHistory = user.getLastXMoves(limit + 1).slice(1);
let count = 1;
let count = 0;
let turnMove: TurnMove | undefined;
// TODO: Confirm whether mirror moving an echoed voice counts for and/or resets a boost
for (const tm of moveHistory) {
if (
!(tm.move === move.id || comboMoves.includes(tm.move))
|| (resetOnFail && tm.result !== MoveResult.SUCCESS)
while (
(
(turnMove = moveHistory.shift())?.move === move.id
|| (comboMoves.length && comboMoves.includes(turnMove?.move ?? Moves.NONE))
)
&& (!resetOnFail || turnMove?.result === MoveResult.SUCCESS)
) {
break;
}
if (count < limit - 1) {
if (count < (limit - 1)) {
count++;
continue;
}
if (resetOnLimit) {
} else if (resetOnLimit) {
count = 0;
}
} else {
break;
}
}
return this.getMultiplier(count);
});
@ -4017,7 +4001,7 @@ export class HpPowerAttr extends VariablePowerAttr {
/**
* Attribute used for moves whose base power scales with the opponent's HP
* Used for {@linkcode Moves.CRUSH_GRIP}, {@linkcode Moves.WRING_OUT}, and {@linkcode Moves.HARD_PRESS}
* Used for Crush Grip, Wring Out, and Hard Press
* maxBasePower 100 for Hard Press, 120 for others
*/
export class OpponentHighHpPowerAttr extends VariablePowerAttr {
@ -4333,7 +4317,6 @@ const hasStockpileStacksCondition: MoveConditionFunc = (user) => {
return !!hasStockpilingTag && hasStockpilingTag.stockpiledCount > 0;
};
/**
* Attribute used for multi-hit moves that increase power in increments of the
* move's base power for each hit, namely Triple Kick and Triple Axel.
@ -5434,17 +5417,11 @@ export class FrenzyAttr extends MoveEffectAttr {
}
// TODO: Disable if used via dancer
// TODO: Add support for moves that don't add the frenzy tag (Uproar, Rollout, etc.)
// If frenzy is not in effect and we don't have anything queued up,
// add 1-2 extra instances of the move to the move queue.
// If frenzy is already in effect, tick down the tag.
if (!user.getTag(BattlerTagType.FRENZY) && user.getMoveQueue().length === 0) {
const turnCount = user.randSeedIntRange(1, 2); // excludes initial use
for (let i = 0; i < turnCount; i++) {
user.pushMoveQueue({ move: move.id, targets: [ target.getBattlerIndex() ], useType: MoveUseType.IGNORE_PP });
}
if (!user.getTag(BattlerTagType.FRENZY) && !user.getMoveQueue().length) {
const turnCount = user.randSeedIntRange(1, 2);
new Array(turnCount).fill(null).map(() => user.getMoveQueue().push({ move: move.id, targets: [ target.getBattlerIndex() ], ignorePP: true }));
user.addTag(BattlerTagType.FRENZY, turnCount, move.id, user.id);
} else {
applyMoveAttrs(AddBattlerTagAttr, user, target, move, args);
@ -5817,24 +5794,23 @@ export class ProtectAttr extends AddBattlerTagAttr {
super(tagType, true);
}
/**
* Condition to fail a protect usage based on random chance.
* Chance starts at 100% and is thirded for each prior successful proctect usage.
* @returns a function that fails the function if its proc chance roll fails
*/
getCondition(): MoveConditionFunc {
return ((user, target, move): boolean => {
const lastMoves = user.getLastXMoves(-1)
let timesUsed = 0;
const moveHistory = user.getLastXMoves();
let turnMove: TurnMove | undefined;
let threshold = 1;
for (const tm of lastMoves) {
if (!allMoves[tm.move].hasAttr(ProtectAttr) || tm.result !== MoveResult.SUCCESS) {
while (moveHistory.length) {
turnMove = moveHistory.shift();
if (!allMoves[turnMove?.move ?? Moves.NONE].hasAttr(ProtectAttr) || turnMove?.result !== MoveResult.SUCCESS) {
break;
}
threshold *= 3;
timesUsed++;
}
return threshold === 1 || user.randSeedInt(threshold) === 0;
if (timesUsed) {
return !user.randSeedInt(Math.pow(3, timesUsed));
}
return true;
});
}
}
@ -7334,14 +7310,13 @@ export class SketchAttr extends MoveEffectAttr {
constructor() {
super(true);
}
/**
* User copies the opponent's last used move, if possible.
* @param user - The {@linkcode Pokemon} using the move
* @param target - The {@linkcode Pokemon} being targeted by the move
* @param move - The {@linkcoed Move} being used
* @param args - Unused
* @returns Whether a move was successfully learnt
* User copies the opponent's last used move, if possible
* @param {Pokemon} user Pokemon that used the move and will replace Sketch with the copied move
* @param {Pokemon} target Pokemon that the user wants to copy a move from
* @param {Move} move Move being used
* @param {any[]} args Unused
* @returns {boolean} true if the function succeeds, otherwise false
*/
apply(user: Pokemon, target: Pokemon, move: Move, args: any[]): boolean {
@ -7881,6 +7856,7 @@ export class AfterYouAttr extends MoveEffectAttr {
/**
* Move effect to force the target to move last, ignoring priority.
* If applied to multiple targets, they move in speed order after all other moves.
* @extends MoveEffectAttr
*/
export class ForceLastAttr extends MoveEffectAttr {
/**
@ -7927,7 +7903,7 @@ const phaseForcedSlower = (phase: MovePhase, target: Pokemon, trickRoom: boolean
let slower: boolean;
// quashed pokemon still have speed ties
if (phase.pokemon.getEffectiveStat(Stat.SPD) === target.getEffectiveStat(Stat.SPD)) {
slower = !target.randSeedInt(2);
slower = target.randSeedInt(2) === 0;
} else {
slower = !trickRoom ? phase.pokemon.getEffectiveStat(Stat.SPD) < target.getEffectiveStat(Stat.SPD) : phase.pokemon.getEffectiveStat(Stat.SPD) > target.getEffectiveStat(Stat.SPD);
}

View File

@ -642,7 +642,7 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container {
/**
* Checks if a pokemon is fainted (ie: its `hp <= 0`).
* It's usually better to call {@linkcode isAllowedInBattle()}
* @param checkStatus - Whether to also check the pokemon's status for {@linkcode StatusEffect.FAINT}; default `false`
* @param checkStatus `true` to also check that the pokemon's status is {@linkcode StatusEffect.FAINT}
* @returns `true` if the pokemon is fainted
*/
public isFainted(checkStatus = false): boolean {
@ -3256,9 +3256,8 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container {
* If it rolls shiny, or if it's already shiny, also sets a random variant and give the Pokemon the associated luck.
*
* The base shiny odds are {@linkcode BASE_SHINY_CHANCE} / `65536`
* @param thresholdOverride - number that is divided by `2^16` (`65536`) to get the shiny chance, overrides {@linkcode shinyThreshold} if set (bypassing shiny rate modifiers such as Shiny Charm)
* @param applyModifiersToOverride - Whether to apply Shiny Charm and event modifiers to {@linkcode thresholdOverride}.
* Does nothing if {@linkcode thresholdOverride} is not set.
* @param thresholdOverride number that is divided by `2^16` (`65536`) to get the shiny chance, overrides {@linkcode shinyThreshold} if set (bypassing shiny rate modifiers such as Shiny Charm)
* @param applyModifiersToOverride If {@linkcode thresholdOverride} is set and this is true, will apply Shiny Charm and event modifiers to {@linkcode thresholdOverride}
* @returns `true` if the Pokemon has been set as a shiny, `false` otherwise
*/
public trySetShinySeed(
@ -3812,7 +3811,8 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container {
ui =>
ui instanceof BattleInfo &&
(ui as BattleInfo) instanceof PlayerBattleInfo === this.isPlayer(),
)[0] as Phaser.GameObjects.GameObject | undefined;
)
.find(() => true);
if (!otherBattleInfo || !this.getFieldIndex()) {
globalScene.fieldUI.sendToBack(this.battleInfo);
globalScene.sendTextToBack(); // Push the top right text objects behind everything else
@ -5140,8 +5140,8 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container {
/**
* Returns a list of the most recent move entries in this Pokemon's move history.
* The retrieved move entries are sorted in order from NEWEST to OLDEST.
* @param moveCount The number of move entries to retrieve.\
* If negative, retrieves the Pokemon's entire move history (equivalent to reversing the output of {@linkcode getMoveHistory()}).
* @param moveCount The number of move entries to retrieve.
* If negative, retrieve the Pokemon's entire move history (equivalent to reversing the output of {@linkcode getMoveHistory()}).
* Default is `1`.
* @returns A list of {@linkcode TurnMove}, as specified above.
*/
@ -5660,19 +5660,22 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container {
if (effect === StatusEffect.SLEEP) {
sleepTurnsRemaining = new NumberHolder(this.randSeedIntRange(2, 4));
this.setFrameRate(4);
// If the user is invulnerable, remove their invulnerability when they fall asleep
// and remove the upcoming attack from the move queue.
const tag = [
// If the user is invulnerable, lets remove their invulnerability when they fall asleep
const invulnerableTags = [
BattlerTagType.UNDERGROUND,
BattlerTagType.UNDERWATER,
BattlerTagType.HIDDEN,
BattlerTagType.FLYING,
].find(t => this.getTag(t));
];
const tag = invulnerableTags.find(t => this.getTag(t));
if (tag) {
this.removeTag(tag);
this.getMoveQueue().shift();
this.getMoveQueue().pop();
}
}
@ -8137,10 +8140,10 @@ export class PokemonMove {
}
/**
* Increments this move's {@linkcode ppUsed} variable (up to a maximum of {@link getMovePp}).
* @param count - Amount of PP to consume; default `1`
* Sets {@link ppUsed} for this move and ensures the value does not exceed {@link getMovePp}
* @param count Amount of PP to use
*/
usePp(count = 1) {
usePp(count: number = 1) {
this.ppUsed = Math.min(this.ppUsed + count, this.getMovePp());
}

View File

@ -135,11 +135,11 @@ export class MoveEffectPhase extends PokemonPhase {
* Compute targets and the results of hit checks of the invoked move against all targets,
* organized by battler index.
*
* **This is *not* a pure function** and has the following side effects:
* - Sets `this.hitChecks` to the results of the hit checks against each target
* - Sets success/failure of `this.moveHistoryEntry` based on the hit check results
* - Sets `user.turnData.hitCount` and `user.turnData.hitsLeft` to 1 if the move
* was unsuccessful against all targets (effectively canceling it)
* **This is *not* a pure function**; it has the following side effects
* - `this.hitChecks` - The results of the hit checks against each target
* - `this.moveHistoryEntry` - Sets success or failure based on the hit check results
* - user.turnData.hitCount and user.turnData.hitsLeft - Both set to 1 if the
* move was unsuccessful against all targets
*
* @returns The targets of the invoked move
* @see {@linkcode hitCheck}
@ -205,9 +205,9 @@ export class MoveEffectPhase extends PokemonPhase {
}
/**
* Apply the move to each of its resolved targets.
* Apply the move to each of the resolved targets.
* @param targets - The resolved set of targets of the move
* @throws - Error if there was an unexpected hit check result
* @throws Error if there was an unexpected hit check result
*/
private applyToTargets(user: Pokemon, targets: Pokemon[]): void {
for (const [i, target] of targets.entries()) {
@ -229,7 +229,6 @@ export class MoveEffectPhase extends PokemonPhase {
case HitCheckResult.NO_EFFECT_NO_MESSAGE:
case HitCheckResult.PROTECTED:
case HitCheckResult.TARGET_NOT_ON_FIELD:
// Apply effects for ineffective moves (e.g. High Jump Kick crash dmg)
applyMoveAttrs(NoEffectAttr, user, target, this.move);
break;
case HitCheckResult.MISS:
@ -643,19 +642,22 @@ export class MoveEffectPhase extends PokemonPhase {
if (!user) {
return false;
}
switch (true) {
// No Guard
case user.hasAbilityWithAttr(AlwaysHitAbAttr) || target.hasAbilityWithAttr(AlwaysHitAbAttr):
// Toxic as poison type
case this.move.hasAttr(ToxicAccuracyAttr) && user.isOfType(PokemonType.POISON):
// Lock On/Mind Reader
case !!user.getTag(BattlerTagType.IGNORE_ACCURACY):
// Spikes and company
case isFieldTargeted(this.move):
if (user.hasAbilityWithAttr(AlwaysHitAbAttr) || target.hasAbilityWithAttr(AlwaysHitAbAttr)) {
return true;
}
if (this.move.hasAttr(ToxicAccuracyAttr) && user.isOfType(PokemonType.POISON)) {
return true;
}
// TODO: Fix lock on / mind reader check.
if (
user.getTag(BattlerTagType.IGNORE_ACCURACY) &&
(user.getLastXMoves().find(() => true)?.targets || []).indexOf(target.getBattlerIndex()) !== -1
) {
return true;
}
if (isFieldTargeted(this.move)) {
return true;
}
return false;
}
/**

View File

@ -27,16 +27,16 @@ export class TurnStartPhase extends FieldPhase {
/**
* This orders the active Pokemon on the field by speed into an BattlerIndex array and returns that array.
* It also checks for Trick Room and reverses the array if it is present.
* @returns An array of {@linkcode BattlerIndex}es containing all on-field pokemon sorted in speed order.
* @returns {@linkcode BattlerIndex[]} the battle indices of all pokemon on the field ordered by speed
*/
getSpeedOrder(): BattlerIndex[] {
const playerField = globalScene.getPlayerField().filter(p => p.isActive()) as Pokemon[];
const enemyField = globalScene.getEnemyField().filter(p => p.isActive()) as Pokemon[];
// Shuffle the list before sorting so speed ties produce random results
// This is seeded with the current turn to prevent an inconsistency with variable turn order
// based on how long since you last reloaded
// We shuffle the list before sorting so speed ties produce random results
let orderedTargets: Pokemon[] = playerField.concat(enemyField);
// We seed it with the current turn to prevent an inconsistency where it
// was varying based on how long since you last reloaded
globalScene.executeWithSeedOffset(
() => {
orderedTargets = randSeedShuffle(orderedTargets);
@ -45,11 +45,11 @@ export class TurnStartPhase extends FieldPhase {
globalScene.waveSeed,
);
// Check for Trick Room and reverse sort order if active.
// Notably, Pokerogue does NOT have the "outspeed trick room" glitch at >1809 spd.
// Next, a check for Trick Room is applied to determine sort order.
const speedReversed = new BooleanHolder(false);
globalScene.arena.applyTags(TrickRoomTag, false, speedReversed);
// Adjust the sort function based on whether Trick Room is active.
orderedTargets.sort((a: Pokemon, b: Pokemon) => {
const aSpeed = a?.getEffectiveStat(Stat.SPD) ?? 0;
const bSpeed = b?.getEffectiveStat(Stat.SPD) ?? 0;
@ -120,8 +120,7 @@ export class TurnStartPhase extends FieldPhase {
}
}
// If there is no difference between the move's calculated priorities,
// check for differences in battlerBypassSpeed and returns the result.
// If there is no difference between the move's calculated priorities, the game checks for differences in battlerBypassSpeed and returns the result.
if (battlerBypassSpeed[a].value !== battlerBypassSpeed[b].value) {
return battlerBypassSpeed[a].value ? -1 : 1;
}

View File

@ -44,6 +44,7 @@ describe("Abilities - Wimp Out", () => {
function confirmSwitch(): void {
const [pokemon1, pokemon2] = game.scene.getPlayerParty();
expect(game.phaseInterceptor.log).toContain("SwitchSummonPhase");
expect(pokemon1.species.speciesId).not.toBe(Species.WIMPOD);
@ -55,34 +56,17 @@ describe("Abilities - Wimp Out", () => {
function confirmNoSwitch(): void {
const [pokemon1, pokemon2] = game.scene.getPlayerParty();
expect(game.phaseInterceptor.log).not.toContain("SwitchSummonPhase");
expect(pokemon2.species.speciesId).not.toBe(Species.WIMPOD);
expect(pokemon1.species.speciesId).toBe(Species.WIMPOD);
expect(pokemon1.isFainted()).toBe(false);
expect(pokemon1.getHpRatio()).toBeLessThan(0.5);
expect(pokemon2.species.speciesId).not.toBe(Species.WIMPOD);
}
it("should switch user out when falling below 50% HP, cancelling any pending moves", async () => {
await game.classicMode.startBattle([Species.WIMPOD, Species.TYRUNT]);
const wimpod = game.scene.getPlayerPokemon()!;
wimpod.hp *= 0.51;
game.move.select(Moves.SPLASH);
game.doSelectPartyPokemon(1);
await game.setTurnOrder([BattlerIndex.ENEMY, BattlerIndex.PLAYER]);
await game.phaseInterceptor.to("DamageAnimPhase", false);
game.phaseInterceptor.clearLogs();
await game.phaseInterceptor.to("TurnEndPhase");
confirmSwitch();
expect(wimpod.turnData.acted).toBe(false);
expect(game.phaseInterceptor.log).not.toContain("MoveEffectPhase");
});
it("should trigger regenerator passive when switching out", async () => {
it("triggers regenerator passive single time when switching out with wimp out", async () => {
game.override.passiveAbility(Abilities.REGENERATOR).startingLevel(5).enemyLevel(100);
await game.classicMode.startBattle([Species.WIMPOD, Species.TYRUNT]);
@ -96,7 +80,7 @@ describe("Abilities - Wimp Out", () => {
confirmSwitch();
});
it("should cause wild pokemon to flee", async () => {
it("It makes wild pokemon flee if triggered", async () => {
game.override.enemyAbility(Abilities.WIMP_OUT);
await game.classicMode.startBattle([Species.GOLISOPOD, Species.TYRUNT]);
@ -106,11 +90,12 @@ describe("Abilities - Wimp Out", () => {
game.move.select(Moves.FALSE_SWIPE);
await game.phaseInterceptor.to("BerryPhase");
expect(enemyPokemon.visible).toBe(false);
expect(enemyPokemon.switchOutStatus).toBe(true);
const isVisible = enemyPokemon.visible;
const hasFled = enemyPokemon.switchOutStatus;
expect(!isVisible && hasFled).toBe(true);
});
it("should not trigger when HP is already below half", async () => {
it("Does not trigger when HP already below half", async () => {
await game.classicMode.startBattle([Species.WIMPOD, Species.TYRUNT]);
const wimpod = game.scene.getPlayerPokemon()!;
wimpod.hp = 5;
@ -122,7 +107,7 @@ describe("Abilities - Wimp Out", () => {
confirmNoSwitch();
});
it("should bypass trapping moves and abilities", async () => {
it("Trapping moves do not prevent Wimp Out from activating.", async () => {
game.override.enemyMoveset([Moves.SPIRIT_SHACKLE]).startingLevel(53).enemyLevel(45);
await game.classicMode.startBattle([Species.WIMPOD, Species.TYRUNT]);
@ -137,7 +122,7 @@ describe("Abilities - Wimp Out", () => {
confirmSwitch();
});
it("should block switching from U-Turn on activation", async () => {
it("If this Ability activates due to being hit by U-turn or Volt Switch, the user of that move will not be switched out.", async () => {
game.override.startingLevel(95).enemyMoveset([Moves.U_TURN]);
await game.classicMode.startBattle([Species.WIMPOD, Species.TYRUNT]);
@ -151,7 +136,7 @@ describe("Abilities - Wimp Out", () => {
confirmSwitch();
});
it("should not block switching from U-Turn on failed activation", async () => {
it("If this Ability does not activate due to being hit by U-turn or Volt Switch, the user of that move will be switched out.", async () => {
game.override.startingLevel(190).startingWave(8).enemyMoveset([Moves.U_TURN]);
await game.classicMode.startBattle([Species.GOLISOPOD, Species.TYRUNT]);
const RIVAL_NINJASK1 = game.scene.getEnemyPokemon()?.id;
@ -160,7 +145,7 @@ describe("Abilities - Wimp Out", () => {
expect(game.scene.getEnemyPokemon()?.id !== RIVAL_NINJASK1);
});
it("Dragon Tail and Circle Throw switch out Pokémon before the Ability activates", async () => {
it("Dragon Tail and Circle Throw switch out Pokémon before the Ability activates.", async () => {
game.override.startingLevel(69).enemyMoveset([Moves.DRAGON_TAIL]);
await game.classicMode.startBattle([Species.WIMPOD, Species.TYRUNT]);
@ -177,7 +162,7 @@ describe("Abilities - Wimp Out", () => {
expect(game.scene.getPlayerPokemon()!.species.speciesId).not.toBe(Species.WIMPOD);
});
it("should trigger from recoil damage", async () => {
it("triggers when recoil damage is taken", async () => {
game.override.moveset([Moves.HEAD_SMASH]).enemyMoveset([Moves.SPLASH]);
await game.classicMode.startBattle([Species.WIMPOD, Species.TYRUNT]);
@ -188,7 +173,7 @@ describe("Abilities - Wimp Out", () => {
confirmSwitch();
});
it("should not activate when the Pokémon cuts its own HP", async () => {
it("It does not activate when the Pokémon cuts its own HP", async () => {
game.override.moveset([Moves.SUBSTITUTE]).enemyMoveset([Moves.SPLASH]);
await game.classicMode.startBattle([Species.WIMPOD, Species.TYRUNT]);
@ -201,19 +186,7 @@ describe("Abilities - Wimp Out", () => {
confirmNoSwitch();
});
it("should not trigger from Sheer Force-boosted moves", async () => {
game.override.enemyAbility(Abilities.SHEER_FORCE).enemyMoveset(Moves.SLUDGE_BOMB).startingLevel(95);
await game.classicMode.startBattle([Species.WIMPOD, Species.TYRUNT]);
game.scene.getPlayerPokemon()!.hp *= 0.51;
game.move.select(Moves.ENDURE);
await game.phaseInterceptor.to("TurnEndPhase");
confirmNoSwitch();
});
it("should not trigger while neutralized", async () => {
it("Does not trigger when neutralized", async () => {
game.override.enemyAbility(Abilities.NEUTRALIZING_GAS).startingLevel(5);
await game.classicMode.startBattle([Species.WIMPOD, Species.TYRUNT]);
@ -250,14 +223,8 @@ describe("Abilities - Wimp Out", () => {
},
);
// TODO: Condense into it.eaches
describe("Post Turn Damage Checks - ", () => {
beforeEach(() => {
game.override.enemyMoveset(Moves.SPLASH);
});
it("Wimp Out will activate due to weather damage", async () => {
game.override.weather(WeatherType.HAIL);
game.override.weather(WeatherType.HAIL).enemyMoveset([Moves.SPLASH]);
await game.classicMode.startBattle([Species.WIMPOD, Species.TYRUNT]);
game.scene.getPlayerPokemon()!.hp *= 0.51;
@ -269,12 +236,37 @@ describe("Abilities - Wimp Out", () => {
confirmSwitch();
});
it("Wimp Out will activate due to post turn status damage", async () => {
game.override.statusEffect(StatusEffect.POISON);
it("Does not trigger when enemy has sheer force", async () => {
game.override.enemyAbility(Abilities.SHEER_FORCE).enemyMoveset(Moves.SLUDGE_BOMB).startingLevel(95);
await game.classicMode.startBattle([Species.WIMPOD, Species.TYRUNT]);
game.scene.getPlayerPokemon()!.hp *= 0.51;
game.move.select(Moves.ENDURE);
await game.phaseInterceptor.to("TurnEndPhase");
confirmNoSwitch();
});
it("Wimp Out will activate due to post turn status damage", async () => {
game.override.statusEffect(StatusEffect.POISON).enemyMoveset([Moves.SPLASH]);
await game.classicMode.startBattle([Species.WIMPOD, Species.TYRUNT]);
game.scene.getPlayerPokemon()!.hp *= 0.51;
game.move.select(Moves.SPLASH);
game.doSelectPartyPokemon(1);
await game.toNextTurn();
confirmSwitch();
});
it("Wimp Out will activate due to bad dreams", async () => {
game.override.statusEffect(StatusEffect.SLEEP).enemyAbility(Abilities.BAD_DREAMS);
await game.classicMode.startBattle([Species.WIMPOD, Species.TYRUNT]);
game.scene.getPlayerPokemon()!.hp *= 0.52;
game.move.select(Moves.SPLASH);
game.doSelectPartyPokemon(1);
await game.toNextTurn();
@ -330,6 +322,45 @@ describe("Abilities - Wimp Out", () => {
confirmSwitch();
});
it("Magic Guard passive should not allow indirect damage to trigger Wimp Out", async () => {
game.scene.arena.addTag(ArenaTagType.STEALTH_ROCK, 1, Moves.STEALTH_ROCK, 0, ArenaTagSide.ENEMY);
game.scene.arena.addTag(ArenaTagType.SPIKES, 1, Moves.SPIKES, 0, ArenaTagSide.ENEMY);
game.override
.passiveAbility(Abilities.MAGIC_GUARD)
.enemyMoveset([Moves.LEECH_SEED])
.weather(WeatherType.HAIL)
.statusEffect(StatusEffect.POISON);
await game.classicMode.startBattle([Species.WIMPOD, Species.TYRUNT]);
game.scene.getPlayerPokemon()!.hp *= 0.51;
game.move.select(Moves.SPLASH);
await game.phaseInterceptor.to("TurnEndPhase");
expect(game.scene.getPlayerParty()[0].getHpRatio()).toEqual(0.51);
expect(game.phaseInterceptor.log).not.toContain("SwitchSummonPhase");
expect(game.scene.getPlayerPokemon()!.species.speciesId).toBe(Species.WIMPOD);
});
it("Wimp Out activating should not cancel a double battle", async () => {
game.override.battleStyle("double").enemyAbility(Abilities.WIMP_OUT).enemyMoveset([Moves.SPLASH]).enemyLevel(1);
await game.classicMode.startBattle([Species.WIMPOD, Species.TYRUNT]);
const enemyLeadPokemon = game.scene.getEnemyParty()[0];
const enemySecPokemon = game.scene.getEnemyParty()[1];
game.move.select(Moves.FALSE_SWIPE, 0, BattlerIndex.ENEMY);
game.move.select(Moves.SPLASH, 1);
await game.phaseInterceptor.to("BerryPhase");
const isVisibleLead = enemyLeadPokemon.visible;
const hasFledLead = enemyLeadPokemon.switchOutStatus;
const isVisibleSec = enemySecPokemon.visible;
const hasFledSec = enemySecPokemon.switchOutStatus;
expect(!isVisibleLead && hasFledLead && isVisibleSec && !hasFledSec).toBe(true);
expect(enemyLeadPokemon.hp).toBeLessThan(enemyLeadPokemon.getMaxHp());
expect(enemySecPokemon.hp).toEqual(enemySecPokemon.getMaxHp());
});
it("Wimp Out will activate due to aftermath", async () => {
game.override
.moveset([Moves.THUNDER_PUNCH])
@ -347,19 +378,6 @@ describe("Abilities - Wimp Out", () => {
confirmSwitch();
});
it("Wimp Out will activate due to bad dreams", async () => {
game.override.statusEffect(StatusEffect.SLEEP).enemyAbility(Abilities.BAD_DREAMS);
await game.classicMode.startBattle([Species.WIMPOD, Species.TYRUNT]);
game.scene.getPlayerPokemon()!.hp *= 0.52;
game.move.select(Moves.SPLASH);
game.doSelectPartyPokemon(1);
await game.toNextTurn();
confirmSwitch();
});
it("Activates due to entry hazards", async () => {
game.scene.arena.addTag(ArenaTagType.STEALTH_ROCK, 1, Moves.STEALTH_ROCK, 0, ArenaTagSide.ENEMY);
game.scene.arena.addTag(ArenaTagType.SPIKES, 1, Moves.SPIKES, 0, ArenaTagSide.ENEMY);
@ -381,27 +399,6 @@ describe("Abilities - Wimp Out", () => {
confirmSwitch();
});
});
it("should not trigger on Magic Guard-prevented damage", async () => {
game.scene.arena.addTag(ArenaTagType.STEALTH_ROCK, 1, Moves.STEALTH_ROCK, 0, ArenaTagSide.ENEMY);
game.scene.arena.addTag(ArenaTagType.SPIKES, 1, Moves.SPIKES, 0, ArenaTagSide.ENEMY);
game.override
.passiveAbility(Abilities.MAGIC_GUARD)
.enemyMoveset([Moves.LEECH_SEED])
.weather(WeatherType.HAIL)
.statusEffect(StatusEffect.POISON);
await game.classicMode.startBattle([Species.WIMPOD, Species.TYRUNT]);
game.scene.getPlayerPokemon()!.hp *= 0.51;
game.move.select(Moves.SPLASH);
await game.phaseInterceptor.to("TurnEndPhase");
expect(game.scene.getPlayerParty()[0].getHpRatio()).toEqual(0.51);
expect(game.phaseInterceptor.log).not.toContain("SwitchSummonPhase");
expect(game.scene.getPlayerPokemon()!.species.speciesId).toBe(Species.WIMPOD);
});
it("triggers status on the wimp out user before a new pokemon is switched in", async () => {
game.override.enemyMoveset(Moves.SLUDGE_BOMB).startingLevel(80);
@ -416,27 +413,7 @@ describe("Abilities - Wimp Out", () => {
confirmSwitch();
});
it("Wimp Out activating should not cancel a double battle", async () => {
game.override.battleStyle("double").enemyAbility(Abilities.WIMP_OUT).enemyMoveset([Moves.SPLASH]).enemyLevel(1);
await game.classicMode.startBattle([Species.WIMPOD, Species.TYRUNT]);
const enemyLeadPokemon = game.scene.getEnemyParty()[0];
const enemySecPokemon = game.scene.getEnemyParty()[1];
game.move.select(Moves.FALSE_SWIPE, 0, BattlerIndex.ENEMY);
game.move.select(Moves.SPLASH, 1);
await game.phaseInterceptor.to("BerryPhase");
const isVisibleLead = enemyLeadPokemon.visible;
const hasFledLead = enemyLeadPokemon.switchOutStatus;
const isVisibleSec = enemySecPokemon.visible;
const hasFledSec = enemySecPokemon.switchOutStatus;
expect(!isVisibleLead && hasFledLead && isVisibleSec && !hasFledSec).toBe(true);
expect(enemyLeadPokemon.hp).toBeLessThan(enemyLeadPokemon.getMaxHp());
expect(enemySecPokemon.hp).toEqual(enemySecPokemon.getMaxHp());
});
it("triggers after last hit of multi hit moves", async () => {
it("triggers after last hit of multi hit move", async () => {
game.override.enemyMoveset(Moves.BULLET_SEED).enemyAbility(Abilities.SKILL_LINK);
await game.classicMode.startBattle([Species.WIMPOD, Species.TYRUNT]);
@ -467,7 +444,6 @@ describe("Abilities - Wimp Out", () => {
expect(enemyPokemon.turnData.hitCount).toBe(2);
confirmSwitch();
});
it("triggers after last hit of Parental Bond", async () => {
game.override.enemyMoveset(Moves.TACKLE).enemyAbility(Abilities.PARENTAL_BOND);
await game.classicMode.startBattle([Species.WIMPOD, Species.TYRUNT]);