mirror of
https://github.com/pagefaultgames/pokerogue.git
synced 2025-08-11 09:59:28 +02:00
Refactored FS to use a positional tag manager
This commit is contained in:
parent
72080a1260
commit
4d5d6b56a9
@ -1,4 +1,6 @@
|
|||||||
import { applyAbAttrs, applyOnGainAbAttrs, applyOnLoseAbAttrs } from "#abilities/apply-ab-attrs";
|
import { applyAbAttrs, applyOnGainAbAttrs, applyOnLoseAbAttrs } from "#abilities/apply-ab-attrs";
|
||||||
|
/** biome-ignore lint/correctness/noUnusedImports: Type-only import for doc comment */
|
||||||
|
import type { BattlerTag } from "#app/data/battler-tags";
|
||||||
import { globalScene } from "#app/global-scene";
|
import { globalScene } from "#app/global-scene";
|
||||||
import { getPokemonNameWithAffix } from "#app/messages";
|
import { getPokemonNameWithAffix } from "#app/messages";
|
||||||
import { CommonBattleAnim } from "#data/battle-anims";
|
import { CommonBattleAnim } from "#data/battle-anims";
|
||||||
@ -13,7 +15,6 @@ import { MoveCategory } from "#enums/MoveCategory";
|
|||||||
import { MoveTarget } from "#enums/MoveTarget";
|
import { MoveTarget } from "#enums/MoveTarget";
|
||||||
import { CommonAnim } from "#enums/move-anims-common";
|
import { CommonAnim } from "#enums/move-anims-common";
|
||||||
import { MoveId } from "#enums/move-id";
|
import { MoveId } from "#enums/move-id";
|
||||||
import { MoveUseMode } from "#enums/move-use-mode";
|
|
||||||
import { PokemonType } from "#enums/pokemon-type";
|
import { PokemonType } from "#enums/pokemon-type";
|
||||||
import { Stat } from "#enums/stat";
|
import { Stat } from "#enums/stat";
|
||||||
import { StatusEffect } from "#enums/status-effect";
|
import { StatusEffect } from "#enums/status-effect";
|
||||||
@ -22,9 +23,6 @@ import type { Pokemon } from "#field/pokemon";
|
|||||||
import { BooleanHolder, NumberHolder, toDmgValue } from "#utils/common";
|
import { BooleanHolder, NumberHolder, toDmgValue } from "#utils/common";
|
||||||
import i18next from "i18next";
|
import i18next from "i18next";
|
||||||
|
|
||||||
/** biome-ignore lint/correctness/noUnusedImports: Type-only import for doc comment */
|
|
||||||
import type { BattlerTag } from "#app/data/battler-tags";
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* An {@linkcode ArenaTag} represents a semi-persistent effect affecting a given side of the field.
|
* An {@linkcode ArenaTag} represents a semi-persistent effect affecting a given side of the field.
|
||||||
* Unlike {@linkcode BattlerTag}s (which are tied to individual {@linkcode Pokemon}), `ArenaTag`s function independently of
|
* Unlike {@linkcode BattlerTag}s (which are tied to individual {@linkcode Pokemon}), `ArenaTag`s function independently of
|
||||||
@ -886,127 +884,6 @@ class ToxicSpikesTag extends ArenaTrapTag {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Interface representing a delayed attack command.
|
|
||||||
* @see {@linkcode DelayedAttackTag}
|
|
||||||
*/
|
|
||||||
interface DelayedAttack {
|
|
||||||
/** The {@linkcode PID | Pokemon.id} of the {@linkcode Pokemon} initiating the attack. */
|
|
||||||
sourceId: number;
|
|
||||||
/** The {@linkcode MoveId} that was used to trigger the delayed attack. */
|
|
||||||
move: MoveId;
|
|
||||||
/** The {@linkcode BattlerIndex} of the attack's target. */
|
|
||||||
targetIndex: BattlerIndex;
|
|
||||||
/**
|
|
||||||
* The number of turns left until activation.
|
|
||||||
* The attack will trigger once its turn count reaches 0, at which point it is removed.
|
|
||||||
*/
|
|
||||||
turnsLeft: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Arena 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),
|
|
||||||
* triggering against a certain slot after the turn count has elapsed.
|
|
||||||
*/
|
|
||||||
export class DelayedAttackTag extends ArenaTag {
|
|
||||||
/** Contains all queued delayed attacks on the field */
|
|
||||||
private delayedAttacks: DelayedAttack[] = [];
|
|
||||||
|
|
||||||
constructor() {
|
|
||||||
super(ArenaTagType.DELAYED_ATTACK, 0);
|
|
||||||
}
|
|
||||||
|
|
||||||
override loadTag(source: ArenaTag | any): void {
|
|
||||||
super.loadTag(source);
|
|
||||||
this.delayedAttacks = source.delayedAttacks;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Queue a delayed attack to be used in some indeterminate number of turns.
|
|
||||||
* @param source - The {@linkcode Pokemon} using the move
|
|
||||||
* @param move - The {@linkcode MoveId} being used
|
|
||||||
* @param targetIndex - The {@linkcode BattlerIndex} being targeted
|
|
||||||
* @param turnCount - The number of turns to delay the attack (_including the turn the move is used_); default `3`
|
|
||||||
*/
|
|
||||||
public queueAttack(source: Pokemon, move: MoveId, targetIndex: BattlerIndex, turnCount = 3): void {
|
|
||||||
this.delayedAttacks.push({ sourceId: source.id, move, targetIndex, turnsLeft: turnCount });
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Check whether a delayed attack can be queued against the given target.
|
|
||||||
* @param targetIndex - The {@linkcode BattlerIndex} of the target Pokemon
|
|
||||||
* @returns Whether another delayed attack can be successfully added.
|
|
||||||
*/
|
|
||||||
public canAddAttack(targetIndex: BattlerIndex): boolean {
|
|
||||||
return this.delayedAttacks.every(atk => atk.targetIndex !== targetIndex);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Tick down all existing delayed attacks, activating them if their timers have elapsed.
|
|
||||||
* @returns Whether this tag should remain (at least 1 delayed attack still active).
|
|
||||||
*/
|
|
||||||
override lapse(_arena: Arena): boolean {
|
|
||||||
for (const attack of this.delayedAttacks) {
|
|
||||||
const source = globalScene.getPokemonById(attack.sourceId);
|
|
||||||
const target: Pokemon | undefined = globalScene.getField()[attack.targetIndex];
|
|
||||||
|
|
||||||
if (--attack.turnsLeft > 0) {
|
|
||||||
// attack still cooking
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!source || !target || source === target || target.isFainted()) {
|
|
||||||
// source/target either nonexistent or the exact same pokemon; silently mark for deletion
|
|
||||||
// TODO: move into an overriddable method if wish is made into a delayed attack
|
|
||||||
attack.turnsLeft = -1;
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.triggerAttack(attack);
|
|
||||||
}
|
|
||||||
|
|
||||||
return this.removeDoneAttacks();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Remove all finished attacks from the current queue.
|
|
||||||
* @returns Whether at least 1 attack has not finished triggering.
|
|
||||||
*/
|
|
||||||
private removeDoneAttacks(): boolean {
|
|
||||||
this.delayedAttacks = this.delayedAttacks.filter(a => a.turnsLeft > 0);
|
|
||||||
return this.delayedAttacks.length > 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Trigger a delayed attack's effects prior to being removed.
|
|
||||||
* @param attack - The {@linkcode DelayedAttack} being activated
|
|
||||||
*/
|
|
||||||
protected triggerAttack(attack: DelayedAttack): void {
|
|
||||||
const source = globalScene.getPokemonById(attack.sourceId)!;
|
|
||||||
const target = globalScene.getField()[attack.targetIndex];
|
|
||||||
|
|
||||||
source.turnData.extraTurns++;
|
|
||||||
globalScene.phaseManager.queueMessage(
|
|
||||||
i18next.t("moveTriggers:tookMoveAttack", {
|
|
||||||
pokemonName: getPokemonNameWithAffix(target),
|
|
||||||
moveName: allMoves[attack.move].name,
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
|
|
||||||
globalScene.phaseManager.unshiftNew(
|
|
||||||
"MoveEffectPhase",
|
|
||||||
attack.sourceId,
|
|
||||||
[attack.targetIndex],
|
|
||||||
allMoves[attack.move],
|
|
||||||
MoveUseMode.TRANSPARENT,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Override on remove func to do nothing. */
|
|
||||||
override onRemove(_arena: Arena): void {}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Arena Tag class for {@link https://bulbapedia.bulbagarden.net/wiki/Stealth_Rock_(move) Stealth Rock}.
|
* Arena Tag class for {@link https://bulbapedia.bulbagarden.net/wiki/Stealth_Rock_(move) Stealth Rock}.
|
||||||
* Applies up to 1 layer of Stealth Rocks, dealing percentage-based damage to any Pokémon
|
* Applies up to 1 layer of Stealth Rocks, dealing percentage-based damage to any Pokémon
|
||||||
@ -1662,8 +1539,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.DELAYED_ATTACK:
|
|
||||||
return new DelayedAttackTag();
|
|
||||||
case ArenaTagType.WISH:
|
case ArenaTagType.WISH:
|
||||||
return new WishTag(turnCount, sourceId, side);
|
return new WishTag(turnCount, sourceId, side);
|
||||||
case ArenaTagType.STEALTH_ROCK:
|
case ArenaTagType.STEALTH_ROCK:
|
||||||
|
@ -25,6 +25,7 @@ import { getBerryEffectFunc } from "#data/berry";
|
|||||||
import { applyChallenges } from "#data/challenge";
|
import { applyChallenges } from "#data/challenge";
|
||||||
import { allAbilities, allMoves } from "#data/data-lists";
|
import { allAbilities, allMoves } from "#data/data-lists";
|
||||||
import { SpeciesFormChangeRevertWeatherFormTrigger } from "#data/form-change-triggers";
|
import { SpeciesFormChangeRevertWeatherFormTrigger } from "#data/form-change-triggers";
|
||||||
|
import { DelayedAttackTag } from "#data/positional-tags/positional-tag";
|
||||||
import {
|
import {
|
||||||
getNonVolatileStatusEffects,
|
getNonVolatileStatusEffects,
|
||||||
getStatusEffectHealText,
|
getStatusEffectHealText,
|
||||||
@ -54,6 +55,7 @@ import { MoveFlags } from "#enums/MoveFlags";
|
|||||||
import { MoveTarget } from "#enums/MoveTarget";
|
import { MoveTarget } from "#enums/MoveTarget";
|
||||||
import { MultiHitType } from "#enums/MultiHitType";
|
import { MultiHitType } from "#enums/MultiHitType";
|
||||||
import { PokemonType } from "#enums/pokemon-type";
|
import { PokemonType } from "#enums/pokemon-type";
|
||||||
|
import { PositionalTagType } from "#enums/positional-tag-type";
|
||||||
import { SpeciesId } from "#enums/species-id";
|
import { SpeciesId } from "#enums/species-id";
|
||||||
import {
|
import {
|
||||||
BATTLE_STATS,
|
BATTLE_STATS,
|
||||||
@ -3134,11 +3136,8 @@ export class DelayedAttackAttr extends OverrideMoveEffectAttr {
|
|||||||
}
|
}
|
||||||
|
|
||||||
getCondition(): MoveConditionFunc {
|
getCondition(): MoveConditionFunc {
|
||||||
return (_user, target, _move) => {
|
|
||||||
// Check the arena if another delayed attack is active and hitting the same slot
|
// Check the arena if another delayed attack is active and hitting the same slot
|
||||||
const delayedTag = globalScene.arena.getTag(DelayedAttackTag);
|
return (_user, target, move) => globalScene.arena.positionalTagManager.canAddTag(PositionalTagType.DELAYED_ATTACK, target.getBattlerIndex(), move.id)
|
||||||
return delayedTag?.canAddAttack(target.getBattlerIndex()) ?? true;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
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 {
|
||||||
@ -3159,18 +3158,13 @@ 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})
|
||||||
|
|
||||||
// Add a Delayed Attack tag to the arena if it doesn't already exist and queue up an extra attack.
|
// Queue up an attack on the given slot.
|
||||||
// TODO: Remove unused params once signature is tweaked to make more sense (none of these get used)
|
globalScene.arena.positionalTagManager.addTag({
|
||||||
globalScene.arena.addTag(ArenaTagType.DELAYED_ATTACK, 123, 69, 420);
|
tagType: PositionalTagType.DELAYED_ATTACK,
|
||||||
|
sourceId: user.id,
|
||||||
// Queue an attack on the added (or existing) tag
|
targetIndex: target.getBattlerIndex(),
|
||||||
const delayedAttackTag = globalScene.arena.getTag(DelayedAttackTag)
|
sourceMove: move.id,
|
||||||
if (!delayedAttackTag) {
|
turnCount: 3})
|
||||||
console.warn("Delayed attack tag not present!")
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
delayedAttackTag.queueAttack(user, move.id, target.getBattlerIndex());
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -9207,7 +9201,6 @@ export function initMoves() {
|
|||||||
.ignoresProtect()
|
.ignoresProtect()
|
||||||
/*
|
/*
|
||||||
* Should not apply abilities or held items if user is off the field
|
* Should not apply abilities or held items if user is off the field
|
||||||
* Triggered move phase occurs after Electrify tag is removed
|
|
||||||
*/
|
*/
|
||||||
.edgeCase(),
|
.edgeCase(),
|
||||||
new AttackMove(MoveId.ROCK_SMASH, PokemonType.FIGHTING, MoveCategory.PHYSICAL, 40, 100, 15, 50, 0, 2)
|
new AttackMove(MoveId.ROCK_SMASH, PokemonType.FIGHTING, MoveCategory.PHYSICAL, 40, 100, 15, 50, 0, 2)
|
||||||
@ -9549,7 +9542,6 @@ export function initMoves() {
|
|||||||
.ignoresProtect()
|
.ignoresProtect()
|
||||||
/*
|
/*
|
||||||
* Should not apply abilities or held items if user is off the field
|
* Should not apply abilities or held items if user is off the field
|
||||||
* Triggered move phase occurs after Electrify tag is removed
|
|
||||||
*/
|
*/
|
||||||
.edgeCase(),
|
.edgeCase(),
|
||||||
new AttackMove(MoveId.PSYCHO_BOOST, PokemonType.PSYCHIC, MoveCategory.SPECIAL, 140, 90, 5, -1, 0, 3)
|
new AttackMove(MoveId.PSYCHO_BOOST, PokemonType.PSYCHIC, MoveCategory.SPECIAL, 140, 90, 5, -1, 0, 3)
|
||||||
|
61
src/data/positional-tags/positional-tag-manager.ts
Normal file
61
src/data/positional-tags/positional-tag-manager.ts
Normal file
@ -0,0 +1,61 @@
|
|||||||
|
import {
|
||||||
|
loadPositionalTag,
|
||||||
|
type PositionalTag,
|
||||||
|
type SerializedPositionalTag,
|
||||||
|
} from "#data/positional-tags/positional-tag";
|
||||||
|
import type { BattlerIndex } from "#enums/battler-index";
|
||||||
|
import type { MoveId } from "#enums/move-id";
|
||||||
|
import type { PositionalTagType } from "#enums/positional-tag-type";
|
||||||
|
|
||||||
|
/** A manager for the {@linkcode PositionalTag}s in the arena. */
|
||||||
|
export class PositionalTagManager {
|
||||||
|
/** Array containing all pending unactivated {@linkcode PositionalTag}s, sorted by order of creation. */
|
||||||
|
private tags: PositionalTag[] = [];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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.
|
||||||
|
*/
|
||||||
|
public addTag(tag: SerializedPositionalTag): void {
|
||||||
|
this.tags.push(loadPositionalTag(tag));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check whether a new {@linkcode PositionalTag} can be added to the battlefield.
|
||||||
|
* @param tagType - The {@linkcode PositionalTagType} being created
|
||||||
|
* @param targetIndex - The {@linkcode BattlerIndex} being targeted
|
||||||
|
* @param sourceMove - The {@linkcode MoveId} causing the attack
|
||||||
|
* @returns Whether the tag can be added.
|
||||||
|
*/
|
||||||
|
public canAddTag(tagType: PositionalTagType, targetIndex: BattlerIndex, sourceMove: MoveId): boolean {
|
||||||
|
return !this.tags.some(t => t.tagType === tagType && t.overlapsWith(targetIndex, sourceMove));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Decrement turn counts of and activate all pending {@linkcode PositionalTag}s on field.
|
||||||
|
* @remarks
|
||||||
|
* If multiple tags trigger simultaneously, they will activate **in order of initial creation**.
|
||||||
|
* (source: [Smogon](<https://www.smogon.com/forums/threads/sword-shield-battle-mechanics-research.3655528/page-64#post-9244179>))
|
||||||
|
*/
|
||||||
|
triggerAllTags(): void {
|
||||||
|
for (const tag of this.tags) {
|
||||||
|
if (--tag.turnCount > 0) {
|
||||||
|
// tag still cooking
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for silent removal
|
||||||
|
if (tag.shouldDisappear()) {
|
||||||
|
tag.turnCount = -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
tag.trigger();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
184
src/data/positional-tags/positional-tag.ts
Normal file
184
src/data/positional-tags/positional-tag.ts
Normal file
@ -0,0 +1,184 @@
|
|||||||
|
// biome-ignore-start lint/correctness/noUnusedImports: TSDoc
|
||||||
|
import { globalScene } from "#app/global-scene";
|
||||||
|
import { getPokemonNameWithAffix } from "#app/messages";
|
||||||
|
import { allMoves } from "#data/data-lists";
|
||||||
|
import type { BattlerIndex } from "#enums/battler-index";
|
||||||
|
import type { MoveId } from "#enums/move-id";
|
||||||
|
import { MoveUseMode } from "#enums/move-use-mode";
|
||||||
|
import { PositionalTagLapseType } from "#enums/positional-tag-lapse-type";
|
||||||
|
import { PositionalTagType } from "#enums/positional-tag-type";
|
||||||
|
import type { Constructor } from "#utils/common";
|
||||||
|
import i18next from "i18next";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Serialized representation of a {@linkcode PositionalTag}.
|
||||||
|
*/
|
||||||
|
export interface SerializedPositionalTag {
|
||||||
|
/**
|
||||||
|
* 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.
|
||||||
|
*/
|
||||||
|
sourceId: number;
|
||||||
|
/**
|
||||||
|
* The {@linkcode MoveId} that created this effect.
|
||||||
|
*/
|
||||||
|
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.
|
||||||
|
* If set to any number `<0` manually, will be silently removed at the end of the next turn without activating.
|
||||||
|
*/
|
||||||
|
turnCount: number;
|
||||||
|
/**
|
||||||
|
* The {@linkcode BattlerIndex} of the Pokemon targeted by the effect.
|
||||||
|
*/
|
||||||
|
targetIndex: BattlerIndex;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A {@linkcode PositionalTag} is a variant of an {@linkcode ArenaTag} that targets a single *slot* of the battlefield.
|
||||||
|
* 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.
|
||||||
|
*/
|
||||||
|
export abstract class PositionalTag implements SerializedPositionalTag {
|
||||||
|
/**
|
||||||
|
* This tag's {@linkcode PositionalTagType | type}.
|
||||||
|
*/
|
||||||
|
public abstract tagType: PositionalTagType;
|
||||||
|
public abstract lapseType: PositionalTagLapseType;
|
||||||
|
public abstract sourceId: number;
|
||||||
|
|
||||||
|
// These have to be public to implement the interface, but are functionally private.
|
||||||
|
constructor(
|
||||||
|
public sourceId: number,
|
||||||
|
public sourceMove: MoveId,
|
||||||
|
public turnCount: number,
|
||||||
|
public targetIndex: BattlerIndex,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
/** Trigger this tag's effects prior to removal. */
|
||||||
|
public abstract trigger(): void;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check whether this tag should be removed without triggering.
|
||||||
|
* @returns Whether this tag should disappear.
|
||||||
|
* By default, requires that the attack's turn count is less than or equal to 0.
|
||||||
|
*/
|
||||||
|
abstract shouldDisappear(): boolean;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check whether this {@linkcode PositionalTag} would overlap with another one.
|
||||||
|
* @param targetIndex - The {@linkcode BattlerIndex} being targeted
|
||||||
|
* @param sourceMove - The {@linkcode MoveId} causing the attack
|
||||||
|
* @returns Whether this tag would overlap with a newly created one.
|
||||||
|
*/
|
||||||
|
public overlapsWith(targetIndex: BattlerIndex, _sourceMove: MoveId): boolean {
|
||||||
|
return this.targetIndex === targetIndex;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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),
|
||||||
|
* triggering against a certain slot after the turn count has elapsed.
|
||||||
|
*/
|
||||||
|
export class DelayedAttackTag extends PositionalTag {
|
||||||
|
public override tagType = PositionalTagType.DELAYED_ATTACK;
|
||||||
|
public override lapseType = PositionalTagLapseType.TURN_END;
|
||||||
|
|
||||||
|
override trigger(): void {
|
||||||
|
const source = globalScene.getPokemonById(this.sourceId)!;
|
||||||
|
const target = globalScene.getField()[this.targetIndex];
|
||||||
|
|
||||||
|
source.turnData.extraTurns++;
|
||||||
|
globalScene.phaseManager.queueMessage(
|
||||||
|
i18next.t("moveTriggers:tookMoveAttack", {
|
||||||
|
pokemonName: getPokemonNameWithAffix(target),
|
||||||
|
moveName: allMoves[this.sourceMove].name,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
globalScene.phaseManager.unshiftNew(
|
||||||
|
"MoveEffectPhase",
|
||||||
|
this.sourceId,
|
||||||
|
[this.targetIndex],
|
||||||
|
allMoves[this.sourceMove],
|
||||||
|
MoveUseMode.TRANSPARENT,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
override shouldDisappear(): boolean {
|
||||||
|
const source = globalScene.getPokemonById(this.sourceId);
|
||||||
|
const target = globalScene.getField()[this.targetIndex];
|
||||||
|
// Silently disappear if either source or target are missing or happen to be the same pokemon
|
||||||
|
// (i.e. targeting oneself)
|
||||||
|
return !source || !target || source === target || target.isFainted();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add a new {@linkcode PositionalTag} to the arena.
|
||||||
|
* @param tag - The {@linkcode SerializedPositionalTag} corresponding to the tag being added
|
||||||
|
* @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 = {
|
||||||
|
[PositionalTagType.DELAYED_ATTACK]: DelayedAttackTag,
|
||||||
|
} satisfies Record<PositionalTagType, Constructor<PositionalTag>>;
|
||||||
|
|
||||||
|
/** Type mapping {@linkcode PositionalTagType}s to instances of their corresponding {@linkcode PositionalTag}s. */
|
||||||
|
export type tagInstanceMap = {
|
||||||
|
[k in keyof typeof positionalTagConstructorMap]: InstanceType<(typeof positionalTagConstructorMap)[k]>;
|
||||||
|
};
|
@ -5,7 +5,6 @@ export enum ArenaTagType {
|
|||||||
SPIKES = "SPIKES",
|
SPIKES = "SPIKES",
|
||||||
TOXIC_SPIKES = "TOXIC_SPIKES",
|
TOXIC_SPIKES = "TOXIC_SPIKES",
|
||||||
MIST = "MIST",
|
MIST = "MIST",
|
||||||
DELAYED_ATTACK = "DELAYED_ATTACK",
|
|
||||||
WISH = "WISH",
|
WISH = "WISH",
|
||||||
STEALTH_ROCK = "STEALTH_ROCK",
|
STEALTH_ROCK = "STEALTH_ROCK",
|
||||||
STICKY_WEB = "STICKY_WEB",
|
STICKY_WEB = "STICKY_WEB",
|
||||||
|
9
src/enums/positional-tag-type.ts
Normal file
9
src/enums/positional-tag-type.ts
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
/**
|
||||||
|
* Enum representing all positional tag types.
|
||||||
|
* @privateRemarks
|
||||||
|
* When adding new tag types, please update `positionalTagConstructorMap` in `src/data/positionalTags`
|
||||||
|
* with the new tag type.
|
||||||
|
*/
|
||||||
|
export enum PositionalTagType {
|
||||||
|
DELAYED_ATTACK = "DELAYED_ATTACK",
|
||||||
|
}
|
@ -7,6 +7,9 @@ import type { ArenaTag } from "#data/arena-tag";
|
|||||||
import { ArenaTrapTag, getArenaTag } from "#data/arena-tag";
|
import { ArenaTrapTag, getArenaTag } from "#data/arena-tag";
|
||||||
import { SpeciesFormChangeRevertWeatherFormTrigger, SpeciesFormChangeWeatherTrigger } from "#data/form-change-triggers";
|
import { SpeciesFormChangeRevertWeatherFormTrigger, SpeciesFormChangeWeatherTrigger } from "#data/form-change-triggers";
|
||||||
import type { PokemonSpecies } from "#data/pokemon-species";
|
import type { PokemonSpecies } from "#data/pokemon-species";
|
||||||
|
// biome-ignore lint/correctness/noUnusedImports: TSDoc
|
||||||
|
import type { PositionalTag } from "#data/positional-tags/positional-tag";
|
||||||
|
import { PositionalTagManager } from "#data/positional-tags/positional-tag-manager";
|
||||||
import { getTerrainClearMessage, getTerrainStartMessage, Terrain, TerrainType } from "#data/terrain";
|
import { getTerrainClearMessage, getTerrainStartMessage, Terrain, TerrainType } from "#data/terrain";
|
||||||
import {
|
import {
|
||||||
getLegendaryWeatherContinuesMessage,
|
getLegendaryWeatherContinuesMessage,
|
||||||
@ -37,11 +40,18 @@ export class Arena {
|
|||||||
public biomeType: BiomeId;
|
public biomeType: BiomeId;
|
||||||
public weather: Weather | null;
|
public weather: Weather | null;
|
||||||
public terrain: Terrain | null;
|
public terrain: Terrain | null;
|
||||||
public tags: ArenaTag[];
|
/** All currently-active {@linkcode ArenaTag}s on both sides of the field. */
|
||||||
|
public tags: ArenaTag[] = [];
|
||||||
|
/**
|
||||||
|
* All currently-active {@linkcode PositionalTag}s on both sides of the field,
|
||||||
|
* sorted by tag type.
|
||||||
|
*/
|
||||||
|
public positionalTagManager: PositionalTagManager = new PositionalTagManager();
|
||||||
|
|
||||||
public bgm: string;
|
public bgm: string;
|
||||||
public ignoreAbilities: boolean;
|
public ignoreAbilities: boolean;
|
||||||
public ignoringEffectSource: BattlerIndex | null;
|
public ignoringEffectSource: BattlerIndex | null;
|
||||||
public playerTerasUsed: number;
|
public playerTerasUsed = 0;
|
||||||
/**
|
/**
|
||||||
* Saves the number of times a party pokemon faints during a arena encounter.
|
* Saves the number of times a party pokemon faints during a arena encounter.
|
||||||
* {@linkcode globalScene.currentBattle.enemyFaints} is the corresponding faint counter for the enemy (this resets every wave).
|
* {@linkcode globalScene.currentBattle.enemyFaints} is the corresponding faint counter for the enemy (this resets every wave).
|
||||||
@ -57,11 +67,9 @@ export class Arena {
|
|||||||
|
|
||||||
constructor(biome: BiomeId, bgm: string, playerFaints = 0) {
|
constructor(biome: BiomeId, bgm: string, playerFaints = 0) {
|
||||||
this.biomeType = biome;
|
this.biomeType = biome;
|
||||||
this.tags = [];
|
|
||||||
this.bgm = bgm;
|
this.bgm = bgm;
|
||||||
this.trainerPool = biomeTrainerPools[biome];
|
this.trainerPool = biomeTrainerPools[biome];
|
||||||
this.updatePoolsForTimeOfDay();
|
this.updatePoolsForTimeOfDay();
|
||||||
this.playerTerasUsed = 0;
|
|
||||||
this.playerFaints = playerFaints;
|
this.playerFaints = playerFaints;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -60,6 +60,7 @@ import { PartyHealPhase } from "#phases/party-heal-phase";
|
|||||||
import { PokemonAnimPhase } from "#phases/pokemon-anim-phase";
|
import { PokemonAnimPhase } from "#phases/pokemon-anim-phase";
|
||||||
import { PokemonHealPhase } from "#phases/pokemon-heal-phase";
|
import { PokemonHealPhase } from "#phases/pokemon-heal-phase";
|
||||||
import { PokemonTransformPhase } from "#phases/pokemon-transform-phase";
|
import { PokemonTransformPhase } from "#phases/pokemon-transform-phase";
|
||||||
|
import { PositionalTagPhase } from "#phases/positional-tag-phase";
|
||||||
import { PostGameOverPhase } from "#phases/post-game-over-phase";
|
import { PostGameOverPhase } from "#phases/post-game-over-phase";
|
||||||
import { PostSummonPhase } from "#phases/post-summon-phase";
|
import { PostSummonPhase } from "#phases/post-summon-phase";
|
||||||
import { PostTurnStatusEffectPhase } from "#phases/post-turn-status-effect-phase";
|
import { PostTurnStatusEffectPhase } from "#phases/post-turn-status-effect-phase";
|
||||||
@ -170,6 +171,7 @@ const PHASES = Object.freeze({
|
|||||||
PokemonAnimPhase,
|
PokemonAnimPhase,
|
||||||
PokemonHealPhase,
|
PokemonHealPhase,
|
||||||
PokemonTransformPhase,
|
PokemonTransformPhase,
|
||||||
|
PositionalTagPhase,
|
||||||
PostGameOverPhase,
|
PostGameOverPhase,
|
||||||
PostSummonPhase,
|
PostSummonPhase,
|
||||||
PostTurnStatusEffectPhase,
|
PostTurnStatusEffectPhase,
|
||||||
|
@ -6,7 +6,6 @@ import { StatusEffect } from "#enums/status-effect";
|
|||||||
import { FieldPhase } from "#phases/field-phase";
|
import { FieldPhase } from "#phases/field-phase";
|
||||||
import { NumberHolder } from "#utils/common";
|
import { NumberHolder } from "#utils/common";
|
||||||
import i18next from "i18next";
|
import i18next from "i18next";
|
||||||
import { ArenaTagType } from "#enums/arena-tag-type";
|
|
||||||
|
|
||||||
export class AttemptRunPhase extends FieldPhase {
|
export class AttemptRunPhase extends FieldPhase {
|
||||||
public readonly phaseName = "AttemptRunPhase";
|
public readonly phaseName = "AttemptRunPhase";
|
||||||
@ -43,10 +42,6 @@ export class AttemptRunPhase extends FieldPhase {
|
|||||||
|
|
||||||
globalScene.clearEnemyHeldItemModifiers();
|
globalScene.clearEnemyHeldItemModifiers();
|
||||||
|
|
||||||
// clear all queued delayed attacks (e.g. from Future Sight)
|
|
||||||
// TODO: review if this is necessary
|
|
||||||
globalScene.arena.removeTag(ArenaTagType.DELAYED_ATTACK);
|
|
||||||
|
|
||||||
enemyField.forEach(enemyPokemon => {
|
enemyField.forEach(enemyPokemon => {
|
||||||
enemyPokemon.hideInfo().then(() => enemyPokemon.destroy());
|
enemyPokemon.hideInfo().then(() => enemyPokemon.destroy());
|
||||||
enemyPokemon.hp = 0;
|
enemyPokemon.hp = 0;
|
||||||
|
@ -2,14 +2,12 @@ import { applyAbAttrs } from "#abilities/apply-ab-attrs";
|
|||||||
import { globalScene } from "#app/global-scene";
|
import { globalScene } from "#app/global-scene";
|
||||||
import { getPokemonNameWithAffix } from "#app/messages";
|
import { getPokemonNameWithAffix } from "#app/messages";
|
||||||
import Overrides from "#app/overrides";
|
import Overrides from "#app/overrides";
|
||||||
import type { DelayedAttackTag } from "#data/arena-tag";
|
|
||||||
import { CenterOfAttentionTag } from "#data/battler-tags";
|
import { CenterOfAttentionTag } from "#data/battler-tags";
|
||||||
import { SpeciesFormChangePreMoveTrigger } from "#data/form-change-triggers";
|
import { SpeciesFormChangePreMoveTrigger } from "#data/form-change-triggers";
|
||||||
import { getStatusEffectActivationText, getStatusEffectHealText } from "#data/status-effect";
|
import { getStatusEffectActivationText, getStatusEffectHealText } from "#data/status-effect";
|
||||||
import { getTerrainBlockMessage } from "#data/terrain";
|
import { getTerrainBlockMessage } from "#data/terrain";
|
||||||
import { getWeatherBlockMessage } from "#data/weather";
|
import { getWeatherBlockMessage } from "#data/weather";
|
||||||
import { AbilityId } from "#enums/ability-id";
|
import { AbilityId } from "#enums/ability-id";
|
||||||
import { ArenaTagType } from "#enums/arena-tag-type";
|
|
||||||
import { BattlerIndex } from "#enums/battler-index";
|
import { BattlerIndex } from "#enums/battler-index";
|
||||||
import { BattlerTagLapseType } from "#enums/battler-tag-lapse-type";
|
import { BattlerTagLapseType } from "#enums/battler-tag-lapse-type";
|
||||||
import { BattlerTagType } from "#enums/battler-tag-type";
|
import { BattlerTagType } from "#enums/battler-tag-type";
|
||||||
|
21
src/phases/positional-tag-phase.ts
Normal file
21
src/phases/positional-tag-phase.ts
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
import { globalScene } from "#app/global-scene";
|
||||||
|
import { Phase } from "#app/phase";
|
||||||
|
// biome-ignore-start lint/correctness/noUnusedImports: TSDocs
|
||||||
|
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";
|
||||||
|
// biome-ignore-end lint/correctness/noUnusedImports: TSDocs
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Phase to trigger all pending post-turn {@linkcode PositionalTag}s.
|
||||||
|
* Occurs before {@linkcode TurnEndPhase} to allow for proper electrify timing.
|
||||||
|
*/
|
||||||
|
export class PositionalTagPhase extends Phase {
|
||||||
|
public readonly phaseName = "PositionalTagPhase";
|
||||||
|
|
||||||
|
public override start(): void {
|
||||||
|
globalScene.arena.positionalTagManager.triggerAllTags();
|
||||||
|
super.end();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
@ -61,8 +61,6 @@ export class TurnEndPhase extends FieldPhase {
|
|||||||
|
|
||||||
this.executeForAll(handlePokemon);
|
this.executeForAll(handlePokemon);
|
||||||
|
|
||||||
// TODO: This needs to be moved up earlier to allow Future Sight to be affected by
|
|
||||||
// Electrify before the latter is removed
|
|
||||||
globalScene.arena.lapseTags();
|
globalScene.arena.lapseTags();
|
||||||
|
|
||||||
if (globalScene.arena.weather && !globalScene.arena.weather.lapse()) {
|
if (globalScene.arena.weather && !globalScene.arena.weather.lapse()) {
|
||||||
|
@ -219,12 +219,16 @@ export class TurnStartPhase extends FieldPhase {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TODO: Re-order these phases to be consistent with mainline turn order:
|
||||||
|
// https://www.smogon.com/forums/threads/sword-shield-battle-mechanics-research.3655528/page-64#post-9244179
|
||||||
|
|
||||||
phaseManager.pushNew("WeatherEffectPhase");
|
phaseManager.pushNew("WeatherEffectPhase");
|
||||||
phaseManager.pushNew("BerryPhase");
|
phaseManager.pushNew("BerryPhase");
|
||||||
|
|
||||||
/** Add a new phase to check who should be taking status damage */
|
/** Add a new phase to check who should be taking status damage */
|
||||||
phaseManager.pushNew("CheckStatusEffectPhase", moveOrder);
|
phaseManager.pushNew("CheckStatusEffectPhase", moveOrder);
|
||||||
|
|
||||||
|
phaseManager.pushNew("PositionalTagPhase");
|
||||||
phaseManager.pushNew("TurnEndPhase");
|
phaseManager.pushNew("TurnEndPhase");
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -1,5 +1,10 @@
|
|||||||
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 {
|
||||||
|
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";
|
||||||
@ -10,6 +15,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 playerTerasUsed: number;
|
public playerTerasUsed: number;
|
||||||
|
|
||||||
constructor(source: Arena | any) {
|
constructor(source: Arena | any) {
|
||||||
@ -31,5 +37,10 @@ export class ArenaData {
|
|||||||
if (source.tags) {
|
if (source.tags) {
|
||||||
this.tags = source.tags.map(t => loadArenaTag(t));
|
this.tags = source.tags.map(t => loadArenaTag(t));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
this.positionalTags =
|
||||||
|
(sourceArena ? sourceArena.positionalTagManager.tags : source.positionalTags)?.map((t: SerializedPositionalTag) =>
|
||||||
|
loadPositionalTag(t),
|
||||||
|
) ?? [];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1096,6 +1096,8 @@ export class GameData {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
globalScene.arena.positionalTagManager.tags = sessionData.arena.positionalTags;
|
||||||
|
|
||||||
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...");
|
||||||
globalScene.modifiers = [];
|
globalScene.modifiers = [];
|
||||||
|
@ -1,20 +1,19 @@
|
|||||||
import { BattlerIndex } from "#enums/battler-index";
|
import { DelayedAttackTag } from "#app/data/positional-tags/positional-tag";
|
||||||
import { allMoves } from "#app/data/data-lists";
|
|
||||||
import { DelayedAttackTag } from "#app/data/arena-tag";
|
|
||||||
import { allAbilities } from "#app/data/data-lists";
|
|
||||||
import { RandomMoveAttr } from "#app/data/moves/move";
|
|
||||||
import { MoveResult } from "#enums/move-result";
|
|
||||||
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 { RandomMoveAttr } from "#data/moves/move";
|
||||||
import { AbilityId } from "#enums/ability-id";
|
import { AbilityId } from "#enums/ability-id";
|
||||||
|
import { BattleType } from "#enums/battle-type";
|
||||||
|
import { BattlerIndex } from "#enums/battler-index";
|
||||||
import { MoveId } from "#enums/move-id";
|
import { MoveId } from "#enums/move-id";
|
||||||
|
import { MoveResult } from "#enums/move-result";
|
||||||
import { PokemonType } from "#enums/pokemon-type";
|
import { PokemonType } from "#enums/pokemon-type";
|
||||||
import { SpeciesId } from "#enums/species-id";
|
import { SpeciesId } from "#enums/species-id";
|
||||||
import GameManager from "#test/testUtils/gameManager";
|
import { GameManager } from "#test/testUtils/gameManager";
|
||||||
import i18next from "i18next";
|
import i18next from "i18next";
|
||||||
import Phaser from "phaser";
|
import Phaser from "phaser";
|
||||||
import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest";
|
import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest";
|
||||||
import { BattleType } from "#enums/battle-type";
|
|
||||||
|
|
||||||
describe("Moves - Delayed Attacks", () => {
|
describe("Moves - Delayed Attacks", () => {
|
||||||
let phaserGame: Phaser.Game;
|
let phaserGame: Phaser.Game;
|
||||||
@ -41,35 +40,42 @@ describe("Moves - Delayed Attacks", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Wait until a number of turns have passed.
|
* Wait until a number of turns have passed and a delayed attack has struck.
|
||||||
* @param numTurns - Number of turns to pass.
|
* @param numTurns - Number of turns to pass.
|
||||||
* @returns: A Promise that resolves once the specified number of turns has elapsed.
|
* @param toEndOfTurn - Whether to advance to the `TurnEndPhase` (true) or the `PositionalTagPhase` (`false`);
|
||||||
|
* default `true`
|
||||||
|
* @returns: A Promise that resolves once the specified number of turns has elapsed
|
||||||
|
* and the specified phase has been reached.
|
||||||
*/
|
*/
|
||||||
async function passTurns(numTurns: number): 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.currentBattle.double && game.scene.getPlayerField()[1]) {
|
||||||
game.move.use(MoveId.SPLASH, BattlerIndex.PLAYER_2);
|
game.move.use(MoveId.SPLASH, BattlerIndex.PLAYER_2);
|
||||||
}
|
}
|
||||||
await game.toNextTurn();
|
}
|
||||||
|
await game.phaseInterceptor.to("PositionalTagPhase");
|
||||||
|
if (toEndOfTurn) {
|
||||||
|
await game.toEndOfTurn();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 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) {
|
function expectFutureSightActive(numAttacks = 1): DelayedAttackTag[] {
|
||||||
const tag = game.scene.arena.getTag(DelayedAttackTag)!;
|
const delayedAttacks = game.scene.arena.positionalTagManager["tags"].filter(t => t instanceof DelayedAttackTag)!;
|
||||||
expect(tag).toBeDefined();
|
expect(delayedAttacks).toHaveLength(numAttacks);
|
||||||
expect(tag["delayedAttacks"]).toHaveLength(numAttacks);
|
return delayedAttacks;
|
||||||
}
|
}
|
||||||
|
|
||||||
it.each<{ name: string; move: MoveId }>([
|
it.each<{ name: string; move: MoveId }>([
|
||||||
{ name: "Future Sight", move: MoveId.FUTURE_SIGHT },
|
{ name: "Future Sight", move: MoveId.FUTURE_SIGHT },
|
||||||
{ name: "Doom Desire", move: MoveId.DOOM_DESIRE },
|
{ name: "Doom Desire", move: MoveId.DOOM_DESIRE },
|
||||||
])("$name should show message and strike 2 turns after use, ignoring player/enemy switches", async ({ move }) => {
|
])("$name should show message and strike 2 turns after use, ignoring player/enemy switches", async ({ move }) => {
|
||||||
game.override.battleType(BattleType.TRAINER)
|
game.override.battleType(BattleType.TRAINER);
|
||||||
await game.classicMode.startBattle([SpeciesId.FEEBAS, SpeciesId.MILOTIC]);
|
await game.classicMode.startBattle([SpeciesId.FEEBAS, SpeciesId.MILOTIC]);
|
||||||
|
|
||||||
game.move.use(move);
|
game.move.use(move);
|
||||||
@ -136,7 +142,7 @@ describe("Moves - Delayed Attacks", () => {
|
|||||||
|
|
||||||
game.move.use(MoveId.FUTURE_SIGHT, BattlerIndex.PLAYER, BattlerIndex.ENEMY);
|
game.move.use(MoveId.FUTURE_SIGHT, BattlerIndex.PLAYER, BattlerIndex.ENEMY);
|
||||||
game.move.use(MoveId.FUTURE_SIGHT, BattlerIndex.PLAYER_2, BattlerIndex.ENEMY_2);
|
game.move.use(MoveId.FUTURE_SIGHT, BattlerIndex.PLAYER_2, BattlerIndex.ENEMY_2);
|
||||||
await game.toEndOfTurn()
|
await game.toEndOfTurn();
|
||||||
|
|
||||||
expectFutureSightActive(2);
|
expectFutureSightActive(2);
|
||||||
expect(enemy1.hp).toBe(enemy1.getMaxHp());
|
expect(enemy1.hp).toBe(enemy1.getMaxHp());
|
||||||
@ -144,7 +150,12 @@ 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);
|
await passTurns(2, false);
|
||||||
|
|
||||||
|
// Both attacks have
|
||||||
|
expectFutureSightActive(0);
|
||||||
|
|
||||||
|
await game.toEndOfTurn();
|
||||||
|
|
||||||
expect(enemy1.hp).toBeLessThan(enemy1.getMaxHp());
|
expect(enemy1.hp).toBeLessThan(enemy1.getMaxHp());
|
||||||
expect(enemy2.hp).toBeLessThan(enemy2.getMaxHp());
|
expect(enemy2.hp).toBeLessThan(enemy2.getMaxHp());
|
||||||
@ -274,7 +285,7 @@ describe("Moves - Delayed Attacks", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
// TODO: this is not implemented
|
// TODO: this is not implemented
|
||||||
it.todo("should not apply Shell Bell recovery, even if user is on field")
|
it.todo("should not apply Shell Bell recovery, even if user is on field");
|
||||||
|
|
||||||
// TODO: Enable once code is added to MEP to do this
|
// TODO: Enable once code is added to MEP to do this
|
||||||
it.todo("should not apply the user's abilities when dealing damage if the user is inactive", async () => {
|
it.todo("should not apply the user's abilities when dealing damage if the user is inactive", async () => {
|
||||||
|
@ -37,6 +37,7 @@ import { NextEncounterPhase } from "#phases/next-encounter-phase";
|
|||||||
import { PartyExpPhase } from "#phases/party-exp-phase";
|
import { PartyExpPhase } from "#phases/party-exp-phase";
|
||||||
import { PartyHealPhase } from "#phases/party-heal-phase";
|
import { PartyHealPhase } from "#phases/party-heal-phase";
|
||||||
import { PokemonTransformPhase } from "#phases/pokemon-transform-phase";
|
import { PokemonTransformPhase } from "#phases/pokemon-transform-phase";
|
||||||
|
import { PositionalTagPhase } from "#phases/positional-tag-phase";
|
||||||
import { PostGameOverPhase } from "#phases/post-game-over-phase";
|
import { PostGameOverPhase } from "#phases/post-game-over-phase";
|
||||||
import { PostSummonPhase } from "#phases/post-summon-phase";
|
import { PostSummonPhase } from "#phases/post-summon-phase";
|
||||||
import { QuietFormChangePhase } from "#phases/quiet-form-change-phase";
|
import { QuietFormChangePhase } from "#phases/quiet-form-change-phase";
|
||||||
@ -142,6 +143,7 @@ export class PhaseInterceptor {
|
|||||||
[LevelCapPhase, this.startPhase],
|
[LevelCapPhase, this.startPhase],
|
||||||
[AttemptRunPhase, this.startPhase],
|
[AttemptRunPhase, this.startPhase],
|
||||||
[SelectBiomePhase, this.startPhase],
|
[SelectBiomePhase, this.startPhase],
|
||||||
|
[PositionalTagPhase, this.startPhase],
|
||||||
[PokemonTransformPhase, this.startPhase],
|
[PokemonTransformPhase, this.startPhase],
|
||||||
[MysteryEncounterPhase, this.startPhase],
|
[MysteryEncounterPhase, this.startPhase],
|
||||||
[MysteryEncounterOptionSelectedPhase, this.startPhase],
|
[MysteryEncounterOptionSelectedPhase, this.startPhase],
|
||||||
|
Loading…
Reference in New Issue
Block a user