Fixed tests and documentation

This commit is contained in:
Bertie690 2025-07-21 21:53:15 -04:00
parent 944fc14fb6
commit b377581075
5 changed files with 192 additions and 49 deletions

View File

@ -3157,9 +3157,13 @@ export class DelayedAttackAttr extends OverrideMoveEffectAttr {
// Display the move animation to foresee an attack
globalScene.phaseManager.unshiftNew("MoveAnimPhase", new MoveChargeAnim(this.chargeAnim, move.id, user));
globalScene.phaseManager.queueMessage(i18next.t(this.chargeText,
// uncomment if any new delayed moves actually use target in the move text.
{pokemonName: getPokemonNameWithAffix(user)/*, targetName: getPokemonNameWithAffix(target) */}))
globalScene.phaseManager.queueMessage(
i18next.t(
this.chargeText,
// uncomment if any new delayed moves actually use target in the move text.
{pokemonName: getPokemonNameWithAffix(user)/*, targetName: getPokemonNameWithAffix(target) */}
)
)
user.pushMoveHistory({move: move.id, targets: [target.getBattlerIndex()], result: MoveResult.OTHER, useMode: useMode, turn: globalScene.currentBattle.turn})
@ -3180,10 +3184,15 @@ export class DelayedAttackAttr extends OverrideMoveEffectAttr {
}
}
/** Attribute to queue a {@linkcode WishTag} to activate in 2 turns. */
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(),
globalScene.arena.positionalTagManager.addTag<PositionalTagType.WISH>({
tagType: PositionalTagType.WISH,
healHp: toDmgValue(user.getMaxHp() / 2),
targetIndex: target.getBattlerIndex(),
turnCount: 2,
pokemonName: getPokemonNameWithAffix(user),
});
return true;
}

View File

