Reverted changes to positional tag battle flyout

This commit is contained in:
Bertie690 2025-08-14 23:30:14 -04:00
parent 4de3226d99
commit 8383df1855
6 changed files with 4 additions and 265 deletions

View File

@ -1,7 +1,5 @@
import { globalScene } from "#app/global-scene";
import { DelayedAttackTag, type PositionalTag, WishTag } from "#data/positional-tags/positional-tag";
import { PositionalTagType } from "#enums/positional-tag-type";
import { PositionalTagAddedEvent } from "#events/arena";
import type { ObjectValues } from "#types/type-helpers";
import type { Constructor } from "#utils/common";
@ -22,12 +20,7 @@ export function loadPositionalTag<T extends PositionalTagType>(data: serializedP
* This function does not perform any checking if the added tag is valid.
*/
export function loadPositionalTag(tag: SerializedPositionalTag): PositionalTag;
export function loadPositionalTag(tag: SerializedPositionalTag): PositionalTag {
// Update the global arena flyout
globalScene.arena.eventTarget.dispatchEvent(new PositionalTagAddedEvent(tag));
// Create the new tag
const { tagType, ...rest } = tag;
export function loadPositionalTag({ tagType, ...rest }: SerializedPositionalTag): PositionalTag {
const tagClass = posTagConstructorMap[tagType];
// @ts-expect-error - tagType always corresponds to the proper constructor for `rest`
return new tagClass(rest);

View File

@ -1,9 +1,7 @@
import { globalScene } from "#app/global-scene";
import { loadPositionalTag } from "#data/positional-tags/load-positional-tag";
import type { PositionalTag } from "#data/positional-tags/positional-tag";
import type { BattlerIndex } from "#enums/battler-index";
import type { PositionalTagType } from "#enums/positional-tag-type";
import { PositionalTagRemovedEvent } from "#events/arena";
/** A manager for the {@linkcode PositionalTag}s in the arena. */
export class PositionalTagManager {
@ -51,16 +49,7 @@ export class PositionalTagManager {
if (tag.shouldTrigger()) {
tag.trigger();
}
this.emitRemove(tag);
}
this.tags = leftoverTags;
}
/**
* Emit a {@linkcode PositionalTagRemovedEvent} whenever a tag is removed from the field.
* @param tag - The {@linkcode PositionalTag} being removed
*/
private emitRemove(tag: PositionalTag): void {
globalScene.arena.eventTarget.dispatchEvent(new PositionalTagRemovedEvent(tag.tagType, tag.targetIndex));
}
}

View File

@ -20,11 +20,6 @@ export const ArenaEventType = {
ARENA_TAG_ADDED: "onArenaTagAdded",
/** Emitted when an existing {@linkcode ArenaTag} is removed */
ARENA_TAG_REMOVED: "onArenaTagRemoved",
/** Emitted when a new {@linkcode PositionalTag} is added */
POSITIONAL_TAG_ADDED: "onPositionalTagAdded",
/** Emitted when an existing {@linkcode PositionalTag} is removed */
POSITIONAL_TAG_REMOVED: "onPositionalTagRemoved",
} as const;
export type ArenaEventType = ObjectValues<typeof ArenaEventType>;

View File

@ -1,12 +1,7 @@
import type { SerializedPositionalTag } from "#data/positional-tags/load-positional-tag";
// biome-ignore lint/correctness/noUnusedImports: TSDoc
import type { PositionalTag } from "#data/positional-tags/positional-tag";
import type { TerrainType } from "#data/terrain";
import { ArenaEventType } from "#enums/arena-event-type";
import type { ArenaTagSide } from "#enums/arena-tag-side";
import type { ArenaTagType } from "#enums/arena-tag-type";
import type { BattlerIndex } from "#enums/battler-index";
import type { PositionalTagType } from "#enums/positional-tag-type";
import type { WeatherType } from "#enums/weather-type";
/**
@ -128,50 +123,3 @@ export class ArenaTagRemovedEvent extends ArenaEvent {
this.side = side;
}
}
/**
* Container class for {@linkcode ArenaEventType.POSITIONAL_TAG_ADDED} events. \
* Emitted whenever a new {@linkcode PositionalTag} is spawned and added to the arena.
* @eventProperty
*/
export class PositionalTagAddedEvent extends ArenaEvent {
declare type: typeof ArenaEventType.POSITIONAL_TAG_ADDED;
/** The {@linkcode SerializedPositionalTag} being added to the arena. */
public tag: SerializedPositionalTag;
/** The {@linkcode PositionalTagType} of the tag being added. */
public tagType: PositionalTagType;
/** The {@linkcode BattlerIndex} targeted by the newly created tag. */
public targetIndex: BattlerIndex;
/** The tag's current duration. */
public duration: number;
constructor(tag: SerializedPositionalTag) {
super(ArenaEventType.POSITIONAL_TAG_ADDED);
this.tag = tag;
}
}
/**
* Container class for {@linkcode ArenaEventType.POSITIONAL_TAG_REMOVED} events. \
* Emitted whenever a currently-active {@linkcode PositionalTag} triggers (or disappears)
* and is removed from the arena.
* @eventProperty
*/
export class PositionalTagRemovedEvent extends ArenaEvent {
declare type: typeof ArenaEventType.POSITIONAL_TAG_REMOVED;
/** The {@linkcode PositionalTagType} of the tag being deleted. */
public tagType: PositionalTagType;
/** The {@linkcode BattlerIndex} targeted by the newly removed tag. */
public targetIndex: BattlerIndex;
constructor(tagType: PositionalTagType, targetIndex: BattlerIndex) {
super(ArenaEventType.POSITIONAL_TAG_ADDED);
this.tagType = tagType;
this.targetIndex = targetIndex;
}
}

View File

@ -1,28 +1,15 @@
import { globalScene } from "#app/global-scene";
// biome-ignore-start lint/correctness/noUnusedImports: TSDocs
import type { ArenaTag } from "#data/arena-tag";
import type { SerializedPositionalTag } from "#data/positional-tags/load-positional-tag";
import type { PositionalTag } from "#data/positional-tags/positional-tag";
import { type Terrain, TerrainType } from "#data/terrain";
import type { Weather } from "#data/weather";
import { ArenaEventType } from "#enums/arena-event-type";
// biome-ignore-end lint/correctness/noUnusedImports: TSDocs
import { ArenaTagSide } from "#enums/arena-tag-side";
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 type { PositionalTagType } from "#enums/positional-tag-type";
import { TextStyle } from "#enums/text-style";
import { WeatherType } from "#enums/weather-type";
import type {
ArenaTagAddedEvent,
ArenaTagRemovedEvent,
PositionalTagAddedEvent,
PositionalTagRemovedEvent,
TerrainChangedEvent,
WeatherChangedEvent,
} from "#events/arena";
import type { ArenaTagAddedEvent, ArenaTagRemovedEvent, TerrainChangedEvent, WeatherChangedEvent } from "#events/arena";
import { BattleSceneEventType } from "#events/battle-scene";
import { addTextObject } from "#ui/text";
import { TimeOfDayWidget } from "#ui/time-of-day-widget";
@ -71,18 +58,6 @@ interface ArenaTagInfo {
tagType?: ArenaTagType;
}
/** Container for info about pending {@linkcode PositionalTag}s. */
interface PositionalTagInfo {
/** The localized name of the effect. */
name: string;
/** The {@linkcode BattlerIndex} that the effect is slated to affect. */
targetIndex: BattlerIndex;
/** The current duration of the effect. */
duration: number;
/** The tag's {@linkcode PositionalTagType}. */
tagType: PositionalTagType;
}
// #endregion interfaces
/**
@ -137,19 +112,13 @@ export class ArenaFlyout extends Phaser.GameObjects.Container {
/** Container for all {@linkcode ArenaTag}s observed by this object. */
private arenaTags: ArenaTagInfo[] = [];
/** Container for all {@linkcode PositionalTag}s observed by this object. */
private positionalTags: PositionalTagInfo[] = [];
// Store callbacks in variables so they can be unsubscribed from when destroyed
private readonly onNewArenaEvent = () => this.onNewArena();
private readonly onTurnEndEvent = () => this.onTurnEnd();
private readonly onWeatherChangedEvent = (event: WeatherChangedEvent) => this.onWeatherChanged(event);
private readonly onTerrainChangedEvent = (event: TerrainChangedEvent) => this.onTerrainChanged(event);
private readonly onArenaTagAddedEvent = (event: ArenaTagAddedEvent) => this.onArenaTagAdded(event);
private readonly onArenaTagRemovedEvent = (event: ArenaTagRemovedEvent) => this.onArenaTagRemoved(event);
private readonly onPositionalTagAddedEvent = (event: PositionalTagAddedEvent) => this.onPositionalTagAdded(event);
// biome-ignore format: Keeps lines in 1 piece
private readonly onPositionalTagRemovedEvent = (event: PositionalTagRemovedEvent) => this.onPositionalTagRemoved(event);
constructor() {
super(globalScene, 0, 0);
@ -267,26 +236,20 @@ export class ArenaFlyout extends Phaser.GameObjects.Container {
*/
private onNewArena() {
this.arenaTags = [];
this.positionalTags = [];
// Subscribe to required events available on battle start
// biome-ignore-start format: Keeps lines in 1 piece
globalScene.arena.eventTarget.addEventListener(ArenaEventType.WEATHER_CHANGED, this.onWeatherChangedEvent);
globalScene.arena.eventTarget.addEventListener(ArenaEventType.TERRAIN_CHANGED, this.onTerrainChangedEvent);
globalScene.arena.eventTarget.addEventListener(ArenaEventType.ARENA_TAG_ADDED, this.onArenaTagAddedEvent);
globalScene.arena.eventTarget.addEventListener(ArenaEventType.ARENA_TAG_REMOVED, this.onArenaTagRemovedEvent);
globalScene.arena.eventTarget.addEventListener(ArenaEventType.POSITIONAL_TAG_ADDED, this.onPositionalTagAddedEvent);
globalScene.arena.eventTarget.addEventListener(ArenaEventType.POSITIONAL_TAG_REMOVED, this.onPositionalTagRemovedEvent);
// biome-ignore-end format: Keeps lines in 1 piece
}
/**
* Iterate through all currently present field effects and decrement their durations.
* Iterate through all currently present tags effects and decrement their durations.
*/
private onTurnEnd() {
// Remove all objects with positive max durations and whose durations have expired.
this.arenaTags = this.arenaTags.filter(info => info.maxDuration === 0 || --info.duration >= 0);
this.positionalTags = this.positionalTags.filter(info => --info.duration > 0);
this.updateFieldText();
}
@ -339,7 +302,6 @@ export class ArenaFlyout extends Phaser.GameObjects.Container {
*/
private onArenaTagRemoved(event: ArenaTagRemovedEvent): void {
const foundIndex = this.arenaTags.findIndex(info => info.tagType === event.tagType && info.side === event.side);
console.log(this.positionalTags, event);
if (foundIndex > -1) {
// If the tag was being tracked, remove it
@ -350,42 +312,6 @@ export class ArenaFlyout extends Phaser.GameObjects.Container {
// #endregion ArenaTags
// #region PositionalTags
/**
* Add a recently-created {@linkcode PositionalTag} to the flyout.
* @param event - The {@linkcode PositionalTagAddedEvent} having been emitted
*/
private onPositionalTagAdded(event: PositionalTagAddedEvent): void {
const name = this.getPositionalTagDisplayName(event.tag);
this.positionalTags.push({
name,
targetIndex: event.tag.targetIndex,
duration: event.tag.turnCount,
tagType: event.tag.tagType,
});
this.updateFieldText();
}
/**
* Remove a recently-activated {@linkcode PositionalTag} from the flyout.
* @param event - The {@linkcode PositionalTagRemovedEvent} having been emitted
*/
private onPositionalTagRemoved(event: PositionalTagRemovedEvent): void {
const foundIndex = this.positionalTags.findIndex(
info => info.tagType === event.tagType && info.targetIndex === event.targetIndex,
);
if (foundIndex > -1) {
// If the tag was being tracked, remove it
this.positionalTags.splice(foundIndex, 1);
this.updateFieldText();
}
}
// #endregion PositionalTags
// #region Weather/Terrain
/**
@ -458,10 +384,6 @@ export class ArenaFlyout extends Phaser.GameObjects.Container {
globalScene.arena.eventTarget.removeEventListener(ArenaEventType.TERRAIN_CHANGED, this.onTerrainChanged);
globalScene.arena.eventTarget.removeEventListener(ArenaEventType.ARENA_TAG_ADDED, this.onArenaTagAddedEvent);
globalScene.arena.eventTarget.removeEventListener(ArenaEventType.ARENA_TAG_REMOVED, this.onArenaTagRemovedEvent);
// biome-ignore format: Keeps lines in 1 piece
globalScene.arena.eventTarget.removeEventListener(ArenaEventType.POSITIONAL_TAG_ADDED, this.onPositionalTagAddedEvent);
// biome-ignore format: Keeps lines in 1 piece
globalScene.arena.eventTarget.removeEventListener(ArenaEventType.POSITIONAL_TAG_REMOVED, this.onPositionalTagRemovedEvent);
super.destroy(fromScene);
}
@ -489,15 +411,6 @@ export class ArenaFlyout extends Phaser.GameObjects.Container {
this.flyoutTextField.text += this.getTagText(this.terrainInfo);
}
// Sort and add all positional tags
this.positionalTags.sort(
// Sort based on tag name, breaking ties by ascending target index.
(infoA, infoB) => infoA.name.localeCompare(infoB.name) || infoA.targetIndex - infoB.targetIndex,
);
for (const tag of this.positionalTags) {
this.getPositionalTagTextObj(tag).text += this.getPosTagText(tag);
}
// Sort and update all arena tag text
this.arenaTags.sort((infoA, infoB) => infoA.duration - infoB.duration);
for (const tag of this.arenaTags) {
@ -505,24 +418,6 @@ export class ArenaFlyout extends Phaser.GameObjects.Container {
}
}
/**
* Helper method to retrieve the flyout text for a given {@linkcode PositionalTag}.
* @param info - The {@linkcode PositionalTagInfo} whose text is being updated
* @returns The text to be added to the container
*/
private getPosTagText(info: PositionalTagInfo): string {
// Avoud showing slot target for single battles
if (!globalScene.currentBattle.double) {
return `${info.name} (${info.duration})\n`;
}
const targetPos = battlerIndexToFieldPosition(info.targetIndex);
const posText = this.localizeEffectName(FieldPosition[targetPos]);
// Ex: "Future Sight (Center, 2)"
return `${info.name} (${posText}, ${info.duration})\n`;
}
/**
* Helper method to retrieve the flyout text for a given effect's info.
* @param info - The {@linkcode ArenaTagInfo}, {@linkcode TerrainInfo} or {@linkcode WeatherInfo} being updated
@ -555,24 +450,6 @@ export class ArenaFlyout extends Phaser.GameObjects.Container {
}
}
/**
* Choose which text object needs to be updated depending on the current tag's target.
* @param info - The {@linkcode PositionalTagInfo} being displayed
* @returns The {@linkcode Phaser.GameObjects.Text} to be updated.
*/
private getPositionalTagTextObj(info: PositionalTagInfo): Phaser.GameObjects.Text {
switch (info.targetIndex) {
case BattlerIndex.PLAYER:
case BattlerIndex.PLAYER_2:
return this.flyoutTextPlayer;
case BattlerIndex.ENEMY:
case BattlerIndex.ENEMY_2:
return this.flyoutTextEnemy;
case BattlerIndex.ATTACKER:
throw new Error("BattlerIndex.ATTACKER used as tag target index for arena flyout!");
}
}
// # endregion Text display functions
// #region Utilities
@ -589,32 +466,5 @@ export class ArenaFlyout extends Phaser.GameObjects.Container {
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
}
/**
* Convert a {@linkcode BattlerIndex} into a field position.
* @param index - The {@linkcode BattlerIndex} to convert
* @returns The resultant field position.
*/
function battlerIndexToFieldPosition(index: BattlerIndex): FieldPosition {
const pos = globalScene.getField()[index]?.fieldPosition;
return pos;
}

View File

@ -1,15 +1,12 @@
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";
import { afterAll, beforeAll, beforeEach, describe, expect, it, type MockInstance } from "vitest";
describe("UI - Arena Flyout", () => {
let phaserGame: Phaser.Game;
@ -57,7 +54,6 @@ describe("UI - Arena Flyout", () => {
// Helper type to get around unexportedness
type infoType = Parameters<(typeof flyout)["getTagText"]>[0];
type posTagInfo = (typeof flyout)["positionalTags"][number];
describe("getTagText", () => {
it.each<{ info: Pick<infoType, "name" | "duration" | "maxDuration">; text: string }>([
@ -68,36 +64,4 @@ describe("UI - Arena Flyout", () => {
expect(got).toBe(text);
});
});
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");
});
});
});