From 9b5c1cdadbc9c546a5c3149cfdc8196d8cea5ec2 Mon Sep 17 00:00:00 2001 From: Benjamin Odom Date: Wed, 29 May 2024 19:29:59 -0500 Subject: [PATCH] Adds a Small Flyout Panel to the Battle Info Object (#1377) * Initial Commit * Update pbinfo_enemy_boss_stats.png * Move to Separate Key * Add Separate Mobile Control for Flyout * Add Setting to Enable/Disable * Add to the Tutorial * Change to BUTTON.V --- index.css | 4 +- index.html | 5 +- .../ui/legacy/pbinfo_enemy_boss_stats.png | Bin 435 -> 429 bytes src/battle-scene.ts | 13 ++ src/enums/buttons.ts | 2 +- src/field/pokemon.ts | 4 + src/inputs-controller.ts | 4 +- src/locales/en/tutorial.ts | 4 +- src/system/settings.ts | 6 + src/ui-inputs.ts | 25 +-- src/ui/battle-flyout.ts | 163 ++++++++++++++++++ src/ui/battle-info.ts | 19 +- src/ui/starter-select-ui-handler.ts | 2 +- 13 files changed, 231 insertions(+), 20 deletions(-) create mode 100644 src/ui/battle-flyout.ts diff --git a/index.css b/index.css index dd47387adee..df305781646 100644 --- a/index.css +++ b/index.css @@ -146,11 +146,11 @@ body { margin-left: 10%; } -#touchControls:not([data-ui-mode='STARTER_SELECT']) #apad .apadRectBtnContainer > .apadSqBtn, #touchControls:not([data-ui-mode='STARTER_SELECT']) #apad .apadSqBtnContainer { +#touchControls:not([data-ui-mode='STARTER_SELECT']) #apad .apadRectBtnContainer > .apadSqBtn, #touchControls:not([data-ui-mode='STARTER_SELECT']) #apad .apadSqBtnContainer > .apadSqBtn { display: none; } -#touchControls:not([data-ui-mode='COMMAND']):not([data-ui-mode='FIGHT']):not([data-ui-mode='BALL']):not([data-ui-mode='TARGET_SELECT']) #apad #apadStats { +#touchControls:not([data-ui-mode='COMMAND']):not([data-ui-mode='FIGHT']):not([data-ui-mode='BALL']):not([data-ui-mode='TARGET_SELECT']) #apad .apadBattle { display: none; } diff --git a/index.html b/index.html index d9d513e1663..e155b1c97ff 100644 --- a/index.html +++ b/index.html @@ -75,7 +75,7 @@
V
-
+
C
@@ -95,6 +95,9 @@
N
+
+ V +
diff --git a/public/images/ui/legacy/pbinfo_enemy_boss_stats.png b/public/images/ui/legacy/pbinfo_enemy_boss_stats.png index 94c9f2a181736d88c6752f2c97ed0bbbee23ebcc..faca8887ff5585c05292bd42e03f190c8284a2e6 100644 GIT binary patch delta 390 zcmdnYyq0-_K|KRYx}&cn1H;CC?mvmF3=9mM1s;*b3=DjSL74G){)!X^21X}O7srr_ zxVLu}79DaBaeJ6MugOD2DCVu5Q?9B}=?SHkM@)qxZ@WiChpPz9+{Ze%UcGaH(n+86 zvf}48Y7R}-lOv1YH}C%Zt8PcE-t}+N6Ru@#Ey>saZ9ljE-fQ_6OnI|b|IT#!tv%t~ zqA%-orY}zFGZ!u7P-I(UFTc8U57WvOM`LUI=8Jw#ua)Bax$@S%bNAK17v9+E9)DEY zAuE1i+4yqS4|K|N!xr;B4q#hkZy6#EW2NVGj%&)fdu#3`>Qx8xGvdZk@;x74dmXuWrXT$i1X-Ni1UMue6;fCl33Kl4s(&tIqIftub+di!kBI!$ z53k({KGDO_;9qy!ZEtR>kJify$6sM*E~amkx-<3lu85+iocd3XG3>Y~qL=b~zofWe zbL4~B*^KjdixsOFs&T)6ZuIQp%&7ARx?9}m9Dn0_Ldfv3pB{Ekvd$@?2>^W=wTb`$ diff --git a/src/battle-scene.ts b/src/battle-scene.ts index db9ba1b2828..394c768d0c1 100644 --- a/src/battle-scene.ts +++ b/src/battle-scene.ts @@ -90,6 +90,7 @@ export default class BattleScene extends SceneBase { public seVolume: number = 1; public gameSpeed: integer = 1; public damageNumbersMode: integer = 0; + public showMovesetFlyout: boolean = true; public showLevelUpStats: boolean = true; public enableTutorials: boolean = import.meta.env.VITE_BYPASS_TUTORIAL === "1"; public enableRetries: boolean = false; @@ -207,6 +208,9 @@ export default class BattleScene extends SceneBase { * * Current Events: * - {@linkcode BattleSceneEventType.MOVE_USED} {@linkcode MoveUsedEvent} + * - {@linkcode BattleSceneEventType.TURN_INIT} {@linkcode TurnInitEvent} + * - {@linkcode BattleSceneEventType.TURN_END} {@linkcode TurnEndEvent} + * - {@linkcode BattleSceneEventType.NEW_ARENA} {@linkcode NewArenaEvent} */ public readonly eventTarget: EventTarget = new EventTarget(); @@ -1346,6 +1350,15 @@ export default class BattleScene extends SceneBase { this.ui?.achvBar.setY(this.game.canvas.height / 6 + offsetY); } + /** + * Pushes all {@linkcode Phaser.GameObjects.Text} objects in the top right to the bottom of the canvas + */ + sendTextToBack(): void { + this.fieldUI.sendToBack(this.biomeWaveText); + this.fieldUI.sendToBack(this.moneyText); + this.fieldUI.sendToBack(this.scoreText); + } + addFaintedEnemyScore(enemy: EnemyPokemon): void { let scoreIncrease = enemy.getSpeciesForm().getBaseExp() * (enemy.level / this.getMaxExpLevel()) * ((enemy.ivs.reduce((iv: integer, total: integer) => total += iv, 0) / 93) * 0.2 + 0.8); this.findModifiers(m => m instanceof PokemonHeldItemModifier && m.pokemonId === enemy.id, false).map(m => scoreIncrease *= (m as PokemonHeldItemModifier).getScoreMultiplier()); diff --git a/src/enums/buttons.ts b/src/enums/buttons.ts index 034c5a2af83..fe26023f8e7 100644 --- a/src/enums/buttons.ts +++ b/src/enums/buttons.ts @@ -13,7 +13,7 @@ export enum Button { CYCLE_GENDER, CYCLE_ABILITY, CYCLE_NATURE, - CYCLE_VARIANT, + V, SPEED_UP, SLOW_DOWN } diff --git a/src/field/pokemon.ts b/src/field/pokemon.ts index 6a44c7ef31a..16be16e508c 100644 --- a/src/field/pokemon.ts +++ b/src/field/pokemon.ts @@ -1495,6 +1495,7 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container { const otherBattleInfo = this.scene.fieldUI.getAll().slice(0, 4).filter(ui => ui instanceof BattleInfo && ((ui as BattleInfo) instanceof PlayerBattleInfo) === this.isPlayer()).find(() => true); if (!otherBattleInfo || !this.getFieldIndex()) { this.scene.fieldUI.sendToBack(this.battleInfo); + this.scene.sendTextToBack(); // Push the top right text objects behind everything else } else { this.scene.fieldUI.moveAbove(this.battleInfo, otherBattleInfo); } @@ -1542,6 +1543,9 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container { toggleStats(visible: boolean): void { this.battleInfo.toggleStats(visible); } + toggleFlyout(visible: boolean): void { + this.battleInfo.flyoutMenu?.toggleFlyout(visible); + } addExp(exp: integer) { const maxExpLevel = this.scene.getMaxExpLevel(); diff --git a/src/inputs-controller.ts b/src/inputs-controller.ts index 9c1589b429b..83d26a83cff 100644 --- a/src/inputs-controller.ts +++ b/src/inputs-controller.ts @@ -255,7 +255,7 @@ export class InputsController { gamepadMapping[this.player.LT] = Button.CYCLE_GENDER; gamepadMapping[this.player.RT] = Button.CYCLE_ABILITY; gamepadMapping[this.player.RC_W] = Button.CYCLE_NATURE; - gamepadMapping[this.player.RC_N] = Button.CYCLE_VARIANT; + gamepadMapping[this.player.RC_N] = Button.V; gamepadMapping[this.player.LS] = Button.SPEED_UP; gamepadMapping[this.player.RS] = Button.SLOW_DOWN; @@ -353,7 +353,7 @@ export class InputsController { [Button.CYCLE_GENDER]: [keyCodes.G], [Button.CYCLE_ABILITY]: [keyCodes.E], [Button.CYCLE_NATURE]: [keyCodes.N], - [Button.CYCLE_VARIANT]: [keyCodes.V], + [Button.V]: [keyCodes.V], [Button.SPEED_UP]: [keyCodes.PLUS], [Button.SLOW_DOWN]: [keyCodes.MINUS] }; diff --git a/src/locales/en/tutorial.ts b/src/locales/en/tutorial.ts index ccbc474c5ad..6361bd7d25f 100644 --- a/src/locales/en/tutorial.ts +++ b/src/locales/en/tutorial.ts @@ -22,7 +22,9 @@ export const tutorial: SimpleTranslationEntries = { "statChange": `Stat changes persist across battles as long as your Pokémon aren't recalled. $Your Pokémon are recalled before a trainer battle and before entering a new biome. - $You can also view the stat changes for the Pokémon on the field by holding C or Shift.`, + $You can view the stat changes for any Pokémon on the field by holding C or Shift. + $You can also view the moveset for an enemy Pokémon by holding V. + $This only reveals moves that you've seen the Pokémon use this battle.`, "selectItem": `After every battle, you are given a choice of 3 random items.\nYou may only pick one. $These range from consumables, to Pokémon held items, to passive permanent items. diff --git a/src/system/settings.ts b/src/system/settings.ts index c38cea8863b..3cfc6ba9be4 100644 --- a/src/system/settings.ts +++ b/src/system/settings.ts @@ -24,6 +24,7 @@ export enum Setting { Money_Format = "MONEY_FORMAT", Sprite_Set = "SPRITE_SET", Move_Animations = "MOVE_ANIMATIONS", + Show_Moveset_Flyout = "SHOW_MOVESET_FLYOUT", Show_Stats_on_Level_Up = "SHOW_LEVEL_UP_STATS", EXP_Gains_Speed = "EXP_GAINS_SPEED", EXP_Party_Display = "EXP_PARTY_DISPLAY", @@ -60,6 +61,7 @@ export const settingOptions: SettingOptions = { [Setting.Money_Format]: ["Normal", "Abbreviated"], [Setting.Sprite_Set]: ["Consistent", "Mixed Animated"], [Setting.Move_Animations]: ["Off", "On"], + [Setting.Show_Moveset_Flyout]: ["Off", "On"], [Setting.Show_Stats_on_Level_Up]: ["Off", "On"], [Setting.EXP_Gains_Speed]: ["Normal", "Fast", "Faster", "Skip"], [Setting.EXP_Party_Display]: ["Normal", "Level Up Notification", "Skip"], @@ -88,6 +90,7 @@ export const settingDefaults: SettingDefaults = { [Setting.Money_Format]: 0, [Setting.Sprite_Set]: 0, [Setting.Move_Animations]: 1, + [Setting.Show_Moveset_Flyout]: 1, [Setting.Show_Stats_on_Level_Up]: 1, [Setting.EXP_Gains_Speed]: 0, [Setting.EXP_Party_Display]: 0, @@ -164,6 +167,9 @@ export function setSetting(scene: BattleScene, setting: Setting, value: integer) case Setting.Move_Animations: scene.moveAnimations = settingOptions[setting][value] === "On"; break; + case Setting.Show_Moveset_Flyout: + scene.showMovesetFlyout = settingOptions[setting][value] === "On"; + break; case Setting.Show_Stats_on_Level_Up: scene.showLevelUpStats = settingOptions[setting][value] === "On"; break; diff --git a/src/ui-inputs.ts b/src/ui-inputs.ts index 1151d4dd181..d443e4d85b7 100644 --- a/src/ui-inputs.ts +++ b/src/ui-inputs.ts @@ -66,7 +66,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.CYCLE_VARIANT]: () => this.buttonCycleOption(Button.CYCLE_VARIANT), + [Button.V]: () => this.buttonCycleOption(Button.V), [Button.SPEED_UP]: () => this.buttonSpeedChange(), [Button.SLOW_DOWN]: () => this.buttonSpeedChange(false), }; @@ -89,7 +89,7 @@ export class UiInputs { [Button.CYCLE_GENDER]: () => undefined, [Button.CYCLE_ABILITY]: () => undefined, [Button.CYCLE_NATURE]: () => undefined, - [Button.CYCLE_VARIANT]: () => undefined, + [Button.V]: () => this.buttonInfo(false), [Button.SPEED_UP]: () => undefined, [Button.SLOW_DOWN]: () => undefined, }; @@ -111,14 +111,17 @@ export class UiInputs { } buttonStats(pressed: boolean = true): void { - if (pressed) { - for (const p of this.scene.getField().filter(p => p?.isActive(true))) { - p.toggleStats(true); - } - } else { - for (const p of this.scene.getField().filter(p => p?.isActive(true))) { - p.toggleStats(false); - } + for (const p of this.scene.getField().filter(p => p?.isActive(true))) { + p.toggleStats(pressed); + } + } + buttonInfo(pressed: boolean = true): void { + if (!this.scene.showMovesetFlyout) { + return; + } + + for (const p of this.scene.getField().filter(p => p?.isActive(true))) { + p.toggleFlyout(pressed); } } @@ -158,6 +161,8 @@ export class UiInputs { buttonCycleOption(button: Button): void { if (this.scene.ui?.getHandler() instanceof StarterSelectUiHandler) { this.scene.ui.processInput(button); + } else if (button === Button.V) { + this.buttonInfo(true); } } diff --git a/src/ui/battle-flyout.ts b/src/ui/battle-flyout.ts new file mode 100644 index 00000000000..3add54920b0 --- /dev/null +++ b/src/ui/battle-flyout.ts @@ -0,0 +1,163 @@ +import { default as Pokemon } from "../field/pokemon"; +import { addTextObject, TextStyle } from "./text"; +import * as Utils from "../utils"; +import BattleScene from "#app/battle-scene.js"; +import { UiTheme } from "#app/enums/ui-theme.js"; +import Move from "#app/data/move.js"; +import { BattleSceneEventType, MoveUsedEvent } from "#app/battle-scene-events.js"; + +/** Container for info about a {@linkcode Move} */ +interface MoveInfo { + /** The {@linkcode Move} itself */ + move: Move, + + /** The maximum PP of the {@linkcode Move} */ + maxPp: number, + /** The amount of PP used by the {@linkcode Move} */ + ppUsed: number, +} + +/** A Flyout Menu attached to each {@linkcode BattleInfo} object on the field UI */ +export default class BattleFlyout extends Phaser.GameObjects.Container { + /** An alias for the scene typecast to a {@linkcode BattleScene} */ + private battleScene: BattleScene; + + /** Is this object linked to a player's Pokemon? */ + private player: boolean; + + /** The Pokemon this object is linked to */ + private pokemon: Pokemon; + + /** The restricted width of the flyout which should be drawn to */ + private flyoutWidth = 118; + /** The restricted height of the flyout which should be drawn to */ + private flyoutHeight = 23; + + /** 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 background {@linkcode Phaser.GameObjects.Sprite;} for the flyout */ + private flyoutBackground: Phaser.GameObjects.Sprite; + + /** The container which defines the drawable dimensions of the flyout */ + private flyoutContainer: Phaser.GameObjects.Container; + + /** The array of {@linkcode Phaser.GameObjects.Text} objects which are drawn on the flyout */ + private flyoutText: Phaser.GameObjects.Text[] = new Array(4); + /** The array of {@linkcode MoveInfo} used to track moves for the {@linkcode Pokemon} linked to the flyout */ + private moveInfo: MoveInfo[] = new Array(); + + private readonly onMoveUsed = (event) => this.updateInfo(event); + + constructor(scene: Phaser.Scene, player: boolean) { + super(scene, 0, 0); + this.battleScene = scene as BattleScene; + + // Note that all player based flyouts are disabled. This is included in case of future development + this.player = player; + + this.translationX = this.player ? -this.flyoutWidth : this.flyoutWidth; + this.anchorX = (this.player ? -130 : -40); + this.anchorY = -2.5 + (this.player ? -18.5 : -13); + + this.flyoutParent = this.scene.add.container(this.anchorX - this.translationX, this.anchorY); + this.flyoutParent.setAlpha(0); + this.add(this.flyoutParent); + + // Load the background image + this.flyoutBackground = this.scene.add.sprite(0, 0, "pbinfo_enemy_boss_stats"); + this.flyoutBackground.setOrigin(0, 0); + + this.flyoutParent.add(this.flyoutBackground); + + this.flyoutContainer = this.scene.add.container(44 + (this.player ? -this.flyoutWidth : 0), 2); + this.flyoutParent.add(this.flyoutContainer); + + // Loops through and sets the position of each text object according to the width and height of the flyout + for (let i = 0; i < 4; i++) { + this.flyoutText[i] = addTextObject( + this.scene, + (this.flyoutWidth / 4) + (this.flyoutWidth / 2) * (i % 2), + (this.flyoutHeight / 4) + (this.flyoutHeight / 2) * (i < 2 ? 0 : 1), "???", TextStyle.BATTLE_INFO); + this.flyoutText[i].setFontSize(45); + this.flyoutText[i].setLineSpacing(-10); + this.flyoutText[i].setAlign("center"); + this.flyoutText[i].setOrigin(); + } + + this.flyoutContainer.add(this.flyoutText); + + this.flyoutContainer.add( + new Phaser.GameObjects.Rectangle(this.scene, this.flyoutWidth / 2, 0, 1, this.flyoutHeight + (this.battleScene.uiTheme === UiTheme.LEGACY ? 1 : 0), 0x212121).setOrigin(0.5, 0)); + this.flyoutContainer.add( + new Phaser.GameObjects.Rectangle(this.scene, 0, this.flyoutHeight / 2, this.flyoutWidth + 6, 1, 0x212121).setOrigin(0, 0.5)); + } + + /** + * Links the given {@linkcode Pokemon} and subscribes to the {@linkcode BattleSceneEventType.MOVE_USED} event + * @param pokemon {@linkcode Pokemon} to link to this flyout + */ + initInfo(pokemon: Pokemon) { + this.pokemon = pokemon; + + this.name = `Flyout ${this.pokemon.name}`; + this.flyoutParent.name = `Flyout Parent ${this.pokemon.name}`; + + this.battleScene.eventTarget.addEventListener(BattleSceneEventType.MOVE_USED, this.onMoveUsed); + } + + /** Sets and formats the text property for all {@linkcode Phaser.GameObjects.Text} in the flyoutText array */ + setText() { + for (let i = 0; i < this.flyoutText.length; i++) { + const flyoutText = this.flyoutText[i]; + const moveInfo = this.moveInfo[i]; + + if (!moveInfo) { + continue; + } + + const currentPp = Math.max(moveInfo.maxPp - moveInfo.ppUsed, 0); + flyoutText.text = `${moveInfo.move.name} ${currentPp}/${moveInfo.maxPp}`; + } + } + + /** Updates all of the {@linkcode MoveInfo} objects in the moveInfo array */ + updateInfo(event: Event) { + const moveUsedEvent = event as MoveUsedEvent; + if (!moveUsedEvent || moveUsedEvent.userId !== this.pokemon?.id) { + return; + } + + const foundInfo = this.moveInfo.find(x => x?.move.id === moveUsedEvent.move.id); + if (foundInfo) { + foundInfo.ppUsed += moveUsedEvent.ppUsed; + } else { + this.moveInfo.push({move: moveUsedEvent.move, maxPp: moveUsedEvent.move.pp, ppUsed: moveUsedEvent.ppUsed}); + } + + this.setText(); + } + + /** Animates the flyout to either show or hide it by applying a fade and translation */ + 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.MOVE_USED, this.onMoveUsed); + + super.destroy(); + } +} diff --git a/src/ui/battle-info.ts b/src/ui/battle-info.ts index 7c981ab8c27..9fb81f89698 100644 --- a/src/ui/battle-info.ts +++ b/src/ui/battle-info.ts @@ -8,6 +8,7 @@ import BattleScene from "../battle-scene"; import { Type, getTypeRgb } from "../data/type"; import { getVariantTint } from "#app/data/variant"; import { BattleStat } from "#app/data/battle-stat"; +import BattleFlyout from "./battle-flyout"; const battleStatOrder = [ BattleStat.ATK, BattleStat.DEF, BattleStat.SPATK, BattleStat.SPDEF, BattleStat.ACC, BattleStat.EVA, BattleStat.SPD ]; @@ -58,6 +59,8 @@ export default class BattleInfo extends Phaser.GameObjects.Container { private statValuesContainer: Phaser.GameObjects.Container; private statNumbers: Phaser.GameObjects.Sprite[]; + public flyoutMenu: BattleFlyout; + constructor(scene: Phaser.Scene, x: number, y: number, player: boolean) { super(scene, x, y); this.baseY = y; @@ -226,6 +229,13 @@ export default class BattleInfo extends Phaser.GameObjects.Container { this.statValuesContainer.add(statNumber); }); + if (!this.player) { + this.flyoutMenu = new BattleFlyout(this.scene, this.player); + this.add(this.flyoutMenu); + + this.moveBelow(this.flyoutMenu, this.box); + } + this.type1Icon = this.scene.add.sprite(player ? -139 : -15, player ? -17 : -15.5, `pbinfo_${player ? "player" : "enemy"}_type1`); this.type1Icon.setName("icon_type_1"); this.type1Icon.setOrigin(0, 0); @@ -246,6 +256,11 @@ export default class BattleInfo extends Phaser.GameObjects.Container { this.updateNameText(pokemon); const nameTextWidth = this.nameText.displayWidth; + this.name = pokemon.name; + this.box.name = pokemon.name; + + this.flyoutMenu?.initInfo(pokemon); + this.genderText.setText(getGenderSymbol(pokemon.gender)); this.genderText.setColor(getGenderColor(pokemon.gender)); this.genderText.setPositionRelative(this.nameText, nameTextWidth, 0); @@ -454,8 +469,8 @@ export default class BattleInfo extends Phaser.GameObjects.Container { this.offset = offset; - this.x += 10 * (offset === this.player ? 1 : -1); - this.y += 27 * (offset ? 1 : -1); + this.x += 10 * (this.offset === this.player ? 1 : -1); + this.y += 27 * (this.offset ? 1 : -1); this.baseY = this.y; } diff --git a/src/ui/starter-select-ui-handler.ts b/src/ui/starter-select-ui-handler.ts index f36e1eb2606..c54bc1924b9 100644 --- a/src/ui/starter-select-ui-handler.ts +++ b/src/ui/starter-select-ui-handler.ts @@ -1313,7 +1313,7 @@ export default class StarterSelectUiHandler extends MessageUiHandler { success = true; } break; - case Button.CYCLE_VARIANT: + case Button.V: if (this.canCycleVariant) { let newVariant = props.variant; do {