mirror of
https://github.com/pagefaultgames/pokerogue.git
synced 2025-08-19 22:09:27 +02:00
Added tests for the arena flyout + improved type inference on tags
This commit is contained in:
parent
a821fc2f80
commit
73993f25c9
@ -42,7 +42,7 @@ const posTagConstructorMap = Object.freeze({
|
||||
[PositionalTagType.WISH]: WishTag,
|
||||
}) satisfies {
|
||||
// NB: This `satisfies` block ensures that all tag types have corresponding entries in the map.
|
||||
[k in PositionalTagType]: Constructor<PositionalTag & { tagType: k }>;
|
||||
[k in PositionalTagType]: Constructor<PositionalTag & { readonly tagType: k }>;
|
||||
};
|
||||
|
||||
/** Type mapping positional tag types to their constructors. */
|
||||
@ -59,11 +59,12 @@ type posTagParamMap = {
|
||||
};
|
||||
|
||||
/**
|
||||
* Type mapping all positional tag types to their constructors' parameters, alongside the `tagType` selector.
|
||||
* Type mapping all positional tag types to their constructors' parameters, alongside the `tagType` selector. \
|
||||
* Equivalent to their serialized representations.
|
||||
* @interface
|
||||
*/
|
||||
export type serializedPosTagMap = {
|
||||
[k in PositionalTagType]: posTagParamMap[k] & { tagType: k };
|
||||
[k in PositionalTagType]: posTagParamMap[k] & Pick<posTagInstanceMap[k], "tagType">;
|
||||
};
|
||||
|
||||
/** Union type containing all serialized {@linkcode PositionalTag}s. */
|
||||
|
@ -7,7 +7,7 @@ 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 { PositionalTagType } from "#enums/positional-tag-type";
|
||||
import type { PositionalTagType } from "#enums/positional-tag-type";
|
||||
import type { Pokemon } from "#field/pokemon";
|
||||
import i18next from "i18next";
|
||||
|
||||
@ -30,7 +30,7 @@ export interface PositionalTagBaseArgs {
|
||||
/**
|
||||
* The {@linkcode BattlerIndex} targeted by this effect.
|
||||
*/
|
||||
targetIndex: BattlerIndex;
|
||||
readonly targetIndex: BattlerIndex;
|
||||
}
|
||||
|
||||
/**
|
||||
@ -39,7 +39,7 @@ export interface PositionalTagBaseArgs {
|
||||
* Multiple tags of the same kind can stack with one another, provided they are affecting different targets.
|
||||
*/
|
||||
export abstract class PositionalTag implements PositionalTagBaseArgs {
|
||||
/** This tag's {@linkcode PositionalTagType | type} */
|
||||
/** This tag's {@linkcode PositionalTagType | type}. */
|
||||
public abstract readonly tagType: PositionalTagType;
|
||||
// These arguments have to be public to implement the interface, but are functionally private
|
||||
// outside this and the tag manager.
|
||||
@ -76,9 +76,9 @@ interface DelayedAttackArgs extends PositionalTagBaseArgs {
|
||||
/**
|
||||
* The {@linkcode Pokemon.id | PID} of the {@linkcode Pokemon} having created this effect.
|
||||
*/
|
||||
sourceId: number;
|
||||
readonly sourceId: number;
|
||||
/** The {@linkcode MoveId} that created this attack. */
|
||||
sourceMove: MoveId;
|
||||
readonly sourceMove: MoveId;
|
||||
}
|
||||
|
||||
/**
|
||||
@ -87,7 +87,7 @@ interface DelayedAttackArgs extends PositionalTagBaseArgs {
|
||||
* triggering against a certain slot after the turn count has elapsed.
|
||||
*/
|
||||
export class DelayedAttackTag extends PositionalTag implements DelayedAttackArgs {
|
||||
public override readonly tagType = PositionalTagType.DELAYED_ATTACK;
|
||||
public declare readonly tagType: PositionalTagType.DELAYED_ATTACK;
|
||||
public readonly sourceMove: MoveId;
|
||||
public readonly sourceId: number;
|
||||
|
||||
@ -133,16 +133,16 @@ export class DelayedAttackTag extends PositionalTag implements DelayedAttackArgs
|
||||
/** Interface containing arguments used to construct a {@linkcode WishTag}. */
|
||||
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;
|
||||
readonly healHp: number;
|
||||
/** The name of the {@linkcode Pokemon} having created the tag. */
|
||||
pokemonName: string;
|
||||
readonly pokemonName: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Tag to implement {@linkcode MoveId.WISH | Wish}.
|
||||
*/
|
||||
export class WishTag extends PositionalTag implements WishArgs {
|
||||
public override readonly tagType = PositionalTagType.WISH;
|
||||
public declare readonly tagType: PositionalTagType.WISH;
|
||||
|
||||
public readonly pokemonName: string;
|
||||
public readonly healHp: number;
|
||||
|
@ -35,4 +35,4 @@ export type ArenaEventType = ObjectValues<typeof ArenaEventType>;
|
||||
{@linkcode TerrainType}
|
||||
{@linkcode PositionalTag}
|
||||
{@linkcode ArenaTag}
|
||||
*/
|
||||
*/
|
||||
|
@ -12,7 +12,7 @@ import { ArenaTagType } from "#enums/arena-tag-type";
|
||||
import { BattlerIndex } from "#enums/battler-index";
|
||||
import { FieldPosition } from "#enums/field-position";
|
||||
import { MoveId } from "#enums/move-id";
|
||||
import { PositionalTagType } from "#enums/positional-tag-type";
|
||||
import type { PositionalTagType } from "#enums/positional-tag-type";
|
||||
import { TextStyle } from "#enums/text-style";
|
||||
import { WeatherType } from "#enums/weather-type";
|
||||
import type {
|
||||
@ -85,38 +85,6 @@ interface PositionalTagInfo {
|
||||
|
||||
// #endregion interfaces
|
||||
|
||||
// #region String functions
|
||||
|
||||
/**
|
||||
* Return the localized text for a given effect.
|
||||
* @param text - The raw text of the effect; assumed to be in `UPPER_SNAKE_CASE` from a reverse mapping.
|
||||
* @returns The localized text for the effect.
|
||||
*/
|
||||
export function localizeEffectName(text: string): string {
|
||||
const effectName = toCamelCase(text);
|
||||
const i18nKey = `arenaFlyout:${effectName}`;
|
||||
const resultName = i18next.t(i18nKey);
|
||||
return resultName;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the localized name of a given {@linkcode PositionalTag}.
|
||||
* @param tag - The raw serialized data for the given tag
|
||||
* @returns The localized text to be displayed on-screen.
|
||||
* @package
|
||||
*/
|
||||
export function getPositionalTagDisplayName(tag: SerializedPositionalTag): string {
|
||||
let tagName: string;
|
||||
if ("sourceMove" in tag) {
|
||||
// Delayed attacks will use the source move's name; other effects rely on type
|
||||
tagName = MoveId[tag.sourceMove];
|
||||
} else {
|
||||
tagName = PositionalTagType[tag.tagType];
|
||||
}
|
||||
|
||||
return localizeEffectName(tagName);
|
||||
}
|
||||
|
||||
/**
|
||||
* Class to display and update the on-screen arena flyout.
|
||||
*/
|
||||
@ -330,7 +298,7 @@ export class ArenaFlyout extends Phaser.GameObjects.Container {
|
||||
* @param event - The {@linkcode ArenaTagAddedEvent} having been emitted
|
||||
*/
|
||||
private onArenaTagAdded(event: ArenaTagAddedEvent): void {
|
||||
const name = localizeEffectName(ArenaTagType[event.tagType]);
|
||||
const name = this.localizeEffectName(ArenaTagType[event.tagType]);
|
||||
// Ternary used to avoid unneeded find
|
||||
const existingTrapTag =
|
||||
event.trapLayers !== undefined
|
||||
@ -389,7 +357,7 @@ export class ArenaFlyout extends Phaser.GameObjects.Container {
|
||||
* @param event - The {@linkcode PositionalTagAddedEvent} having been emitted
|
||||
*/
|
||||
private onPositionalTagAdded(event: PositionalTagAddedEvent): void {
|
||||
const name = getPositionalTagDisplayName(event.tag);
|
||||
const name = this.getPositionalTagDisplayName(event.tag);
|
||||
|
||||
this.positionalTags.push({
|
||||
name,
|
||||
@ -433,7 +401,7 @@ export class ArenaFlyout extends Phaser.GameObjects.Container {
|
||||
}
|
||||
|
||||
this.weatherInfo = {
|
||||
name: localizeEffectName(WeatherType[event.weatherType]),
|
||||
name: this.localizeEffectName(WeatherType[event.weatherType]),
|
||||
maxDuration: event.duration,
|
||||
duration: event.duration,
|
||||
weatherType: event.weatherType,
|
||||
@ -455,7 +423,7 @@ export class ArenaFlyout extends Phaser.GameObjects.Container {
|
||||
}
|
||||
|
||||
this.terrainInfo = {
|
||||
name: localizeEffectName(TerrainType[event.terrainType]),
|
||||
name: this.localizeEffectName(TerrainType[event.terrainType]),
|
||||
maxDuration: event.duration,
|
||||
duration: event.duration,
|
||||
terrainType: event.terrainType,
|
||||
@ -549,7 +517,7 @@ export class ArenaFlyout extends Phaser.GameObjects.Container {
|
||||
}
|
||||
|
||||
const targetPos = battlerIndexToFieldPosition(info.targetIndex);
|
||||
const posText = localizeEffectName(FieldPosition[targetPos]);
|
||||
const posText = this.localizeEffectName(FieldPosition[targetPos]);
|
||||
|
||||
// Ex: "Future Sight (Center, 2)"
|
||||
return `${info.name} (${posText}, ${info.duration})\n`;
|
||||
@ -606,6 +574,39 @@ export class ArenaFlyout extends Phaser.GameObjects.Container {
|
||||
}
|
||||
|
||||
// # endregion Text display functions
|
||||
|
||||
// #region Utilities
|
||||
|
||||
/**
|
||||
* Return the localized text for a given effect.
|
||||
* @param text - The raw text of the effect; assumed to be in `UPPER_SNAKE_CASE` from a reverse mapping.
|
||||
* @returns The localized text for the effect.
|
||||
*/
|
||||
private localizeEffectName(text: string): string {
|
||||
const effectName = toCamelCase(text);
|
||||
const i18nKey = `arenaFlyout:${effectName}`;
|
||||
const resultName = i18next.t(i18nKey);
|
||||
return resultName;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the localized name of a given {@linkcode PositionalTag}.
|
||||
* @param tag - The raw serialized data for the given tag
|
||||
* @returns The localized text to be displayed on-screen.
|
||||
*/
|
||||
private getPositionalTagDisplayName(tag: SerializedPositionalTag): string {
|
||||
let tagName: string;
|
||||
if ("sourceMove" in tag) {
|
||||
// Delayed attacks will use the source move's name; other effects use type directly
|
||||
tagName = MoveId[tag.sourceMove];
|
||||
} else {
|
||||
tagName = tag.tagType;
|
||||
}
|
||||
|
||||
return this.localizeEffectName(tagName);
|
||||
}
|
||||
|
||||
// #endregion Utility emthods
|
||||
}
|
||||
|
||||
/**
|
||||
@ -614,22 +615,6 @@ export class ArenaFlyout extends Phaser.GameObjects.Container {
|
||||
* @returns The resultant field position.
|
||||
*/
|
||||
function battlerIndexToFieldPosition(index: BattlerIndex): FieldPosition {
|
||||
let pos: FieldPosition;
|
||||
switch (index) {
|
||||
case BattlerIndex.ATTACKER:
|
||||
throw new Error("Cannot convert BattlerIndex.ATTACKER to a field position!");
|
||||
case BattlerIndex.PLAYER:
|
||||
case BattlerIndex.ENEMY:
|
||||
pos = FieldPosition.LEFT;
|
||||
break;
|
||||
case BattlerIndex.PLAYER_2:
|
||||
case BattlerIndex.ENEMY_2:
|
||||
pos = FieldPosition.RIGHT;
|
||||
break;
|
||||
}
|
||||
// In single battles, left positions become center
|
||||
if (!globalScene.currentBattle.double && pos === FieldPosition.LEFT) {
|
||||
pos = FieldPosition.CENTER;
|
||||
}
|
||||
const pos = globalScene.getField()[index]?.fieldPosition;
|
||||
return pos;
|
||||
}
|
||||
|
@ -1,13 +1,12 @@
|
||||
import i18next, { type ParseKeys } from "i18next";
|
||||
import { vi } from "vitest";
|
||||
import { type MockInstance, vi } from "vitest";
|
||||
|
||||
/**
|
||||
* Sets up the i18next mock.
|
||||
* Includes a i18next.t mocked implementation only returning the raw key (`(key) => key`)
|
||||
* Mock i18next's {@linkcode t} function to only produce the raw key.
|
||||
*
|
||||
* @returns A spy/mock of i18next
|
||||
* @returns A {@linkcode MockInstance} for `i18next.t`
|
||||
*/
|
||||
export function mockI18next() {
|
||||
export function mockI18next(): MockInstance<(typeof i18next)["t"]> {
|
||||
return vi.spyOn(i18next, "t").mockImplementation((key: ParseKeys) => key);
|
||||
}
|
||||
|
||||
|
@ -1,25 +1,24 @@
|
||||
import type { SerializedPositionalTag, serializedPosTagMap } from "#data/positional-tags/load-positional-tag";
|
||||
import type { DelayedAttackTag, WishTag } from "#data/positional-tags/positional-tag";
|
||||
import type { PositionalTagType } from "#enums/positional-tag-type";
|
||||
import type { Mutable, NonFunctionPropertiesRecursive } from "#types/type-helpers";
|
||||
import type { NonFunctionPropertiesRecursive } from "#types/type-helpers";
|
||||
import { describe, expectTypeOf, it } from "vitest";
|
||||
|
||||
// Needed to get around properties being readonly in certain classes
|
||||
type NonFunctionMutable<T> = Mutable<NonFunctionPropertiesRecursive<T>>;
|
||||
|
||||
describe("serializedPositionalTagMap", () => {
|
||||
it("should contain representations of each tag's serialized form", () => {
|
||||
expectTypeOf<serializedPosTagMap[PositionalTagType.DELAYED_ATTACK]>().branded.toEqualTypeOf<
|
||||
NonFunctionMutable<DelayedAttackTag>
|
||||
NonFunctionPropertiesRecursive<DelayedAttackTag>
|
||||
>();
|
||||
expectTypeOf<serializedPosTagMap[PositionalTagType.WISH]>().branded.toEqualTypeOf<
|
||||
NonFunctionPropertiesRecursive<WishTag>
|
||||
>();
|
||||
expectTypeOf<serializedPosTagMap[PositionalTagType.WISH]>().branded.toEqualTypeOf<NonFunctionMutable<WishTag>>();
|
||||
});
|
||||
});
|
||||
|
||||
describe("SerializedPositionalTag", () => {
|
||||
it("should accept a union of all serialized tag forms", () => {
|
||||
expectTypeOf<SerializedPositionalTag>().branded.toEqualTypeOf<
|
||||
NonFunctionMutable<DelayedAttackTag> | NonFunctionMutable<WishTag>
|
||||
NonFunctionPropertiesRecursive<DelayedAttackTag> | NonFunctionPropertiesRecursive<WishTag>
|
||||
>();
|
||||
});
|
||||
it("should accept a union of all unserialized tag forms", () => {
|
||||
|
92
test/ui/flyouts/arena-flyout.test.ts
Normal file
92
test/ui/flyouts/arena-flyout.test.ts
Normal file
@ -0,0 +1,92 @@
|
||||
import type { SerializedPositionalTag } from "#data/positional-tags/load-positional-tag";
|
||||
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 { GameManager } from "#test/test-utils/game-manager";
|
||||
import { mockI18next } from "#test/test-utils/test-utils";
|
||||
import type { ArenaFlyout } from "#ui/arena-flyout";
|
||||
import type i18next from "i18next";
|
||||
import Phaser from "phaser";
|
||||
import { afterAll, beforeAll, beforeEach, describe, expect, it, type MockInstance, vi } from "vitest";
|
||||
|
||||
describe("UI - Arena Flyout", () => {
|
||||
let phaserGame: Phaser.Game;
|
||||
let game: GameManager;
|
||||
let flyout: ArenaFlyout;
|
||||
let tSpy: MockInstance<(typeof i18next)["t"]>;
|
||||
|
||||
beforeAll(async () => {
|
||||
phaserGame = new Phaser.Game({
|
||||
type: Phaser.HEADLESS,
|
||||
});
|
||||
|
||||
game = new GameManager(phaserGame);
|
||||
game.override
|
||||
.ability(AbilityId.BALL_FETCH)
|
||||
.battleStyle("double")
|
||||
.criticalHits(false)
|
||||
.enemySpecies(SpeciesId.MAGIKARP)
|
||||
.enemyAbility(AbilityId.BALL_FETCH)
|
||||
.enemyMoveset(MoveId.SPLASH)
|
||||
.startingLevel(100)
|
||||
.enemyLevel(100);
|
||||
|
||||
await game.classicMode.startBattle([SpeciesId.FEEBAS]);
|
||||
|
||||
flyout = game.scene.arenaFlyout;
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
// Reset i18n mock before each test
|
||||
tSpy = mockI18next();
|
||||
});
|
||||
|
||||
afterAll(() => {
|
||||
game.phaseInterceptor.restoreOg();
|
||||
});
|
||||
|
||||
describe("localizeEffectName", () => {
|
||||
it("should retrieve locales from an effect name", () => {
|
||||
const name = flyout["localizeEffectName"]("STEALTH_ROCK");
|
||||
expect(name).toBe("arenaFlyout:stealthRock");
|
||||
expect(tSpy).toHaveBeenCalledExactlyOnceWith("arenaFlyout:stealthRock");
|
||||
});
|
||||
});
|
||||
|
||||
// Helper type to get around unexportedness
|
||||
type posTagInfo = (typeof flyout)["positionalTags"][number];
|
||||
|
||||
describe("getPositionalTagDisplayName", () => {
|
||||
it.each([
|
||||
{ tag: { tagType: PositionalTagType.WISH }, name: "arenaFlyout:wish" },
|
||||
{
|
||||
tag: { sourceMove: MoveId.FUTURE_SIGHT, tagType: PositionalTagType.DELAYED_ATTACK },
|
||||
name: "arenaFlyout:futureSight",
|
||||
},
|
||||
{
|
||||
tag: { sourceMove: MoveId.DOOM_DESIRE, tagType: PositionalTagType.DELAYED_ATTACK },
|
||||
name: "arenaFlyout:doomDesire",
|
||||
},
|
||||
])("should get the name of a Positional Tag", ({ tag, name }) => {
|
||||
const got = flyout["getPositionalTagDisplayName"](tag as SerializedPositionalTag);
|
||||
expect(got).toBe(name);
|
||||
});
|
||||
});
|
||||
|
||||
describe("getPosTagText", () => {
|
||||
it.each<{ tag: Pick<posTagInfo, "duration" | "name" | "targetIndex">; output: string; double?: boolean }>([
|
||||
{ tag: { duration: 2, name: "Wish", targetIndex: BattlerIndex.PLAYER }, output: "Wish (2)" },
|
||||
{
|
||||
tag: { duration: 1, name: "Future Sight", targetIndex: BattlerIndex.ENEMY_2 },
|
||||
double: true,
|
||||
output: "Future Sight (arenaFlyout:right, 1)",
|
||||
},
|
||||
])("should produce the correct text", ({ tag, output, double = false }) => {
|
||||
vi.spyOn(game.scene.currentBattle, "double", "get").mockReturnValue(double);
|
||||
const text = flyout["getPosTagText"](tag as posTagInfo);
|
||||
expect(text).toBe(output + "\n");
|
||||
});
|
||||
});
|
||||
});
|
Loading…
Reference in New Issue
Block a user