From 98cff12fd1f625d826c6d4e6eb4da549978b661f Mon Sep 17 00:00:00 2001 From: Benjamin Odom Date: Mon, 3 Jun 2024 21:18:09 -0500 Subject: [PATCH] [QoL] Add Arena Info Flyout for Weather, Terrain, etc. (#1734) * Initial Commit * Add Time of Day Icons and Remove Fight UI Dependancy * Rename to Match * Update battle-scene.ts * Add Settings * Add Comments --- public/images/ui/dawn_icon.png | Bin 0 -> 581 bytes public/images/ui/day_icon.png | Bin 0 -> 593 bytes public/images/ui/dusk_icon.png | Bin 0 -> 534 bytes public/images/ui/legacy/dawn_icon.png | Bin 0 -> 327 bytes public/images/ui/legacy/day_icon.png | Bin 0 -> 285 bytes public/images/ui/legacy/dusk_icon.png | Bin 0 -> 300 bytes public/images/ui/legacy/night_icon.png | Bin 0 -> 288 bytes public/images/ui/night_icon.png | Bin 0 -> 686 bytes src/battle-scene.ts | 15 + src/field/arena-events.ts | 31 +- src/field/arena.ts | 12 +- src/loading-scene.ts | 5 + src/system/settings/settings.ts | 11 + src/ui-inputs.ts | 12 +- src/ui/arena-flyout.ts | 384 +++++++++++++++++++++++++ src/ui/ui.ts | 15 + 16 files changed, 472 insertions(+), 13 deletions(-) create mode 100644 public/images/ui/dawn_icon.png create mode 100644 public/images/ui/day_icon.png create mode 100644 public/images/ui/dusk_icon.png create mode 100644 public/images/ui/legacy/dawn_icon.png create mode 100644 public/images/ui/legacy/day_icon.png create mode 100644 public/images/ui/legacy/dusk_icon.png create mode 100644 public/images/ui/legacy/night_icon.png create mode 100644 public/images/ui/night_icon.png create mode 100644 src/ui/arena-flyout.ts diff --git a/public/images/ui/dawn_icon.png b/public/images/ui/dawn_icon.png new file mode 100644 index 0000000000000000000000000000000000000000..e04e6024aa0b097dff236c4b4a1ca56e24bd4531 GIT binary patch literal 581 zcmV-L0=oT)P)Px#1ZP1_K>z@;j|==^1poj532;bRa{vGi!vFvd!vV){sAK>D0oqAKK~y+Tjgw7F z+)xyT-z3KIYf#5lXmn6ysHiY4rh?E`L2PFyYFF*bh4TYusNk>AAK=EFZrluX;ihf` z1(niL3$4;BiYZmckCExL(ZrmSA2^(Ia_4>TxpNZL^A#r@6w^~U;zcVG<9rvtKhI!m^CQgsn#9(Ms+}HZe|Bkp5;l z?D7Z;eB|sVj34tXc1T;M;l9cQAI1Bag46iy?X37hBJJ$XACoZf2^!sk5V`=D1CXre ThUT}f00000NkvXXu0mjfz();` literal 0 HcmV?d00001 diff --git a/public/images/ui/day_icon.png b/public/images/ui/day_icon.png new file mode 100644 index 0000000000000000000000000000000000000000..fe41acffd9cfb2096a87a86cadbbc7f57ea136a8 GIT binary patch literal 593 zcmV-X0Px#1ZP1_K>z@;j|==^1poj532;bRa{vGi!T|WK~y+TV`M-B ztSZKZK&lVI!vICU@0{(2vcUju0J28!qzndI{}2!p4i0Zz!?1PU3@8s_5ZC~iX6L9B z2G{TikPuF=d*up-z02l+m|#PgK`dq=spIIHRrx+L_(&aP&=;4!@HjxU~y#~ z9fto*Yz+4=pL75B?s+3K)OPpC(q;yB4i>N?kY?*wa~Zg=En#4Lc#J`k;UmLWAuYHe zqOz(Chu1FyS^S9!$Ok(e8CVLRVc>eTn}PBDTQCN(L1M@Ns;3WR0E4+lAc!If!1RC( zKnKU)nlOA5b7lB1C=A9RHb@K`z-qwlPy7r=#gZAm*q4AYhz%0M22c$6^zkQ@1uTAV>bk`KybV0iKT6T{M@FByKWEnr~y$4~@GiL%E3nKe8a zSQMK{C;?r5t0)5r9S0kT0@ f09+#kFfafBum-cC{w~Yc00000NkvXXu0mjf3PJjV literal 0 HcmV?d00001 diff --git a/public/images/ui/dusk_icon.png b/public/images/ui/dusk_icon.png new file mode 100644 index 0000000000000000000000000000000000000000..e848fa313459ce44809049ffe80f8a0ce298da7c GIT binary patch literal 534 zcmV+x0_pvUP)Px#1ZP1_K>z@;j|==^1poj532;bRa{vGi!vFvd!vV){sAK>D0jo(wK~y+Tjg!kt z!%z^0|J-OmisGeGQ9-*EM2ZVVUA73~6Da5-vnB%CR}p2kH{Vt{ur@=C(bPW6(Vf9i9@dVitK?fa!ot~o5cNzb&O5LAnTt{_I8`Z zN{_#$v(qOw5eAaX)x}(sSbhN3mLW~YSr`-(^9y~#(dk_Bs{hw|P%QU4W8>NHLMEq9 z#M(HZPe>e;dtJ!xd|RlpHY~O)oZz6`>!u@vc64MwYc|U^P&?wFoSqRf9Sk}oP1H0V z>aWhJA>3>#BVC4ZdjE|?tswrcuH*3Cgs6yJl-lL77`>jjlBOe1qm9p5)?WB YKRnE+uEonOR{#J207*qoM6N<$g63c9P5=M^ literal 0 HcmV?d00001 diff --git a/public/images/ui/legacy/dawn_icon.png b/public/images/ui/legacy/dawn_icon.png new file mode 100644 index 0000000000000000000000000000000000000000..eb24a799ab96cdd032ef8a57595bd602482bc62b GIT binary patch literal 327 zcmV-N0l5B&P)Px#|4BqaR5(v#WWWyo;}B;=7e^NWYdloaj-u}J-REF__WDUMakxHY1O7joJQ=R> z&ciQoHp~E+0MsC`UYG$WnqdlwF$7`2;>ipQA5eXWZa`p;D#Q9Iw~@WW2r}URGmvH& zKr#fM3t(XaHsHth5U}=lZ|ot)Kn)?ufPjM6V7<@Yy#v!z;-j#}1hOC~Fu^YHP`8I` zM2}~XLFlmn3k$dbdZM~G(hWp7-Nq5CJlp_FWm~v{ub&_Rz|ALsqA5nt5X@h4={#Kg z_}#;ZbbxFKUgtn1L7GA72%ZioH2`GVe_{=R*$#FBnsdRK370`ojVSssvM1C)qO&sq ZXBK-4;%UQk00000NkvXXu0mjf008%?gY^Ia literal 0 HcmV?d00001 diff --git a/public/images/ui/legacy/day_icon.png b/public/images/ui/legacy/day_icon.png new file mode 100644 index 0000000000000000000000000000000000000000..310ba50dcf306bc4fb559b4a96ae9da8d53bc40b GIT binary patch literal 285 zcmV+&0pk9NP)Px#*GWV{R5(v#WWWNXm0bQ~6MuSd4Lki*gF1_po zv-QNwQM@3Ur2*!@o^}H+etu0q13Vg$4I$`x1_lO@W>8|HjsdXLNUSShwuAIx%}lrq j0%^pVnNUKXgzO9eAn$t--D6GR00000NkvXXu0mjfSW0br literal 0 HcmV?d00001 diff --git a/public/images/ui/legacy/dusk_icon.png b/public/images/ui/legacy/dusk_icon.png new file mode 100644 index 0000000000000000000000000000000000000000..f383ebf52463d1dbe123698d570ef84645f209ba GIT binary patch literal 300 zcmeAS@N?(olHy`uVBq!ia0vp^0wB!61|;P_|4#%`jKx9jP7LeL$-D$|o_M-AhFJI~ zryO8fV%hpHS-Ae!Us>f{7F`TJ$y57S%>MG^hQ~D=oyglH&2Xv5d-1YO8eNSWrS%W< zGDzD(vPBlle_=Ij|i#u**btO<>n`h6yTW&MwS94z}&R_d(F-aBRZD zWY+CZA1$s<*tSn5tKlnKmr(;xNB4f_@ajjW4B0A7l%$(3pY;$@IJ-c^QbO8@`> literal 0 HcmV?d00001 diff --git a/public/images/ui/legacy/night_icon.png b/public/images/ui/legacy/night_icon.png new file mode 100644 index 0000000000000000000000000000000000000000..1796081c05bf64e52bb85513f3f59c65f1d23efd GIT binary patch literal 288 zcmeAS@N?(olHy`uVBq!ia0vp^0wB!61|;P_|4#%`jKx9jP7LeL$-D$|Zg{#lhFJI~ zrySt>_gEm^MciGSA)Dt@#=%wfcl)GfSj5P0V*Fp28MEV1r=new{GEUmjtP4WmwjJPUkYp-i46Ji6XKZ6^@Aq^6xNdE! zn%?`xENVAc7wx%ZA)0K=A?CNBKp}(SL`uxMMrj504BqmC-|hJiPx#1ZP1_K>z@;j|==^1poj532;bRa{vGi!2kdb!2!6DYwZ940z*keK~y+Tjgw7G z6HySy|Ld})w8R#%SgRFjfY?|yp%)U2(Sz(wC4rj>Q9P?`ylB+mO%thJ^lH3#FkHPE zB|S)uDORQ0sA(!32Q^=kB% zz9&q9=0tnD6KxJJ-a|*^` z>BVPrF;*py@$r+mGkqFD%NB}#SuCNWvlYI6H(o{(;9ZF9F~_S*QY&V{*YWWFEG1i5 zZ{oN|i^COp*N`Y_`p0NnrZMZH%57 zqHp9#76)o7aCBJ4V97bNM6umd$gO1LPIos7`Jc3U1Ag=m?4`9@Eh3jmLumY_#G#R6aCvuP_R#~brD%h+ zHK^|B*$1!B&t2G^HnJRVw@=>z>BS`3#C#NAQ*4k{*dXzMX!8_`xvZ?RpW^6nu?efw zvREavE56FP3X*SMQ!+zoRP<5iurAEY)ZC3hI{D5>(this.arenaFlyout, this.fieldOverlay); + this.updateUIPositions(); this.damageNumberHandler = new DamageNumberHandler(); @@ -1272,6 +1280,13 @@ export default class BattleScene extends SceneBase { return sprite; } + moveBelowOverlay(gameObject: T) { + this.fieldUI.moveBelow(gameObject, this.fieldOverlay); + } + processInfoButton(pressed: boolean): void { + this.arenaFlyout.toggleFlyout(pressed); + } + showFieldOverlay(duration: integer): Promise { return new Promise(resolve => { this.tweens.add({ diff --git a/src/field/arena-events.ts b/src/field/arena-events.ts index 1cc632030a5..b3d03fdfcae 100644 --- a/src/field/arena-events.ts +++ b/src/field/arena-events.ts @@ -10,8 +10,10 @@ export enum ArenaEventType { /** Triggers when a {@linkcode TerrainType} is added, overlapped, or removed */ TERRAIN_CHANGED = "onTerrainChanged", - /** Triggers when a {@linkcode ArenaTagType} is added or removed */ - TAG_CHANGED = "onTagChanged", + /** Triggers when a {@linkcode ArenaTagType} is added */ + TAG_ADDED = "onTagAdded", + /** Triggers when a {@linkcode ArenaTagType} is removed */ + TAG_REMOVED = "onTagRemoved", } /** @@ -59,17 +61,34 @@ export class TerrainChangedEvent extends ArenaEvent { this.newTerrainType = newTerrainType; } } + /** - * Container class for {@linkcode ArenaEventType.TAG_CHANGED} events + * Container class for {@linkcode ArenaEventType.TAG_ADDED} events * @extends ArenaEvent */ -export class TagChangedEvent extends ArenaEvent { - /** The {@linkcode ArenaTagType} being set */ +export class TagAddedEvent extends ArenaEvent { + /** The {@linkcode ArenaTagType} being added */ public arenaTagType: ArenaTagType; /** The {@linkcode ArenaTagSide} the tag is being placed on */ public arenaTagSide: ArenaTagSide; constructor(arenaTagType: ArenaTagType, arenaTagSide: ArenaTagSide, duration: number) { - super(ArenaEventType.TAG_CHANGED, duration); + super(ArenaEventType.TAG_ADDED, duration); + + this.arenaTagType = arenaTagType; + this.arenaTagSide = arenaTagSide; + } +} +/** + * Container class for {@linkcode ArenaEventType.TAG_REMOVED} events + * @extends ArenaEvent +*/ +export class TagRemovedEvent extends ArenaEvent { + /** The {@linkcode ArenaTagType} being removed */ + public arenaTagType: ArenaTagType; + /** The {@linkcode ArenaTagSide} the tag was being placed on */ + public arenaTagSide: ArenaTagSide; + constructor(arenaTagType: ArenaTagType, arenaTagSide: ArenaTagSide, duration: number) { + super(ArenaEventType.TAG_REMOVED, duration); this.arenaTagType = arenaTagType; this.arenaTagSide = arenaTagSide; diff --git a/src/field/arena.ts b/src/field/arena.ts index eac2eafb265..f6390e40db5 100644 --- a/src/field/arena.ts +++ b/src/field/arena.ts @@ -19,7 +19,7 @@ import { Terrain, TerrainType } from "../data/terrain"; import { PostTerrainChangeAbAttr, PostWeatherChangeAbAttr, applyPostTerrainChangeAbAttrs, applyPostWeatherChangeAbAttrs } from "../data/ability"; import Pokemon from "./pokemon"; import * as Overrides from "../overrides"; -import { WeatherChangedEvent, TerrainChangedEvent, TagChangedEvent } from "./arena-events"; +import { WeatherChangedEvent, TerrainChangedEvent, TagAddedEvent, TagRemovedEvent } from "./arena-events"; export class Arena { public scene: BattleScene; @@ -550,7 +550,7 @@ export class Arena { this.tags.push(newTag); newTag.onAdd(this); - this.eventTarget.dispatchEvent(new TagChangedEvent(newTag.tagType, newTag.side, newTag.turnCount)); + this.eventTarget.dispatchEvent(new TagAddedEvent(newTag.tagType, newTag.side, newTag.turnCount)); return true; } @@ -577,6 +577,8 @@ export class Arena { this.tags.filter(t => !(t.lapse(this))).forEach(t => { t.onRemove(this); this.tags.splice(this.tags.indexOf(t), 1); + + this.eventTarget.dispatchEvent(new TagRemovedEvent(t.tagType, t.side, t.turnCount)); }); } @@ -586,6 +588,8 @@ export class Arena { if (tag) { tag.onRemove(this); tags.splice(tags.indexOf(tag), 1); + + this.eventTarget.dispatchEvent(new TagRemovedEvent(tag.tagType, tag.side, tag.turnCount)); } return !!tag; } @@ -595,6 +599,8 @@ export class Arena { if (tag) { tag.onRemove(this); this.tags.splice(this.tags.indexOf(tag), 1); + + this.eventTarget.dispatchEvent(new TagRemovedEvent(tag.tagType, tag.side, tag.turnCount)); } return !!tag; } @@ -603,6 +609,8 @@ export class Arena { removeAllTags(): void { while (this.tags.length) { this.tags[0].onRemove(this); + this.eventTarget.dispatchEvent(new TagRemovedEvent(this.tags[0].tagType, this.tags[0].side, this.tags[0].turnCount)); + this.tags.splice(0, 1); } } diff --git a/src/loading-scene.ts b/src/loading-scene.ts index fe63b39f805..0b15358c4bc 100644 --- a/src/loading-scene.ts +++ b/src/loading-scene.ts @@ -95,6 +95,11 @@ export class LoadingScene extends SceneBase { this.loadImage("type_tera", "ui"); this.loadAtlas("type_bgs", "ui"); + this.loadImage("dawn_icon", "ui"); + this.loadImage("day_icon", "ui"); + this.loadImage("dusk_icon", "ui"); + this.loadImage("night_icon", "ui"); + this.loadImage("pb_tray_overlay_player", "ui"); this.loadImage("pb_tray_overlay_enemy", "ui"); this.loadAtlas("pb_tray_ball", "ui"); diff --git a/src/system/settings/settings.ts b/src/system/settings/settings.ts index e68e5ea8704..cf657d0e828 100644 --- a/src/system/settings/settings.ts +++ b/src/system/settings/settings.ts @@ -52,6 +52,7 @@ export const SettingKeys = { Sprite_Set: "SPRITE_SET", Move_Animations: "MOVE_ANIMATIONS", Show_Moveset_Flyout: "SHOW_MOVESET_FLYOUT", + Show_Arena_Flyout: "SHOW_ARENA_FLYOUT", Show_Stats_on_Level_Up: "SHOW_LEVEL_UP_STATS", EXP_Gains_Speed: "EXP_GAINS_SPEED", EXP_Party_Display: "EXP_PARTY_DISPLAY", @@ -189,6 +190,13 @@ export const Setting: Array = [ default: 1, type: SettingType.ACCESSIBILITY }, + { + key: SettingKeys.Show_Arena_Flyout, + label: "Show Battle Effects Flyout", + options: OFF_ON, + default: 1, + type: SettingType.ACCESSIBILITY + }, { key: SettingKeys.Show_Stats_on_Level_Up, label: "Show Stats on Level Up", @@ -343,6 +351,9 @@ export function setSetting(scene: BattleScene, setting: string, value: integer): case SettingKeys.Show_Moveset_Flyout: scene.showMovesetFlyout = Setting[index].options[value] === "On"; break; + case SettingKeys.Show_Arena_Flyout: + scene.showArenaFlyout = Setting[index].options[value] === "On"; + break; case SettingKeys.Show_Stats_on_Level_Up: scene.showLevelUpStats = Setting[index].options[value] === "On"; break; diff --git a/src/ui-inputs.ts b/src/ui-inputs.ts index c9bdc5feaf5..d4815ad5a7c 100644 --- a/src/ui-inputs.ts +++ b/src/ui-inputs.ts @@ -69,7 +69,7 @@ export class UiInputs { [Button.CYCLE_GENDER]: () => this.buttonCycleOption(Button.CYCLE_GENDER), [Button.CYCLE_ABILITY]: () => this.buttonCycleOption(Button.CYCLE_ABILITY), [Button.CYCLE_NATURE]: () => this.buttonCycleOption(Button.CYCLE_NATURE), - [Button.V]: () => this.buttonCycleOption(Button.V), + [Button.V]: () => this.buttonCycleOption(Button.V), [Button.SPEED_UP]: () => this.buttonSpeedChange(), [Button.SLOW_DOWN]: () => this.buttonSpeedChange(false), }; @@ -119,12 +119,14 @@ export class UiInputs { } } buttonInfo(pressed: boolean = true): void { - if (!this.scene.showMovesetFlyout) { - return; + if (this.scene.showMovesetFlyout ) { + for (const p of this.scene.getField().filter(p => p?.isActive(true))) { + p.toggleFlyout(pressed); + } } - for (const p of this.scene.getField().filter(p => p?.isActive(true))) { - p.toggleFlyout(pressed); + if (this.scene.showArenaFlyout) { + this.scene.ui.processInfoButton(pressed); } } diff --git a/src/ui/arena-flyout.ts b/src/ui/arena-flyout.ts new file mode 100644 index 00000000000..73660ca4457 --- /dev/null +++ b/src/ui/arena-flyout.ts @@ -0,0 +1,384 @@ +import * as Utils from "../utils"; +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/field/arena-events.js"; +import { BattleSceneEventType, TurnEndEvent } from "#app/battle-scene-events.js"; +import { ArenaTagType } from "#app/data/enums/arena-tag-type.js"; +import { TimeOfDay } from "#app/data/enums/time-of-day.js"; + +/** 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; + + /** The {@linkcode Phaser.GameObjects.Sprite} that represents the current time of day */ + private timeOfDayIcon: Phaser.GameObjects.Sprite; + + /** 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 onNewArenaEvent = (event: Event) => this.onNewArena(event); + private onTurnInitEvent = (event: Event) => this.onTurnInit(event); + private onTurnEndEvent = (event: Event) => this.onTurnEnd(event); + + private 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.timeOfDayIcon = this.scene.add.sprite((this.flyoutWidth / 2) + (this.flyoutWindowHeader.displayWidth / 2), 0, "dawn_icon").setOrigin(); + this.timeOfDayIcon.setVisible(false); + + this.flyoutContainer.add(this.timeOfDayIcon); + + 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_INIT, this.onTurnInitEvent); + this.battleScene.eventTarget.addEventListener(BattleSceneEventType.TURN_END, this.onTurnEndEvent); + } + + private setTimeOfDayIcon() { + this.timeOfDayIcon.setTexture(TimeOfDay[this.battleScene.arena.getTimeOfDay()].toLowerCase() + "_icon"); + } + + private onTurnInit(event: Event) { + this.setTimeOfDayIcon(); + } + + 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); + + this.setTimeOfDayIcon(); + } + + /** + * 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? + */ + 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, + }); + } + + 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(); + } +} diff --git a/src/ui/ui.ts b/src/ui/ui.ts index b2df4d22259..0f33a9cb6c7 100644 --- a/src/ui/ui.ts +++ b/src/ui/ui.ts @@ -222,6 +222,21 @@ export default class UI extends Phaser.GameObjects.Container { return this.handlers[Mode.MESSAGE] as BattleMessageUiHandler; } + processInfoButton(pressed: boolean) { + if (this.overlayActive) { + return false; + } + + const battleScene = this.scene as BattleScene; + if ([Mode.CONFIRM, Mode.COMMAND, Mode.FIGHT, Mode.MESSAGE].includes(this.mode)) { + battleScene?.processInfoButton(pressed); + return true; + } + + battleScene?.processInfoButton(false); + return true; + } + processInput(button: Button): boolean { if (this.overlayActive) { return false;