pokerogue/src/ui/arena-flyout.ts
Matthew dc30dd33b2
[QoL] Reorganize Enum Locations (#2185)
* moving enums

* import updates

* fix tsconfig paths importing (#2184)

* reverse index.ts addition

---------

Co-authored-by: Devin Korb <meepdarknessmeep@gmail.com>
2024-06-13 18:44:23 -04:00

371 lines
15 KiB
TypeScript

import { addTextObject, TextStyle } from "./text";
import BattleScene from "#app/battle-scene.js";
import { ArenaTagSide } from "#app/data/arena-tag.js";
import { WeatherType } from "#app/data/weather.js";
import { TerrainType } from "#app/data/terrain.js";
import { addWindow, WindowVariant } from "./ui-theme";
import { ArenaEvent, ArenaEventType, TagAddedEvent, TagRemovedEvent, TerrainChangedEvent, WeatherChangedEvent } from "#app/events/arena.js";
import { BattleSceneEventType, TurnEndEvent } from "../events/battle-scene";
import { ArenaTagType } from "#enums/arena-tag-type";
import TimeOfDayWidget from "./time-of-day-widget";
import * as Utils from "../utils";
/** Enum used to differentiate {@linkcode Arena} effects */
enum ArenaEffectType {
PLAYER,
WEATHER,
TERRAIN,
FIELD,
ENEMY,
}
/** Container for info about an {@linkcode Arena}'s effects */
interface ArenaEffectInfo {
/** The enum string representation of the effect */
name: string;
/** {@linkcode ArenaEffectType} type of effect */
type: ArenaEffectType,
/** The maximum duration set by the effect */
maxDuration: number;
/** The current duration left on the effect */
duration: number;
}
export default class ArenaFlyout extends Phaser.GameObjects.Container {
/** An alias for the scene typecast to a {@linkcode BattleScene} */
private battleScene: BattleScene;
/** The restricted width of the flyout which should be drawn to */
private flyoutWidth = 170;
/** The restricted height of the flyout which should be drawn to */
private flyoutHeight = 51;
/** The amount of translation animation on the x-axis */
private translationX: number;
/** The x-axis point where the flyout should sit when activated */
private anchorX: number;
/** The y-axis point where the flyout should sit when activated */
private anchorY: number;
/** The initial container which defines where the flyout should be attached */
private flyoutParent: Phaser.GameObjects.Container;
/** The container which defines the drawable dimensions of the flyout */
private flyoutContainer: Phaser.GameObjects.Container;
/** The background {@linkcode Phaser.GameObjects.NineSlice} window for the flyout */
private flyoutWindow: Phaser.GameObjects.NineSlice;
/** The header {@linkcode Phaser.GameObjects.NineSlice} window for the flyout */
private flyoutWindowHeader: Phaser.GameObjects.NineSlice;
/** The {@linkcode Phaser.GameObjects.Text} that goes inside of the header */
private flyoutTextHeader: Phaser.GameObjects.Text;
private timeOfDayWidget: TimeOfDayWidget;
/** The {@linkcode Phaser.GameObjects.Text} header used to indicate the player's effects */
private flyoutTextHeaderPlayer: Phaser.GameObjects.Text;
/** The {@linkcode Phaser.GameObjects.Text} header used to indicate the enemy's effects */
private flyoutTextHeaderEnemy: Phaser.GameObjects.Text;
/** The {@linkcode Phaser.GameObjects.Text} header used to indicate neutral effects */
private flyoutTextHeaderField: Phaser.GameObjects.Text;
/** The {@linkcode Phaser.GameObjects.Text} used to indicate the player's effects */
private flyoutTextPlayer: Phaser.GameObjects.Text;
/** The {@linkcode Phaser.GameObjects.Text} used to indicate the enemy's effects */
private flyoutTextEnemy: Phaser.GameObjects.Text;
/** The {@linkcode Phaser.GameObjects.Text} used to indicate neutral effects */
private flyoutTextField: Phaser.GameObjects.Text;
/** Container for all field effects observed by this object */
private readonly fieldEffectInfo: ArenaEffectInfo[] = [];
// Stores callbacks in a variable so they can be unsubscribed from when destroyed
private readonly onNewArenaEvent = (event: Event) => this.onNewArena(event);
private readonly onTurnEndEvent = (event: Event) => this.onTurnEnd(event);
private readonly onFieldEffectChangedEvent = (event: Event) => this.onFieldEffectChanged(event);
constructor(scene: Phaser.Scene) {
super(scene, 0, 0);
this.battleScene = this.scene as BattleScene;
this.translationX = this.flyoutWidth;
this.anchorX = 0;
this.anchorY = -98;
this.flyoutParent = this.scene.add.container(this.anchorX - this.translationX, this.anchorY);
this.flyoutParent.setAlpha(0);
this.add(this.flyoutParent);
this.flyoutContainer = this.scene.add.container(0, 0);
this.flyoutParent.add(this.flyoutContainer);
this.flyoutWindow = addWindow(this.scene as BattleScene, 0, 0, this.flyoutWidth, this.flyoutHeight, false, false, 0, 0, WindowVariant.THIN);
this.flyoutContainer.add(this.flyoutWindow);
this.flyoutWindowHeader = addWindow(this.scene as BattleScene, this.flyoutWidth / 2, 0, this.flyoutWidth / 2, 14, false, false, 0, 0, WindowVariant.XTHIN);
this.flyoutWindowHeader.setOrigin();
this.flyoutContainer.add(this.flyoutWindowHeader);
this.flyoutTextHeader = addTextObject(this.scene, this.flyoutWidth / 2, 0, "Active Battle Effects", TextStyle.BATTLE_INFO);
this.flyoutTextHeader.setFontSize(54);
this.flyoutTextHeader.setAlign("center");
this.flyoutTextHeader.setOrigin();
this.flyoutContainer.add(this.flyoutTextHeader);
this.timeOfDayWidget = new TimeOfDayWidget(this.scene, (this.flyoutWidth / 2) + (this.flyoutWindowHeader.displayWidth / 2));
this.flyoutContainer.add(this.timeOfDayWidget);
this.flyoutTextHeaderPlayer = addTextObject(this.scene, 6, 5, "Player", TextStyle.SUMMARY_BLUE);
this.flyoutTextHeaderPlayer.setFontSize(54);
this.flyoutTextHeaderPlayer.setAlign("left");
this.flyoutTextHeaderPlayer.setOrigin(0, 0);
this.flyoutContainer.add(this.flyoutTextHeaderPlayer);
this.flyoutTextHeaderField = addTextObject(this.scene, this.flyoutWidth / 2, 5, "Neutral", TextStyle.SUMMARY_GREEN);
this.flyoutTextHeaderField.setFontSize(54);
this.flyoutTextHeaderField.setAlign("center");
this.flyoutTextHeaderField.setOrigin(0.5, 0);
this.flyoutContainer.add(this.flyoutTextHeaderField);
this.flyoutTextHeaderEnemy = addTextObject(this.scene, this.flyoutWidth - 6, 5, "Enemy", TextStyle.SUMMARY_RED);
this.flyoutTextHeaderEnemy.setFontSize(54);
this.flyoutTextHeaderEnemy.setAlign("right");
this.flyoutTextHeaderEnemy.setOrigin(1, 0);
this.flyoutContainer.add(this.flyoutTextHeaderEnemy);
this.flyoutTextPlayer = addTextObject(this.scene, 6, 13, "", TextStyle.BATTLE_INFO);
this.flyoutTextPlayer.setLineSpacing(-1);
this.flyoutTextPlayer.setFontSize(48);
this.flyoutTextPlayer.setAlign("left");
this.flyoutTextPlayer.setOrigin(0, 0);
this.flyoutContainer.add(this.flyoutTextPlayer);
this.flyoutTextField = addTextObject(this.scene, this.flyoutWidth / 2, 13, "", TextStyle.BATTLE_INFO);
this.flyoutTextField.setLineSpacing(-1);
this.flyoutTextField.setFontSize(48);
this.flyoutTextField.setAlign("center");
this.flyoutTextField.setOrigin(0.5, 0);
this.flyoutContainer.add(this.flyoutTextField);
this.flyoutTextEnemy = addTextObject(this.scene, this.flyoutWidth - 6, 13, "", TextStyle.BATTLE_INFO);
this.flyoutTextEnemy.setLineSpacing(-1);
this.flyoutTextEnemy.setFontSize(48);
this.flyoutTextEnemy.setAlign("right");
this.flyoutTextEnemy.setOrigin(1, 0);
this.flyoutContainer.add(this.flyoutTextEnemy);
this.name = "Fight Flyout";
this.flyoutParent.name = "Fight Flyout Parent";
// Subscribes to required events available on game start
this.battleScene.eventTarget.addEventListener(BattleSceneEventType.NEW_ARENA, this.onNewArenaEvent);
this.battleScene.eventTarget.addEventListener(BattleSceneEventType.TURN_END, this.onTurnEndEvent);
}
private onNewArena(event: Event) {
this.fieldEffectInfo.length = 0;
// Subscribes to required events available on battle start
this.battleScene.arena.eventTarget.addEventListener(ArenaEventType.WEATHER_CHANGED, this.onFieldEffectChangedEvent);
this.battleScene.arena.eventTarget.addEventListener(ArenaEventType.TERRAIN_CHANGED, this.onFieldEffectChangedEvent);
this.battleScene.arena.eventTarget.addEventListener(ArenaEventType.TAG_ADDED, this.onFieldEffectChangedEvent);
this.battleScene.arena.eventTarget.addEventListener(ArenaEventType.TAG_REMOVED, this.onFieldEffectChangedEvent);
}
/**
* Formats a string to title case
* @param unformattedText Text to be formatted
* @returns the formatted string
*/
private formatText(unformattedText: string): string {
const text = unformattedText.split("_");
for (let i = 0; i < text.length; i++) {
text[i] = text[i].charAt(0).toUpperCase() + text[i].substring(1).toLowerCase();
}
return text.join(" ");
}
/** Clears out the current string stored in all arena effect texts */
private clearText() {
this.flyoutTextPlayer.text = "";
this.flyoutTextField.text = "";
this.flyoutTextEnemy.text = "";
}
/** Parses through all set Arena Effects and puts them into the proper {@linkcode Phaser.GameObjects.Text} object */
private updateFieldText() {
this.clearText();
this.fieldEffectInfo.sort((infoA, infoB) => infoA.duration - infoB.duration);
for (let i = 0; i < this.fieldEffectInfo.length; i++) {
const fieldEffectInfo = this.fieldEffectInfo[i];
// Creates a proxy object to decide which text object needs to be updated
let textObject: Phaser.GameObjects.Text;
switch (fieldEffectInfo.type) {
case ArenaEffectType.PLAYER:
textObject = this.flyoutTextPlayer;
break;
case ArenaEffectType.WEATHER:
case ArenaEffectType.TERRAIN:
case ArenaEffectType.FIELD:
textObject = this.flyoutTextField;
break;
case ArenaEffectType.ENEMY:
textObject = this.flyoutTextEnemy;
break;
}
textObject.text += this.formatText(fieldEffectInfo.name);
if (fieldEffectInfo.type === ArenaEffectType.TERRAIN) {
textObject.text += " Terrain"; // Adds 'Terrain' since the enum does not contain it
}
if (fieldEffectInfo.maxDuration !== 0) {
textObject.text += " " + fieldEffectInfo.duration + "/" + fieldEffectInfo.maxDuration;
}
textObject.text += "\n";
}
}
/**
* Parses the {@linkcode Event} being passed and updates the state of the fieldEffectInfo array
* @param event {@linkcode Event} being sent
*/
private onFieldEffectChanged(event: Event) {
const arenaEffectChangedEvent = event as ArenaEvent;
if (!arenaEffectChangedEvent) {
return;
}
let foundIndex: number;
switch (arenaEffectChangedEvent.constructor) {
case TagAddedEvent:
const tagAddedEvent = arenaEffectChangedEvent as TagAddedEvent;
this.fieldEffectInfo.push({
name: ArenaTagType[tagAddedEvent.arenaTagType],
type: tagAddedEvent.arenaTagSide === ArenaTagSide.BOTH
? ArenaEffectType.FIELD
: tagAddedEvent.arenaTagSide === ArenaTagSide.PLAYER
? ArenaEffectType.PLAYER
: ArenaEffectType.ENEMY,
maxDuration: tagAddedEvent.duration,
duration: tagAddedEvent.duration});
break;
case TagRemovedEvent:
const tagRemovedEvent = arenaEffectChangedEvent as TagRemovedEvent;
foundIndex = this.fieldEffectInfo.findIndex(info => info.name === ArenaTagType[tagRemovedEvent.arenaTagType]);
if (foundIndex !== -1) { // If the tag was being tracked, remove it
this.fieldEffectInfo.splice(foundIndex, 1);
}
break;
case WeatherChangedEvent:
case TerrainChangedEvent:
const fieldEffectChangedEvent = arenaEffectChangedEvent as WeatherChangedEvent | TerrainChangedEvent;
// Stores the old Weather/Terrain name in case it's in the array already
const oldName =
fieldEffectChangedEvent instanceof WeatherChangedEvent
? WeatherType[fieldEffectChangedEvent.oldWeatherType]
: TerrainType[fieldEffectChangedEvent.oldTerrainType];
// Stores the new Weather/Terrain info
const newInfo = {
name:
fieldEffectChangedEvent instanceof WeatherChangedEvent
? WeatherType[fieldEffectChangedEvent.newWeatherType]
: TerrainType[fieldEffectChangedEvent.newTerrainType],
type: fieldEffectChangedEvent instanceof WeatherChangedEvent
? ArenaEffectType.WEATHER
: ArenaEffectType.TERRAIN,
maxDuration: fieldEffectChangedEvent.duration,
duration: fieldEffectChangedEvent.duration};
foundIndex = this.fieldEffectInfo.findIndex(info => [newInfo.name, oldName].includes(info.name));
if (foundIndex === -1) {
if (newInfo.name !== undefined) {
this.fieldEffectInfo.push(newInfo); // Adds the info to the array if it doesn't already exist and is defined
}
} else if (!newInfo.name) {
this.fieldEffectInfo.splice(foundIndex, 1); // Removes the old info if the new one is undefined
} else {
this.fieldEffectInfo[foundIndex] = newInfo; // Otherwise, replace the old info
}
break;
}
this.updateFieldText();
}
/**
* Iterates through the fieldEffectInfo array and decrements the duration of each item
* @param event {@linkcode Event} being sent
*/
private onTurnEnd(event: Event) {
const turnEndEvent = event as TurnEndEvent;
if (!turnEndEvent) {
return;
}
const fieldEffectInfo: ArenaEffectInfo[] = [];
this.fieldEffectInfo.forEach(i => fieldEffectInfo.push(i));
for (let i = 0; i < fieldEffectInfo.length; i++) {
const info = fieldEffectInfo[i];
if (info.maxDuration === 0) {
continue;
}
--info.duration;
if (info.duration <= 0) { // Removes the item if the duration has expired
this.fieldEffectInfo.splice(this.fieldEffectInfo.indexOf(info), 1);
}
}
this.updateFieldText();
}
/**
* Animates the flyout to either show or hide it by applying a fade and translation
* @param visible Should the flyout be shown?
*/
public toggleFlyout(visible: boolean): void {
this.scene.tweens.add({
targets: this.flyoutParent,
x: visible ? this.anchorX : this.anchorX - this.translationX,
duration: Utils.fixedInt(125),
ease: "Sine.easeInOut",
alpha: visible ? 1 : 0,
onComplete: () => this.timeOfDayWidget.parentVisible = visible,
});
}
public destroy(fromScene?: boolean): void {
this.battleScene.eventTarget.removeEventListener(BattleSceneEventType.NEW_ARENA, this.onNewArenaEvent);
this.battleScene.eventTarget.removeEventListener(BattleSceneEventType.TURN_END, this.onTurnEndEvent);
this.battleScene.arena.eventTarget.removeEventListener(ArenaEventType.WEATHER_CHANGED, this.onFieldEffectChangedEvent);
this.battleScene.arena.eventTarget.removeEventListener(ArenaEventType.TERRAIN_CHANGED, this.onFieldEffectChangedEvent);
this.battleScene.arena.eventTarget.removeEventListener(ArenaEventType.TAG_ADDED, this.onFieldEffectChangedEvent);
this.battleScene.arena.eventTarget.removeEventListener(ArenaEventType.TAG_REMOVED, this.onFieldEffectChangedEvent);
super.destroy(fromScene);
}
}