mirror of
https://github.com/pagefaultgames/pokerogue.git
synced 2025-08-19 13:59:27 +02:00
Add second and third failure sequences
This commit is contained in:
parent
6b34ea3c46
commit
a77e3c911f
@ -1,3 +1,5 @@
|
|||||||
|
// 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 { allMoves } from "#data/data-lists";
|
import { allMoves } from "#data/data-lists";
|
||||||
@ -45,29 +47,44 @@ export class FirstMoveCondition extends MoveCondition {
|
|||||||
return user.tempSummonData.waveTurnCount === 1;
|
return user.tempSummonData.waveTurnCount === 1;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// TODO: Update AI move selection logic to not require this method at all
|
||||||
|
// Currently, it is used to avoid having the AI select the move if its condition will fail
|
||||||
getUserBenefitScore(user: Pokemon, _target: Pokemon, _move: Move): number {
|
getUserBenefitScore(user: Pokemon, _target: Pokemon, _move: Move): number {
|
||||||
return this.apply(user, _target, _move) ? 10 : -20;
|
return this.apply(user, _target, _move) ? 10 : -20;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Condition that forces moves to fail against the final boss in classic and the major boss in endless
|
||||||
|
* @remarks
|
||||||
|
* ⚠️ Only works reliably for single-target moves as only one target is provided; should not be used for multi-target moves
|
||||||
|
* @see {@linkcode GameMode.isBattleClassicFinalBoss}
|
||||||
|
* @see {@linkcode GameMode.isEndlessMinorBoss}
|
||||||
|
*/
|
||||||
|
export const failAgainstFinalBossCondition = new MoveCondition((_user, target) => {
|
||||||
|
const gameMode = globalScene.gameMode;
|
||||||
|
const currentWave = globalScene.currentBattle.waveIndex;
|
||||||
|
return (
|
||||||
|
target.isEnemy() && (gameMode.isBattleClassicFinalBoss(currentWave) || gameMode.isEndlessMinorBoss(currentWave))
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Condition used by the move {@link https://bulbapedia.bulbagarden.net/wiki/Upper_Hand_(move) | Upper Hand}.
|
* Condition used by the move {@link https://bulbapedia.bulbagarden.net/wiki/Upper_Hand_(move) | Upper Hand}.
|
||||||
* Moves with this condition are only successful when the target has selected
|
* Moves with this condition are only successful when the target has selected
|
||||||
* a high-priority attack (after factoring in priority-boosting effects) and
|
* a high-priority attack (after factoring in priority-boosting effects) and
|
||||||
* hasn't moved yet this turn.
|
* hasn't moved yet this turn.
|
||||||
*/
|
*/
|
||||||
export class UpperHandCondition extends MoveCondition {
|
export const UpperHandCondition = new MoveCondition((_user, target) => {
|
||||||
public override readonly func: MoveConditionFunc = (_user, target) => {
|
const targetCommand = globalScene.currentBattle.turnCommands[target.getBattlerIndex()];
|
||||||
const targetCommand = globalScene.currentBattle.turnCommands[target.getBattlerIndex()];
|
return (
|
||||||
return (
|
targetCommand?.command === Command.FIGHT &&
|
||||||
targetCommand?.command === Command.FIGHT &&
|
!target.turnData.acted &&
|
||||||
!target.turnData.acted &&
|
!!targetCommand.move?.move &&
|
||||||
!!targetCommand.move?.move &&
|
allMoves[targetCommand.move.move].category !== MoveCategory.STATUS &&
|
||||||
allMoves[targetCommand.move.move].category !== MoveCategory.STATUS &&
|
allMoves[targetCommand.move.move].getPriority(target) > 0
|
||||||
allMoves[targetCommand.move.move].getPriority(target) > 0
|
);
|
||||||
);
|
});
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A restriction that prevents a move from being selected
|
* A restriction that prevents a move from being selected
|
||||||
|
@ -93,7 +93,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, FirstMoveCondition, GravityUseRestriction, MoveCondition, MoveRestriction, UpperHandCondition } from "#moves/move-condition";
|
import { ConsecutiveUseRestriction, failAgainstFinalBossCondition, FirstMoveCondition, GravityUseRestriction, 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}.
|
||||||
@ -122,6 +122,10 @@ export abstract class Move implements Localizable {
|
|||||||
* @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 )
|
||||||
|
*/
|
||||||
|
private conditionsSeq2: 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
|
||||||
@ -380,13 +384,15 @@ export abstract class Move implements Localizable {
|
|||||||
* Adds a condition to this move (in addition to any provided by its prior {@linkcode MoveAttr}s).
|
* Adds a condition to this move (in addition to any provided by its prior {@linkcode MoveAttr}s).
|
||||||
* The move will fail upon use if at least 1 of its conditions is not met.
|
* The move will fail upon use if at least 1 of its conditions is not met.
|
||||||
* @param condition - The {@linkcode MoveCondition} or {@linkcode MoveConditionFunc} to add to the conditions array.
|
* @param condition - The {@linkcode MoveCondition} or {@linkcode MoveConditionFunc} to add to the conditions array.
|
||||||
|
* @param checkSequence - The sequence number where the failure check occurs
|
||||||
* @returns `this` for method chaining
|
* @returns `this` for method chaining
|
||||||
*/
|
*/
|
||||||
condition(condition: MoveCondition | MoveConditionFunc): this {
|
condition(condition: MoveCondition | MoveConditionFunc, checkSequence: 2 | 3 = 3): this {
|
||||||
|
const conditionsArray = checkSequence === 2 ? this.conditionsSeq2 : this.conditions;
|
||||||
if (typeof condition === "function") {
|
if (typeof condition === "function") {
|
||||||
condition = new MoveCondition(condition);
|
condition = new MoveCondition(condition);
|
||||||
}
|
}
|
||||||
this.conditions.push(condition);
|
conditionsArray.push(condition);
|
||||||
|
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
@ -762,13 +768,15 @@ export abstract class Move implements Localizable {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Applies each {@linkcode MoveCondition} function of this move to the params, determines if the move can be used prior to calling each attribute's apply()
|
* Applies each {@linkcode MoveCondition} function of this move to the params, determines if the move can be used prior to calling each attribute's apply()
|
||||||
* @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
|
||||||
* @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): boolean {
|
applyConditions(user: Pokemon, target: Pokemon, sequence: number = 3): boolean {
|
||||||
return this.conditions.every(cond => cond.apply(user, target, this));
|
const conditionsArray = sequence === 2 ? this.conditionsSeq2 : this.conditions;
|
||||||
|
return conditionsArray.every(cond => cond.apply(user, target, this));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@ -9105,6 +9113,7 @@ export function initMoves() {
|
|||||||
new SelfStatusMove(MoveId.DESTINY_BOND, PokemonType.GHOST, -1, 5, -1, 0, 2)
|
new SelfStatusMove(MoveId.DESTINY_BOND, PokemonType.GHOST, -1, 5, -1, 0, 2)
|
||||||
.ignoresProtect()
|
.ignoresProtect()
|
||||||
.attr(DestinyBondAttr)
|
.attr(DestinyBondAttr)
|
||||||
|
.condition(failAgainstFinalBossCondition, 2)
|
||||||
.condition((user, target, move) => {
|
.condition((user, target, move) => {
|
||||||
// Retrieves user's previous move, returns empty array if no moves have been used
|
// Retrieves user's previous move, returns empty array if no moves have been used
|
||||||
const lastTurnMove = user.getLastXMoves(1);
|
const lastTurnMove = user.getLastXMoves(1);
|
||||||
@ -9389,6 +9398,8 @@ export function initMoves() {
|
|||||||
.unimplemented(),
|
.unimplemented(),
|
||||||
new StatusMove(MoveId.ROLE_PLAY, PokemonType.PSYCHIC, -1, 10, -1, 0, 3)
|
new StatusMove(MoveId.ROLE_PLAY, PokemonType.PSYCHIC, -1, 10, -1, 0, 3)
|
||||||
.ignoresSubstitute()
|
.ignoresSubstitute()
|
||||||
|
// TODO: Enable / remove once balance reaches a consensus on ability overrides during boss fights
|
||||||
|
// .condition(failAgainstFinalBossCondition, 3)
|
||||||
.attr(AbilityCopyAttr),
|
.attr(AbilityCopyAttr),
|
||||||
new SelfStatusMove(MoveId.WISH, PokemonType.NORMAL, -1, 10, -1, 0, 3)
|
new SelfStatusMove(MoveId.WISH, PokemonType.NORMAL, -1, 10, -1, 0, 3)
|
||||||
.attr(WishAttr)
|
.attr(WishAttr)
|
||||||
@ -9436,6 +9447,8 @@ export function initMoves() {
|
|||||||
new StatusMove(MoveId.IMPRISON, PokemonType.PSYCHIC, 100, 10, -1, 0, 3)
|
new StatusMove(MoveId.IMPRISON, PokemonType.PSYCHIC, 100, 10, -1, 0, 3)
|
||||||
.ignoresSubstitute()
|
.ignoresSubstitute()
|
||||||
.attr(AddArenaTagAttr, ArenaTagType.IMPRISON, 1, true, false)
|
.attr(AddArenaTagAttr, ArenaTagType.IMPRISON, 1, true, false)
|
||||||
|
// TODO: Enable / remove once balance reaches a consensus on imprison interaction during the final boss fight
|
||||||
|
// .condition(failAgainstFinalBossCondition, 2)
|
||||||
.target(MoveTarget.ENEMY_SIDE),
|
.target(MoveTarget.ENEMY_SIDE),
|
||||||
new SelfStatusMove(MoveId.REFRESH, PokemonType.NORMAL, -1, 20, -1, 0, 3)
|
new SelfStatusMove(MoveId.REFRESH, PokemonType.NORMAL, -1, 20, -1, 0, 3)
|
||||||
.attr(HealStatusEffectAttr, true, [ StatusEffect.PARALYSIS, StatusEffect.POISON, StatusEffect.TOXIC, StatusEffect.BURN ])
|
.attr(HealStatusEffectAttr, true, [ StatusEffect.PARALYSIS, StatusEffect.POISON, StatusEffect.TOXIC, StatusEffect.BURN ])
|
||||||
@ -9767,6 +9780,8 @@ export function initMoves() {
|
|||||||
.edgeCase(), // May or may not need to ignore remotely called moves depending on how it works
|
.edgeCase(), // May or may not need to ignore remotely called moves depending on how it works
|
||||||
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
|
||||||
|
// .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((user, target, move) => {
|
||||||
@ -9999,9 +10014,13 @@ export function initMoves() {
|
|||||||
.attr(AddArenaTagAttr, ArenaTagType.WIDE_GUARD, 1, true, true)
|
.attr(AddArenaTagAttr, ArenaTagType.WIDE_GUARD, 1, true, true)
|
||||||
.condition(failIfLastCondition),
|
.condition(failIfLastCondition),
|
||||||
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
|
||||||
|
// .condition(failAgainstFinalBossCondition, 2)
|
||||||
.attr(AverageStatsAttr, [ Stat.DEF, Stat.SPDEF ], "moveTriggers:sharedGuard"),
|
.attr(AverageStatsAttr, [ Stat.DEF, Stat.SPDEF ], "moveTriggers:sharedGuard"),
|
||||||
new StatusMove(MoveId.POWER_SPLIT, PokemonType.PSYCHIC, -1, 10, -1, 0, 5)
|
new StatusMove(MoveId.POWER_SPLIT, PokemonType.PSYCHIC, -1, 10, -1, 0, 5)
|
||||||
.attr(AverageStatsAttr, [ Stat.ATK, Stat.SPATK ], "moveTriggers:sharedPower"),
|
.attr(AverageStatsAttr, [ Stat.ATK, Stat.SPATK ], "moveTriggers:sharedPower"),
|
||||||
|
// TODO: Enable / remove once balance reaches a consensus on imprison interaction during the final boss fight
|
||||||
|
// .condition(failAgainstFinalBossCondition, 2)
|
||||||
new StatusMove(MoveId.WONDER_ROOM, PokemonType.PSYCHIC, -1, 10, -1, 0, 5)
|
new StatusMove(MoveId.WONDER_ROOM, PokemonType.PSYCHIC, -1, 10, -1, 0, 5)
|
||||||
.ignoresProtect()
|
.ignoresProtect()
|
||||||
.target(MoveTarget.BOTH_SIDES)
|
.target(MoveTarget.BOTH_SIDES)
|
||||||
@ -10071,9 +10090,13 @@ export function initMoves() {
|
|||||||
.attr(TargetAtkUserAtkAttr),
|
.attr(TargetAtkUserAtkAttr),
|
||||||
new StatusMove(MoveId.SIMPLE_BEAM, PokemonType.NORMAL, 100, 15, -1, 0, 5)
|
new StatusMove(MoveId.SIMPLE_BEAM, PokemonType.NORMAL, 100, 15, -1, 0, 5)
|
||||||
.attr(AbilityChangeAttr, AbilityId.SIMPLE)
|
.attr(AbilityChangeAttr, AbilityId.SIMPLE)
|
||||||
|
// TODO: Enable / remove once balance reaches a consensus on ability overrides during boss fights
|
||||||
|
// .condition(failAgainstFinalBossCondition, 3)
|
||||||
.reflectable(),
|
.reflectable(),
|
||||||
new StatusMove(MoveId.ENTRAINMENT, PokemonType.NORMAL, 100, 15, -1, 0, 5)
|
new StatusMove(MoveId.ENTRAINMENT, PokemonType.NORMAL, 100, 15, -1, 0, 5)
|
||||||
.attr(AbilityGiveAttr)
|
.attr(AbilityGiveAttr)
|
||||||
|
// TODO: Enable / remove once balance reaches a consensus on ability overrides during boss fights
|
||||||
|
// .condition(failAgainstFinalBossCondition, 3)
|
||||||
.reflectable(),
|
.reflectable(),
|
||||||
new StatusMove(MoveId.AFTER_YOU, PokemonType.NORMAL, -1, 15, -1, 0, 5)
|
new StatusMove(MoveId.AFTER_YOU, PokemonType.NORMAL, -1, 15, -1, 0, 5)
|
||||||
.ignoresProtect()
|
.ignoresProtect()
|
||||||
@ -10659,7 +10682,12 @@ export function initMoves() {
|
|||||||
new AttackMove(MoveId.POLLEN_PUFF, PokemonType.BUG, MoveCategory.SPECIAL, 90, 100, 15, -1, 0, 7)
|
new AttackMove(MoveId.POLLEN_PUFF, PokemonType.BUG, MoveCategory.SPECIAL, 90, 100, 15, -1, 0, 7)
|
||||||
.attr(StatusCategoryOnAllyAttr)
|
.attr(StatusCategoryOnAllyAttr)
|
||||||
.attr(HealOnAllyAttr, 0.5, true, false)
|
.attr(HealOnAllyAttr, 0.5, true, false)
|
||||||
.ballBombMove(),
|
.ballBombMove()
|
||||||
|
// Fail if used against an ally that is affected by heal block, during the second failure check
|
||||||
|
.condition(
|
||||||
|
(user, target) => target.getAlly() === user && !!target.getTag(BattlerTagType.HEAL_BLOCK),
|
||||||
|
2
|
||||||
|
),
|
||||||
new AttackMove(MoveId.ANCHOR_SHOT, PokemonType.STEEL, MoveCategory.PHYSICAL, 80, 100, 20, 100, 0, 7)
|
new AttackMove(MoveId.ANCHOR_SHOT, PokemonType.STEEL, MoveCategory.PHYSICAL, 80, 100, 20, 100, 0, 7)
|
||||||
.attr(AddBattlerTagAttr, BattlerTagType.TRAPPED, false, false, 1, 1, true),
|
.attr(AddBattlerTagAttr, BattlerTagType.TRAPPED, false, false, 1, 1, true),
|
||||||
new StatusMove(MoveId.PSYCHIC_TERRAIN, PokemonType.PSYCHIC, -1, 10, -1, 0, 7)
|
new StatusMove(MoveId.PSYCHIC_TERRAIN, PokemonType.PSYCHIC, -1, 10, -1, 0, 7)
|
||||||
@ -10672,16 +10700,21 @@ export function initMoves() {
|
|||||||
new AttackMove(MoveId.POWER_TRIP, PokemonType.DARK, MoveCategory.PHYSICAL, 20, 100, 10, -1, 0, 7)
|
new AttackMove(MoveId.POWER_TRIP, PokemonType.DARK, MoveCategory.PHYSICAL, 20, 100, 10, -1, 0, 7)
|
||||||
.attr(PositiveStatStagePowerAttr),
|
.attr(PositiveStatStagePowerAttr),
|
||||||
new AttackMove(MoveId.BURN_UP, PokemonType.FIRE, MoveCategory.SPECIAL, 130, 100, 5, -1, 0, 7)
|
new AttackMove(MoveId.BURN_UP, PokemonType.FIRE, MoveCategory.SPECIAL, 130, 100, 5, -1, 0, 7)
|
||||||
.condition((user) => {
|
.condition(
|
||||||
const userTypes = user.getTypes(true);
|
// Pass `true` to `ForDefend` as it should fail if the user is terastallized to a type that is not FIRE
|
||||||
return userTypes.includes(PokemonType.FIRE);
|
user => user.isOfType(PokemonType.FIRE, true, true),
|
||||||
})
|
2
|
||||||
|
)
|
||||||
.attr(HealStatusEffectAttr, true, StatusEffect.FREEZE)
|
.attr(HealStatusEffectAttr, true, StatusEffect.FREEZE)
|
||||||
.attr(AddBattlerTagAttr, BattlerTagType.BURNED_UP, true, false)
|
.attr(AddBattlerTagAttr, BattlerTagType.BURNED_UP, true, false)
|
||||||
.attr(RemoveTypeAttr, PokemonType.FIRE, (user) => {
|
.attr(RemoveTypeAttr, PokemonType.FIRE, (user) => {
|
||||||
globalScene.phaseManager.queueMessage(i18next.t("moveTriggers:burnedItselfOut", { pokemonName: getPokemonNameWithAffix(user) }));
|
globalScene.phaseManager.queueMessage(i18next.t("moveTriggers:burnedItselfOut", { pokemonName: getPokemonNameWithAffix(user) }));
|
||||||
}),
|
}),
|
||||||
new StatusMove(MoveId.SPEED_SWAP, PokemonType.PSYCHIC, -1, 10, -1, 0, 7)
|
new StatusMove(MoveId.SPEED_SWAP, PokemonType.PSYCHIC, -1, 10, -1, 0, 7)
|
||||||
|
// Note: the 3 is NOT a typo; unlike power split / guard split which happen in the second failure sequence, speed
|
||||||
|
// swap's check happens in the third
|
||||||
|
// TODO: Enable / remove once balance reaches a consensus on imprison interaction during the final boss fight
|
||||||
|
// .condition(failAgainstFinalBossCondition, 3)
|
||||||
.attr(SwapStatAttr, Stat.SPD)
|
.attr(SwapStatAttr, Stat.SPD)
|
||||||
.ignoresSubstitute(),
|
.ignoresSubstitute(),
|
||||||
new AttackMove(MoveId.SMART_STRIKE, PokemonType.STEEL, MoveCategory.PHYSICAL, 70, -1, 10, -1, 0, 7),
|
new AttackMove(MoveId.SMART_STRIKE, PokemonType.STEEL, MoveCategory.PHYSICAL, 70, -1, 10, -1, 0, 7),
|
||||||
@ -10908,8 +10941,12 @@ export function initMoves() {
|
|||||||
true),
|
true),
|
||||||
new SelfStatusMove(MoveId.NO_RETREAT, PokemonType.FIGHTING, -1, 5, -1, 0, 8)
|
new SelfStatusMove(MoveId.NO_RETREAT, PokemonType.FIGHTING, -1, 5, -1, 0, 8)
|
||||||
.attr(StatStageChangeAttr, [ Stat.ATK, Stat.DEF, Stat.SPATK, Stat.SPDEF, Stat.SPD ], 1, true)
|
.attr(StatStageChangeAttr, [ Stat.ATK, Stat.DEF, Stat.SPATK, Stat.SPDEF, Stat.SPD ], 1, true)
|
||||||
.attr(AddBattlerTagAttr, BattlerTagType.NO_RETREAT, true, false)
|
.attr(AddBattlerTagAttr, BattlerTagType.NO_RETREAT, true, true /* NOT ADDED if already trapped */)
|
||||||
.condition((user, target, move) => user.getTag(TrappedTag)?.tagType !== BattlerTagType.NO_RETREAT), // fails if the user is currently trapped by No Retreat
|
.condition(
|
||||||
|
// fails if the user is currently trapped specifically from no retreat
|
||||||
|
user => user.getTag(TrappedTag)?.tagType !== BattlerTagType.NO_RETREAT,
|
||||||
|
2
|
||||||
|
),
|
||||||
new StatusMove(MoveId.TAR_SHOT, PokemonType.ROCK, 100, 15, -1, 0, 8)
|
new StatusMove(MoveId.TAR_SHOT, PokemonType.ROCK, 100, 15, -1, 0, 8)
|
||||||
.attr(StatStageChangeAttr, [ Stat.SPD ], -1)
|
.attr(StatStageChangeAttr, [ Stat.SPD ], -1)
|
||||||
.attr(AddBattlerTagAttr, BattlerTagType.TAR_SHOT, false)
|
.attr(AddBattlerTagAttr, BattlerTagType.TAR_SHOT, false)
|
||||||
@ -11472,10 +11509,11 @@ export function initMoves() {
|
|||||||
.slicingMove()
|
.slicingMove()
|
||||||
.triageMove(),
|
.triageMove(),
|
||||||
new AttackMove(MoveId.DOUBLE_SHOCK, PokemonType.ELECTRIC, MoveCategory.PHYSICAL, 120, 100, 5, -1, 0, 9)
|
new AttackMove(MoveId.DOUBLE_SHOCK, PokemonType.ELECTRIC, MoveCategory.PHYSICAL, 120, 100, 5, -1, 0, 9)
|
||||||
.condition((user) => {
|
.condition(
|
||||||
const userTypes = user.getTypes(true);
|
// Pass `true` to `isOfType` to fail if the user is terastallized to a type other than ELECTRIC
|
||||||
return userTypes.includes(PokemonType.ELECTRIC);
|
user => user.isOfType(PokemonType.ELECTRIC, true, true),
|
||||||
})
|
2
|
||||||
|
)
|
||||||
.attr(AddBattlerTagAttr, BattlerTagType.DOUBLE_SHOCKED, true, false)
|
.attr(AddBattlerTagAttr, BattlerTagType.DOUBLE_SHOCKED, true, false)
|
||||||
.attr(RemoveTypeAttr, PokemonType.ELECTRIC, (user) => {
|
.attr(RemoveTypeAttr, PokemonType.ELECTRIC, (user) => {
|
||||||
globalScene.phaseManager.queueMessage(i18next.t("moveTriggers:usedUpAllElectricity", { pokemonName: getPokemonNameWithAffix(user) }));
|
globalScene.phaseManager.queueMessage(i18next.t("moveTriggers:usedUpAllElectricity", { pokemonName: getPokemonNameWithAffix(user) }));
|
||||||
@ -11573,7 +11611,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(new UpperHandCondition()),
|
.condition(UpperHandCondition),
|
||||||
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)
|
||||||
);
|
);
|
||||||
|
@ -2375,7 +2375,7 @@ export abstract class Pokemon extends Phaser.GameObjects.Container {
|
|||||||
* @param source {@linkcode Pokemon} The attacking Pokémon.
|
* @param source {@linkcode Pokemon} The attacking Pokémon.
|
||||||
* @param move {@linkcode Move} The move being used by the attacking Pokémon.
|
* @param move {@linkcode Move} The move being used by the attacking Pokémon.
|
||||||
* @param ignoreAbility Whether to ignore abilities that might affect type effectiveness or immunity (defaults to `false`).
|
* @param ignoreAbility Whether to ignore abilities that might affect type effectiveness or immunity (defaults to `false`).
|
||||||
* @param simulated Whether to apply abilities via simulated calls (defaults to `true`)
|
* @param simulated - Whether to apply abilities via simulated calls (defaults to `true`). This should only be false during the move effect phase
|
||||||
* @param cancelled {@linkcode BooleanHolder} Stores whether the move was cancelled by a non-type-based immunity.
|
* @param cancelled {@linkcode BooleanHolder} Stores whether the move was cancelled by a non-type-based immunity.
|
||||||
* @param useIllusion - Whether we want the attack move effectiveness on the illusion or not
|
* @param useIllusion - Whether we want the attack move effectiveness on the illusion or not
|
||||||
* @returns The type damage multiplier, indicating the effectiveness of the move
|
* @returns The type damage multiplier, indicating the effectiveness of the move
|
||||||
@ -2429,7 +2429,9 @@ export abstract class Pokemon extends Phaser.GameObjects.Container {
|
|||||||
applyAbAttrs("MoveImmunityAbAttr", commonAbAttrParams);
|
applyAbAttrs("MoveImmunityAbAttr", commonAbAttrParams);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!cancelledHolder.value) {
|
// Do not check queenly majesty unless this is being simulated
|
||||||
|
// This is because the move effect phase should not check queenly majesty, as that is handled by the move phase
|
||||||
|
if (simulated && !cancelledHolder.value) {
|
||||||
const defendingSidePlayField = this.isPlayer() ? globalScene.getPlayerField() : globalScene.getEnemyField();
|
const defendingSidePlayField = this.isPlayer() ? globalScene.getPlayerField() : globalScene.getEnemyField();
|
||||||
defendingSidePlayField.forEach(p =>
|
defendingSidePlayField.forEach(p =>
|
||||||
applyAbAttrs("FieldPriorityMoveImmunityAbAttr", {
|
applyAbAttrs("FieldPriorityMoveImmunityAbAttr", {
|
||||||
|
@ -257,6 +257,7 @@ export class CommandPhase extends FieldPhase {
|
|||||||
: cursor > -1 && !playerPokemon.getMoveset().some(m => m.isUsable(playerPokemon)[0]);
|
: cursor > -1 && !playerPokemon.getMoveset().some(m => m.isUsable(playerPokemon)[0]);
|
||||||
|
|
||||||
if (!canUse && !useStruggle) {
|
if (!canUse && !useStruggle) {
|
||||||
|
console.error("Cannot use move:", reason);
|
||||||
this.queueFightErrorMessage(reason);
|
this.queueFightErrorMessage(reason);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
@ -2,8 +2,6 @@ import { applyAbAttrs } from "#abilities/apply-ab-attrs";
|
|||||||
import { globalScene } from "#app/global-scene";
|
import { globalScene } from "#app/global-scene";
|
||||||
import { getPokemonNameWithAffix } from "#app/messages";
|
import { getPokemonNameWithAffix } from "#app/messages";
|
||||||
import Overrides from "#app/overrides";
|
import Overrides from "#app/overrides";
|
||||||
// biome-ignore lint/correctness/noUnusedImports: Used in a tsdoc comment
|
|
||||||
import type { TauntTag, TruantTag } from "#data/battler-tags";
|
|
||||||
import { CenterOfAttentionTag } from "#data/battler-tags";
|
import { CenterOfAttentionTag } from "#data/battler-tags";
|
||||||
import { SpeciesFormChangePreMoveTrigger } from "#data/form-change-triggers";
|
import { SpeciesFormChangePreMoveTrigger } from "#data/form-change-triggers";
|
||||||
import { getStatusEffectActivationText, getStatusEffectHealText } from "#data/status-effect";
|
import { getStatusEffectActivationText, getStatusEffectHealText } from "#data/status-effect";
|
||||||
@ -113,7 +111,9 @@ export class MovePhase extends BattlePhase {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Check the first round of failure checks.
|
* Check the first round of failure checks
|
||||||
|
*
|
||||||
|
* @returns Whether the move failed
|
||||||
*
|
*
|
||||||
* @remarks
|
* @remarks
|
||||||
* Based on battle mechanics research conducted primarily by Smogon, checks happen in the following order (as of Gen 9):
|
* Based on battle mechanics research conducted primarily by Smogon, checks happen in the following order (as of Gen 9):
|
||||||
@ -147,6 +147,8 @@ export class MovePhase extends BattlePhase {
|
|||||||
this.checkPreUseInterrupt() ||
|
this.checkPreUseInterrupt() ||
|
||||||
this.checkTagCancel(BattlerTagType.FLINCHED) ||
|
this.checkTagCancel(BattlerTagType.FLINCHED) ||
|
||||||
this.checkTagCancel(BattlerTagType.DISABLED, true) ||
|
this.checkTagCancel(BattlerTagType.DISABLED, true) ||
|
||||||
|
this.checkTagCancel(BattlerTagType.HEAL_BLOCK) ||
|
||||||
|
this.checkTagCancel(BattlerTagType.THROAT_CHOPPED) ||
|
||||||
this.checkGravity() ||
|
this.checkGravity() ||
|
||||||
this.checkTagCancel(BattlerTagType.TAUNT, true) ||
|
this.checkTagCancel(BattlerTagType.TAUNT, true) ||
|
||||||
this.checkTagCancel(BattlerTagType.IMPRISON) ||
|
this.checkTagCancel(BattlerTagType.IMPRISON) ||
|
||||||
@ -160,6 +162,115 @@ export class MovePhase extends BattlePhase {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handle the status interactions for sleep and freeze that happen after passing the first failure check
|
||||||
|
*
|
||||||
|
* @remarks
|
||||||
|
* - If the user is asleep but can use the move, the sleep animation and message is still shown
|
||||||
|
* - If the user is frozen but is thawed from its move, the user's status is cured and the thaw message is shown
|
||||||
|
*/
|
||||||
|
private post1stFailSleepOrThaw(): void {
|
||||||
|
const user = this.pokemon;
|
||||||
|
// If the move was successful, then... play the "sleeping" animation if the user is asleep but uses something like rest / snore
|
||||||
|
// Cure the user's freeze and queue the thaw message from unfreezing due to move use
|
||||||
|
if (!isIgnoreStatus(this.useMode)) {
|
||||||
|
if (user.status?.effect === StatusEffect.SLEEP) {
|
||||||
|
// Commence the sleeping animation and message, which happens anyway
|
||||||
|
// TODO...
|
||||||
|
} else if (this.thaw) {
|
||||||
|
this.cureStatus(
|
||||||
|
StatusEffect.FREEZE,
|
||||||
|
i18next.t("statusEffect:freeze.healByMove", {
|
||||||
|
pokemonName: getPokemonNameWithAffix(user),
|
||||||
|
moveName: this.move.getMove().name,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Second failure check that occurs after the "Pokemon used move" text is shown but BEFORE the move has been registered
|
||||||
|
* as being the last move used (for the purposes of something like Copycat)
|
||||||
|
*
|
||||||
|
* @remarks
|
||||||
|
* Other than powder, each failure condition is mutually exclusive (as they are tied to specific moves), so order does not matter.
|
||||||
|
* Notably, this failure check only includes failure conditions intrinsic to the move itself, ther than Powder (which marks the end of this failure check)
|
||||||
|
*
|
||||||
|
*
|
||||||
|
* - 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
|
||||||
|
* - No Retreat while already under its effects
|
||||||
|
* - (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
|
||||||
|
*
|
||||||
|
* After all checks, Powder causing the user to explode
|
||||||
|
*/
|
||||||
|
protected secondFailureCheck(): boolean {
|
||||||
|
const move = this.move.getMove();
|
||||||
|
const user = this.pokemon;
|
||||||
|
if (!move.applyConditions(user, this.getActiveTargetPokemon()[0], 2)) {
|
||||||
|
this.failed = true;
|
||||||
|
// Note: If any of the moves have custom failure messages, this needs to be changed
|
||||||
|
// As of Gen 9, none do. (Except maybe pollen puff? Need to check)
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
// 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;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Third failure check is from moves and abilities themselves
|
||||||
|
*
|
||||||
|
* @returns Whether the move failed
|
||||||
|
*
|
||||||
|
* @remarks
|
||||||
|
* - Conditional attributes of the move
|
||||||
|
* - Weather blocking the move
|
||||||
|
* - Terrain blocking the move
|
||||||
|
* - Queenly Majesty / Dazzling
|
||||||
|
*/
|
||||||
|
protected thirdFailureCheck(): boolean {
|
||||||
|
/**
|
||||||
|
* Move conditions assume the move has a single target
|
||||||
|
* TODO: is this sustainable?
|
||||||
|
*/
|
||||||
|
const move = this.move.getMove();
|
||||||
|
const targets = this.getActiveTargetPokemon();
|
||||||
|
const arena = globalScene.arena;
|
||||||
|
const user = this.pokemon;
|
||||||
|
const failsConditions = !move.applyConditions(user, targets[0]);
|
||||||
|
const failedDueToWeather = arena.isMoveWeatherCancelled(user, move);
|
||||||
|
const failedDueToTerrain = arena.isMoveTerrainCancelled(user, this.targets, move);
|
||||||
|
let failed = failsConditions || failedDueToWeather || failedDueToTerrain;
|
||||||
|
|
||||||
|
// Apply queenly majesty / dazzling
|
||||||
|
if (!failed) {
|
||||||
|
const defendingSidePlayField = user.isPlayer() ? globalScene.getPlayerField() : globalScene.getEnemyField();
|
||||||
|
const cancelled = new BooleanHolder(false);
|
||||||
|
defendingSidePlayField.forEach((pokemon: Pokemon) => {
|
||||||
|
applyAbAttrs("FieldPriorityMoveImmunityAbAttr", {
|
||||||
|
pokemon,
|
||||||
|
opponent: user,
|
||||||
|
move,
|
||||||
|
cancelled,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
failed = cancelled.value;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (failed) {
|
||||||
|
this.failMove(true, failedDueToWeather, failedDueToTerrain);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
public start(): void {
|
public start(): void {
|
||||||
super.start();
|
super.start();
|
||||||
|
|
||||||
@ -168,25 +279,52 @@ export class MovePhase extends BattlePhase {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const user = this.pokemon;
|
||||||
|
|
||||||
// Removing gigaton hammer always happens first
|
// Removing gigaton hammer always happens first
|
||||||
this.pokemon.removeTag(BattlerTagType.ALWAYS_GET_HIT);
|
user.removeTag(BattlerTagType.ALWAYS_GET_HIT);
|
||||||
console.log(MoveId[this.move.moveId], enumValueToKey(MoveUseMode, this.useMode));
|
console.log(MoveId[this.move.moveId], enumValueToKey(MoveUseMode, this.useMode));
|
||||||
|
|
||||||
// For the purposes of payback and kin, the pokemon is considered to have acted
|
// For the purposes of payback and kin, the pokemon is considered to have acted
|
||||||
// if it attempted to move at all.
|
// if it attempted to move at all.
|
||||||
this.pokemon.turnData.acted = true;
|
user.turnData.acted = true;
|
||||||
// TODO: skip this check for moves like metronome.
|
const useMode = this.useMode;
|
||||||
if (this.firstFailureCheck()) {
|
const virtual = isVirtual(useMode);
|
||||||
|
if (!virtual && this.firstFailureCheck()) {
|
||||||
// Lapse all other pre-move tags
|
// Lapse all other pre-move tags
|
||||||
this.pokemon.lapseTags(BattlerTagLapseType.PRE_MOVE);
|
user.lapseTags(BattlerTagLapseType.PRE_MOVE);
|
||||||
this.end();
|
this.end();
|
||||||
|
|
||||||
|
/*
|
||||||
|
On cartridge, certain things *react* to move failures, depending on failure reason
|
||||||
|
The following would happen at this time on cartridge:
|
||||||
|
- Steadfast giving user speed boost if failed due to flinch
|
||||||
|
- Protect, detect, ally switch, etc, resetting consecutive use count
|
||||||
|
- Rollout / ice ball "unlocking"
|
||||||
|
- protect / ally switch / other moves resetting their consecutive use count
|
||||||
|
- and others
|
||||||
|
In Pokerogue, these are instead handled by their respective methods, which generally
|
||||||
|
*/
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Begin second failure checks..
|
// 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.
|
||||||
|
// For now, this is 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
|
||||||
|
// 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.
|
||||||
|
// The sleep message and animation are also played if the user is asleep but using a move anyway (snore, sleep talk, etc)
|
||||||
|
this.post1stFailSleepOrThaw();
|
||||||
|
|
||||||
// Reset hit-related turn data when starting follow-up moves (e.g. Metronomed moves, Dancer repeats)
|
// Reset hit-related turn data when starting follow-up moves (e.g. Metronomed moves, Dancer repeats)
|
||||||
if (isVirtual(this.useMode)) {
|
if (virtual) {
|
||||||
this.pokemon.turnData.hitsLeft = -1;
|
this.pokemon.turnData.hitsLeft = -1;
|
||||||
this.pokemon.turnData.hitCount = 0;
|
this.pokemon.turnData.hitCount = 0;
|
||||||
}
|
}
|
||||||
@ -202,10 +340,30 @@ export class MovePhase extends BattlePhase {
|
|||||||
globalScene.arena.setIgnoreAbilities(true, this.pokemon.getBattlerIndex());
|
globalScene.arena.setIgnoreAbilities(true, this.pokemon.getBattlerIndex());
|
||||||
}
|
}
|
||||||
|
|
||||||
this.resolveRedirectTarget();
|
// At this point, move's type changing and multi-target effects *should* be applied
|
||||||
|
// Pokerogue's current implementation applies these effects during the move effect phase
|
||||||
|
// as there is not (yet) a notion of a move-in-flight for determinations to occur
|
||||||
|
|
||||||
|
this.resolveRedirectTarget();
|
||||||
this.resolveCounterAttackTarget();
|
this.resolveCounterAttackTarget();
|
||||||
|
|
||||||
|
// Move is announced
|
||||||
|
this.showMoveText();
|
||||||
|
|
||||||
|
// Stance change happens
|
||||||
|
const charging = this.move.getMove().isChargingMove() && !this.pokemon.getTag(BattlerTagType.CHARGING);
|
||||||
|
// Stance change happens now if the move is about to be executed
|
||||||
|
if (!charging) {
|
||||||
|
globalScene.triggerPokemonFormChange(this.pokemon, SpeciesFormChangePreMoveTrigger);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.secondFailureCheck()) {
|
||||||
|
this.showFailedText();
|
||||||
|
this.handlePreMoveFailures();
|
||||||
|
this.end();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (!(this.failed || this.cancelled)) {
|
if (!(this.failed || this.cancelled)) {
|
||||||
this.resolveFinalPreMoveCancellationChecks();
|
this.resolveFinalPreMoveCancellationChecks();
|
||||||
}
|
}
|
||||||
@ -213,7 +371,7 @@ export class MovePhase extends BattlePhase {
|
|||||||
// Cancel, charge or use the move as applicable.
|
// Cancel, charge or use the move as applicable.
|
||||||
if (this.cancelled || this.failed) {
|
if (this.cancelled || this.failed) {
|
||||||
this.handlePreMoveFailures();
|
this.handlePreMoveFailures();
|
||||||
} else if (this.move.getMove().isChargingMove() && !this.pokemon.getTag(BattlerTagType.CHARGING)) {
|
} else if (charging) {
|
||||||
this.chargeMove();
|
this.chargeMove();
|
||||||
} else {
|
} else {
|
||||||
this.useMove();
|
this.useMove();
|
||||||
@ -222,7 +380,7 @@ export class MovePhase extends BattlePhase {
|
|||||||
this.end();
|
this.end();
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Check for cancellation edge cases - no targets remaining, or {@linkcode MoveId.NONE} is in the queue */
|
/** Check for cancellation edge cases - no targets remaining */
|
||||||
protected resolveFinalPreMoveCancellationChecks(): void {
|
protected resolveFinalPreMoveCancellationChecks(): void {
|
||||||
const targets = this.getActiveTargetPokemon();
|
const targets = this.getActiveTargetPokemon();
|
||||||
const moveQueue = this.pokemon.getMoveQueue();
|
const moveQueue = this.pokemon.getMoveQueue();
|
||||||
@ -231,7 +389,6 @@ export class MovePhase extends BattlePhase {
|
|||||||
(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.showMoveText();
|
|
||||||
this.showFailedText();
|
this.showFailedText();
|
||||||
this.cancel();
|
this.cancel();
|
||||||
} else {
|
} else {
|
||||||
@ -246,10 +403,12 @@ export class MovePhase extends BattlePhase {
|
|||||||
/**
|
/**
|
||||||
* Queue the status cure message, reset the status, and update the Pokemon info display
|
* Queue the status cure message, reset the status, and update the Pokemon info display
|
||||||
* @param effect - The effect being cured
|
* @param effect - The effect being cured
|
||||||
|
* @param msg - A custom message to display when curing the status effect (used for curing freeze due to move use)
|
||||||
*/
|
*/
|
||||||
private cureStatus(effect: StatusEffect): void {
|
private cureStatus(effect: StatusEffect, msg?: string): void {
|
||||||
const pokemon = this.pokemon;
|
const pokemon = this.pokemon;
|
||||||
globalScene.phaseManager.queueMessage(getStatusEffectHealText(effect, getPokemonNameWithAffix(pokemon)));
|
// Freeze healed by move uses its own msg
|
||||||
|
globalScene.phaseManager.queueMessage(msg ?? getStatusEffectHealText(effect, getPokemonNameWithAffix(pokemon)));
|
||||||
pokemon.resetStatus();
|
pokemon.resetStatus();
|
||||||
pokemon.updateInfo();
|
pokemon.updateInfo();
|
||||||
}
|
}
|
||||||
@ -317,9 +476,23 @@ export class MovePhase extends BattlePhase {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Heal the user if it thaws from the move or random chance
|
// Check if move use would heal the user
|
||||||
// Check if the user will thaw due to a move
|
|
||||||
|
|
||||||
|
if (Overrides.STATUS_ACTIVATION_OVERRIDE) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if the move will heal
|
||||||
|
const move = this.move.getMove();
|
||||||
|
if (
|
||||||
|
move.findAttr(attr => attr.selfTarget && attr.is("HealStatusEffectAttr") && attr.isOfEffect(StatusEffect.FREEZE))
|
||||||
|
) {
|
||||||
|
// On cartridge, burn up will not cure if it would fail
|
||||||
|
if (move.id === MoveId.BURN_UP && !this.pokemon.isOfType(PokemonType.FIRE)) {
|
||||||
|
}
|
||||||
|
this.thaw = true;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
if (
|
if (
|
||||||
Overrides.STATUS_ACTIVATION_OVERRIDE === false ||
|
Overrides.STATUS_ACTIVATION_OVERRIDE === false ||
|
||||||
this.move
|
this.move
|
||||||
@ -370,7 +543,17 @@ export class MovePhase extends BattlePhase {
|
|||||||
if (moveName.endsWith(" (N)")) {
|
if (moveName.endsWith(" (N)")) {
|
||||||
failedText = i18next.t("battle:moveNotImplemented", { moveName: moveName.replace(" (N)", "") });
|
failedText = i18next.t("battle:moveNotImplemented", { moveName: moveName.replace(" (N)", "") });
|
||||||
} else if (moveId === MoveId.NONE || this.targets.length === 0) {
|
} else if (moveId === MoveId.NONE || this.targets.length === 0) {
|
||||||
// TODO: Create a locale key with some failure text
|
this.cancel();
|
||||||
|
|
||||||
|
const pokemonName = this.pokemon.name;
|
||||||
|
const warningText =
|
||||||
|
moveId === MoveId.NONE
|
||||||
|
? `${pokemonName} is attempting to use MoveId.NONE`
|
||||||
|
: `${pokemonName} is attempting to use a move with no targets`;
|
||||||
|
|
||||||
|
console.warn(warningText);
|
||||||
|
|
||||||
|
return true;
|
||||||
} else if (
|
} else if (
|
||||||
this.pokemon.isPlayer() &&
|
this.pokemon.isPlayer() &&
|
||||||
applyChallenges(ChallengeType.POKEMON_MOVE, moveId, usability) &&
|
applyChallenges(ChallengeType.POKEMON_MOVE, moveId, usability) &&
|
||||||
@ -460,85 +643,54 @@ export class MovePhase extends BattlePhase {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected usePP(): void {
|
||||||
|
if (!isIgnorePP(this.useMode)) {
|
||||||
|
const move = this.move;
|
||||||
|
// "commit" to using the move, deducting PP.
|
||||||
|
const ppUsed = 1 + this.getPpIncreaseFromPressure(this.getActiveTargetPokemon());
|
||||||
|
move.usePp(ppUsed);
|
||||||
|
globalScene.eventTarget.dispatchEvent(new MoveUsedEvent(this.pokemon.id, this.move.getMove(), this.move.ppUsed));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
protected useMove(): void {
|
protected useMove(): void {
|
||||||
const targets = this.getActiveTargetPokemon();
|
const user = this.pokemon;
|
||||||
const moveQueue = this.pokemon.getMoveQueue();
|
|
||||||
const move = this.move.getMove();
|
|
||||||
|
|
||||||
// form changes happen even before we know that the move wll execute.
|
|
||||||
globalScene.triggerPokemonFormChange(this.pokemon, SpeciesFormChangePreMoveTrigger);
|
|
||||||
|
|
||||||
// Clear out any two turn moves once they've been used.
|
// Clear out any two turn moves once they've been used.
|
||||||
// TODO: Refactor move queues and remove this assignment;
|
// TODO: Refactor move queues and remove this assignment;
|
||||||
// Move queues should be handled by the calling `CommandPhase` or a manager for it
|
// Move queues should be handled by the calling `CommandPhase` or a manager for it
|
||||||
// @ts-expect-error - useMode is readonly and shouldn't normally be assigned to
|
// @ts-expect-error - useMode is readonly and shouldn't normally be assigned to
|
||||||
this.useMode = moveQueue.shift()?.useMode ?? this.useMode;
|
this.useMode = user.getMoveQueue().shift()?.useMode ?? this.useMode;
|
||||||
|
|
||||||
if (this.pokemon.getTag(BattlerTagType.CHARGING)?.sourceMove === this.move.moveId) {
|
if (user.getTag(BattlerTagType.CHARGING)?.sourceMove === this.move.moveId) {
|
||||||
this.pokemon.lapseTag(BattlerTagType.CHARGING);
|
user.lapseTag(BattlerTagType.CHARGING);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!isIgnorePP(this.useMode)) {
|
if (!this.thirdFailureCheck()) {
|
||||||
// "commit" to using the move, deducting PP.
|
this.executeMove();
|
||||||
const ppUsed = 1 + this.getPpIncreaseFromPressure(targets);
|
|
||||||
this.move.usePp(ppUsed);
|
|
||||||
globalScene.eventTarget.dispatchEvent(new MoveUsedEvent(this.pokemon.id, move, this.move.ppUsed));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Determine if the move is successful (meaning that its damage/effects can be attempted)
|
|
||||||
* by checking that all of the following are true:
|
|
||||||
* - Conditional attributes of the move are all met
|
|
||||||
* - Weather does not block the move
|
|
||||||
* - Terrain does not block the move
|
|
||||||
*/
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Move conditions assume the move has a single target
|
|
||||||
* TODO: is this sustainable?
|
|
||||||
*/
|
|
||||||
const failsConditions = !move.applyConditions(this.pokemon, targets[0]);
|
|
||||||
const failedDueToWeather = globalScene.arena.isMoveWeatherCancelled(this.pokemon, move);
|
|
||||||
const failedDueToTerrain = globalScene.arena.isMoveTerrainCancelled(this.pokemon, this.targets, move);
|
|
||||||
const failed = failsConditions || failedDueToWeather || failedDueToTerrain;
|
|
||||||
|
|
||||||
if (failed) {
|
|
||||||
this.failMove(true, failedDueToWeather, failedDueToTerrain);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.executeMove();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 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 move = this.move.getMove();
|
const move = this.move.getMove();
|
||||||
const targets = this.getActiveTargetPokemon();
|
const opponent = this.getActiveTargetPokemon()[0];
|
||||||
|
const targets = this.targets;
|
||||||
// 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;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 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: this.pokemon, move, opponent: targets[0] });
|
applyAbAttrs("PokemonTypeChangeAbAttr", { pokemon, move, opponent });
|
||||||
this.showMoveText();
|
this.showMoveText();
|
||||||
globalScene.phaseManager.unshiftNew(
|
globalScene.phaseManager.unshiftNew("MoveEffectPhase", pokemon.getBattlerIndex(), targets, move, this.useMode);
|
||||||
"MoveEffectPhase",
|
|
||||||
this.pokemon.getBattlerIndex(),
|
|
||||||
this.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: this.pokemon, targets: this.targets });
|
applyAbAttrs("PostMoveUsedAbAttr", { pokemon, move: this.move, source: pokemon, targets: targets });
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -671,6 +823,7 @@ export class MovePhase extends BattlePhase {
|
|||||||
const redirectTarget = new NumberHolder(currentTarget);
|
const redirectTarget = new NumberHolder(currentTarget);
|
||||||
|
|
||||||
// check move redirection abilities of every pokemon *except* the user.
|
// check move redirection abilities of every pokemon *except* the user.
|
||||||
|
// TODO: Make storm drain, lightning rod, etc, redirect at this point for type changing moves
|
||||||
globalScene
|
globalScene
|
||||||
.getField(true)
|
.getField(true)
|
||||||
.filter(p => p !== this.pokemon)
|
.filter(p => p !== this.pokemon)
|
||||||
@ -785,10 +938,7 @@ export class MovePhase extends BattlePhase {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (this.failed) {
|
if (this.failed) {
|
||||||
// TODO: should this consider struggle?
|
this.usePP();
|
||||||
const ppUsed = isIgnorePP(this.useMode) ? 0 : 1;
|
|
||||||
this.move.usePp(ppUsed);
|
|
||||||
globalScene.eventTarget.dispatchEvent(new MoveUsedEvent(this.pokemon?.id, this.move.getMove(), ppUsed));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
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)) {
|
||||||
|
Loading…
Reference in New Issue
Block a user