@ -12,14 +12,11 @@ import type { Pokemon } from "#field/pokemon";
import i18next from "i18next";
/**
* Baseline arguments used to construct all {@linkcode PositionalTag}s.
* Baseline arguments used to construct all {@linkcode PositionalTag}s,
* the contents of which are serialized and used to construct new tags. \
* Does not contain the `tagType` parameter (which is used to select the proper class constructor to use).
*/
export interface PositionalTagBaseArgs {
/**
* The {@linkcode Pokemon.id | PID} of the {@linkcode Pokemon} having created the effect.
*/
sourceId: number;
/**
* 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.
@ -42,12 +39,10 @@ export interface PositionalTagBaseArgs {
export abstract class PositionalTag implements PositionalTagBaseArgs {
public abstract readonly tagType: PositionalTagType;
// These arguments have to be public to implement the interface, but are functionally private.
public sourceId: number;
public turnCount: number;
public targetIndex: BattlerIndex;
constructor({ sourceId, turnCount, targetIndex }: PositionalTagBaseArgs) {
this.sourceId = sourceId;
constructor({ turnCount, targetIndex }: PositionalTagBaseArgs) {
this.turnCount = turnCount;
this.targetIndex = targetIndex;
}
@ -79,6 +74,10 @@ export abstract class PositionalTag implements PositionalTagBaseArgs {
}
interface DelayedAttackArgs extends PositionalTagBaseArgs {
/**
* The {@linkcode Pokemon.id | PID} of the {@linkcode Pokemon} having created this effect.
*/
sourceId: number;
/** The {@linkcode MoveId} that created this attack. */
sourceMove: MoveId;
}
@ -91,9 +90,11 @@ interface DelayedAttackArgs extends PositionalTagBaseArgs {
export class DelayedAttackTag extends PositionalTag implements DelayedAttackArgs {
public override readonly tagType = PositionalTagType.DELAYED_ATTACK;
public sourceMove: MoveId;
public sourceId: number;
constructor({ sourceId, turnCount, targetIndex, sourceMove }: DelayedAttackArgs) {
super({ sourceId, turnCount, targetIndex });
super({ turnCount, targetIndex });
this.sourceId = sourceId;
this.sourceMove = sourceMove;
}
@ -130,6 +131,10 @@ export class DelayedAttackTag extends PositionalTag implements DelayedAttackArgs
interface WishArgs extends PositionalTagBaseArgs {
/** The amount of {@linkcode Stat.HP | HP} to heal; set to 50% of the user's max HP during move usage. */
healHp: number;
/**
* The name of the {@linkcode Pokemon} having created the tag..
*/
pokemonName: string;
}
/**
@ -138,17 +143,29 @@ interface WishArgs extends PositionalTagBaseArgs {
export class WishTag extends PositionalTag implements WishArgs {
public override readonly tagType = PositionalTagType.WISH;
readonly pokemonName: string;
public healHp: number;
constructor({ sourceId, turnCount, targetIndex, healHp }: WishArgs) {
super({ sourceId, turnCount, targetIndex });
constructor({ turnCount, targetIndex, healHp, pokemonName }: WishArgs) {
super({ turnCount, targetIndex });
this.healHp = healHp;
this.pokemonName = pokemonName;
}
public trigger(): void {
// TODO: Rename this locales key - wish shows a message on REMOVAL, dumbass
globalScene.phaseManager.queueMessage(
i18next.t("arenaTag:wishTagOnAdd", {
pokemonName: this.pokemonName,
}),
);
globalScene.phaseManager.unshiftNew("PokemonHealPhase", this.targetIndex, this.healHp, null, true, false);
}
public shouldDisappear(): boolean {
// Disappear if no target.
// The source need not exist at the time of activation (since all we need is a simple message)
return !!this.getTarget();
}
}

View File

@ -19,7 +19,7 @@ import { MoveFlags } from "#enums/MoveFlags";
import { MoveTarget } from "#enums/MoveTarget";
import { MoveId } from "#enums/move-id";
import { MoveResult } from "#enums/move-result";
import { isReflected, isVirtual, MoveUseMode } from "#enums/move-use-mode";
import { isReflected, MoveUseMode } from "#enums/move-use-mode";
import { PokemonType } from "#enums/pokemon-type";
import type { Pokemon } from "#field/pokemon";
import {
@ -244,22 +244,15 @@ export class MoveEffectPhase extends PokemonPhase {
globalScene.currentBattle.lastPlayerInvolved = this.fieldIndex;
}
const isDelayedAttack = this.move.hasAttr("DelayedAttackAttr");
/** If the user was somehow removed from the field and it's not a delayed attack, end this phase */
if (!user.isOnField()) {
if (!isDelayedAttack) {
super.end();
return;
}
if (!user.scene) {
/*
* This happens if the Pokemon that used the delayed attack gets caught and released
* on the turn the attack would have triggered. Having access to the global scene
* in the future may solve this entirely, so for now we just cancel the hit
*/
super.end();
return;
}
if (!user.scene) {
/*
* This happens if the Pokemon that used the delayed attack gets caught and released
* on the turn the attack would have triggered. Having access to the global scene
* in the future may solve this entirely, so for now we just cancel the hit
*/
console.warn("User scene bye bye bye skibidi rizz");
super.end();
return;
}
const move = this.move;
@ -270,18 +263,12 @@ export class MoveEffectPhase extends PokemonPhase {
*/
const overridden = new BooleanHolder(false);
console.log(this.useMode);
// Apply effects to override a move effect.
// Assuming single target here works as this is (currently)
// only used for Future Sight, calling and Pledge moves.
// TODO: change if any other move effect overrides are introduced
applyMoveAttrs(
"OverrideMoveEffectAttr",
user,
this.getFirstTarget() ?? null,
move,
overridden,
isVirtual(this.useMode),
);
applyMoveAttrs("OverrideMoveEffectAttr", user, this.getFirstTarget() ?? null, move, overridden, this.useMode);
// If other effects were overriden, stop this phase before they can be applied
if (overridden.value) {

View File

@ -1,4 +1,3 @@
import { DelayedAttackTag } from "#app/data/positional-tags/positional-tag";
import { getPokemonNameWithAffix } from "#app/messages";
import { AttackTypeBoosterModifier } from "#app/modifier/modifier";
import { allMoves } from "#data/data-lists";
@ -8,6 +7,7 @@ import { BattlerIndex } from "#enums/battler-index";
import { MoveId } from "#enums/move-id";
import { MoveResult } from "#enums/move-result";
import { PokemonType } from "#enums/pokemon-type";
import { PositionalTagType } from "#enums/positional-tag-type";
import { SpeciesId } from "#enums/species-id";
import { GameManager } from "#test/testUtils/gameManager";
import i18next from "i18next";
@ -68,7 +68,9 @@ describe("Moves - Delayed Attacks", () => {
* @param numAttacks - The number of delayed attacks that should be queued; default `1`
*/
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.tagType === PositionalTagType.DELAYED_ATTACK,
);
expect(delayedAttacks).toHaveLength(numAttacks);
}
@ -171,8 +173,8 @@ describe("Moves - Delayed Attacks", () => {
expectFutureSightActive(4);
game.move.use(MoveId.TAILWIND);
game.move.use(MoveId.COTTON_SPORE);
game.move.use(MoveId.TAILWIND, BattlerIndex.PLAYER);
game.move.use(MoveId.COTTON_SPORE, BattlerIndex.PLAYER_2);
await passTurns(1, false);
expect(game.field.getSpeedOrder()).not.toEqual(usageOrder);
@ -251,12 +253,10 @@ describe("Moves - Delayed Attacks", () => {
expectFutureSightActive(1);
await passTurns(1);
game.move.use(MoveId.SPLASH);
await game.killPokemon(enemy2);
await game.toNextTurn();
await passTurns(2);
expectFutureSightActive(0);
expect(enemy1.hp).toBe(enemy1.getMaxHp());
expect(game.textInterceptor.logs).not.toContain(
i18next.t("moveTriggers:tookMoveAttack", {
@ -279,7 +279,8 @@ describe("Moves - Delayed Attacks", () => {
expectFutureSightActive(1);
await passTurns(1);
game.move.use(MoveId.SPLASH, BattlerIndex.PLAYER);
await game.toNextTurn();
game.move.use(MoveId.SPLASH, BattlerIndex.PLAYER);
await game.move.forceEnemyMove(MoveId.ELECTRIFY, BattlerIndex.PLAYER);

129
test/moves/wish.test.ts Normal file
View File

@ -0,0 +1,129 @@
import { getPokemonNameWithAffix } from "#app/messages";
import { AbilityId } from "#enums/ability-id";
import { BattlerIndex } from "#enums/battler-index";
import { MoveId } from "#enums/move-id";
import { PositionalTagType } from "#enums/positional-tag-type";
import { SpeciesId } from "#enums/species-id";
import { Stat } from "#enums/stat";
import { GameManager } from "#test/testUtils/gameManager";
import { toDmgValue } from "#utils/common";
import i18next from "i18next";
import Phaser from "phaser";
import { afterEach, beforeAll, beforeEach, describe, expect, it } from "vitest";
describe("Move - Wish", () => {
let phaserGame: Phaser.Game;
let game: GameManager;
beforeAll(() => {
phaserGame = new Phaser.Game({
type: Phaser.HEADLESS,
});
});
afterEach(() => {
game.phaseInterceptor.restoreOg();
});
beforeEach(() => {
game = new GameManager(phaserGame);
game.override
.ability(AbilityId.BALL_FETCH)
.battleStyle("single")
.criticalHits(false)
.enemySpecies(SpeciesId.MAGIKARP)
.enemyAbility(AbilityId.BALL_FETCH)
.enemyMoveset(MoveId.SPLASH)
.startingLevel(100)
.enemyLevel(100);
});
/**
* Expect that wish is active with the specified number of attacks.
* @param numAttacks - The number of wish instances that should be queued; default `1`
*/
function expectWishActive(numAttacks = 1) {
const wishes = game.scene.arena.positionalTagManager["tags"].filter(t => t.tagType === PositionalTagType.WISH);
expect(wishes).toHaveLength(numAttacks);
}
it("should heal the Pokemon in the current slot for 50% of the user's maximum HP", async () => {
await game.classicMode.startBattle([SpeciesId.ALOMOMOLA, SpeciesId.BLISSEY]);
const [alomomola, blissey] = game.scene.getPlayerParty();
alomomola.hp = 1;
blissey.hp = 1;
game.move.use(MoveId.WISH);
await game.toNextTurn();
expectWishActive();
game.doSwitchPokemon(1);
await game.toEndOfTurn();
expectWishActive(0);
expect(game.textInterceptor.logs).toContain(
i18next.t("arenaTag:wishTagOnAdd", {
pokemonName: getPokemonNameWithAffix(blissey),
}),
);
expect(alomomola.hp).toBe(1);
expect(blissey.hp).toBe(toDmgValue(alomomola.getMaxHp() / 2) + 1);
});
it("should function independently of Future Sight", async () => {
await game.classicMode.startBattle([SpeciesId.ALOMOMOLA, SpeciesId.BLISSEY]);
const [alomomola, blissey] = game.scene.getPlayerParty();
alomomola.hp = 1;
blissey.hp = 1;
game.move.use(MoveId.WISH);
await game.move.forceEnemyMove(MoveId.FUTURE_SIGHT);
await game.setTurnOrder([BattlerIndex.PLAYER, BattlerIndex.ENEMY]);
await game.toNextTurn();
expectWishActive(1);
});
it("should work in double battles and triggerin order of creation", async () => {
game.override.battleStyle("double");
await game.classicMode.startBattle([SpeciesId.ALOMOMOLA, SpeciesId.BLISSEY]);
const [alomomola, blissey] = game.scene.getPlayerParty();
alomomola.hp = 1;
blissey.hp = 1;
const oldOrder = game.field.getSpeedOrder();
game.move.use(MoveId.WISH, BattlerIndex.PLAYER);
game.move.use(MoveId.WISH, BattlerIndex.PLAYER_2);
await game.toNextTurn();
expectWishActive(2);
// Lower speed to change turn order
alomomola.setStatStage(Stat.SPD, 6);
blissey.setStatStage(Stat.SPD, -6);
const newOrder = game.field.getSpeedOrder();
expect(newOrder).not.toEqual(oldOrder);
game.move.use(MoveId.SPLASH, BattlerIndex.PLAYER);
game.move.use(MoveId.SPLASH, BattlerIndex.PLAYER_2);
await game.phaseInterceptor.to("PositionalTagPhase");
// Both wishes have activated and added healing phases
expectWishActive(0);
const healPhases = game.scene.phaseManager.phaseQueue.filter(p => p.is("PokemonHealPhase"));
expect(healPhases).toHaveLength(4);
expect(healPhases.map(php => php["battlerIndex"])).toEqual(oldOrder);
await game.toEndOfTurn();
expect(alomomola.hp).toBe(toDmgValue(alomomola.getMaxHp() / 2) + 1);
expect(blissey.hp).toBe(toDmgValue(blissey.getMaxHp() / 2) + 1);
});
});