mirror of
https://github.com/pagefaultgames/pokerogue.git
synced 2025-08-10 17:39:31 +02:00
Added strong typing to the manager, finished save load stufff
This commit is contained in:
parent
4d5d6b56a9
commit
e3fb95ffc1
@ -8,7 +8,6 @@ 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 { MoveCategory } from "#enums/MoveCategory";
|
import { MoveCategory } from "#enums/MoveCategory";
|
||||||
@ -536,45 +535,6 @@ export class NoCritTag extends ArenaTag {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Arena Tag class for {@link https://bulbapedia.bulbagarden.net/wiki/Wish_(move) | Wish}.
|
|
||||||
* Heals the Pokémon in the user's position the turn after Wish is used.
|
|
||||||
*/
|
|
||||||
class WishTag extends ArenaTag {
|
|
||||||
private battlerIndex: BattlerIndex;
|
|
||||||
private triggerMessage: string;
|
|
||||||
private healHp: number;
|
|
||||||
|
|
||||||
constructor(turnCount: number, sourceId: number, side: ArenaTagSide) {
|
|
||||||
super(ArenaTagType.WISH, turnCount, MoveId.WISH, sourceId, side);
|
|
||||||
}
|
|
||||||
|
|
||||||
onAdd(_arena: Arena): void {
|
|
||||||
const source = this.getSourcePokemon();
|
|
||||||
if (!source) {
|
|
||||||
console.warn(`Failed to get source Pokemon for WishTag on add message; id: ${this.sourceId}`);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
super.onAdd(_arena);
|
|
||||||
this.healHp = toDmgValue(source.getMaxHp() / 2);
|
|
||||||
|
|
||||||
globalScene.phaseManager.queueMessage(
|
|
||||||
i18next.t("arenaTag:wishTagOnAdd", {
|
|
||||||
pokemonNameWithAffix: getPokemonNameWithAffix(source),
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
onRemove(_arena: Arena): void {
|
|
||||||
const target = globalScene.getField()[this.battlerIndex];
|
|
||||||
if (target?.isActive(true)) {
|
|
||||||
globalScene.phaseManager.queueMessage(this.triggerMessage);
|
|
||||||
globalScene.phaseManager.unshiftNew("PokemonHealPhase", target.getBattlerIndex(), this.healHp, null, true, false);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Abstract class to implement weakened moves of a specific type.
|
* Abstract class to implement weakened moves of a specific type.
|
||||||
*/
|
*/
|
||||||
@ -1539,8 +1499,6 @@ export function getArenaTag(
|
|||||||
return new SpikesTag(sourceId, side);
|
return new SpikesTag(sourceId, side);
|
||||||
case ArenaTagType.TOXIC_SPIKES:
|
case ArenaTagType.TOXIC_SPIKES:
|
||||||
return new ToxicSpikesTag(sourceId, side);
|
return new ToxicSpikesTag(sourceId, side);
|
||||||
case ArenaTagType.WISH:
|
|
||||||
return new WishTag(turnCount, sourceId, side);
|
|
||||||
case ArenaTagType.STEALTH_ROCK:
|
case ArenaTagType.STEALTH_ROCK:
|
||||||
return new StealthRockTag(sourceId, side);
|
return new StealthRockTag(sourceId, side);
|
||||||
case ArenaTagType.STICKY_WEB:
|
case ArenaTagType.STICKY_WEB:
|
||||||
|
@ -3105,7 +3105,7 @@ export class OverrideMoveEffectAttr extends MoveAttr {
|
|||||||
* @param target - The {@linkcode Pokemon} targeted by the move
|
* @param target - The {@linkcode Pokemon} targeted by the move
|
||||||
* @param move - The {@linkcode Move} being used
|
* @param move - The {@linkcode Move} being used
|
||||||
* @param args -
|
* @param args -
|
||||||
* `[0]`: A {@linkcode BooleanHolder} containing whether move effects were successfully overridden; should be set to `true` on success
|
* `[0]`: A {@linkcode BooleanHolder} containing whether move effects were successfully overridden; should be set to `true` on success \
|
||||||
* `[1]`: The {@linkcode MoveUseMode} dictating how this move was used.
|
* `[1]`: The {@linkcode MoveUseMode} dictating how this move was used.
|
||||||
* @returns `true` if the move effect was successfully overridden.
|
* @returns `true` if the move effect was successfully overridden.
|
||||||
*/
|
*/
|
||||||
@ -3114,6 +3114,16 @@ export class OverrideMoveEffectAttr extends MoveAttr {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Abstract class for moves that add {@linkcode PositionalTag}s to the field. */
|
||||||
|
abstract class AddPositionalTagAttr extends OverrideMoveEffectAttr {
|
||||||
|
protected abstract readonly tagType: PositionalTagType;
|
||||||
|
|
||||||
|
public override getCondition(): MoveConditionFunc {
|
||||||
|
// Check the arena if another similar positional tag is active and affecting the same slot
|
||||||
|
return (_user, target, move) => globalScene.arena.positionalTagManager.canAddTag(this.tagType, target.getBattlerIndex(), move.id)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Attribute to implement delayed attacks, such as {@linkcode MoveId.FUTURE_SIGHT} or {@linkcode MoveId.DOOM_DESIRE}.
|
* Attribute to implement delayed attacks, such as {@linkcode MoveId.FUTURE_SIGHT} or {@linkcode MoveId.DOOM_DESIRE}.
|
||||||
* Delays the attack's effect with a {@linkcode DelayedAttackTag},
|
* Delays the attack's effect with a {@linkcode DelayedAttackTag},
|
||||||
@ -3135,16 +3145,11 @@ export class DelayedAttackAttr extends OverrideMoveEffectAttr {
|
|||||||
this.chargeText = chargeKey;
|
this.chargeText = chargeKey;
|
||||||
}
|
}
|
||||||
|
|
||||||
getCondition(): MoveConditionFunc {
|
|
||||||
// Check the arena if another delayed attack is active and hitting the same slot
|
|
||||||
return (_user, target, move) => globalScene.arena.positionalTagManager.canAddTag(PositionalTagType.DELAYED_ATTACK, target.getBattlerIndex(), move.id)
|
|
||||||
}
|
|
||||||
|
|
||||||
apply(user: Pokemon, target: Pokemon, move: Move, args: [overridden: BooleanHolder, useMode: MoveUseMode]): boolean {
|
apply(user: Pokemon, target: Pokemon, move: Move, args: [overridden: BooleanHolder, useMode: MoveUseMode]): boolean {
|
||||||
const useMode = args[1];
|
const useMode = args[1];
|
||||||
if (useMode === MoveUseMode.TRANSPARENT) {
|
if (useMode === MoveUseMode.TRANSPARENT) {
|
||||||
// don't trigger if already queueing an indirect attack
|
// don't trigger if already queueing an indirect attack
|
||||||
return true;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
const overridden = args[0];
|
const overridden = args[0];
|
||||||
@ -3159,14 +3164,34 @@ export class DelayedAttackAttr extends OverrideMoveEffectAttr {
|
|||||||
user.pushMoveHistory({move: move.id, targets: [target.getBattlerIndex()], result: MoveResult.OTHER, useMode: useMode, turn: globalScene.currentBattle.turn})
|
user.pushMoveHistory({move: move.id, targets: [target.getBattlerIndex()], result: MoveResult.OTHER, useMode: useMode, turn: globalScene.currentBattle.turn})
|
||||||
|
|
||||||
// Queue up an attack on the given slot.
|
// Queue up an attack on the given slot.
|
||||||
globalScene.arena.positionalTagManager.addTag({
|
globalScene.arena.positionalTagManager.addTag<PositionalTagType.DELAYED_ATTACK>({
|
||||||
tagType: PositionalTagType.DELAYED_ATTACK,
|
tagType: PositionalTagType.DELAYED_ATTACK,
|
||||||
sourceId: user.id,
|
sourceId: user.id,
|
||||||
targetIndex: target.getBattlerIndex(),
|
targetIndex: target.getBattlerIndex(),
|
||||||
sourceMove: move.id,
|
sourceMove: move.id,
|
||||||
turnCount: 3})
|
turnCount: 3
|
||||||
|
})
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public override getCondition(): MoveConditionFunc {
|
||||||
|
// Check the arena if another similar attack is active and affecting the same slot
|
||||||
|
return (_user, target, move) => globalScene.arena.positionalTagManager.canAddTag(PositionalTagType.DELAYED_ATTACK, target.getBattlerIndex(), move.id)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class WishAttr extends MoveEffectAttr {
|
||||||
|
apply(user: Pokemon, target: Pokemon, _move: Move): boolean {
|
||||||
|
globalScene.arena.positionalTagManager.addTag<PositionalTagType.WISH>({tagType: PositionalTagType.WISH, sourceId: user.id, healHp: toDmgValue(user.getMaxHp() / 2), targetIndex: target.getBattlerIndex(),
|
||||||
|
turnCount: 2,
|
||||||
|
});
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public override getCondition(): MoveConditionFunc {
|
||||||
|
// Check the arena if another similar attack is active and affecting the same slot
|
||||||
|
return (_user, target, move) => globalScene.arena.positionalTagManager.canAddTag(PositionalTagType.WISH, target.getBattlerIndex(), move.id)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -9288,8 +9313,8 @@ export function initMoves() {
|
|||||||
.ignoresSubstitute()
|
.ignoresSubstitute()
|
||||||
.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)
|
||||||
.triageMove()
|
.attr(WishAttr)
|
||||||
.attr(AddArenaTagAttr, ArenaTagType.WISH, 2, true),
|
.triageMove(),
|
||||||
new SelfStatusMove(MoveId.ASSIST, PokemonType.NORMAL, -1, 20, -1, 0, 3)
|
new SelfStatusMove(MoveId.ASSIST, PokemonType.NORMAL, -1, 20, -1, 0, 3)
|
||||||
.attr(RandomMovesetMoveAttr, invalidAssistMoves, true),
|
.attr(RandomMovesetMoveAttr, invalidAssistMoves, true),
|
||||||
new SelfStatusMove(MoveId.INGRAIN, PokemonType.GRASS, -1, 20, -1, 0, 3)
|
new SelfStatusMove(MoveId.INGRAIN, PokemonType.GRASS, -1, 20, -1, 0, 3)
|
||||||
|
63
src/data/positional-tags/load-positional-tag.ts
Normal file
63
src/data/positional-tags/load-positional-tag.ts
Normal file
@ -0,0 +1,63 @@
|
|||||||
|
import { DelayedAttackTag, type PositionalTag, WishTag } from "#data/positional-tags/positional-tag";
|
||||||
|
import { PositionalTagType } from "#enums/positional-tag-type";
|
||||||
|
import type { EnumValues } from "#types/enum-types";
|
||||||
|
import type { Constructor } from "#utils/common";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add a new {@linkcode PositionalTag} to the arena.
|
||||||
|
* @param tagType - The {@linkcode PositionalTagType} to create
|
||||||
|
* @param args - The arguments needed to instantize the given tag
|
||||||
|
* @remarks
|
||||||
|
* This function does not perform any checking if the added tag is valid.
|
||||||
|
*/
|
||||||
|
export function loadPositionalTag<T extends PositionalTagType>({
|
||||||
|
tagType,
|
||||||
|
...args
|
||||||
|
}: serializedPosTagParamMap[T]): posTagInstanceMap[T];
|
||||||
|
/**
|
||||||
|
* Add a new {@linkcode PositionalTag} to the arena.
|
||||||
|
* @param tag - The {@linkcode SerializedPositionalTag} to instantiate
|
||||||
|
* @remarks
|
||||||
|
* This function does not perform any checking if the added tag is valid.
|
||||||
|
*/
|
||||||
|
export function loadPositionalTag(tag: SerializedPositionalTag): PositionalTag;
|
||||||
|
export function loadPositionalTag<T extends PositionalTagType>({
|
||||||
|
tagType,
|
||||||
|
...rest
|
||||||
|
}: serializedPosTagParamMap[T]): posTagInstanceMap[T] {
|
||||||
|
// Note: We need 2 type assertions here:
|
||||||
|
// 1 because TS doesn't narrow the type of TagClass correctly based on `T`.
|
||||||
|
// It converts it into `new (DelayedAttackTag | WishTag) => DelayedAttackTag & WishTag`
|
||||||
|
const tagClass = posTagConstructorMap[tagType] as new (args: posTagParamMap[T]) => posTagInstanceMap[T];
|
||||||
|
// 2 because TS doesn't narrow `Omit<{tagType: T} & posTagParamMap[T], "tagType"> into `posTagParamMap[T]`
|
||||||
|
return new tagClass(rest as unknown as posTagParamMap[T]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Const object mapping tag types to their constructors. */
|
||||||
|
const posTagConstructorMap = Object.freeze({
|
||||||
|
[PositionalTagType.DELAYED_ATTACK]: DelayedAttackTag,
|
||||||
|
[PositionalTagType.WISH]: WishTag,
|
||||||
|
}) satisfies {
|
||||||
|
[k in PositionalTagType]: Constructor<PositionalTag & { tagType: k }>;
|
||||||
|
};
|
||||||
|
|
||||||
|
/** Type mapping tag types to their constructors. */
|
||||||
|
type posTagMap = typeof posTagConstructorMap;
|
||||||
|
|
||||||
|
/** Type mapping all positional tag types to their instances. */
|
||||||
|
type posTagInstanceMap = {
|
||||||
|
[k in PositionalTagType]: InstanceType<posTagMap[k]>;
|
||||||
|
};
|
||||||
|
|
||||||
|
/** Type mapping all positional tag types to their constructors' parameters. */
|
||||||
|
type posTagParamMap = {
|
||||||
|
[k in PositionalTagType]: ConstructorParameters<posTagMap[k]>[0];
|
||||||
|
};
|
||||||
|
|
||||||
|
/** Type mapping all positional tag types to their constructors' parameters, alongside the `tagType` selector. */
|
||||||
|
type serializedPosTagParamMap = {
|
||||||
|
[k in PositionalTagType]: posTagParamMap[k] & { tagType: k };
|
||||||
|
};
|
||||||
|
|
||||||
|
/** Union type containing all serialized {@linkcode PositionalTag}s. */
|
||||||
|
export type SerializedPositionalTag = EnumValues<serializedPosTagParamMap>;
|
@ -1,8 +1,5 @@
|
|||||||
import {
|
import { loadPositionalTag } from "#data/positional-tags/load-positional-tag";
|
||||||
loadPositionalTag,
|
import type { PositionalTag } from "#data/positional-tags/positional-tag";
|
||||||
type PositionalTag,
|
|
||||||
type SerializedPositionalTag,
|
|
||||||
} from "#data/positional-tags/positional-tag";
|
|
||||||
import type { BattlerIndex } from "#enums/battler-index";
|
import type { BattlerIndex } from "#enums/battler-index";
|
||||||
import type { MoveId } from "#enums/move-id";
|
import type { MoveId } from "#enums/move-id";
|
||||||
import type { PositionalTagType } from "#enums/positional-tag-type";
|
import type { PositionalTagType } from "#enums/positional-tag-type";
|
||||||
@ -10,19 +7,14 @@ import type { PositionalTagType } from "#enums/positional-tag-type";
|
|||||||
/** A manager for the {@linkcode PositionalTag}s in the arena. */
|
/** A manager for the {@linkcode PositionalTag}s in the arena. */
|
||||||
export class PositionalTagManager {
|
export class PositionalTagManager {
|
||||||
/** Array containing all pending unactivated {@linkcode PositionalTag}s, sorted by order of creation. */
|
/** Array containing all pending unactivated {@linkcode PositionalTag}s, sorted by order of creation. */
|
||||||
private tags: PositionalTag[] = [];
|
public tags: PositionalTag[] = [];
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Add a new {@linkcode SerializedPositionalTag} to the arena.
|
* Add a new {@linkcode PositionalTag} to the arena.
|
||||||
* @param tagType - The {@linkcode PositionalTagType} to create
|
|
||||||
* @param sourceId - The {@linkcode Pokemon.id | PID} of the Pokemon adding the tag
|
|
||||||
* @param sourceMove - The {@linkcode MoveId} causing the attack
|
|
||||||
* @param turnCount - The number of turns to delay the effect (_including the current turn_).
|
|
||||||
* @param targetIndex - The {@linkcode BattlerIndex} being targeted
|
|
||||||
* @remarks
|
* @remarks
|
||||||
* This function does not perform any checking if the added tag is valid.
|
* This function does not perform any checking if the added tag is valid.
|
||||||
*/
|
*/
|
||||||
public addTag(tag: SerializedPositionalTag): void {
|
public addTag<T extends PositionalTagType = never>(tag: Parameters<typeof loadPositionalTag<T>>[0]): void {
|
||||||
this.tags.push(loadPositionalTag(tag));
|
this.tags.push(loadPositionalTag(tag));
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -40,19 +32,21 @@ export class PositionalTagManager {
|
|||||||
/**
|
/**
|
||||||
* Decrement turn counts of and activate all pending {@linkcode PositionalTag}s on field.
|
* Decrement turn counts of and activate all pending {@linkcode PositionalTag}s on field.
|
||||||
* @remarks
|
* @remarks
|
||||||
* If multiple tags trigger simultaneously, they will activate **in order of initial creation**.
|
* If multiple tags trigger simultaneously, they will activate **in order of initial creation**, NOT speed order.
|
||||||
* (source: [Smogon](<https://www.smogon.com/forums/threads/sword-shield-battle-mechanics-research.3655528/page-64#post-9244179>))
|
* (Source: [Smogon](<https://www.smogon.com/forums/threads/sword-shield-battle-mechanics-research.3655528/page-64#post-9244179>))
|
||||||
*/
|
*/
|
||||||
triggerAllTags(): void {
|
public triggerAllTags(): void {
|
||||||
|
const leftoverTags: PositionalTag[] = [];
|
||||||
for (const tag of this.tags) {
|
for (const tag of this.tags) {
|
||||||
if (--tag.turnCount > 0) {
|
// Check for silent removal, immediately removing tags that.
|
||||||
// tag still cooking
|
if (!tag.shouldDisappear()) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check for silent removal
|
if (--tag.turnCount > 0) {
|
||||||
if (tag.shouldDisappear()) {
|
// tag still cooking
|
||||||
tag.turnCount = -1;
|
leftoverTags.push(tag);
|
||||||
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
tag.trigger();
|
tag.trigger();
|
||||||
|
@ -1,36 +1,31 @@
|
|||||||
// biome-ignore-start lint/correctness/noUnusedImports: TSDoc
|
|
||||||
import { globalScene } from "#app/global-scene";
|
import { globalScene } from "#app/global-scene";
|
||||||
import { getPokemonNameWithAffix } from "#app/messages";
|
import { getPokemonNameWithAffix } from "#app/messages";
|
||||||
|
// biome-ignore-start lint/correctness/noUnusedImports: TSDoc
|
||||||
|
import type { ArenaTag } from "#data/arena-tag";
|
||||||
|
// biome-ignore-end lint/correctness/noUnusedImports: TSDoc
|
||||||
import { allMoves } from "#data/data-lists";
|
import { allMoves } from "#data/data-lists";
|
||||||
import type { BattlerIndex } from "#enums/battler-index";
|
import type { BattlerIndex } from "#enums/battler-index";
|
||||||
import type { MoveId } from "#enums/move-id";
|
import type { MoveId } from "#enums/move-id";
|
||||||
import { MoveUseMode } from "#enums/move-use-mode";
|
import { MoveUseMode } from "#enums/move-use-mode";
|
||||||
import { PositionalTagLapseType } from "#enums/positional-tag-lapse-type";
|
|
||||||
import { PositionalTagType } from "#enums/positional-tag-type";
|
import { PositionalTagType } from "#enums/positional-tag-type";
|
||||||
import type { Constructor } from "#utils/common";
|
import type { Pokemon } from "#field/pokemon";
|
||||||
import i18next from "i18next";
|
import i18next from "i18next";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Serialized representation of a {@linkcode PositionalTag}.
|
* Baseline arguments used to construct all {@linkcode PositionalTag}s.
|
||||||
|
* Does not contain the `tagType` parameter (which is used to select the proper class constructor to use).
|
||||||
*/
|
*/
|
||||||
export interface SerializedPositionalTag {
|
export interface PositionalTagBaseArgs {
|
||||||
/**
|
|
||||||
* This tag's {@linkcode PositionalTagType | type}.
|
|
||||||
* Tags with similar types are considered "the same" for the purposes of overlaps.
|
|
||||||
*/
|
|
||||||
tagType: PositionalTagType;
|
|
||||||
/**
|
/**
|
||||||
* The {@linkcode Pokemon.id | PID} of the {@linkcode Pokemon} having created the effect.
|
* The {@linkcode Pokemon.id | PID} of the {@linkcode Pokemon} having created the effect.
|
||||||
*/
|
*/
|
||||||
sourceId: number;
|
sourceId: number;
|
||||||
/**
|
/**
|
||||||
* The {@linkcode MoveId} that created this effect.
|
* The number of turns remaining until activation. \
|
||||||
*/
|
|
||||||
sourceMove: MoveId;
|
|
||||||
/**
|
|
||||||
* The number of turns remaining until activation.
|
|
||||||
* Decremented by 1 at the end of each turn until reaching 0, at which point it will {@linkcode trigger} and be removed.
|
* Decremented by 1 at the end of each turn until reaching 0, at which point it will {@linkcode trigger} and be removed.
|
||||||
* If set to any number `<0` manually, will be silently removed at the end of the next turn without activating.
|
* @remarks
|
||||||
|
* If this is set to any number `<0` manually (such as through the effects of {@linkcode PositionalTag.shouldDisappear | shouldDisappear}),
|
||||||
|
* this tag will be silently removed at the end of the next turn _without activating any effects_.
|
||||||
*/
|
*/
|
||||||
turnCount: number;
|
turnCount: number;
|
||||||
/**
|
/**
|
||||||
@ -44,21 +39,18 @@ export interface SerializedPositionalTag {
|
|||||||
* Each tag can last one or more turns, triggering various effects on removal.
|
* Each tag can last one or more turns, triggering various effects on removal.
|
||||||
* Multiple tags of the same kind can stack with one another, provided they are affecting different targets.
|
* Multiple tags of the same kind can stack with one another, provided they are affecting different targets.
|
||||||
*/
|
*/
|
||||||
export abstract class PositionalTag implements SerializedPositionalTag {
|
export abstract class PositionalTag implements PositionalTagBaseArgs {
|
||||||
/**
|
public abstract readonly tagType: PositionalTagType;
|
||||||
* This tag's {@linkcode PositionalTagType | type}.
|
// These arguments have to be public to implement the interface, but are functionally private.
|
||||||
*/
|
public sourceId: number;
|
||||||
public abstract tagType: PositionalTagType;
|
public turnCount: number;
|
||||||
public abstract lapseType: PositionalTagLapseType;
|
public targetIndex: BattlerIndex;
|
||||||
public abstract sourceId: number;
|
|
||||||
|
|
||||||
// These have to be public to implement the interface, but are functionally private.
|
constructor({ sourceId, turnCount, targetIndex }: PositionalTagBaseArgs) {
|
||||||
constructor(
|
this.sourceId = sourceId;
|
||||||
public sourceId: number,
|
this.turnCount = turnCount;
|
||||||
public sourceMove: MoveId,
|
this.targetIndex = targetIndex;
|
||||||
public turnCount: number,
|
}
|
||||||
public targetIndex: BattlerIndex,
|
|
||||||
) {}
|
|
||||||
|
|
||||||
/** Trigger this tag's effects prior to removal. */
|
/** Trigger this tag's effects prior to removal. */
|
||||||
public abstract trigger(): void;
|
public abstract trigger(): void;
|
||||||
@ -66,7 +58,8 @@ export abstract class PositionalTag implements SerializedPositionalTag {
|
|||||||
/**
|
/**
|
||||||
* Check whether this tag should be removed without triggering.
|
* Check whether this tag should be removed without triggering.
|
||||||
* @returns Whether this tag should disappear.
|
* @returns Whether this tag should disappear.
|
||||||
* By default, requires that the attack's turn count is less than or equal to 0.
|
* @privateRemarks
|
||||||
|
* Silent removal is accomplished by setting the attack's turn count to -1.
|
||||||
*/
|
*/
|
||||||
abstract shouldDisappear(): boolean;
|
abstract shouldDisappear(): boolean;
|
||||||
|
|
||||||
@ -79,20 +72,34 @@ export abstract class PositionalTag implements SerializedPositionalTag {
|
|||||||
public overlapsWith(targetIndex: BattlerIndex, _sourceMove: MoveId): boolean {
|
public overlapsWith(targetIndex: BattlerIndex, _sourceMove: MoveId): boolean {
|
||||||
return this.targetIndex === targetIndex;
|
return this.targetIndex === targetIndex;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public getTarget(): Pokemon | undefined {
|
||||||
|
return globalScene.getField()[this.targetIndex];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
interface DelayedAttackArgs extends PositionalTagBaseArgs {
|
||||||
|
/** The {@linkcode MoveId} that created this attack. */
|
||||||
|
sourceMove: MoveId;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Tag to manage execution of delayed attacks, such as {@linkcode MoveId.FUTURE_SIGHT} or {@linkcode MoveId.DOOM_DESIRE}.
|
* Tag to manage execution of delayed attacks, such as {@linkcode MoveId.FUTURE_SIGHT} or {@linkcode MoveId.DOOM_DESIRE}. \
|
||||||
* Delayed attacks do nothing for the first several turns after use (including the turn the move is used),
|
* Delayed attacks do nothing for the first several turns after use (including the turn the move is used),
|
||||||
* triggering against a certain slot after the turn count has elapsed.
|
* triggering against a certain slot after the turn count has elapsed.
|
||||||
*/
|
*/
|
||||||
export class DelayedAttackTag extends PositionalTag {
|
export class DelayedAttackTag extends PositionalTag implements DelayedAttackArgs {
|
||||||
public override tagType = PositionalTagType.DELAYED_ATTACK;
|
public override readonly tagType = PositionalTagType.DELAYED_ATTACK;
|
||||||
public override lapseType = PositionalTagLapseType.TURN_END;
|
public sourceMove: MoveId;
|
||||||
|
|
||||||
|
constructor({ sourceId, turnCount, targetIndex, sourceMove }: DelayedAttackArgs) {
|
||||||
|
super({ sourceId, turnCount, targetIndex });
|
||||||
|
this.sourceMove = sourceMove;
|
||||||
|
}
|
||||||
|
|
||||||
override trigger(): void {
|
override trigger(): void {
|
||||||
const source = globalScene.getPokemonById(this.sourceId)!;
|
const source = globalScene.getPokemonById(this.sourceId)!;
|
||||||
const target = globalScene.getField()[this.targetIndex];
|
const target = this.getTarget()!;
|
||||||
|
|
||||||
source.turnData.extraTurns++;
|
source.turnData.extraTurns++;
|
||||||
globalScene.phaseManager.queueMessage(
|
globalScene.phaseManager.queueMessage(
|
||||||
@ -113,72 +120,35 @@ export class DelayedAttackTag extends PositionalTag {
|
|||||||
|
|
||||||
override shouldDisappear(): boolean {
|
override shouldDisappear(): boolean {
|
||||||
const source = globalScene.getPokemonById(this.sourceId);
|
const source = globalScene.getPokemonById(this.sourceId);
|
||||||
const target = globalScene.getField()[this.targetIndex];
|
const target = this.getTarget();
|
||||||
// Silently disappear if either source or target are missing or happen to be the same pokemon
|
// Silently disappear if either source or target are missing or happen to be the same pokemon
|
||||||
// (i.e. targeting oneself)
|
// (i.e. targeting oneself)
|
||||||
return !source || !target || source === target || target.isFainted();
|
return !source || !target || source === target || target.isFainted();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
interface WishArgs extends PositionalTagBaseArgs {
|
||||||
* Add a new {@linkcode PositionalTag} to the arena.
|
/** The amount of {@linkcode Stat.HP | HP} to heal; set to 50% of the user's max HP during move usage. */
|
||||||
* @param tag - The {@linkcode SerializedPositionalTag} corresponding to the tag being added
|
healHp: number;
|
||||||
* @remarks
|
|
||||||
* This function does not perform any checking if the added tag is valid.
|
|
||||||
*/
|
|
||||||
export function loadPositionalTag(
|
|
||||||
tag: SerializedPositionalTag,
|
|
||||||
): InstanceType<(typeof positionalTagConstructorMap)[(typeof tag)["tagType"]]>;
|
|
||||||
/**
|
|
||||||
* Add a new {@linkcode PositionalTag} to the arena.
|
|
||||||
* @param tagType - The {@linkcode PositionalTagType} to create
|
|
||||||
* @param sourceId - The {@linkcode Pokemon.id | PID} of the Pokemon adding the tag
|
|
||||||
* @param sourceMove - The {@linkcode MoveId} causing the attack
|
|
||||||
* @param turnCount - The number of turns to delay the effect (_including the current turn_).
|
|
||||||
* @param targetIndex - The {@linkcode BattlerIndex} being targeted
|
|
||||||
* @remarks
|
|
||||||
* This function does not perform any checking if the added tag is valid.
|
|
||||||
*/
|
|
||||||
export function loadPositionalTag<T extends PositionalTagType>({
|
|
||||||
tagType,
|
|
||||||
sourceId,
|
|
||||||
sourceMove,
|
|
||||||
turnCount,
|
|
||||||
targetIndex,
|
|
||||||
}: {
|
|
||||||
tagType: T;
|
|
||||||
sourceId: number;
|
|
||||||
sourceMove: MoveId;
|
|
||||||
turnCount: number;
|
|
||||||
targetIndex: BattlerIndex;
|
|
||||||
}): tagInstanceMap[T];
|
|
||||||
/**
|
|
||||||
* Add a new {@linkcode SerializedPositionalTag} to the arena.
|
|
||||||
* @param tagType - The {@linkcode PositionalTagType} to create
|
|
||||||
* @param sourceId - The {@linkcode Pokemon.id | PID} of the Pokemon adding the tag
|
|
||||||
* @param sourceMove - The {@linkcode MoveId} causing the attack
|
|
||||||
* @param turnCount - The number of turns to delay the effect (_including the current turn_).
|
|
||||||
* @param targetIndex - The {@linkcode BattlerIndex} being targeted
|
|
||||||
* @remarks
|
|
||||||
* This function does not perform any checking if the added tag is valid.
|
|
||||||
*/
|
|
||||||
export function loadPositionalTag({
|
|
||||||
tagType,
|
|
||||||
sourceId,
|
|
||||||
sourceMove,
|
|
||||||
turnCount,
|
|
||||||
targetIndex,
|
|
||||||
}: SerializedPositionalTag): PositionalTag {
|
|
||||||
const tagClass = positionalTagConstructorMap[tagType];
|
|
||||||
return new tagClass(sourceId, sourceMove, turnCount, targetIndex);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Const object mapping tag types to their constructors. */
|
/**
|
||||||
const positionalTagConstructorMap = {
|
* Tag to implement {@linkcode MoveId.WISH | Wish}.
|
||||||
[PositionalTagType.DELAYED_ATTACK]: DelayedAttackTag,
|
*/
|
||||||
} satisfies Record<PositionalTagType, Constructor<PositionalTag>>;
|
export class WishTag extends PositionalTag implements WishArgs {
|
||||||
|
public override readonly tagType = PositionalTagType.WISH;
|
||||||
|
|
||||||
/** Type mapping {@linkcode PositionalTagType}s to instances of their corresponding {@linkcode PositionalTag}s. */
|
public healHp: number;
|
||||||
export type tagInstanceMap = {
|
constructor({ sourceId, turnCount, targetIndex, healHp }: WishArgs) {
|
||||||
[k in keyof typeof positionalTagConstructorMap]: InstanceType<(typeof positionalTagConstructorMap)[k]>;
|
super({ sourceId, turnCount, targetIndex });
|
||||||
};
|
this.healHp = healHp;
|
||||||
|
}
|
||||||
|
|
||||||
|
public trigger(): void {
|
||||||
|
globalScene.phaseManager.unshiftNew("PokemonHealPhase", this.targetIndex, this.healHp, null, true, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
public shouldDisappear(): boolean {
|
||||||
|
return !!this.getTarget();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -5,7 +5,6 @@ export enum ArenaTagType {
|
|||||||
SPIKES = "SPIKES",
|
SPIKES = "SPIKES",
|
||||||
TOXIC_SPIKES = "TOXIC_SPIKES",
|
TOXIC_SPIKES = "TOXIC_SPIKES",
|
||||||
MIST = "MIST",
|
MIST = "MIST",
|
||||||
WISH = "WISH",
|
|
||||||
STEALTH_ROCK = "STEALTH_ROCK",
|
STEALTH_ROCK = "STEALTH_ROCK",
|
||||||
STICKY_WEB = "STICKY_WEB",
|
STICKY_WEB = "STICKY_WEB",
|
||||||
TRICK_ROOM = "TRICK_ROOM",
|
TRICK_ROOM = "TRICK_ROOM",
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
import type { PostDancingMoveAbAttr } from "#abilities/ability";
|
import type { PostDancingMoveAbAttr } from "#abilities/ability";
|
||||||
import type { BattlerTagLapseType } from "#enums/battler-tag-lapse-type";
|
|
||||||
import type { DelayedAttackAttr } from "#app/@types/move-types";
|
import type { DelayedAttackAttr } from "#app/@types/move-types";
|
||||||
|
import type { BattlerTagLapseType } from "#enums/battler-tag-lapse-type";
|
||||||
|
import type { EnumValues } from "#types/enum-types";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Enum representing all the possible means through which a given move can be executed.
|
* Enum representing all the possible means through which a given move can be executed.
|
||||||
@ -71,7 +72,7 @@ export const MoveUseMode = {
|
|||||||
TRANSPARENT: 6
|
TRANSPARENT: 6
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
export type MoveUseMode = (typeof MoveUseMode)[keyof typeof MoveUseMode];
|
export type MoveUseMode = EnumValues<typeof MoveUseMode>;
|
||||||
|
|
||||||
// # HELPER FUNCTIONS
|
// # HELPER FUNCTIONS
|
||||||
// Please update the markdown tables if any new `MoveUseMode`s get added.
|
// Please update the markdown tables if any new `MoveUseMode`s get added.
|
||||||
|
@ -6,4 +6,5 @@
|
|||||||
*/
|
*/
|
||||||
export enum PositionalTagType {
|
export enum PositionalTagType {
|
||||||
DELAYED_ATTACK = "DELAYED_ATTACK",
|
DELAYED_ATTACK = "DELAYED_ATTACK",
|
||||||
|
WISH = "WISH",
|
||||||
}
|
}
|
||||||
|
@ -2,7 +2,6 @@ import { globalScene } from "#app/global-scene";
|
|||||||
import { Phase } from "#app/phase";
|
import { Phase } from "#app/phase";
|
||||||
// biome-ignore-start lint/correctness/noUnusedImports: TSDocs
|
// biome-ignore-start lint/correctness/noUnusedImports: TSDocs
|
||||||
import type { PositionalTag } from "#data/positional-tags/positional-tag";
|
import type { PositionalTag } from "#data/positional-tags/positional-tag";
|
||||||
import { PositionalTagLapseType } from "#enums/positional-tag-lapse-type";
|
|
||||||
import type { TurnEndPhase } from "#phases/turn-end-phase";
|
import type { TurnEndPhase } from "#phases/turn-end-phase";
|
||||||
// biome-ignore-end lint/correctness/noUnusedImports: TSDocs
|
// biome-ignore-end lint/correctness/noUnusedImports: TSDocs
|
||||||
|
|
||||||
|
@ -1,10 +1,6 @@
|
|||||||
import type { ArenaTag } from "#data/arena-tag";
|
import type { ArenaTag } from "#data/arena-tag";
|
||||||
import { loadArenaTag } from "#data/arena-tag";
|
import { loadArenaTag } from "#data/arena-tag";
|
||||||
import {
|
import type { SerializedPositionalTag } from "#data/positional-tags/load-positional-tag";
|
||||||
loadPositionalTag,
|
|
||||||
type PositionalTag,
|
|
||||||
type SerializedPositionalTag,
|
|
||||||
} from "#data/positional-tags/positional-tag";
|
|
||||||
import { Terrain } from "#data/terrain";
|
import { Terrain } from "#data/terrain";
|
||||||
import { Weather } from "#data/weather";
|
import { Weather } from "#data/weather";
|
||||||
import type { BiomeId } from "#enums/biome-id";
|
import type { BiomeId } from "#enums/biome-id";
|
||||||
@ -15,7 +11,7 @@ export class ArenaData {
|
|||||||
public weather: Weather | null;
|
public weather: Weather | null;
|
||||||
public terrain: Terrain | null;
|
public terrain: Terrain | null;
|
||||||
public tags: ArenaTag[];
|
public tags: ArenaTag[];
|
||||||
public positionalTags: PositionalTag[] = [];
|
public positionalTags: SerializedPositionalTag[] = [];
|
||||||
public playerTerasUsed: number;
|
public playerTerasUsed: number;
|
||||||
|
|
||||||
constructor(source: Arena | any) {
|
constructor(source: Arena | any) {
|
||||||
@ -38,9 +34,6 @@ export class ArenaData {
|
|||||||
this.tags = source.tags.map(t => loadArenaTag(t));
|
this.tags = source.tags.map(t => loadArenaTag(t));
|
||||||
}
|
}
|
||||||
|
|
||||||
this.positionalTags =
|
this.positionalTags = (sourceArena ? sourceArena.positionalTagManager.tags : source.positionalTags) ?? [];
|
||||||
(sourceArena ? sourceArena.positionalTagManager.tags : source.positionalTags)?.map((t: SerializedPositionalTag) =>
|
|
||||||
loadPositionalTag(t),
|
|
||||||
) ?? [];
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -16,6 +16,7 @@ import { allMoves, allSpecies } from "#data/data-lists";
|
|||||||
import type { Egg } from "#data/egg";
|
import type { Egg } from "#data/egg";
|
||||||
import { pokemonFormChanges } from "#data/pokemon-forms";
|
import { pokemonFormChanges } from "#data/pokemon-forms";
|
||||||
import type { PokemonSpecies } from "#data/pokemon-species";
|
import type { PokemonSpecies } from "#data/pokemon-species";
|
||||||
|
import { loadPositionalTag } from "#data/positional-tags/load-positional-tag";
|
||||||
import { TerrainType } from "#data/terrain";
|
import { TerrainType } from "#data/terrain";
|
||||||
import { AbilityAttr } from "#enums/ability-attr";
|
import { AbilityAttr } from "#enums/ability-attr";
|
||||||
import { BattleType } from "#enums/battle-type";
|
import { BattleType } from "#enums/battle-type";
|
||||||
@ -1096,7 +1097,9 @@ export class GameData {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
globalScene.arena.positionalTagManager.tags = sessionData.arena.positionalTags;
|
globalScene.arena.positionalTagManager.tags = sessionData.arena.positionalTags.map(tag =>
|
||||||
|
loadPositionalTag(tag),
|
||||||
|
);
|
||||||
|
|
||||||
if (globalScene.modifiers.length) {
|
if (globalScene.modifiers.length) {
|
||||||
console.warn("Existing modifiers not cleared on session load, deleting...");
|
console.warn("Existing modifiers not cleared on session load, deleting...");
|
||||||
|
@ -2,7 +2,6 @@ import { DelayedAttackTag } from "#app/data/positional-tags/positional-tag";
|
|||||||
import { getPokemonNameWithAffix } from "#app/messages";
|
import { getPokemonNameWithAffix } from "#app/messages";
|
||||||
import { AttackTypeBoosterModifier } from "#app/modifier/modifier";
|
import { AttackTypeBoosterModifier } from "#app/modifier/modifier";
|
||||||
import { allMoves } from "#data/data-lists";
|
import { allMoves } from "#data/data-lists";
|
||||||
import { RandomMoveAttr } from "#data/moves/move";
|
|
||||||
import { AbilityId } from "#enums/ability-id";
|
import { AbilityId } from "#enums/ability-id";
|
||||||
import { BattleType } from "#enums/battle-type";
|
import { BattleType } from "#enums/battle-type";
|
||||||
import { BattlerIndex } from "#enums/battler-index";
|
import { BattlerIndex } from "#enums/battler-index";
|
||||||
@ -50,9 +49,13 @@ describe("Moves - Delayed Attacks", () => {
|
|||||||
async function passTurns(numTurns: number, toEndOfTurn = true): Promise<void> {
|
async function passTurns(numTurns: number, toEndOfTurn = true): Promise<void> {
|
||||||
for (let i = 0; i < numTurns; i++) {
|
for (let i = 0; i < numTurns; i++) {
|
||||||
game.move.use(MoveId.SPLASH, BattlerIndex.PLAYER);
|
game.move.use(MoveId.SPLASH, BattlerIndex.PLAYER);
|
||||||
if (game.scene.currentBattle.double && game.scene.getPlayerField()[1]) {
|
if (game.scene.getPlayerField()[1]) {
|
||||||
game.move.use(MoveId.SPLASH, BattlerIndex.PLAYER_2);
|
game.move.use(MoveId.SPLASH, BattlerIndex.PLAYER_2);
|
||||||
}
|
}
|
||||||
|
await game.move.forceEnemyMove(MoveId.SPLASH);
|
||||||
|
if (game.scene.getEnemyField()[1]) {
|
||||||
|
await game.move.forceEnemyMove(MoveId.SPLASH);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
await game.phaseInterceptor.to("PositionalTagPhase");
|
await game.phaseInterceptor.to("PositionalTagPhase");
|
||||||
if (toEndOfTurn) {
|
if (toEndOfTurn) {
|
||||||
@ -63,12 +66,10 @@ describe("Moves - Delayed Attacks", () => {
|
|||||||
/**
|
/**
|
||||||
* Expect that future sight is active with the specified number of attacks.
|
* Expect that future sight is active with the specified number of attacks.
|
||||||
* @param numAttacks - The number of delayed attacks that should be queued; default `1`
|
* @param numAttacks - The number of delayed attacks that should be queued; default `1`
|
||||||
* @returns The queued tags.
|
|
||||||
*/
|
*/
|
||||||
function expectFutureSightActive(numAttacks = 1): DelayedAttackTag[] {
|
function expectFutureSightActive(numAttacks = 1) {
|
||||||
const delayedAttacks = game.scene.arena.positionalTagManager["tags"].filter(t => t instanceof DelayedAttackTag)!;
|
const delayedAttacks = game.scene.arena.positionalTagManager["tags"].filter(t => t instanceof DelayedAttackTag)!;
|
||||||
expect(delayedAttacks).toHaveLength(numAttacks);
|
expect(delayedAttacks).toHaveLength(numAttacks);
|
||||||
return delayedAttacks;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
it.each<{ name: string; move: MoveId }>([
|
it.each<{ name: string; move: MoveId }>([
|
||||||
@ -117,18 +118,17 @@ describe("Moves - Delayed Attacks", () => {
|
|||||||
expect(bronzong.getLastXMoves()[0].result).toBe(MoveResult.FAIL);
|
expect(bronzong.getLastXMoves()[0].result).toBe(MoveResult.FAIL);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should still be delayed when copied by other moves", async () => {
|
it("should still be delayed when called by other moves", async () => {
|
||||||
vi.spyOn(RandomMoveAttr.prototype, "getMoveOverride").mockReturnValue(MoveId.FUTURE_SIGHT);
|
|
||||||
await game.classicMode.startBattle([SpeciesId.BRONZONG]);
|
await game.classicMode.startBattle([SpeciesId.BRONZONG]);
|
||||||
|
|
||||||
game.move.use(MoveId.METRONOME);
|
game.move.use(MoveId.METRONOME);
|
||||||
|
game.move.forceMetronomeMove(MoveId.FUTURE_SIGHT);
|
||||||
await game.toNextTurn();
|
await game.toNextTurn();
|
||||||
|
|
||||||
|
expectFutureSightActive();
|
||||||
const enemy = game.field.getEnemyPokemon();
|
const enemy = game.field.getEnemyPokemon();
|
||||||
expect(enemy.hp).toBe(enemy.getMaxHp());
|
expect(enemy.hp).toBe(enemy.getMaxHp());
|
||||||
|
|
||||||
expectFutureSightActive();
|
|
||||||
|
|
||||||
await passTurns(2);
|
await passTurns(2);
|
||||||
|
|
||||||
expect(enemy.hp).toBeLessThan(enemy.getMaxHp());
|
expect(enemy.hp).toBeLessThan(enemy.getMaxHp());
|
||||||
@ -150,10 +150,7 @@ describe("Moves - Delayed Attacks", () => {
|
|||||||
expect(karp.getLastXMoves()[0].result).toBe(MoveResult.OTHER);
|
expect(karp.getLastXMoves()[0].result).toBe(MoveResult.OTHER);
|
||||||
expect(feebas.getLastXMoves()[0].result).toBe(MoveResult.OTHER);
|
expect(feebas.getLastXMoves()[0].result).toBe(MoveResult.OTHER);
|
||||||
|
|
||||||
await passTurns(2, false);
|
await passTurns(2);
|
||||||
|
|
||||||
// Both attacks have
|
|
||||||
expectFutureSightActive(0);
|
|
||||||
|
|
||||||
await game.toEndOfTurn();
|
await game.toEndOfTurn();
|
||||||
|
|
||||||
@ -161,6 +158,33 @@ describe("Moves - Delayed Attacks", () => {
|
|||||||
expect(enemy2.hp).toBeLessThan(enemy2.getMaxHp());
|
expect(enemy2.hp).toBeLessThan(enemy2.getMaxHp());
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("should trigger multiple pending attacks in order of creation, even if that order changes later on", async () => {
|
||||||
|
game.override.battleStyle("double");
|
||||||
|
await game.classicMode.startBattle([SpeciesId.MAGIKARP, SpeciesId.FEEBAS]);
|
||||||
|
|
||||||
|
game.move.use(MoveId.FUTURE_SIGHT, BattlerIndex.PLAYER, BattlerIndex.ENEMY);
|
||||||
|
game.move.use(MoveId.FUTURE_SIGHT, BattlerIndex.PLAYER_2, BattlerIndex.ENEMY_2);
|
||||||
|
await game.move.forceEnemyMove(MoveId.FUTURE_SIGHT, BattlerIndex.PLAYER);
|
||||||
|
await game.move.forceEnemyMove(MoveId.FUTURE_SIGHT, BattlerIndex.PLAYER_2);
|
||||||
|
const usageOrder = game.field.getSpeedOrder();
|
||||||
|
await game.toNextTurn();
|
||||||
|
|
||||||
|
expectFutureSightActive(4);
|
||||||
|
|
||||||
|
game.move.use(MoveId.TAILWIND);
|
||||||
|
game.move.use(MoveId.COTTON_SPORE);
|
||||||
|
await passTurns(1, false);
|
||||||
|
|
||||||
|
expect(game.field.getSpeedOrder()).not.toEqual(usageOrder);
|
||||||
|
|
||||||
|
// All attacks have concluded at this point, unshifting new `MoveEffectPhase`s to the queue.
|
||||||
|
expectFutureSightActive(0);
|
||||||
|
|
||||||
|
const MEPs = game.scene.phaseManager.phaseQueue.filter(p => p.is("MoveEffectPhase"));
|
||||||
|
expect(MEPs).toHaveLength(4);
|
||||||
|
expect(MEPs.map(mep => mep["battlerIndex"])).toEqual(usageOrder);
|
||||||
|
});
|
||||||
|
|
||||||
it("should vanish silently if it would otherwise hit the user", async () => {
|
it("should vanish silently if it would otherwise hit the user", async () => {
|
||||||
game.override.battleStyle("double");
|
game.override.battleStyle("double");
|
||||||
await game.classicMode.startBattle([SpeciesId.MAGIKARP, SpeciesId.FEEBAS, SpeciesId.MIENFOO]);
|
await game.classicMode.startBattle([SpeciesId.MAGIKARP, SpeciesId.FEEBAS, SpeciesId.MIENFOO]);
|
||||||
@ -242,18 +266,13 @@ describe("Moves - Delayed Attacks", () => {
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
// TODO: ArenaTags currently procs concurrently with battler tag removal in `TurnEndPhase`,
|
it("should consider type changes at moment of execution while ignoring redirection", async () => {
|
||||||
// meaning the queued `MoveEffectPhase` no longer has Electrify applied to it
|
|
||||||
it.todo("should consider type changes at moment of execution while ignoring redirection", async () => {
|
|
||||||
game.override.battleStyle("double");
|
game.override.battleStyle("double");
|
||||||
await game.classicMode.startBattle([SpeciesId.MAGIKARP]);
|
await game.classicMode.startBattle([SpeciesId.MAGIKARP]);
|
||||||
|
|
||||||
// fake left enemy having lightning rod
|
// fake left enemy having lightning rod
|
||||||
const [enemy1, enemy2] = game.scene.getEnemyField();
|
const [enemy1, enemy2] = game.scene.getEnemyField();
|
||||||
game.field.mockAbility(enemy1, AbilityId.LIGHTNING_ROD);
|
game.field.mockAbility(enemy1, AbilityId.LIGHTNING_ROD);
|
||||||
// helps with logging
|
|
||||||
vi.spyOn(enemy1, "getNameToRender").mockReturnValue("Karp 1");
|
|
||||||
vi.spyOn(enemy2, "getNameToRender").mockReturnValue("Karp 2");
|
|
||||||
|
|
||||||
game.move.use(MoveId.FUTURE_SIGHT, BattlerIndex.PLAYER, BattlerIndex.ENEMY_2);
|
game.move.use(MoveId.FUTURE_SIGHT, BattlerIndex.PLAYER, BattlerIndex.ENEMY_2);
|
||||||
await game.toNextTurn();
|
await game.toNextTurn();
|
||||||
@ -264,14 +283,14 @@ describe("Moves - Delayed Attacks", () => {
|
|||||||
|
|
||||||
game.move.use(MoveId.SPLASH, BattlerIndex.PLAYER);
|
game.move.use(MoveId.SPLASH, BattlerIndex.PLAYER);
|
||||||
await game.move.forceEnemyMove(MoveId.ELECTRIFY, BattlerIndex.PLAYER);
|
await game.move.forceEnemyMove(MoveId.ELECTRIFY, BattlerIndex.PLAYER);
|
||||||
await game.phaseInterceptor.to("TurnEndPhase");
|
await game.phaseInterceptor.to("PositionalTagPhase");
|
||||||
await game.phaseInterceptor.to("MoveEffectPhase", false);
|
await game.phaseInterceptor.to("MoveEffectPhase", false);
|
||||||
|
|
||||||
// Wait until all normal attacks have triggered, then check pending MEP
|
// Wait until all normal attacks have triggered, then check pending MEP
|
||||||
const karp = game.field.getPlayerPokemon();
|
const karp = game.field.getPlayerPokemon();
|
||||||
const typeMock = vi.spyOn(karp, "getMoveType");
|
const typeMock = vi.spyOn(karp, "getMoveType");
|
||||||
|
|
||||||
await game.toNextTurn();
|
await game.toEndOfTurn();
|
||||||
|
|
||||||
expect(enemy1.hp).toBe(enemy1.getMaxHp());
|
expect(enemy1.hp).toBe(enemy1.getMaxHp());
|
||||||
expect(enemy2.hp).toBeLessThan(enemy2.getMaxHp());
|
expect(enemy2.hp).toBeLessThan(enemy2.getMaxHp());
|
||||||
@ -337,4 +356,7 @@ describe("Moves - Delayed Attacks", () => {
|
|||||||
expect(powerMock).toHaveLastReturnedWith(120);
|
expect(powerMock).toHaveLastReturnedWith(120);
|
||||||
expect(typeBoostSpy).not.toHaveBeenCalled();
|
expect(typeBoostSpy).not.toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// TODO: Implement and move to a power spot's test file
|
||||||
|
it.todo("Should activate ally's power spot when switched in during single battles");
|
||||||
});
|
});
|
||||||
|
@ -1,9 +1,8 @@
|
|||||||
import { AbilityId } from "#enums/ability-id";
|
import { AbilityId } from "#enums/ability-id";
|
||||||
import { ArenaTagSide } from "#enums/arena-tag-side";
|
|
||||||
import { ArenaTagType } from "#enums/arena-tag-type";
|
|
||||||
import { BattlerIndex } from "#enums/battler-index";
|
import { BattlerIndex } from "#enums/battler-index";
|
||||||
import { BattlerTagType } from "#enums/battler-tag-type";
|
import { BattlerTagType } from "#enums/battler-tag-type";
|
||||||
import { MoveId } from "#enums/move-id";
|
import { MoveId } from "#enums/move-id";
|
||||||
|
import { PositionalTagType } from "#enums/positional-tag-type";
|
||||||
import { SpeciesId } from "#enums/species-id";
|
import { SpeciesId } from "#enums/species-id";
|
||||||
import { WeatherType } from "#enums/weather-type";
|
import { WeatherType } from "#enums/weather-type";
|
||||||
import { GameManager } from "#test/testUtils/gameManager";
|
import { GameManager } from "#test/testUtils/gameManager";
|
||||||
@ -68,22 +67,25 @@ describe("Moves - Heal Block", () => {
|
|||||||
expect(enemy.isFullHp()).toBe(false);
|
expect(enemy.isFullHp()).toBe(false);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should stop delayed heals, such as from Wish", async () => {
|
it("should prevent Wish from restoring HP", async () => {
|
||||||
await game.classicMode.startBattle([SpeciesId.CHARIZARD]);
|
await game.classicMode.startBattle([SpeciesId.CHARIZARD]);
|
||||||
|
|
||||||
const player = game.scene.getPlayerPokemon()!;
|
const player = game.field.getPlayerPokemon()!;
|
||||||
|
|
||||||
player.damageAndUpdate(player.getMaxHp() - 1);
|
player.hp = 1;
|
||||||
|
|
||||||
game.move.select(MoveId.WISH);
|
game.move.use(MoveId.WISH);
|
||||||
await game.phaseInterceptor.to("TurnEndPhase");
|
await game.toNextTurn();
|
||||||
|
|
||||||
expect(game.scene.arena.getTagOnSide(ArenaTagType.WISH, ArenaTagSide.PLAYER)).toBeDefined();
|
expect(game.scene.arena.positionalTagManager.tags.find(t => t.tagType === PositionalTagType.WISH)).toHaveLength(1);
|
||||||
while (game.scene.arena.getTagOnSide(ArenaTagType.WISH, ArenaTagSide.PLAYER)) {
|
|
||||||
game.move.select(MoveId.SPLASH);
|
|
||||||
await game.phaseInterceptor.to("TurnEndPhase");
|
|
||||||
}
|
|
||||||
|
|
||||||
|
game.move.use(MoveId.SPLASH);
|
||||||
|
await game.toNextTurn();
|
||||||
|
game.move.use(MoveId.SPLASH);
|
||||||
|
await game.toNextTurn();
|
||||||
|
|
||||||
|
// wish triggered, but did NOT heal the player
|
||||||
|
expect(game.scene.arena.positionalTagManager.tags.find(t => t.tagType === PositionalTagType.WISH)).toHaveLength(0);
|
||||||
expect(player.hp).toBe(1);
|
expect(player.hp).toBe(1);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -309,10 +309,16 @@ export class MoveHelper extends GameManagerHelper {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Force the move used by Metronome to be a specific move.
|
* Force the next move(s) used by Metronome to be a specific move. \
|
||||||
* @param move - The move to force metronome to use
|
* Triggers during the next upcoming {@linkcode MoveEffectPhase} that Metronome is used.
|
||||||
* @param once - If `true`, uses {@linkcode MockInstance#mockReturnValueOnce} when mocking, else uses {@linkcode MockInstance#mockReturnValue}.
|
* @param move - The move to force Metronome to call
|
||||||
|
* @param once - If `true`, mocks the return value exactly once; default `false`
|
||||||
* @returns The spy that for Metronome that was mocked (Usually unneeded).
|
* @returns The spy that for Metronome that was mocked (Usually unneeded).
|
||||||
|
* @example
|
||||||
|
* ```ts
|
||||||
|
* game.move.use(MoveId.METRONOME);
|
||||||
|
* game.move.forceMetronomeMove(MoveId.FUTURE_SIGHT); // Can be in any order
|
||||||
|
* ```
|
||||||
*/
|
*/
|
||||||
public forceMetronomeMove(move: MoveId, once = false): MockInstance {
|
public forceMetronomeMove(move: MoveId, once = false): MockInstance {
|
||||||
const spy = vi.spyOn(allMoves[MoveId.METRONOME].getAttrs("RandomMoveAttr")[0], "getMoveOverride");
|
const spy = vi.spyOn(allMoves[MoveId.METRONOME].getAttrs("RandomMoveAttr")[0], "getMoveOverride");
|
||||||
|
Loading…
Reference in New Issue
Block a user