mirror of
https://github.com/pagefaultgames/pokerogue.git
synced 2025-08-26 01:09:29 +02:00
Compare commits
12 Commits
ed0716b985
...
37ea2f799b
Author | SHA1 | Date | |
---|---|---|---|
|
37ea2f799b | ||
|
0da37a0f0c | ||
|
ce3fe897c3 | ||
|
605d61cec9 | ||
|
d55cfbcf38 | ||
|
8f9fca50a1 | ||
|
6018e11233 | ||
|
d798ebb545 | ||
|
f771e29b0f | ||
|
d2f8495c1f | ||
|
6fae3cbacd | ||
|
b552779316 |
@ -1,13 +1,24 @@
|
|||||||
|
import type { Pokemon } from "#field/pokemon";
|
||||||
import type {
|
import type {
|
||||||
AttackMove,
|
AttackMove,
|
||||||
ChargingAttackMove,
|
ChargingAttackMove,
|
||||||
ChargingSelfStatusMove,
|
ChargingSelfStatusMove,
|
||||||
|
Move,
|
||||||
MoveAttr,
|
MoveAttr,
|
||||||
MoveAttrConstructorMap,
|
MoveAttrConstructorMap,
|
||||||
SelfStatusMove,
|
SelfStatusMove,
|
||||||
StatusMove,
|
StatusMove,
|
||||||
} from "#moves/move";
|
} from "#moves/move";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A generic function producing a message during a Move's execution.
|
||||||
|
* @param user - The {@linkcode Pokemon} using the move
|
||||||
|
* @param target - The {@linkcode Pokemon} targeted by the move
|
||||||
|
* @param move - The {@linkcode Move} being used
|
||||||
|
* @returns a string
|
||||||
|
*/
|
||||||
|
export type MoveMessageFunc = (user: Pokemon, target: Pokemon, move: Move) => string;
|
||||||
|
|
||||||
export type MoveAttrFilter = (attr: MoveAttr) => boolean;
|
export type MoveAttrFilter = (attr: MoveAttr) => boolean;
|
||||||
|
|
||||||
export type * from "#moves/move";
|
export type * from "#moves/move";
|
||||||
|
@ -1670,6 +1670,7 @@ export class MoveTypeChangeAbAttr extends PreAttackAbAttr {
|
|||||||
constructor(
|
constructor(
|
||||||
private newType: PokemonType,
|
private newType: PokemonType,
|
||||||
private powerMultiplier: number,
|
private powerMultiplier: number,
|
||||||
|
// TODO: all moves with this attr solely check the move being used...
|
||||||
private condition?: PokemonAttackCondition,
|
private condition?: PokemonAttackCondition,
|
||||||
) {
|
) {
|
||||||
super(false);
|
super(false);
|
||||||
|
@ -10,6 +10,7 @@ import { allMoves } from "#data/data-lists";
|
|||||||
import { AbilityId } from "#enums/ability-id";
|
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 type { BattlerIndex } from "#enums/battler-index";
|
||||||
import { BattlerTagType } from "#enums/battler-tag-type";
|
import { BattlerTagType } from "#enums/battler-tag-type";
|
||||||
import { HitResult } from "#enums/hit-result";
|
import { HitResult } from "#enums/hit-result";
|
||||||
import { CommonAnim } from "#enums/move-anims-common";
|
import { CommonAnim } from "#enums/move-anims-common";
|
||||||
@ -28,7 +29,7 @@ import type {
|
|||||||
SerializableArenaTagType,
|
SerializableArenaTagType,
|
||||||
} from "#types/arena-tags";
|
} from "#types/arena-tags";
|
||||||
import type { Mutable } from "#types/type-helpers";
|
import type { Mutable } from "#types/type-helpers";
|
||||||
import { BooleanHolder, NumberHolder, toDmgValue } from "#utils/common";
|
import { BooleanHolder, isNullOrUndefined, NumberHolder, toDmgValue } from "#utils/common";
|
||||||
import i18next from "i18next";
|
import i18next from "i18next";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -1583,6 +1584,138 @@ export class SuppressAbilitiesTag extends SerializableArenaTag {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Interface containing data related to a queued healing effect from
|
||||||
|
* {@link https://bulbapedia.bulbagarden.net/wiki/Healing_Wish_(move) | Healing Wish}
|
||||||
|
* or {@link https://bulbapedia.bulbagarden.net/wiki/Lunar_Dance_(move) | Lunar Dance}.
|
||||||
|
*/
|
||||||
|
interface PendingHealEffect {
|
||||||
|
/** The {@linkcode Pokemon.id | PID} of the {@linkcode Pokemon} that created the effect. */
|
||||||
|
readonly sourceId: number;
|
||||||
|
/** The {@linkcode MoveId} of the move that created the effect. */
|
||||||
|
readonly moveId: MoveId;
|
||||||
|
/** If `true`, also restores the target's PP when the effect activates. */
|
||||||
|
readonly restorePP: boolean;
|
||||||
|
/** The message to display when the effect activates */
|
||||||
|
readonly healMessage: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Arena tag to contain stored healing effects, namely from
|
||||||
|
* {@link https://bulbapedia.bulbagarden.net/wiki/Healing_Wish_(move) | Healing Wish}
|
||||||
|
* and {@link https://bulbapedia.bulbagarden.net/wiki/Lunar_Dance_(move) | Lunar Dance}.
|
||||||
|
* When a damaged Pokemon first enters the effect's {@linkcode BattlerIndex | field position},
|
||||||
|
* their HP is fully restored, and they are cured of any non-volatile status condition.
|
||||||
|
* If the effect is from Lunar Dance, their PP is also restored.
|
||||||
|
*/
|
||||||
|
export class PendingHealTag extends SerializableArenaTag {
|
||||||
|
public readonly tagType = ArenaTagType.PENDING_HEAL;
|
||||||
|
/** All pending healing effects, organized by {@linkcode BattlerIndex} */
|
||||||
|
public readonly pendingHeals: Partial<Record<BattlerIndex, PendingHealEffect[]>> = {};
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
super(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Adds a pending healing effect to the field. Effects under the same move *and*
|
||||||
|
* target index as an existing effect are ignored.
|
||||||
|
* @param targetIndex - The {@linkcode BattlerIndex} under which the effect applies
|
||||||
|
* @param healEffect - The {@linkcode PendingHealEffect | data} for the pending heal effect
|
||||||
|
*/
|
||||||
|
public queueHeal(targetIndex: BattlerIndex, healEffect: PendingHealEffect): void {
|
||||||
|
const existingHealEffects = this.pendingHeals[targetIndex];
|
||||||
|
if (existingHealEffects) {
|
||||||
|
if (!existingHealEffects.some(he => he.moveId === healEffect.moveId)) {
|
||||||
|
existingHealEffects.push(healEffect);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
this.pendingHeals[targetIndex] = [healEffect];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Removes default on-remove message */
|
||||||
|
override onRemove(_arena: Arena): void {}
|
||||||
|
|
||||||
|
/** This arena tag is removed at the end of the turn if no pending healing effects are on the field */
|
||||||
|
override lapse(_arena: Arena): boolean {
|
||||||
|
for (const key in this.pendingHeals) {
|
||||||
|
if (this.pendingHeals[key].length > 0) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Applies a pending healing effect on the given target index. If an effect is found for
|
||||||
|
* the index, the Pokemon at that index is healed to full HP, is cured of any non-volatile status,
|
||||||
|
* and has its PP fully restored (if the effect is from Lunar Dance).
|
||||||
|
* @param arena - The {@linkcode Arena} containing this tag
|
||||||
|
* @param simulated - If `true`, suppresses changes to game state
|
||||||
|
* @param pokemon - The {@linkcode Pokemon} receiving the healing effect
|
||||||
|
* @returns `true` if the target Pokemon was healed by this effect
|
||||||
|
* @todo This should also be called when a Pokemon moves into a new position via Ally Switch
|
||||||
|
*/
|
||||||
|
override apply(arena: Arena, simulated: boolean, pokemon: Pokemon): boolean {
|
||||||
|
const targetIndex = pokemon.getBattlerIndex();
|
||||||
|
const targetEffects = this.pendingHeals[targetIndex];
|
||||||
|
|
||||||
|
if (simulated) {
|
||||||
|
return !!targetEffects?.length;
|
||||||
|
}
|
||||||
|
|
||||||
|
const healEffect = targetEffects?.find(effect => this.canApply(effect, pokemon));
|
||||||
|
if (targetEffects && healEffect) {
|
||||||
|
const { sourceId, moveId, restorePP, healMessage } = healEffect;
|
||||||
|
const sourcePokemon = globalScene.getPokemonById(sourceId);
|
||||||
|
if (!sourcePokemon) {
|
||||||
|
console.warn(`Source of pending ${allMoves[moveId].name} effect is undefined!`);
|
||||||
|
targetEffects.splice(targetEffects.indexOf(healEffect), 1);
|
||||||
|
// Re-evaluate after the invalid heal effect is removed
|
||||||
|
return this.apply(arena, simulated, pokemon);
|
||||||
|
}
|
||||||
|
|
||||||
|
globalScene.phaseManager.unshiftNew(
|
||||||
|
"PokemonHealPhase",
|
||||||
|
targetIndex,
|
||||||
|
pokemon.getMaxHp(),
|
||||||
|
healMessage,
|
||||||
|
true,
|
||||||
|
false,
|
||||||
|
false,
|
||||||
|
true,
|
||||||
|
false,
|
||||||
|
restorePP,
|
||||||
|
);
|
||||||
|
|
||||||
|
targetEffects.splice(targetEffects.indexOf(healEffect), 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
return !isNullOrUndefined(healEffect);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Determines if the given {@linkcode PendingHealEffect} can immediately heal
|
||||||
|
* the given target {@linkcode Pokemon}.
|
||||||
|
* @param healEffect - The {@linkcode PendingHealEffect} to evaluate
|
||||||
|
* @param pokemon - The {@linkcode Pokemon} to evaluate against
|
||||||
|
* @returns `true` if the Pokemon can be healed by the effect
|
||||||
|
*/
|
||||||
|
private canApply(healEffect: PendingHealEffect, pokemon: Pokemon): boolean {
|
||||||
|
return (
|
||||||
|
!pokemon.isFullHp() ||
|
||||||
|
!isNullOrUndefined(pokemon.status) ||
|
||||||
|
(healEffect.restorePP && pokemon.getMoveset().some(mv => mv.ppUsed > 0))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
override loadTag(source: BaseArenaTag & Pick<PendingHealTag, "tagType" | "pendingHeals">): void {
|
||||||
|
super.loadTag(source);
|
||||||
|
(this as Mutable<this>).pendingHeals = source.pendingHeals;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// TODO: swap `sourceMove` and `sourceId` and make `sourceMove` an optional parameter
|
// TODO: swap `sourceMove` and `sourceId` and make `sourceMove` an optional parameter
|
||||||
export function getArenaTag(
|
export function getArenaTag(
|
||||||
tagType: ArenaTagType,
|
tagType: ArenaTagType,
|
||||||
@ -1646,6 +1779,8 @@ export function getArenaTag(
|
|||||||
return new FairyLockTag(turnCount, sourceId);
|
return new FairyLockTag(turnCount, sourceId);
|
||||||
case ArenaTagType.NEUTRALIZING_GAS:
|
case ArenaTagType.NEUTRALIZING_GAS:
|
||||||
return new SuppressAbilitiesTag(sourceId);
|
return new SuppressAbilitiesTag(sourceId);
|
||||||
|
case ArenaTagType.PENDING_HEAL:
|
||||||
|
return new PendingHealTag();
|
||||||
default:
|
default:
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
@ -1694,5 +1829,6 @@ export type ArenaTagTypeMap = {
|
|||||||
[ArenaTagType.GRASS_WATER_PLEDGE]: GrassWaterPledgeTag;
|
[ArenaTagType.GRASS_WATER_PLEDGE]: GrassWaterPledgeTag;
|
||||||
[ArenaTagType.FAIRY_LOCK]: FairyLockTag;
|
[ArenaTagType.FAIRY_LOCK]: FairyLockTag;
|
||||||
[ArenaTagType.NEUTRALIZING_GAS]: SuppressAbilitiesTag;
|
[ArenaTagType.NEUTRALIZING_GAS]: SuppressAbilitiesTag;
|
||||||
|
[ArenaTagType.PENDING_HEAL]: PendingHealTag;
|
||||||
[ArenaTagType.NONE]: NoneTag;
|
[ArenaTagType.NONE]: NoneTag;
|
||||||
};
|
};
|
||||||
|
@ -6,7 +6,7 @@ import { loggedInUser } from "#app/account";
|
|||||||
import type { GameMode } from "#app/game-mode";
|
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 type { ArenaTrapTag } from "#data/arena-tag";
|
import type { ArenaTrapTag, PendingHealTag } from "#data/arena-tag";
|
||||||
import { WeakenMoveTypeTag } from "#data/arena-tag";
|
import { WeakenMoveTypeTag } from "#data/arena-tag";
|
||||||
import { MoveChargeAnim } from "#data/battle-anims";
|
import { MoveChargeAnim } from "#data/battle-anims";
|
||||||
import {
|
import {
|
||||||
@ -86,7 +86,7 @@ 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 { 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 } 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";
|
||||||
import { BooleanHolder, type Constructor, isNullOrUndefined, NumberHolder, randSeedFloat, randSeedInt, randSeedItem, toDmgValue } from "#utils/common";
|
import { BooleanHolder, type Constructor, isNullOrUndefined, NumberHolder, randSeedFloat, randSeedInt, randSeedItem, toDmgValue } from "#utils/common";
|
||||||
import { getEnumValues } from "#utils/enums";
|
import { getEnumValues } from "#utils/enums";
|
||||||
@ -1357,20 +1357,20 @@ export class MoveHeaderAttr extends MoveAttr {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Header attribute to queue a message at the beginning of a turn.
|
* Header attribute to queue a message at the beginning of a turn.
|
||||||
* @see {@link MoveHeaderAttr}
|
|
||||||
*/
|
*/
|
||||||
export class MessageHeaderAttr extends MoveHeaderAttr {
|
export class MessageHeaderAttr extends MoveHeaderAttr {
|
||||||
private message: string | ((user: Pokemon, move: Move) => string);
|
/** The message to display, or a function producing one. */
|
||||||
|
private message: string | MoveMessageFunc;
|
||||||
|
|
||||||
constructor(message: string | ((user: Pokemon, move: Move) => string)) {
|
constructor(message: string | MoveMessageFunc) {
|
||||||
super();
|
super();
|
||||||
this.message = message;
|
this.message = message;
|
||||||
}
|
}
|
||||||
|
|
||||||
apply(user: Pokemon, target: Pokemon, move: Move, args: any[]): boolean {
|
apply(user: Pokemon, target: Pokemon, move: Move): boolean {
|
||||||
const message = typeof this.message === "string"
|
const message = typeof this.message === "string"
|
||||||
? this.message
|
? this.message
|
||||||
: this.message(user, move);
|
: this.message(user, target, move);
|
||||||
|
|
||||||
if (message) {
|
if (message) {
|
||||||
globalScene.phaseManager.queueMessage(message);
|
globalScene.phaseManager.queueMessage(message);
|
||||||
@ -1418,21 +1418,21 @@ export class BeakBlastHeaderAttr extends AddBattlerTagHeaderAttr {
|
|||||||
*/
|
*/
|
||||||
export class PreMoveMessageAttr extends MoveAttr {
|
export class PreMoveMessageAttr extends MoveAttr {
|
||||||
/** The message to display or a function returning one */
|
/** The message to display or a function returning one */
|
||||||
private message: string | ((user: Pokemon, target: Pokemon, move: Move) => string | undefined);
|
private message: string | MoveMessageFunc;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Create a new {@linkcode PreMoveMessageAttr} to display a message before move execution.
|
* Create a new {@linkcode PreMoveMessageAttr} to display a message before move execution.
|
||||||
* @param message - The message to display before move use, either as a string or a function producing one.
|
* @param message - The message to display before move use, either` a literal string or a function producing one.
|
||||||
* @remarks
|
* @remarks
|
||||||
* If {@linkcode message} evaluates to an empty string (`''`), no message will be displayed
|
* If {@linkcode message} evaluates to an empty string (`""`), no message will be displayed
|
||||||
* (though the move will still succeed).
|
* (though the move will still succeed).
|
||||||
*/
|
*/
|
||||||
constructor(message: string | ((user: Pokemon, target: Pokemon, move: Move) => string)) {
|
constructor(message: string | MoveMessageFunc) {
|
||||||
super();
|
super();
|
||||||
this.message = message;
|
this.message = message;
|
||||||
}
|
}
|
||||||
|
|
||||||
apply(user: Pokemon, target: Pokemon, move: Move, _args: any[]): boolean {
|
apply(user: Pokemon, target: Pokemon, move: Move): boolean {
|
||||||
const message = typeof this.message === "function"
|
const message = typeof this.message === "function"
|
||||||
? this.message(user, target, move)
|
? this.message(user, target, move)
|
||||||
: this.message;
|
: this.message;
|
||||||
@ -1453,18 +1453,17 @@ export class PreMoveMessageAttr extends MoveAttr {
|
|||||||
* @extends MoveAttr
|
* @extends MoveAttr
|
||||||
*/
|
*/
|
||||||
export class PreUseInterruptAttr extends MoveAttr {
|
export class PreUseInterruptAttr extends MoveAttr {
|
||||||
protected message?: string | ((user: Pokemon, target: Pokemon, move: Move) => string);
|
protected message: string | MoveMessageFunc;
|
||||||
protected overridesFailedMessage: boolean;
|
|
||||||
protected conditionFunc: MoveConditionFunc;
|
protected conditionFunc: MoveConditionFunc;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Create a new MoveInterruptedMessageAttr.
|
* 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.
|
* @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 | ((user: Pokemon, target: Pokemon, move: Move) => string), conditionFunc?: MoveConditionFunc) {
|
constructor(message: string | MoveMessageFunc, conditionFunc: MoveConditionFunc) {
|
||||||
super();
|
super();
|
||||||
this.message = message;
|
this.message = message;
|
||||||
this.conditionFunc = conditionFunc ?? (() => true);
|
this.conditionFunc = conditionFunc;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -1485,11 +1484,9 @@ export class PreUseInterruptAttr extends MoveAttr {
|
|||||||
*/
|
*/
|
||||||
override getFailedText(user: Pokemon, target: Pokemon, move: Move): string | undefined {
|
override getFailedText(user: Pokemon, target: Pokemon, move: Move): string | undefined {
|
||||||
if (this.message && this.conditionFunc(user, target, move)) {
|
if (this.message && this.conditionFunc(user, target, move)) {
|
||||||
const message =
|
return typeof this.message === "string"
|
||||||
typeof this.message === "string"
|
? this.message
|
||||||
? (this.message as string)
|
|
||||||
: this.message(user, target, move);
|
: this.message(user, target, move);
|
||||||
return message;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -1694,18 +1691,31 @@ export class SurviveDamageAttr extends ModifiedDamageAttr {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export class SplashAttr extends MoveEffectAttr {
|
/**
|
||||||
apply(user: Pokemon, target: Pokemon, move: Move, args: any[]): boolean {
|
* Move attribute to display arbitrary text during a move's execution.
|
||||||
globalScene.phaseManager.queueMessage(i18next.t("moveTriggers:splash"));
|
*/
|
||||||
return true;
|
export class MessageAttr extends MoveEffectAttr {
|
||||||
}
|
/** The message to display, either as a string or a function returning one. */
|
||||||
|
private message: string | MoveMessageFunc;
|
||||||
|
|
||||||
|
constructor(message: string | MoveMessageFunc, options?: MoveEffectAttrOptions) {
|
||||||
|
// TODO: Do we need to respect `selfTarget` if we're just displaying text?
|
||||||
|
super(false, options)
|
||||||
|
this.message = message;
|
||||||
}
|
}
|
||||||
|
|
||||||
export class CelebrateAttr extends MoveEffectAttr {
|
override apply(user: Pokemon, target: Pokemon, move: Move): boolean {
|
||||||
apply(user: Pokemon, target: Pokemon, move: Move, args: any[]): boolean {
|
const message = typeof this.message === "function"
|
||||||
globalScene.phaseManager.queueMessage(i18next.t("moveTriggers:celebrate", { playerName: loggedInUser?.username }));
|
? this.message(user, target, move)
|
||||||
|
: this.message;
|
||||||
|
|
||||||
|
// TODO: Consider changing if/when MoveAttr `apply` return values become significant
|
||||||
|
if (message) {
|
||||||
|
globalScene.phaseManager.queueMessage(message, 500);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export class RecoilAttr extends MoveEffectAttr {
|
export class RecoilAttr extends MoveEffectAttr {
|
||||||
@ -2094,24 +2104,15 @@ export class SacrificialFullRestoreAttr extends SacrificialAttr {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
// We don't know which party member will be chosen, so pick the highest max HP in the party
|
// Add a tag to the field if it doesn't already exist, then queue a delayed healing effect in the user's current slot.
|
||||||
const party = user.isPlayer() ? globalScene.getPlayerParty() : globalScene.getEnemyParty();
|
globalScene.arena.addTag(ArenaTagType.PENDING_HEAL, 0, move.id, user.id); // Arguments after first go completely unused
|
||||||
const maxPartyMemberHp = party.map(p => p.getMaxHp()).reduce((maxHp: number, hp: number) => Math.max(hp, maxHp), 0);
|
const tag = globalScene.arena.getTag(ArenaTagType.PENDING_HEAL) as PendingHealTag;
|
||||||
|
tag.queueHeal(user.getBattlerIndex(), {
|
||||||
const pm = globalScene.phaseManager;
|
sourceId: user.id,
|
||||||
|
moveId: move.id,
|
||||||
pm.pushPhase(
|
restorePP: this.restorePP,
|
||||||
pm.create("PokemonHealPhase",
|
healMessage: i18next.t(this.moveMessage, { pokemonName: getPokemonNameWithAffix(user) }),
|
||||||
user.getBattlerIndex(),
|
});
|
||||||
maxPartyMemberHp,
|
|
||||||
i18next.t(this.moveMessage, { pokemonName: getPokemonNameWithAffix(user) }),
|
|
||||||
true,
|
|
||||||
false,
|
|
||||||
false,
|
|
||||||
true,
|
|
||||||
false,
|
|
||||||
this.restorePP),
|
|
||||||
true);
|
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
@ -5931,38 +5932,6 @@ export class ProtectAttr extends AddBattlerTagAttr {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export class IgnoreAccuracyAttr extends AddBattlerTagAttr {
|
|
||||||
constructor() {
|
|
||||||
super(BattlerTagType.IGNORE_ACCURACY, true, false, 2);
|
|
||||||
}
|
|
||||||
|
|
||||||
apply(user: Pokemon, target: Pokemon, move: Move, args: any[]): boolean {
|
|
||||||
if (!super.apply(user, target, move, args)) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
globalScene.phaseManager.queueMessage(i18next.t("moveTriggers:tookAimAtTarget", { pokemonName: getPokemonNameWithAffix(user), targetName: getPokemonNameWithAffix(target) }));
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export class FaintCountdownAttr extends AddBattlerTagAttr {
|
|
||||||
constructor() {
|
|
||||||
super(BattlerTagType.PERISH_SONG, false, true, 4);
|
|
||||||
}
|
|
||||||
|
|
||||||
apply(user: Pokemon, target: Pokemon, move: Move, args: any[]): boolean {
|
|
||||||
if (!super.apply(user, target, move, args)) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
globalScene.phaseManager.queueMessage(i18next.t("moveTriggers:faintCountdown", { pokemonName: getPokemonNameWithAffix(target), turnCount: this.turnCountMin - 1 }));
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Attribute to remove all Substitutes from the field.
|
* Attribute to remove all Substitutes from the field.
|
||||||
* @extends MoveEffectAttr
|
* @extends MoveEffectAttr
|
||||||
@ -6603,8 +6572,10 @@ export class ChillyReceptionAttr extends ForceSwitchOutAttr {
|
|||||||
return (user, target, move) => globalScene.arena.weather?.weatherType !== WeatherType.SNOW || super.getSwitchOutCondition()(user, target, move);
|
return (user, target, move) => globalScene.arena.weather?.weatherType !== WeatherType.SNOW || super.getSwitchOutCondition()(user, target, move);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export class RemoveTypeAttr extends MoveEffectAttr {
|
export class RemoveTypeAttr extends MoveEffectAttr {
|
||||||
|
|
||||||
|
// TODO: Remove the message callback
|
||||||
private removedType: PokemonType;
|
private removedType: PokemonType;
|
||||||
private messageCallback: ((user: Pokemon) => void) | undefined;
|
private messageCallback: ((user: Pokemon) => void) | undefined;
|
||||||
|
|
||||||
@ -8299,8 +8270,6 @@ const MoveAttrs = Object.freeze({
|
|||||||
RandomLevelDamageAttr,
|
RandomLevelDamageAttr,
|
||||||
ModifiedDamageAttr,
|
ModifiedDamageAttr,
|
||||||
SurviveDamageAttr,
|
SurviveDamageAttr,
|
||||||
SplashAttr,
|
|
||||||
CelebrateAttr,
|
|
||||||
RecoilAttr,
|
RecoilAttr,
|
||||||
SacrificialAttr,
|
SacrificialAttr,
|
||||||
SacrificialAttrOnHit,
|
SacrificialAttrOnHit,
|
||||||
@ -8443,8 +8412,7 @@ const MoveAttrs = Object.freeze({
|
|||||||
RechargeAttr,
|
RechargeAttr,
|
||||||
TrapAttr,
|
TrapAttr,
|
||||||
ProtectAttr,
|
ProtectAttr,
|
||||||
IgnoreAccuracyAttr,
|
MessageAttr,
|
||||||
FaintCountdownAttr,
|
|
||||||
RemoveAllSubstitutesAttr,
|
RemoveAllSubstitutesAttr,
|
||||||
HitsTagAttr,
|
HitsTagAttr,
|
||||||
HitsTagForDoubleDamageAttr,
|
HitsTagForDoubleDamageAttr,
|
||||||
@ -8938,7 +8906,7 @@ export function initMoves() {
|
|||||||
new AttackMove(MoveId.PSYWAVE, PokemonType.PSYCHIC, MoveCategory.SPECIAL, -1, 100, 15, -1, 0, 1)
|
new AttackMove(MoveId.PSYWAVE, PokemonType.PSYCHIC, MoveCategory.SPECIAL, -1, 100, 15, -1, 0, 1)
|
||||||
.attr(RandomLevelDamageAttr),
|
.attr(RandomLevelDamageAttr),
|
||||||
new SelfStatusMove(MoveId.SPLASH, PokemonType.NORMAL, -1, 40, -1, 0, 1)
|
new SelfStatusMove(MoveId.SPLASH, PokemonType.NORMAL, -1, 40, -1, 0, 1)
|
||||||
.attr(SplashAttr)
|
.attr(MessageAttr, i18next.t("moveTriggers:splash"))
|
||||||
.condition(failOnGravityCondition),
|
.condition(failOnGravityCondition),
|
||||||
new SelfStatusMove(MoveId.ACID_ARMOR, PokemonType.POISON, -1, 20, -1, 0, 1)
|
new SelfStatusMove(MoveId.ACID_ARMOR, PokemonType.POISON, -1, 20, -1, 0, 1)
|
||||||
.attr(StatStageChangeAttr, [ Stat.DEF ], 2, true),
|
.attr(StatStageChangeAttr, [ Stat.DEF ], 2, true),
|
||||||
@ -9000,7 +8968,10 @@ export function initMoves() {
|
|||||||
.attr(AddBattlerTagAttr, BattlerTagType.TRAPPED, false, true, 1)
|
.attr(AddBattlerTagAttr, BattlerTagType.TRAPPED, false, true, 1)
|
||||||
.reflectable(),
|
.reflectable(),
|
||||||
new StatusMove(MoveId.MIND_READER, PokemonType.NORMAL, -1, 5, -1, 0, 2)
|
new StatusMove(MoveId.MIND_READER, PokemonType.NORMAL, -1, 5, -1, 0, 2)
|
||||||
.attr(IgnoreAccuracyAttr),
|
.attr(AddBattlerTagAttr, BattlerTagType.IGNORE_ACCURACY, true, false, 2)
|
||||||
|
.attr(MessageAttr, (user, target) =>
|
||||||
|
i18next.t("moveTriggers:tookAimAtTarget", { pokemonName: getPokemonNameWithAffix(user), targetName: getPokemonNameWithAffix(target) })
|
||||||
|
),
|
||||||
new StatusMove(MoveId.NIGHTMARE, PokemonType.GHOST, 100, 15, -1, 0, 2)
|
new StatusMove(MoveId.NIGHTMARE, PokemonType.GHOST, 100, 15, -1, 0, 2)
|
||||||
.attr(AddBattlerTagAttr, BattlerTagType.NIGHTMARE)
|
.attr(AddBattlerTagAttr, BattlerTagType.NIGHTMARE)
|
||||||
.condition(targetSleptOrComatoseCondition),
|
.condition(targetSleptOrComatoseCondition),
|
||||||
@ -9088,7 +9059,9 @@ export function initMoves() {
|
|||||||
return lastTurnMove.length === 0 || lastTurnMove[0].move !== move.id || lastTurnMove[0].result !== MoveResult.SUCCESS;
|
return lastTurnMove.length === 0 || lastTurnMove[0].move !== move.id || lastTurnMove[0].result !== MoveResult.SUCCESS;
|
||||||
}),
|
}),
|
||||||
new StatusMove(MoveId.PERISH_SONG, PokemonType.NORMAL, -1, 5, -1, 0, 2)
|
new StatusMove(MoveId.PERISH_SONG, PokemonType.NORMAL, -1, 5, -1, 0, 2)
|
||||||
.attr(FaintCountdownAttr)
|
.attr(AddBattlerTagAttr, BattlerTagType.PERISH_SONG, false, true, 4)
|
||||||
|
.attr(MessageAttr, (_user, target) =>
|
||||||
|
i18next.t("moveTriggers:faintCountdown", { pokemonName: getPokemonNameWithAffix(target), turnCount: 3 }))
|
||||||
.ignoresProtect()
|
.ignoresProtect()
|
||||||
.soundBased()
|
.soundBased()
|
||||||
.condition(failOnBossCondition)
|
.condition(failOnBossCondition)
|
||||||
@ -9104,7 +9077,10 @@ export function initMoves() {
|
|||||||
.attr(MultiHitAttr)
|
.attr(MultiHitAttr)
|
||||||
.makesContact(false),
|
.makesContact(false),
|
||||||
new StatusMove(MoveId.LOCK_ON, PokemonType.NORMAL, -1, 5, -1, 0, 2)
|
new StatusMove(MoveId.LOCK_ON, PokemonType.NORMAL, -1, 5, -1, 0, 2)
|
||||||
.attr(IgnoreAccuracyAttr),
|
.attr(AddBattlerTagAttr, BattlerTagType.IGNORE_ACCURACY, true, false, 2)
|
||||||
|
.attr(MessageAttr, (user, target) =>
|
||||||
|
i18next.t("moveTriggers:tookAimAtTarget", { pokemonName: getPokemonNameWithAffix(user), targetName: getPokemonNameWithAffix(target) })
|
||||||
|
),
|
||||||
new AttackMove(MoveId.OUTRAGE, PokemonType.DRAGON, MoveCategory.PHYSICAL, 120, 100, 10, -1, 0, 2)
|
new AttackMove(MoveId.OUTRAGE, PokemonType.DRAGON, MoveCategory.PHYSICAL, 120, 100, 10, -1, 0, 2)
|
||||||
.attr(FrenzyAttr)
|
.attr(FrenzyAttr)
|
||||||
.attr(MissEffectAttr, frenzyMissFunc)
|
.attr(MissEffectAttr, frenzyMissFunc)
|
||||||
@ -9331,8 +9307,8 @@ export function initMoves() {
|
|||||||
&& (user.status.effect === StatusEffect.BURN || user.status.effect === StatusEffect.POISON || user.status.effect === StatusEffect.TOXIC || user.status.effect === StatusEffect.PARALYSIS) ? 2 : 1)
|
&& (user.status.effect === StatusEffect.BURN || user.status.effect === StatusEffect.POISON || user.status.effect === StatusEffect.TOXIC || user.status.effect === StatusEffect.PARALYSIS) ? 2 : 1)
|
||||||
.attr(BypassBurnDamageReductionAttr),
|
.attr(BypassBurnDamageReductionAttr),
|
||||||
new AttackMove(MoveId.FOCUS_PUNCH, PokemonType.FIGHTING, MoveCategory.PHYSICAL, 150, 100, 20, -1, -3, 3)
|
new AttackMove(MoveId.FOCUS_PUNCH, PokemonType.FIGHTING, MoveCategory.PHYSICAL, 150, 100, 20, -1, -3, 3)
|
||||||
.attr(MessageHeaderAttr, (user, move) => i18next.t("moveTriggers:isTighteningFocus", { pokemonName: getPokemonNameWithAffix(user) }))
|
.attr(MessageHeaderAttr, (user) => i18next.t("moveTriggers:isTighteningFocus", { pokemonName: getPokemonNameWithAffix(user) }))
|
||||||
.attr(PreUseInterruptAttr, (user, target, move) => i18next.t("moveTriggers:lostFocus", { pokemonName: getPokemonNameWithAffix(user) }), user => !!user.turnData.attacksReceived.find(r => r.damage))
|
.attr(PreUseInterruptAttr, (user) => i18next.t("moveTriggers:lostFocus", { pokemonName: getPokemonNameWithAffix(user) }), user => user.turnData.attacksReceived.some(r => r.damage > 0))
|
||||||
.punchingMove(),
|
.punchingMove(),
|
||||||
new AttackMove(MoveId.SMELLING_SALTS, PokemonType.NORMAL, MoveCategory.PHYSICAL, 70, 100, 10, -1, 0, 3)
|
new AttackMove(MoveId.SMELLING_SALTS, PokemonType.NORMAL, MoveCategory.PHYSICAL, 70, 100, 10, -1, 0, 3)
|
||||||
.attr(MovePowerMultiplierAttr, (user, target, move) => target.status?.effect === StatusEffect.PARALYSIS ? 2 : 1)
|
.attr(MovePowerMultiplierAttr, (user, target, move) => target.status?.effect === StatusEffect.PARALYSIS ? 2 : 1)
|
||||||
@ -10433,7 +10409,8 @@ export function initMoves() {
|
|||||||
new AttackMove(MoveId.DAZZLING_GLEAM, PokemonType.FAIRY, MoveCategory.SPECIAL, 80, 100, 10, -1, 0, 6)
|
new AttackMove(MoveId.DAZZLING_GLEAM, PokemonType.FAIRY, MoveCategory.SPECIAL, 80, 100, 10, -1, 0, 6)
|
||||||
.target(MoveTarget.ALL_NEAR_ENEMIES),
|
.target(MoveTarget.ALL_NEAR_ENEMIES),
|
||||||
new SelfStatusMove(MoveId.CELEBRATE, PokemonType.NORMAL, -1, 40, -1, 0, 6)
|
new SelfStatusMove(MoveId.CELEBRATE, PokemonType.NORMAL, -1, 40, -1, 0, 6)
|
||||||
.attr(CelebrateAttr),
|
// NB: This needs a lambda function as the user will not be logged in by the time the moves are initialized
|
||||||
|
.attr(MessageAttr, () => i18next.t("moveTriggers:celebrate", { playerName: loggedInUser?.username })),
|
||||||
new StatusMove(MoveId.HOLD_HANDS, PokemonType.NORMAL, -1, 40, -1, 0, 6)
|
new StatusMove(MoveId.HOLD_HANDS, PokemonType.NORMAL, -1, 40, -1, 0, 6)
|
||||||
.ignoresSubstitute()
|
.ignoresSubstitute()
|
||||||
.target(MoveTarget.NEAR_ALLY),
|
.target(MoveTarget.NEAR_ALLY),
|
||||||
@ -10608,7 +10585,12 @@ export function initMoves() {
|
|||||||
.attr(StatStageChangeAttr, [ Stat.SPD ], -1)
|
.attr(StatStageChangeAttr, [ Stat.SPD ], -1)
|
||||||
.reflectable(),
|
.reflectable(),
|
||||||
new SelfStatusMove(MoveId.LASER_FOCUS, PokemonType.NORMAL, -1, 30, -1, 0, 7)
|
new SelfStatusMove(MoveId.LASER_FOCUS, PokemonType.NORMAL, -1, 30, -1, 0, 7)
|
||||||
.attr(AddBattlerTagAttr, BattlerTagType.ALWAYS_CRIT, true, false),
|
.attr(AddBattlerTagAttr, BattlerTagType.ALWAYS_CRIT, true, false)
|
||||||
|
.attr(MessageAttr, (user) =>
|
||||||
|
i18next.t("battlerTags:laserFocusOnAdd", {
|
||||||
|
pokemonNameWithAffix: getPokemonNameWithAffix(user),
|
||||||
|
}),
|
||||||
|
),
|
||||||
new StatusMove(MoveId.GEAR_UP, PokemonType.STEEL, -1, 20, -1, 0, 7)
|
new StatusMove(MoveId.GEAR_UP, PokemonType.STEEL, -1, 20, -1, 0, 7)
|
||||||
.attr(StatStageChangeAttr, [ Stat.ATK, Stat.SPATK ], 1, false, { condition: (user, target, move) => !![ AbilityId.PLUS, AbilityId.MINUS ].find(a => target.hasAbility(a, false)) })
|
.attr(StatStageChangeAttr, [ Stat.ATK, Stat.SPATK ], 1, false, { condition: (user, target, move) => !![ AbilityId.PLUS, AbilityId.MINUS ].find(a => target.hasAbility(a, false)) })
|
||||||
.ignoresSubstitute()
|
.ignoresSubstitute()
|
||||||
|
@ -36,5 +36,6 @@ export enum ArenaTagType {
|
|||||||
WATER_FIRE_PLEDGE = "WATER_FIRE_PLEDGE",
|
WATER_FIRE_PLEDGE = "WATER_FIRE_PLEDGE",
|
||||||
GRASS_WATER_PLEDGE = "GRASS_WATER_PLEDGE",
|
GRASS_WATER_PLEDGE = "GRASS_WATER_PLEDGE",
|
||||||
FAIRY_LOCK = "FAIRY_LOCK",
|
FAIRY_LOCK = "FAIRY_LOCK",
|
||||||
NEUTRALIZING_GAS = "NEUTRALIZING_GAS"
|
NEUTRALIZING_GAS = "NEUTRALIZING_GAS",
|
||||||
|
PENDING_HEAL = "PENDING_HEAL"
|
||||||
}
|
}
|
||||||
|
@ -229,7 +229,6 @@ export class PhaseManager {
|
|||||||
|
|
||||||
/** overrides default of inserting phases to end of phaseQueuePrepend array. Useful for inserting Phases "out of order" */
|
/** overrides default of inserting phases to end of phaseQueuePrepend array. Useful for inserting Phases "out of order" */
|
||||||
private phaseQueuePrependSpliceIndex = -1;
|
private phaseQueuePrependSpliceIndex = -1;
|
||||||
private nextCommandPhaseQueue: Phase[] = [];
|
|
||||||
|
|
||||||
/** Storage for {@linkcode PhasePriorityQueue}s which hold phases whose order dynamically changes */
|
/** Storage for {@linkcode PhasePriorityQueue}s which hold phases whose order dynamically changes */
|
||||||
private dynamicPhaseQueues: PhasePriorityQueue[];
|
private dynamicPhaseQueues: PhasePriorityQueue[];
|
||||||
@ -285,13 +284,12 @@ export class PhaseManager {
|
|||||||
/**
|
/**
|
||||||
* Adds a phase to nextCommandPhaseQueue, as long as boolean passed in is false
|
* Adds a phase to nextCommandPhaseQueue, as long as boolean passed in is false
|
||||||
* @param phase {@linkcode Phase} the phase to add
|
* @param phase {@linkcode Phase} the phase to add
|
||||||
* @param defer boolean on which queue to add to, defaults to false, and adds to phaseQueue
|
|
||||||
*/
|
*/
|
||||||
pushPhase(phase: Phase, defer = false): void {
|
pushPhase(phase: Phase): void {
|
||||||
if (this.getDynamicPhaseType(phase) !== undefined) {
|
if (this.getDynamicPhaseType(phase) !== undefined) {
|
||||||
this.pushDynamicPhase(phase);
|
this.pushDynamicPhase(phase);
|
||||||
} else {
|
} else {
|
||||||
(!defer ? this.phaseQueue : this.nextCommandPhaseQueue).push(phase);
|
this.phaseQueue.push(phase);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -318,7 +316,7 @@ export class PhaseManager {
|
|||||||
* Clears all phase-related stuff, including all phase queues, the current and standby phases, and a splice index
|
* Clears all phase-related stuff, including all phase queues, the current and standby phases, and a splice index
|
||||||
*/
|
*/
|
||||||
clearAllPhases(): void {
|
clearAllPhases(): void {
|
||||||
for (const queue of [this.phaseQueue, this.phaseQueuePrepend, this.conditionalQueue, this.nextCommandPhaseQueue]) {
|
for (const queue of [this.phaseQueue, this.phaseQueuePrepend, this.conditionalQueue]) {
|
||||||
queue.splice(0, queue.length);
|
queue.splice(0, queue.length);
|
||||||
}
|
}
|
||||||
this.dynamicPhaseQueues.forEach(queue => queue.clear());
|
this.dynamicPhaseQueues.forEach(queue => queue.clear());
|
||||||
@ -600,10 +598,6 @@ export class PhaseManager {
|
|||||||
* Moves everything from nextCommandPhaseQueue to phaseQueue (keeping order)
|
* Moves everything from nextCommandPhaseQueue to phaseQueue (keeping order)
|
||||||
*/
|
*/
|
||||||
private populatePhaseQueue(): void {
|
private populatePhaseQueue(): void {
|
||||||
if (this.nextCommandPhaseQueue.length) {
|
|
||||||
this.phaseQueue.push(...this.nextCommandPhaseQueue);
|
|
||||||
this.nextCommandPhaseQueue.splice(0, this.nextCommandPhaseQueue.length);
|
|
||||||
}
|
|
||||||
this.phaseQueue.push(new TurnInitPhase());
|
this.phaseQueue.push(new TurnInitPhase());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -63,7 +63,8 @@ export class PokemonHealPhase extends CommonAnimPhase {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const hasMessage = !!this.message;
|
const hasMessage = !!this.message;
|
||||||
const healOrDamage = !pokemon.isFullHp() || this.hpHealed < 0;
|
const canRestorePP = this.fullRestorePP && pokemon.getMoveset().some(mv => mv.ppUsed > 0);
|
||||||
|
const healOrDamage = !pokemon.isFullHp() || this.hpHealed < 0 || canRestorePP;
|
||||||
const healBlock = pokemon.getTag(BattlerTagType.HEAL_BLOCK) as HealBlockTag;
|
const healBlock = pokemon.getTag(BattlerTagType.HEAL_BLOCK) as HealBlockTag;
|
||||||
let lastStatusEffect = StatusEffect.NONE;
|
let lastStatusEffect = StatusEffect.NONE;
|
||||||
|
|
||||||
|
@ -2,6 +2,7 @@ import { applyAbAttrs } from "#abilities/apply-ab-attrs";
|
|||||||
import { globalScene } from "#app/global-scene";
|
import { globalScene } from "#app/global-scene";
|
||||||
import { ArenaTrapTag } from "#data/arena-tag";
|
import { ArenaTrapTag } from "#data/arena-tag";
|
||||||
import { MysteryEncounterPostSummonTag } from "#data/battler-tags";
|
import { MysteryEncounterPostSummonTag } from "#data/battler-tags";
|
||||||
|
import { ArenaTagType } from "#enums/arena-tag-type";
|
||||||
import { BattlerTagType } from "#enums/battler-tag-type";
|
import { BattlerTagType } from "#enums/battler-tag-type";
|
||||||
import { StatusEffect } from "#enums/status-effect";
|
import { StatusEffect } from "#enums/status-effect";
|
||||||
import { PokemonPhase } from "#phases/pokemon-phase";
|
import { PokemonPhase } from "#phases/pokemon-phase";
|
||||||
@ -16,6 +17,9 @@ export class PostSummonPhase extends PokemonPhase {
|
|||||||
if (pokemon.status?.effect === StatusEffect.TOXIC) {
|
if (pokemon.status?.effect === StatusEffect.TOXIC) {
|
||||||
pokemon.status.toxicTurnCount = 0;
|
pokemon.status.toxicTurnCount = 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
globalScene.arena.applyTags(ArenaTagType.PENDING_HEAL, false, pokemon);
|
||||||
|
|
||||||
globalScene.arena.applyTags(ArenaTrapTag, false, pokemon);
|
globalScene.arena.applyTags(ArenaTrapTag, false, pokemon);
|
||||||
|
|
||||||
// If this is mystery encounter and has post summon phase tag, apply post summon effects
|
// If this is mystery encounter and has post summon phase tag, apply post summon effects
|
||||||
|
@ -287,6 +287,12 @@ export class ArenaFlyout extends Phaser.GameObjects.Container {
|
|||||||
switch (arenaEffectChangedEvent.constructor) {
|
switch (arenaEffectChangedEvent.constructor) {
|
||||||
case TagAddedEvent: {
|
case TagAddedEvent: {
|
||||||
const tagAddedEvent = arenaEffectChangedEvent as TagAddedEvent;
|
const tagAddedEvent = arenaEffectChangedEvent as TagAddedEvent;
|
||||||
|
|
||||||
|
const excludedTags = [ArenaTagType.PENDING_HEAL];
|
||||||
|
if (excludedTags.includes(tagAddedEvent.arenaTagType)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const isArenaTrapTag = globalScene.arena.getTag(tagAddedEvent.arenaTagType) instanceof ArenaTrapTag;
|
const isArenaTrapTag = globalScene.arena.getTag(tagAddedEvent.arenaTagType) instanceof ArenaTrapTag;
|
||||||
let arenaEffectType: ArenaEffectType;
|
let arenaEffectType: ArenaEffectType;
|
||||||
|
|
||||||
|
245
test/moves/healing-wish-lunar-dance.test.ts
Normal file
245
test/moves/healing-wish-lunar-dance.test.ts
Normal file
@ -0,0 +1,245 @@
|
|||||||
|
import { AbilityId } from "#enums/ability-id";
|
||||||
|
import { ArenaTagType } from "#enums/arena-tag-type";
|
||||||
|
import { Challenges } from "#enums/challenges";
|
||||||
|
import { MoveId } from "#enums/move-id";
|
||||||
|
import { MoveResult } from "#enums/move-result";
|
||||||
|
import { PokemonType } from "#enums/pokemon-type";
|
||||||
|
import { SpeciesId } from "#enums/species-id";
|
||||||
|
import { StatusEffect } from "#enums/status-effect";
|
||||||
|
import { GameManager } from "#test/test-utils/game-manager";
|
||||||
|
import Phaser from "phaser";
|
||||||
|
import { afterEach, beforeAll, beforeEach, describe, expect, it } from "vitest";
|
||||||
|
|
||||||
|
describe("Moves - Lunar Dance and Healing Wish", () => {
|
||||||
|
let phaserGame: Phaser.Game;
|
||||||
|
let game: GameManager;
|
||||||
|
|
||||||
|
beforeAll(() => {
|
||||||
|
phaserGame = new Phaser.Game({
|
||||||
|
type: Phaser.HEADLESS,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
game.phaseInterceptor.restoreOg();
|
||||||
|
});
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
game = new GameManager(phaserGame);
|
||||||
|
game.override.battleStyle("double").enemyAbility(AbilityId.BALL_FETCH).enemyMoveset(MoveId.SPLASH);
|
||||||
|
});
|
||||||
|
|
||||||
|
describe.each([
|
||||||
|
{ moveName: "Healing Wish", moveId: MoveId.HEALING_WISH },
|
||||||
|
{ moveName: "Lunar Dance", moveId: MoveId.LUNAR_DANCE },
|
||||||
|
])("$moveName", ({ moveId }) => {
|
||||||
|
it("should sacrifice the user to restore the switched in Pokemon's HP", async () => {
|
||||||
|
await game.classicMode.startBattle([SpeciesId.BULBASAUR, SpeciesId.CHARMANDER, SpeciesId.SQUIRTLE]);
|
||||||
|
|
||||||
|
const [bulbasaur, charmander, squirtle] = game.scene.getPlayerParty();
|
||||||
|
squirtle.hp = 1;
|
||||||
|
|
||||||
|
game.move.use(MoveId.SPLASH, 0);
|
||||||
|
game.move.use(moveId, 1);
|
||||||
|
game.doSelectPartyPokemon(2);
|
||||||
|
|
||||||
|
await game.toNextTurn();
|
||||||
|
|
||||||
|
expect(bulbasaur.isFullHp()).toBe(true);
|
||||||
|
expect(charmander.isFainted()).toBe(true);
|
||||||
|
expect(squirtle.isFullHp()).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should sacrifice the user to cure the switched in Pokemon's status", async () => {
|
||||||
|
game.override.statusEffect(StatusEffect.BURN);
|
||||||
|
|
||||||
|
await game.classicMode.startBattle([SpeciesId.BULBASAUR, SpeciesId.CHARMANDER, SpeciesId.SQUIRTLE]);
|
||||||
|
const [bulbasaur, charmander, squirtle] = game.scene.getPlayerParty();
|
||||||
|
|
||||||
|
game.move.use(MoveId.SPLASH, 0);
|
||||||
|
game.move.use(moveId, 1);
|
||||||
|
game.doSelectPartyPokemon(2);
|
||||||
|
|
||||||
|
await game.toNextTurn();
|
||||||
|
|
||||||
|
expect(bulbasaur.status?.effect).toBe(StatusEffect.BURN);
|
||||||
|
expect(charmander.isFainted()).toBe(true);
|
||||||
|
expect(squirtle.status?.effect).toBeUndefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should fail if the user has no non-fainted allies in their party", async () => {
|
||||||
|
game.override.battleStyle("single");
|
||||||
|
|
||||||
|
await game.classicMode.startBattle([SpeciesId.BULBASAUR, SpeciesId.CHARMANDER]);
|
||||||
|
const [bulbasaur, charmander] = game.scene.getPlayerParty();
|
||||||
|
|
||||||
|
game.move.use(MoveId.MEMENTO);
|
||||||
|
game.doSelectPartyPokemon(1);
|
||||||
|
|
||||||
|
await game.toNextTurn();
|
||||||
|
|
||||||
|
expect(bulbasaur.isFainted()).toBe(true);
|
||||||
|
expect(charmander.isActive(true)).toBe(true);
|
||||||
|
|
||||||
|
game.move.use(moveId);
|
||||||
|
|
||||||
|
await game.toEndOfTurn();
|
||||||
|
|
||||||
|
expect(charmander.isFullHp());
|
||||||
|
expect(charmander.getLastXMoves()[0].result).toBe(MoveResult.FAIL);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should fail if the user has no challenge-eligible allies", async () => {
|
||||||
|
game.override.battleStyle("single");
|
||||||
|
// Mono normal challenge
|
||||||
|
game.challengeMode.addChallenge(Challenges.SINGLE_TYPE, PokemonType.NORMAL + 1, 0);
|
||||||
|
await game.challengeMode.startBattle([SpeciesId.RATICATE, SpeciesId.ODDISH]);
|
||||||
|
|
||||||
|
const raticate = game.field.getPlayerPokemon();
|
||||||
|
|
||||||
|
game.move.use(moveId);
|
||||||
|
await game.toNextTurn();
|
||||||
|
|
||||||
|
expect(raticate.isFullHp()).toBe(true);
|
||||||
|
expect(raticate.getLastXMoves()[0].result).toEqual(MoveResult.FAIL);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should store its effect if the switched-in Pokemon would be unaffected", async () => {
|
||||||
|
game.override.battleStyle("single");
|
||||||
|
|
||||||
|
await game.classicMode.startBattle([SpeciesId.BULBASAUR, SpeciesId.CHARMANDER, SpeciesId.SQUIRTLE]);
|
||||||
|
|
||||||
|
const [bulbasaur, charmander, squirtle] = game.scene.getPlayerParty();
|
||||||
|
squirtle.hp = 1;
|
||||||
|
|
||||||
|
game.move.use(moveId);
|
||||||
|
game.doSelectPartyPokemon(1);
|
||||||
|
|
||||||
|
await game.toNextTurn();
|
||||||
|
|
||||||
|
// Bulbasaur fainted and stored a healing effect
|
||||||
|
expect(bulbasaur.isFainted()).toBe(true);
|
||||||
|
expect(charmander.isFullHp()).toBe(true);
|
||||||
|
expect(game.phaseInterceptor.log).not.toContain("PokemonHealPhase");
|
||||||
|
expect(game.scene.arena.getTag(ArenaTagType.PENDING_HEAL)).toBeDefined();
|
||||||
|
|
||||||
|
await game.toNextTurn();
|
||||||
|
|
||||||
|
// Switch to damaged Squirtle. HW/LD's effect should activate
|
||||||
|
|
||||||
|
await game.toEndOfTurn();
|
||||||
|
expect(squirtle.isFullHp()).toBe(true);
|
||||||
|
expect(game.scene.arena.getTag(ArenaTagType.PENDING_HEAL)).toBeUndefined();
|
||||||
|
|
||||||
|
// Set Charmander's HP to 1, then switch back to Charmander.
|
||||||
|
// HW/LD shouldn't activate again
|
||||||
|
charmander.hp = 1;
|
||||||
|
game.doSwitchPokemon(2);
|
||||||
|
|
||||||
|
await game.toEndOfTurn();
|
||||||
|
expect(charmander.hp).toBe(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should only store one charge of the effect at a time", async () => {
|
||||||
|
game.override.battleStyle("single");
|
||||||
|
|
||||||
|
await game.classicMode.startBattle([
|
||||||
|
SpeciesId.BULBASAUR,
|
||||||
|
SpeciesId.CHARMANDER,
|
||||||
|
SpeciesId.SQUIRTLE,
|
||||||
|
SpeciesId.PIKACHU,
|
||||||
|
]);
|
||||||
|
|
||||||
|
const [bulbasaur, charmander, squirtle, pikachu] = game.scene.getPlayerParty();
|
||||||
|
[squirtle, pikachu].forEach(p => (p.hp = 1));
|
||||||
|
|
||||||
|
// Use HW/LD and send in Charmander. HW/LD's effect should be stored
|
||||||
|
game.move.use(moveId);
|
||||||
|
game.doSelectPartyPokemon(1);
|
||||||
|
|
||||||
|
await game.toNextTurn();
|
||||||
|
expect(bulbasaur.isFainted()).toBe(true);
|
||||||
|
expect(charmander.isFullHp()).toBe(true);
|
||||||
|
expect(charmander.isFullHp());
|
||||||
|
expect(game.phaseInterceptor.log).not.toContain("PokemonHealPhase");
|
||||||
|
expect(game.scene.arena.getTag(ArenaTagType.PENDING_HEAL)).toBeDefined();
|
||||||
|
|
||||||
|
// Use HW/LD again, sending in Squirtle. HW/LD should activate and heal Squirtle
|
||||||
|
game.move.use(moveId);
|
||||||
|
game.doSelectPartyPokemon(2);
|
||||||
|
|
||||||
|
expect(charmander.isFainted()).toBe(true);
|
||||||
|
expect(squirtle.isFullHp()).toBe(true);
|
||||||
|
expect(squirtle.isFullHp());
|
||||||
|
|
||||||
|
// Switch again to Pikachu. HW/LD's effect shouldn't be present
|
||||||
|
game.doSwitchPokemon(3);
|
||||||
|
|
||||||
|
expect(pikachu.isFullHp()).toBe(false);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("Lunar Dance should sacrifice the user to restore the switched in Pokemon's PP", async () => {
|
||||||
|
game.override.battleStyle("single");
|
||||||
|
|
||||||
|
await game.classicMode.startBattle([SpeciesId.BULBASAUR, SpeciesId.CHARMANDER]);
|
||||||
|
|
||||||
|
const [bulbasaur, charmander] = game.scene.getPlayerParty();
|
||||||
|
|
||||||
|
game.move.use(MoveId.SPLASH);
|
||||||
|
await game.toNextTurn();
|
||||||
|
|
||||||
|
game.doSwitchPokemon(1);
|
||||||
|
await game.toNextTurn();
|
||||||
|
|
||||||
|
game.move.use(MoveId.LUNAR_DANCE);
|
||||||
|
game.doSelectPartyPokemon(1);
|
||||||
|
|
||||||
|
await game.toNextTurn();
|
||||||
|
expect(charmander.isFainted()).toBeTruthy();
|
||||||
|
bulbasaur.getMoveset().forEach(mv => expect(mv.ppUsed).toBe(0));
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should stack with each other", async () => {
|
||||||
|
game.override.battleStyle("single");
|
||||||
|
|
||||||
|
await game.classicMode.startBattle([
|
||||||
|
SpeciesId.BULBASAUR,
|
||||||
|
SpeciesId.CHARMANDER,
|
||||||
|
SpeciesId.SQUIRTLE,
|
||||||
|
SpeciesId.PIKACHU,
|
||||||
|
]);
|
||||||
|
|
||||||
|
const [bulbasaur, charmander, squirtle, pikachu] = game.scene.getPlayerParty();
|
||||||
|
[squirtle, pikachu].forEach(p => {
|
||||||
|
p.hp = 1;
|
||||||
|
p.getMoveset().forEach(mv => (mv.ppUsed = 1));
|
||||||
|
});
|
||||||
|
|
||||||
|
game.move.use(MoveId.LUNAR_DANCE);
|
||||||
|
game.doSelectPartyPokemon(1);
|
||||||
|
|
||||||
|
await game.toNextTurn();
|
||||||
|
expect(bulbasaur.isFainted()).toBe(true);
|
||||||
|
expect(charmander.isFullHp()).toBe(true);
|
||||||
|
expect(game.phaseInterceptor.log).not.toContain("PokemonHealPhase");
|
||||||
|
expect(game.scene.arena.getTag(ArenaTagType.PENDING_HEAL)).toBeDefined();
|
||||||
|
|
||||||
|
game.move.use(MoveId.HEALING_WISH);
|
||||||
|
game.doSelectPartyPokemon(2);
|
||||||
|
|
||||||
|
// Lunar Dance should apply first since it was used first, restoring Squirtle's HP and PP
|
||||||
|
await game.toNextTurn();
|
||||||
|
expect(squirtle.isFullHp()).toBe(true);
|
||||||
|
squirtle.getMoveset().forEach(mv => expect(mv.ppUsed).toBe(0));
|
||||||
|
expect(game.scene.arena.getTag(ArenaTagType.PENDING_HEAL)).toBeDefined();
|
||||||
|
|
||||||
|
game.doSwitchPokemon(3);
|
||||||
|
|
||||||
|
// Healing Wish should apply on the next switch, restoring Pikachu's HP
|
||||||
|
await game.toEndOfTurn();
|
||||||
|
expect(pikachu.isFullHp()).toBe(true);
|
||||||
|
pikachu.getMoveset().forEach(mv => expect(mv.ppUsed).toBe(1));
|
||||||
|
expect(game.scene.arena.getTag(ArenaTagType.PENDING_HEAL)).toBeUndefined();
|
||||||
|
});
|
||||||
|
});
|
@ -1,73 +0,0 @@
|
|||||||
import { AbilityId } from "#enums/ability-id";
|
|
||||||
import { MoveId } from "#enums/move-id";
|
|
||||||
import { SpeciesId } from "#enums/species-id";
|
|
||||||
import { StatusEffect } from "#enums/status-effect";
|
|
||||||
import { GameManager } from "#test/test-utils/game-manager";
|
|
||||||
import Phaser from "phaser";
|
|
||||||
import { afterEach, beforeAll, beforeEach, describe, expect, it } from "vitest";
|
|
||||||
|
|
||||||
describe("Moves - Lunar Dance", () => {
|
|
||||||
let phaserGame: Phaser.Game;
|
|
||||||
let game: GameManager;
|
|
||||||
|
|
||||||
beforeAll(() => {
|
|
||||||
phaserGame = new Phaser.Game({
|
|
||||||
type: Phaser.HEADLESS,
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
afterEach(() => {
|
|
||||||
game.phaseInterceptor.restoreOg();
|
|
||||||
});
|
|
||||||
|
|
||||||
beforeEach(() => {
|
|
||||||
game = new GameManager(phaserGame);
|
|
||||||
game.override
|
|
||||||
.statusEffect(StatusEffect.BURN)
|
|
||||||
.battleStyle("double")
|
|
||||||
.enemyAbility(AbilityId.BALL_FETCH)
|
|
||||||
.enemyMoveset(MoveId.SPLASH);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should full restore HP, PP and status of switched in pokemon, then fail second use because no remaining backup pokemon in party", async () => {
|
|
||||||
await game.classicMode.startBattle([SpeciesId.BULBASAUR, SpeciesId.ODDISH, SpeciesId.RATTATA]);
|
|
||||||
|
|
||||||
const [bulbasaur, oddish, rattata] = game.scene.getPlayerParty();
|
|
||||||
game.move.changeMoveset(bulbasaur, [MoveId.LUNAR_DANCE, MoveId.SPLASH]);
|
|
||||||
game.move.changeMoveset(oddish, [MoveId.LUNAR_DANCE, MoveId.SPLASH]);
|
|
||||||
game.move.changeMoveset(rattata, [MoveId.LUNAR_DANCE, MoveId.SPLASH]);
|
|
||||||
|
|
||||||
game.move.select(MoveId.SPLASH, 0);
|
|
||||||
game.move.select(MoveId.SPLASH, 1);
|
|
||||||
await game.toNextTurn();
|
|
||||||
|
|
||||||
// Bulbasaur should still be burned and have used a PP for splash and not at max hp
|
|
||||||
expect(bulbasaur.status?.effect).toBe(StatusEffect.BURN);
|
|
||||||
expect(bulbasaur.moveset[1]?.ppUsed).toBe(1);
|
|
||||||
expect(bulbasaur.hp).toBeLessThan(bulbasaur.getMaxHp());
|
|
||||||
|
|
||||||
// Switch out Bulbasaur for Rattata so we can swtich bulbasaur back in with lunar dance
|
|
||||||
game.doSwitchPokemon(2);
|
|
||||||
game.move.select(MoveId.SPLASH, 1);
|
|
||||||
await game.toNextTurn();
|
|
||||||
|
|
||||||
game.move.select(MoveId.SPLASH, 0);
|
|
||||||
game.move.select(MoveId.LUNAR_DANCE);
|
|
||||||
game.doSelectPartyPokemon(2);
|
|
||||||
await game.phaseInterceptor.to("SwitchPhase", false);
|
|
||||||
await game.toNextTurn();
|
|
||||||
|
|
||||||
// Bulbasaur should NOT have any status and have full PP for splash and be at max hp
|
|
||||||
expect(bulbasaur.status?.effect).toBeUndefined();
|
|
||||||
expect(bulbasaur.moveset[1]?.ppUsed).toBe(0);
|
|
||||||
expect(bulbasaur.isFullHp()).toBe(true);
|
|
||||||
|
|
||||||
game.move.select(MoveId.SPLASH, 0);
|
|
||||||
game.move.select(MoveId.LUNAR_DANCE);
|
|
||||||
await game.toNextTurn();
|
|
||||||
|
|
||||||
// Using Lunar dance again should fail because nothing in party and rattata should be alive
|
|
||||||
expect(rattata.status?.effect).toBe(StatusEffect.BURN);
|
|
||||||
expect(rattata.hp).toBeLessThan(rattata.getMaxHp());
|
|
||||||
});
|
|
||||||
});
|
|
@ -1,4 +1,3 @@
|
|||||||
import { globalScene } from "#app/global-scene";
|
|
||||||
import { Status } from "#data/status-effect";
|
import { Status } from "#data/status-effect";
|
||||||
import { AbilityId } from "#enums/ability-id";
|
import { AbilityId } from "#enums/ability-id";
|
||||||
import { BattleType } from "#enums/battle-type";
|
import { BattleType } from "#enums/battle-type";
|
||||||
@ -179,18 +178,13 @@ describe("Moves - Whirlwind", () => {
|
|||||||
const eligibleEnemy = enemyParty.filter(p => p.hp > 0 && p.isAllowedInBattle());
|
const eligibleEnemy = enemyParty.filter(p => p.hp > 0 && p.isAllowedInBattle());
|
||||||
expect(eligibleEnemy.length).toBe(1);
|
expect(eligibleEnemy.length).toBe(1);
|
||||||
|
|
||||||
// Spy on the queueMessage function
|
|
||||||
const queueSpy = vi.spyOn(globalScene.phaseManager, "queueMessage");
|
|
||||||
|
|
||||||
// Player uses Whirlwind; opponent uses Splash
|
// Player uses Whirlwind; opponent uses Splash
|
||||||
game.move.select(MoveId.WHIRLWIND);
|
game.move.select(MoveId.WHIRLWIND);
|
||||||
await game.move.selectEnemyMove(MoveId.SPLASH);
|
await game.move.selectEnemyMove(MoveId.SPLASH);
|
||||||
await game.toNextTurn();
|
await game.toNextTurn();
|
||||||
|
|
||||||
// Verify that the failure message is displayed for Whirlwind
|
const player = game.field.getPlayerPokemon();
|
||||||
expect(queueSpy).toHaveBeenCalledWith(expect.stringContaining("But it failed"));
|
expect(player).toHaveUsedMove({ move: MoveId.WHIRLWIND, result: MoveResult.FAIL });
|
||||||
// Verify the opponent's Splash message
|
|
||||||
expect(queueSpy).toHaveBeenCalledWith(expect.stringContaining("But nothing happened!"));
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should not pull in the other trainer's pokemon in a partner trainer battle", async () => {
|
it("should not pull in the other trainer's pokemon in a partner trainer battle", async () => {
|
||||||
|
Loading…
Reference in New Issue
Block a user