diff --git a/src/battle-scene.ts b/src/battle-scene.ts index c68ff0dac40..53b58cb8a41 100644 --- a/src/battle-scene.ts +++ b/src/battle-scene.ts @@ -71,15 +71,20 @@ const expSpriteKeys: string[] = []; export let starterColors: StarterColors; interface StarterColors { - [key: string]: [string, string] + [key: string]: [string, string] } export interface PokeballCounts { - [pb: string]: integer; + [pb: string]: integer; } export type AnySound = Phaser.Sound.WebAudioSound | Phaser.Sound.HTML5AudioSound | Phaser.Sound.NoAudioSound; +export interface InfoToggle { + toggleInfo(force?: boolean): void; + isActive(): boolean; +} + export default class BattleScene extends SceneBase { public rexUI: UIPlugin; public inputController: InputsController; @@ -97,6 +102,7 @@ export default class BattleScene extends SceneBase { public showArenaFlyout: boolean = true; public showLevelUpStats: boolean = true; public enableTutorials: boolean = import.meta.env.VITE_BYPASS_TUTORIAL === "1"; + public enableMoveInfo: boolean = true; public enableRetries: boolean = false; /** * Determines the condition for a notification should be shown for Candy Upgrades @@ -120,17 +126,17 @@ export default class BattleScene extends SceneBase { public skipSeenDialogues: boolean = false; /** - * Defines the experience gain display mode. - * - * @remarks - * The `expParty` can have several modes: - * - `0` - Default: The normal experience gain display, nothing changed. - * - `1` - Level Up Notification: Displays the level up in the small frame instead of a message. - * - `2` - Skip: No level up frame nor message. - * - * Modes `1` and `2` are still compatible with stats display, level up, new move, etc. - * @default 0 - Uses the default normal experience gain display. - */ + * Defines the experience gain display mode. + * + * @remarks + * The `expParty` can have several modes: + * - `0` - Default: The normal experience gain display, nothing changed. + * - `1` - Level Up Notification: Displays the level up in the small frame instead of a message. + * - `2` - Skip: No level up frame nor message. + * + * Modes `1` and `2` are still compatible with stats display, level up, new move, etc. + * @default 0 - Uses the default normal experience gain display. + */ public expParty: integer = 0; public hpBarSpeed: integer = 0; public fusionPaletteSwaps: boolean = true; @@ -209,6 +215,8 @@ export default class BattleScene extends SceneBase { public rngSeedOverride: string = ""; public rngOffset: integer = 0; + private infoToggles: InfoToggle[] = []; + /** * Allows subscribers to listen for events * @@ -515,7 +523,7 @@ export default class BattleScene extends SceneBase { this.playTimeTimer = this.time.addEvent({ delay: Utils.fixedInt(1000), repeat: -1, - callback: () => { + callback: () => { if (this.gameData) { this.gameData.gameStats.playTime++; } @@ -599,25 +607,25 @@ export default class BattleScene extends SceneBase { /*const loadPokemonAssets: Promise[] = []; - for (let s of Object.keys(speciesStarters)) { - const species = getPokemonSpecies(parseInt(s)); - loadPokemonAssets.push(species.loadAssets(this, false, 0, false)); - } + for (let s of Object.keys(speciesStarters)) { + const species = getPokemonSpecies(parseInt(s)); + loadPokemonAssets.push(species.loadAssets(this, false, 0, false)); + } - Promise.all(loadPokemonAssets).then(() => { - const starterCandyColors = {}; - const rgbaToHexFunc = (r, g, b) => [r, g, b].map(x => x.toString(16).padStart(2, '0')).join(''); + Promise.all(loadPokemonAssets).then(() => { + const starterCandyColors = {}; + const rgbaToHexFunc = (r, g, b) => [r, g, b].map(x => x.toString(16).padStart(2, '0')).join(''); - for (let s of Object.keys(speciesStarters)) { - const species = getPokemonSpecies(parseInt(s)); + for (let s of Object.keys(speciesStarters)) { + const species = getPokemonSpecies(parseInt(s)); - starterCandyColors[species.speciesId] = species.generateCandyColors(this).map(c => rgbaToHexFunc(c[0], c[1], c[2])); - } + starterCandyColors[species.speciesId] = species.generateCandyColors(this).map(c => rgbaToHexFunc(c[0], c[1], c[2])); + } - console.log(JSON.stringify(starterCandyColors)); + console.log(JSON.stringify(starterCandyColors)); - resolve(); - });*/ + resolve(); + });*/ resolve(); }); @@ -682,6 +690,16 @@ export default class BattleScene extends SceneBase { : ret; } + // store info toggles to be accessible by the ui + addInfoToggle(infoToggle: InfoToggle): void { + this.infoToggles.push(infoToggle); + } + + // return the stored info toggles; used by ui-inputs + getInfoToggles(activeOnly: boolean = false): InfoToggle[] { + return activeOnly ? this.infoToggles.filter(t => t?.isActive()) : this.infoToggles; + } + getPokemonById(pokemonId: integer): Pokemon { const findInParty = (party: Pokemon[]) => party.find(p => p.id === pokemonId); return findInParty(this.getParty()) || findInParty(this.getEnemyParty()); @@ -728,7 +746,7 @@ export default class BattleScene extends SceneBase { const container = this.add.container(x, y); const icon = this.add.sprite(0, 0, pokemon.getIconAtlasKey(ignoreOverride)); - icon.setFrame(pokemon.getIconId(true)); + icon.setFrame(pokemon.getIconId(true)); // Temporary fix to show pokemon's default icon if variant icon doesn't exist if (icon.frame.name !== pokemon.getIconId(true)) { console.log(`${pokemon.name}'s variant icon does not exist. Replacing with default.`); @@ -1336,7 +1354,7 @@ export default class BattleScene extends SceneBase { return; } const formattedMoney = - this.moneyFormat === MoneyFormat.ABBREVIATED ? Utils.formatFancyLargeNumber(this.money, 3) : this.money.toLocaleString(); + this.moneyFormat === MoneyFormat.ABBREVIATED ? Utils.formatFancyLargeNumber(this.money, 3) : this.money.toLocaleString(); this.moneyText.setText(`₽${formattedMoney}`); this.fieldUI.moveAbove(this.moneyText, this.luckText); if (forceVisible) { @@ -1926,7 +1944,7 @@ export default class BattleScene extends SceneBase { const newItemModifier = itemModifier.clone() as PokemonHeldItemModifier; newItemModifier.pokemonId = target.id; const matchingModifier = target.scene.findModifier(m => m instanceof PokemonHeldItemModifier - && (m as PokemonHeldItemModifier).matchType(itemModifier) && m.pokemonId === target.id, target.isPlayer()) as PokemonHeldItemModifier; + && (m as PokemonHeldItemModifier).matchType(itemModifier) && m.pokemonId === target.id, target.isPlayer()) as PokemonHeldItemModifier; let removeOld = true; if (matchingModifier) { const maxStackCount = matchingModifier.getMaxStackCount(target.scene); @@ -2030,8 +2048,8 @@ export default class BattleScene extends SceneBase { } /** - * Removes all modifiers from enemy of PersistentModifier type - */ + * Removes all modifiers from enemy of PersistentModifier type + */ clearEnemyModifiers(): void { const modifiersToRemove = this.enemyModifiers.filter(m => m instanceof PersistentModifier); for (const m of modifiersToRemove) { @@ -2041,8 +2059,8 @@ export default class BattleScene extends SceneBase { } /** - * Removes all modifiers from enemy of PokemonHeldItemModifier type - */ + * Removes all modifiers from enemy of PokemonHeldItemModifier type + */ clearEnemyHeldItemModifiers(): void { const modifiersToRemove = this.enemyModifiers.filter(m => m instanceof PokemonHeldItemModifier); for (const m of modifiersToRemove) { diff --git a/src/loading-scene.ts b/src/loading-scene.ts index 395e4f534c5..34b2f9d6be7 100644 --- a/src/loading-scene.ts +++ b/src/loading-scene.ts @@ -127,6 +127,7 @@ export class LoadingScene extends SceneBase { this.loadImage("summary_stats_overlay_exp", "ui"); this.loadImage("summary_moves", "ui"); this.loadImage("summary_moves_effect", "ui"); + this.loadImage("summary_moves_effect_type", "ui"); this.loadImage("summary_moves_overlay_row", "ui"); this.loadImage("summary_moves_overlay_pp", "ui"); this.loadAtlas("summary_moves_cursor", "ui"); diff --git a/src/locales/de/modifier-type.ts b/src/locales/de/modifier-type.ts index 9f70c31ca63..ff9cf28632c 100644 --- a/src/locales/de/modifier-type.ts +++ b/src/locales/de/modifier-type.ts @@ -99,6 +99,10 @@ export const modifierType: ModifierTypeTranslationEntries = { name: "TM{{moveId}} - {{moveName}}", description: "Bringt einem Pokémon {{moveName}} bei", }, + "TmModifierTypeWithInfo": { + name: "TM{{moveId}} - {{moveName}}", + description: "Bringt einem Pokémon {{moveName}} bei\n(Halte C oder Shift für mehr Infos)", + }, "EvolutionItemModifierType": { description: "Erlaubt es bestimmten Pokémon sich zu entwickeln", }, diff --git a/src/locales/en/modifier-type.ts b/src/locales/en/modifier-type.ts index dac87e1d939..0f7b83d2b3e 100644 --- a/src/locales/en/modifier-type.ts +++ b/src/locales/en/modifier-type.ts @@ -99,6 +99,10 @@ export const modifierType: ModifierTypeTranslationEntries = { name: "TM{{moveId}} - {{moveName}}", description: "Teach {{moveName}} to a Pokémon", }, + "TmModifierTypeWithInfo": { + name: "TM{{moveId}} - {{moveName}}", + description: "Teach {{moveName}} to a Pokémon\n(Hold C or Shift for more info)", + }, "EvolutionItemModifierType": { description: "Causes certain Pokémon to evolve", }, diff --git a/src/locales/es/modifier-type.ts b/src/locales/es/modifier-type.ts index 7b5b1e0c90b..f6565486bb1 100644 --- a/src/locales/es/modifier-type.ts +++ b/src/locales/es/modifier-type.ts @@ -99,6 +99,10 @@ export const modifierType: ModifierTypeTranslationEntries = { name: "MT{{moveId}} - {{moveName}}", description: "Enseña {{moveName}} a un Pokémon", }, + "TmModifierTypeWithInfo": { + name: "MT{{moveId}} - {{moveName}}", + description: "Enseña {{moveName}} a un Pokémon\n(Hold C or Shift for more info)", + }, "EvolutionItemModifierType": { description: "Hace que ciertos Pokémon evolucionen", }, diff --git a/src/locales/fr/modifier-type.ts b/src/locales/fr/modifier-type.ts index 8315910adb3..dd70fd9205e 100644 --- a/src/locales/fr/modifier-type.ts +++ b/src/locales/fr/modifier-type.ts @@ -99,6 +99,10 @@ export const modifierType: ModifierTypeTranslationEntries = { name: "CT{{moveId}} - {{moveName}}", description: "Apprend la capacité {{moveName}} à un Pokémon", }, + "TmModifierTypeWithInfo": { + name: "CT{{moveId}} - {{moveName}}", + description: "Apprend la capacité {{moveName}} à un Pokémon\n(Hold C or Shift for more info)", + }, "EvolutionItemModifierType": { description: "Permet à certains Pokémon d’évoluer", }, diff --git a/src/locales/it/modifier-type.ts b/src/locales/it/modifier-type.ts index b311aa1e8fa..ac313e2444c 100644 --- a/src/locales/it/modifier-type.ts +++ b/src/locales/it/modifier-type.ts @@ -99,6 +99,10 @@ export const modifierType: ModifierTypeTranslationEntries = { name: "MT{{moveId}} - {{moveName}}", description: "Insegna {{moveName}} a un Pokémon", }, + "TmModifierTypeWithInfo": { + name: "MT{{moveId}} - {{moveName}}", + description: "Insegna {{moveName}} a un Pokémon\n(Hold C or Shift for more info)", + }, "EvolutionItemModifierType": { description: "Fa evolvere determinate specie di Pokémon", }, diff --git a/src/locales/ko/modifier-type.ts b/src/locales/ko/modifier-type.ts index 5d54018cc96..fce9d25b629 100644 --- a/src/locales/ko/modifier-type.ts +++ b/src/locales/ko/modifier-type.ts @@ -99,6 +99,10 @@ export const modifierType: ModifierTypeTranslationEntries = { name: "No.{{moveId}} {{moveName}}", description: "포켓몬에게 {{moveName}}[[를]] 가르침", }, + "TmModifierTypeWithInfo": { + name: "No.{{moveId}} {{moveName}}", + description: "포켓몬에게 {{moveName}}를(을) 가르침\n(Hold C or Shift for more info)", + }, "EvolutionItemModifierType": { description: "어느 특정 포켓몬을 진화", }, diff --git a/src/locales/pt_BR/modifier-type.ts b/src/locales/pt_BR/modifier-type.ts index 4865cfb64a2..da0364e9b6e 100644 --- a/src/locales/pt_BR/modifier-type.ts +++ b/src/locales/pt_BR/modifier-type.ts @@ -99,6 +99,10 @@ export const modifierType: ModifierTypeTranslationEntries = { name: "TM{{moveId}} - {{moveName}}", description: "Ensina {{moveName}} a um Pokémon", }, + "TmModifierTypeWithInfo": { + name: "TM{{moveId}} - {{moveName}}", + description: "Ensina {{moveName}} a um Pokémon\n(Hold C or Shift for more info)", + }, "EvolutionItemModifierType": { description: "Faz certos Pokémon evoluírem", }, diff --git a/src/locales/zh_CN/modifier-type.ts b/src/locales/zh_CN/modifier-type.ts index 7230f21e330..456cc7bb8fb 100644 --- a/src/locales/zh_CN/modifier-type.ts +++ b/src/locales/zh_CN/modifier-type.ts @@ -99,6 +99,10 @@ export const modifierType: ModifierTypeTranslationEntries = { name: "招式学习器 {{moveId}} - {{moveName}}", description: "教会一只宝可梦{{moveName}}", }, + "TmModifierTypeWithInfo": { + name: "招式学习器 {{moveId}} - {{moveName}}", + description: "教会一只宝可梦{{moveName}}\n(Hold C or Shift for more info)", + }, "EvolutionItemModifierType": { description: "使某些宝可梦进化", }, diff --git a/src/locales/zh_TW/modifier-type.ts b/src/locales/zh_TW/modifier-type.ts index 1ad51965937..a6e73ebfc28 100644 --- a/src/locales/zh_TW/modifier-type.ts +++ b/src/locales/zh_TW/modifier-type.ts @@ -98,6 +98,10 @@ export const modifierType: ModifierTypeTranslationEntries = { name: "招式學習器 {{moveId}} - {{moveName}}", description: "教會一隻寶可夢{{moveName}}", }, + TmModifierTypeWithInfo: { + name: "TM{{moveId}} - {{moveName}}", + description: "教會一隻寶可夢{{moveName}}\n(Hold C or Shift for more info)", + }, EvolutionItemModifierType: { description: "使某些寶可夢進化" }, FormChangeItemModifierType: { description: "使某些寶可夢更改形態" }, FusePokemonModifierType: { diff --git a/src/modifier/modifier-type.ts b/src/modifier/modifier-type.ts index e404781bc8d..3d03f1710f7 100644 --- a/src/modifier/modifier-type.ts +++ b/src/modifier/modifier-type.ts @@ -23,6 +23,7 @@ import { ModifierTier } from "./modifier-tier"; import { Nature, getNatureName, getNatureStatMultiplier } from "#app/data/nature"; import i18next from "#app/plugins/i18n"; import { getModifierTierTextTint } from "#app/ui/text"; +import * as Overrides from "../overrides"; const outputModifierData = false; const useMaxWeightForOutput = false; @@ -721,7 +722,7 @@ export class TmModifierType extends PokemonModifierType { } getDescription(scene: BattleScene): string { - return i18next.t("modifierType:ModifierType.TmModifierType.description", { moveName: allMoves[this.moveId].name }); + return i18next.t(scene.enableMoveInfo ? "modifierType:ModifierType.TmModifierTypeWithInfo.description" : "modifierType:ModifierType.TmModifierType.description", { moveName: allMoves[this.moveId].name }); } } @@ -1673,6 +1674,14 @@ export function getPlayerModifierTypeOptions(count: integer, party: PlayerPokemo } options.push(candidate); }); + // OVERRIDE IF NECESSARY + if (Overrides.ITEM_REWARD_OVERRIDE?.length) { + options.forEach((mod, i) => { + // @ts-ignore: keeps throwing don't use string as index error in typedoc run + const override = modifierTypes[Overrides.ITEM_REWARD_OVERRIDE[i]]?.(); + mod.type = (override instanceof ModifierTypeGenerator ? override.generateType(party) : override) || mod.type; + }); + } return options; } diff --git a/src/overrides.ts b/src/overrides.ts index 661f2d14253..148dc352ae9 100644 --- a/src/overrides.ts +++ b/src/overrides.ts @@ -110,3 +110,11 @@ export const OPP_MODIFIER_OVERRIDE: Array = []; export const STARTING_HELD_ITEMS_OVERRIDE: Array = []; export const OPP_HELD_ITEMS_OVERRIDE: Array = []; + +/** + * An array of items by keys as defined in the "modifierTypes" object in the "modifier/modifier-type.ts" file. + * Items listed will replace the normal rolls. + * If less items are listed than rolled, only some items will be replaced + * If more items are listed than rolled, only the first X items will be shown, where X is the number of items rolled. + */ +export const ITEM_REWARD_OVERRIDE: Array = []; diff --git a/src/system/settings/settings.ts b/src/system/settings/settings.ts index c407aff6927..81971a08d69 100644 --- a/src/system/settings/settings.ts +++ b/src/system/settings/settings.ts @@ -44,6 +44,7 @@ export const SettingKeys = { UI_Theme: "UI_THEME", Window_Type: "WINDOW_TYPE", Tutorials: "TUTORIALS", + Move_Info: "MOVE_INFO", Enable_Retries: "ENABLE_RETRIES", Skip_Seen_Dialogues: "SKIP_SEEN_DIALOGUES", Candy_Upgrade_Notification: "CANDY_UPGRADE_NOTIFICATION", @@ -132,6 +133,13 @@ export const Setting: Array = [ default: 1, type: SettingType.GENERAL }, + { + key: SettingKeys.Move_Info, + label: "Move Info", + options: OFF_ON, + default: 1, + type: SettingType.ACCESSIBILITY + }, { key: SettingKeys.Enable_Retries, label: "Enable Retries", @@ -312,6 +320,9 @@ export function setSetting(scene: BattleScene, setting: string, value: integer): case SettingKeys.Tutorials: scene.enableTutorials = Setting[index].options[value] === "On"; break; + case SettingKeys.Move_Info: + scene.enableMoveInfo = Setting[index].options[value] === "On"; + break; case SettingKeys.Enable_Retries: scene.enableRetries = Setting[index].options[value] === "On"; break; diff --git a/src/ui-inputs.ts b/src/ui-inputs.ts index d4815ad5a7c..ebe055d8de1 100644 --- a/src/ui-inputs.ts +++ b/src/ui-inputs.ts @@ -114,6 +114,11 @@ export class UiInputs { } buttonStats(pressed: boolean = true): void { + // allow access to Button.STATS as a toggle for other elements + for (const t of this.scene.getInfoToggles(true)) { + t.toggleInfo(pressed); + } + // handle normal pokemon battle ui for (const p of this.scene.getField().filter(p => p?.isActive(true))) { p.toggleStats(pressed); } diff --git a/src/ui/abstact-option-select-ui-handler.ts b/src/ui/abstact-option-select-ui-handler.ts index 925bbefc930..568c8208eac 100644 --- a/src/ui/abstact-option-select-ui-handler.ts +++ b/src/ui/abstact-option-select-ui-handler.ts @@ -14,15 +14,17 @@ export interface OptionSelectConfig { maxOptions?: integer; delay?: integer; noCancel?: boolean; + supportHover?: boolean; } export interface OptionSelectItem { label: string; handler: () => boolean; + onHover?: () => void; keepOpen?: boolean; overrideSound?: boolean; item?: string; - itemArgs?: any[] + itemArgs?: any[]; } const scrollUpLabel = "↑"; @@ -193,6 +195,10 @@ export default abstract class AbstractOptionSelectUiHandler extends UiHandler { } break; } + if (this.config?.supportHover) { + // handle hover code if the element supports hover-handlers and the option has the optional hover-handler set. + this.config?.options[this.cursor + (this.scrollCursor - (this.scrollCursor ? 1 : 0))]?.onHover?.(); + } } if (success && playSound) { diff --git a/src/ui/modifier-select-ui-handler.ts b/src/ui/modifier-select-ui-handler.ts index 37718243b8b..f6738a33d98 100644 --- a/src/ui/modifier-select-ui-handler.ts +++ b/src/ui/modifier-select-ui-handler.ts @@ -1,5 +1,5 @@ import BattleScene from "../battle-scene"; -import { getPlayerShopModifierTypeOptionsForWave, ModifierTypeOption } from "../modifier/modifier-type"; +import { getPlayerShopModifierTypeOptionsForWave, ModifierTypeOption, TmModifierType } from "../modifier/modifier-type"; import { getPokeballAtlasKey, PokeballType } from "../data/pokeball"; import { addTextObject, getModifierTierTextTint, getTextColor, TextStyle } from "./text"; import AwaitableUiHandler from "./awaitable-ui-handler"; @@ -7,6 +7,8 @@ import { Mode } from "./ui"; import { LockModifierTiersModifier, PokemonHeldItemModifier } from "../modifier/modifier"; import { handleTutorial, Tutorial } from "../tutorial"; import {Button} from "../enums/buttons"; +import MoveInfoOverlay from "./move-info-overlay"; +import { allMoves } from "../data/move"; export const SHOP_OPTIONS_ROW_LIMIT = 6; @@ -17,6 +19,7 @@ export default class ModifierSelectUiHandler extends AwaitableUiHandler { private transferButtonContainer: Phaser.GameObjects.Container; private rerollCostText: Phaser.GameObjects.Text; private lockRarityButtonText: Phaser.GameObjects.Text; + private moveInfoOverlay : MoveInfoOverlay; private rowCursor: integer = 0; private player: boolean; @@ -73,6 +76,21 @@ export default class ModifierSelectUiHandler extends AwaitableUiHandler { this.lockRarityButtonText = addTextObject(this.scene, -4, -2, "Lock Rarities", TextStyle.PARTY); this.lockRarityButtonText.setOrigin(0, 0); this.lockRarityButtonContainer.add(this.lockRarityButtonText); + + // prepare move overlay + const overlayScale = 1; + this.moveInfoOverlay = new MoveInfoOverlay(this.scene, { + delayVisibility: true, + scale: overlayScale, + onSide: true, + right: true, + x: 1, + y: -MoveInfoOverlay.getHeight(overlayScale, true) -1, + width: (this.scene.game.canvas.width / 6) - 2, + }); + ui.add(this.moveInfoOverlay); + // register the overlay to receive toggle events + this.scene.addInfoToggle(this.moveInfoOverlay); } show(args: any[]): boolean { @@ -293,6 +311,8 @@ export default class ModifierSelectUiHandler extends AwaitableUiHandler { this.cursorObj.setScale(this.rowCursor === 1 ? 2 : this.rowCursor >= 2 ? 1.5 : 1); + // the modifier selection has been updated, always hide the overlay + this.moveInfoOverlay.clear(); if (this.rowCursor) { const sliceWidth = (this.scene.game.canvas.width / 6) / (options.length + 2); if (this.rowCursor < 2) { @@ -300,7 +320,13 @@ export default class ModifierSelectUiHandler extends AwaitableUiHandler { } else { this.cursorObj.setPosition(sliceWidth * (cursor + 1) + (sliceWidth * 0.5) - 16, (-this.scene.game.canvas.height / 12 - this.scene.game.canvas.height / 32) - (-16 + 28 * (this.rowCursor - (this.shopOptionsRows.length - 1)))); } - ui.showText(options[this.cursor].modifierTypeOption.type.getDescription(this.scene)); + + const type = options[this.cursor].modifierTypeOption.type; + ui.showText(type.getDescription(this.scene)); + if (type instanceof TmModifierType) { + // prepare the move overlay to be shown with the toggle + this.moveInfoOverlay.show(allMoves[type.moveId]); + } } else if (!cursor) { this.cursorObj.setPosition(6, this.lockRarityButtonContainer.visible ? -72 : -60); ui.showText("Spend money to reroll your item options."); diff --git a/src/ui/move-info-overlay.ts b/src/ui/move-info-overlay.ts new file mode 100644 index 00000000000..cba2a2a16dc --- /dev/null +++ b/src/ui/move-info-overlay.ts @@ -0,0 +1,194 @@ +import BattleScene, {InfoToggle} from "../battle-scene"; +import { TextStyle, addTextObject } from "./text"; +import { addWindow } from "./ui-theme"; +import * as Utils from "../utils"; +import Move, { MoveCategory } from "../data/move"; +import { Type } from "../data/type"; +import i18next from "i18next"; + +export interface MoveInfoOverlaySettings { + delayVisibility?: boolean; // if true, showing the overlay will only set it to active and populate the fields and the handler using this field has to manually call setVisible later. + scale?:number; // scale the box? A scale of 0.5 is recommended + top?: boolean; // should the effect box be on top? + right?: boolean; // should the effect box be on the right? + onSide?: boolean; // should the effect be on the side? ignores top argument if true + //location and width of the component; unaffected by scaling + x?: number; + y?: number; + width?: number; // default is always half the screen, regardless of scale +} + +const EFF_HEIGHT = 46; +const EFF_WIDTH = 82; +const DESC_HEIGHT = 46; +const BORDER = 8; +const GLOBAL_SCALE = 6; + +export default class MoveInfoOverlay extends Phaser.GameObjects.Container implements InfoToggle { + public active: boolean = false; + + private move: Move; + + private desc: Phaser.GameObjects.Text; + private descScroll : Phaser.Tweens.Tween = null; + + private val: Phaser.GameObjects.Container; + private pp: Phaser.GameObjects.Text; + private pow: Phaser.GameObjects.Text; + private acc: Phaser.GameObjects.Text; + private typ: Phaser.GameObjects.Sprite; + private cat: Phaser.GameObjects.Sprite; + + private options : MoveInfoOverlaySettings; + + constructor(scene: BattleScene, options?: MoveInfoOverlaySettings) { + if (options?.onSide) { + options.top = false; + } + super(scene, options?.x, options?.y); + const scale = options?.scale || 1; // set up the scale + this.setScale(scale); + this.options = options || {}; + + // prepare the description box + const width = (options?.width || MoveInfoOverlay.getWidth(scale, scene)) / scale; // divide by scale as we always want this to be half a window wide + const descBg = addWindow(scene, (options?.onSide && !options?.right ? EFF_WIDTH : 0), options?.top ? EFF_HEIGHT : 0, width - (options?.onSide ? EFF_WIDTH : 0), DESC_HEIGHT); + descBg.setOrigin(0, 0); + this.add(descBg); + + // set up the description; wordWrap uses true pixels, unaffected by any scaling, while other values are affected + this.desc = addTextObject(scene, (options?.onSide && !options?.right ? EFF_WIDTH : 0) + BORDER, (options?.top ? EFF_HEIGHT : 0) + BORDER - 2, "", TextStyle.BATTLE_INFO, { wordWrap: { width: (width - (BORDER - 2) * 2 - (options?.onSide ? EFF_WIDTH : 0)) * GLOBAL_SCALE } }); + + // limit the text rendering, required for scrolling later on + const maskPointOrigin = { + x: (options?.x || 0), + y: (options?.y || 0), + }; + if (maskPointOrigin.x < 0) { + maskPointOrigin.x += this.scene.game.canvas.width / GLOBAL_SCALE; + } + if (maskPointOrigin.y < 0) { + maskPointOrigin.y += this.scene.game.canvas.height / GLOBAL_SCALE; + } + + const moveDescriptionTextMaskRect = this.scene.make.graphics(); + moveDescriptionTextMaskRect.fillStyle(0xFF0000); + moveDescriptionTextMaskRect.fillRect( + maskPointOrigin.x + ((options?.onSide && !options?.right ? EFF_WIDTH : 0) + BORDER) * scale, maskPointOrigin.y + ((options?.top ? EFF_HEIGHT : 0) + BORDER - 2) * scale, + width - ((options?.onSide ? EFF_WIDTH : 0) - BORDER * 2) * scale, (DESC_HEIGHT - (BORDER - 2) * 2) * scale); + moveDescriptionTextMaskRect.setScale(6); + const moveDescriptionTextMask = this.createGeometryMask(moveDescriptionTextMaskRect); + + this.add(this.desc); + this.desc.setMask(moveDescriptionTextMask); + + // prepare the effect box + this.val = new Phaser.GameObjects.Container(scene, options?.right ? width - EFF_WIDTH : 0, options?.top || options?.onSide ? 0 : DESC_HEIGHT); + this.add(this.val); + + const valuesBg = addWindow(scene, 0, 0, EFF_WIDTH, EFF_HEIGHT); + valuesBg.setOrigin(0, 0); + this.val.add(valuesBg); + + this.typ = this.scene.add.sprite(25, EFF_HEIGHT - 35,`types${Utils.verifyLang(i18next.language) ? `_${i18next.language}` : ""}` , "unknown"); + this.typ.setScale(0.8); + this.val.add(this.typ); + + this.cat = this.scene.add.sprite(57, EFF_HEIGHT - 35, "categories", "physical"); + this.val.add(this.cat); + + const ppTxt = addTextObject(scene, 12, EFF_HEIGHT - 25, "PP", TextStyle.MOVE_INFO_CONTENT); + ppTxt.setOrigin(0.0, 0.5); + ppTxt.setText(i18next.t("fightUiHandler:pp")); + this.val.add(ppTxt); + + this.pp = addTextObject(scene, 70, EFF_HEIGHT - 25, "--", TextStyle.MOVE_INFO_CONTENT); + this.pp.setOrigin(1, 0.5); + this.val.add(this.pp); + + const powTxt = addTextObject(scene, 12, EFF_HEIGHT - 17, "POWER", TextStyle.MOVE_INFO_CONTENT); + powTxt.setOrigin(0.0, 0.5); + powTxt.setText(i18next.t("fightUiHandler:power")); + this.val.add(powTxt); + + this.pow = addTextObject(scene, 70, EFF_HEIGHT - 17, "---", TextStyle.MOVE_INFO_CONTENT); + this.pow.setOrigin(1, 0.5); + this.val.add(this.pow); + + const accTxt = addTextObject(scene, 12, EFF_HEIGHT - 9, "ACC", TextStyle.MOVE_INFO_CONTENT); + accTxt.setOrigin(0.0, 0.5); + accTxt.setText(i18next.t("fightUiHandler:accuracy")); + this.val.add(accTxt); + + this.acc = addTextObject(scene, 70, EFF_HEIGHT - 9, "---", TextStyle.MOVE_INFO_CONTENT); + this.acc.setOrigin(1, 0.5); + this.val.add(this.acc); + + // hide this component for now + this.setVisible(false); + } + + // show this component with infos for the specific move + show(move : Move):boolean { + if (!(this.scene as BattleScene).enableMoveInfo) { + return; // move infos have been disabled + } + this.move = move; + this.pow.setText(move.power >= 0 ? move.power.toString() : "---"); + this.acc.setText(move.accuracy >= 0 ? move.accuracy.toString() : "---"); + this.pp.setText(move.pp >= 0 ? move.pp.toString() : "---"); + this.typ.setTexture(`types${Utils.verifyLang(i18next.language) ? `_${i18next.language}` : ""}`, Type[move.type].toLowerCase()); + this.cat.setFrame(MoveCategory[move.category].toLowerCase()); + + this.desc.setText(move?.effect || ""); + + // stop previous scrolling effects + if (this.descScroll) { + this.descScroll.remove(); + this.descScroll = null; + } + + // determine if we need to add new scrolling effects + const moveDescriptionLineCount = Math.floor(this.desc.displayHeight * (96 / 72) / 14.83); + if (moveDescriptionLineCount > 3) { + // generate scrolling effects + this.descScroll = this.scene.tweens.add({ + targets: this.desc, + delay: Utils.fixedInt(2000), + loop: -1, + hold: Utils.fixedInt(2000), + duration: Utils.fixedInt((moveDescriptionLineCount - 3) * 2000), + y: `-=${14.83 * (72 / 96) * (moveDescriptionLineCount - 3)}` + }); + } + + if (!this.options.delayVisibility) { + this.setVisible(true); + } + this.active = true; + return true; + } + + clear() { + this.setVisible(false); + this.active = false; + } + + toggleInfo(force?: boolean): void { + this.setVisible(force ?? !this.visible); + } + + isActive(): boolean { + return this.active; + } + + // width of this element + static getWidth(scale:number, scene: BattleScene):number { + return scene.game.canvas.width / GLOBAL_SCALE / 2; + } + + // height of this element + static getHeight(scale:number, onSide?: boolean):number { + return (onSide ? Math.max(EFF_HEIGHT, DESC_HEIGHT) : (EFF_HEIGHT + DESC_HEIGHT)) * scale; + } +} diff --git a/src/ui/party-ui-handler.ts b/src/ui/party-ui-handler.ts index b62242e3c18..c51fea747a9 100644 --- a/src/ui/party-ui-handler.ts +++ b/src/ui/party-ui-handler.ts @@ -17,6 +17,7 @@ import { addWindow } from "./ui-theme"; import { SpeciesFormChangeItemTrigger } from "../data/pokemon-forms"; import { getVariantTint } from "#app/data/variant"; import {Button} from "../enums/buttons"; +import MoveInfoOverlay from "./move-info-overlay"; const defaultMessage = "Choose a Pokémon."; @@ -73,6 +74,7 @@ export default class PartyUiHandler extends MessageUiHandler { private partySlots: PartySlot[]; private partyCancelButton: PartyCancelButton; private partyMessageBox: Phaser.GameObjects.NineSlice; + private moveInfoOverlay: MoveInfoOverlay; private optionsMode: boolean; private optionsScroll: boolean; @@ -179,6 +181,17 @@ export default class PartyUiHandler extends MessageUiHandler { this.iconAnimHandler = new PokemonIconAnimHandler(); this.iconAnimHandler.setup(this.scene); + // prepare move overlay. in case it appears to be too big, set the overlayScale to .5 + const overlayScale = 1; + this.moveInfoOverlay = new MoveInfoOverlay(this.scene, { + scale: overlayScale, + top: true, + x: 1, + y: -MoveInfoOverlay.getHeight(overlayScale) - 1, //this.scene.game.canvas.height / 6 - MoveInfoOverlay.getHeight(overlayScale) - 29, + width: this.scene.game.canvas.width / 12 - 30, + }); + ui.add(this.moveInfoOverlay); + this.options = []; this.partySlots = []; @@ -191,6 +204,9 @@ export default class PartyUiHandler extends MessageUiHandler { super.show(args); + // reset the infoOverlay + this.moveInfoOverlay.clear(); + this.partyUiMode = args[0] as PartyUiMode; this.fieldIndex = args.length > 1 ? args[1] as integer : -1; @@ -244,6 +260,8 @@ export default class PartyUiHandler extends MessageUiHandler { ui.playSelect(); return true; } else if (this.partyUiMode === PartyUiMode.REMEMBER_MOVE_MODIFIER && option !== PartyOption.CANCEL) { + // clear overlay on cancel + this.moveInfoOverlay.clear(); const filterResult = (this.selectFilter as PokemonSelectFilter)(pokemon); if (filterResult === null) { this.selectCallback(this.cursor, option); @@ -408,6 +426,19 @@ export default class PartyUiHandler extends MessageUiHandler { success = this.setCursor(this.optionsCursor < this.options.length - 1 ? this.optionsCursor + 1 : 0); /** Move cursor */ break; } + + // show move description + if (this.partyUiMode === PartyUiMode.REMEMBER_MOVE_MODIFIER) { + const option = this.options[this.optionsCursor]; + const pokemon = this.scene.getParty()[this.cursor]; + const move = allMoves[pokemon.getLearnableLevelMoves()[option]]; + if (move) { + this.moveInfoOverlay.show(move); + } else { + // or hide the overlay, in case it's the cancel button + this.moveInfoOverlay.clear(); + } + } } } else { if (button === Button.ACTION) { @@ -625,6 +656,11 @@ export default class PartyUiHandler extends MessageUiHandler { ? pokemon.getLearnableLevelMoves() : null; + if (this.partyUiMode === PartyUiMode.REMEMBER_MOVE_MODIFIER && learnableLevelMoves?.length) { + // show the move overlay with info for the first move + this.moveInfoOverlay.show(allMoves[learnableLevelMoves[0]]); + } + const itemModifiers = this.partyUiMode === PartyUiMode.MODIFIER_TRANSFER ? this.scene.findModifiers(m => m instanceof PokemonHeldItemModifier && (m as PokemonHeldItemModifier).getTransferrable(true) && (m as PokemonHeldItemModifier).pokemonId === pokemon.id) as PokemonHeldItemModifier[] @@ -874,6 +910,8 @@ export default class PartyUiHandler extends MessageUiHandler { } clearOptions() { + // hide the overlay + this.moveInfoOverlay.clear(); this.optionsMode = false; this.optionsScroll = false; this.optionsScrollCursor = 0; @@ -895,6 +933,8 @@ export default class PartyUiHandler extends MessageUiHandler { clear() { super.clear(); + // hide the overlay + this.moveInfoOverlay.clear(); this.partyContainer.setVisible(false); this.clearPartySlots(); } diff --git a/src/ui/starter-select-ui-handler.ts b/src/ui/starter-select-ui-handler.ts index dd7a949f275..d30d4c4e14c 100644 --- a/src/ui/starter-select-ui-handler.ts +++ b/src/ui/starter-select-ui-handler.ts @@ -31,6 +31,7 @@ import { StatsContainer } from "./stats-container"; import { TextStyle, addBBCodeTextObject, addTextObject } from "./text"; import { Mode } from "./ui"; import { addWindow } from "./ui-theme"; +import MoveInfoOverlay from "./move-info-overlay"; export type StarterSelectCallback = (starters: Starter[]) => void; @@ -192,6 +193,7 @@ export default class StarterSelectUiHandler extends MessageUiHandler { private starterSelectMessageBoxContainer: Phaser.GameObjects.Container; private statsContainer: StatsContainer; private pokemonFormText: Phaser.GameObjects.Text; + private moveInfoOverlay : MoveInfoOverlay; private genMode: boolean; private statsMode: boolean; @@ -661,6 +663,15 @@ export default class StarterSelectUiHandler extends MessageUiHandler { this.message.setOrigin(0, 0); this.starterSelectMessageBoxContainer.add(this.message); + const overlayScale = 1; // scale for the move info. "2/3" might be another good option... + this.moveInfoOverlay = new MoveInfoOverlay(this.scene, { + scale: overlayScale, + top: true, + x: 1, + y: this.scene.game.canvas.height / 6 - MoveInfoOverlay.getHeight(overlayScale) - 29, + }); + this.starterSelectContainer.add(this.moveInfoOverlay); + const date = new Date(); date.setUTCHours(0, 0, 0, 0); @@ -1058,6 +1069,8 @@ export default class StarterSelectUiHandler extends MessageUiHandler { const showSwapOptions = (moveset: StarterMoveset) => { ui.setMode(Mode.STARTER_SELECT).then(() => { ui.showText(i18next.t("starterSelectUiHandler:selectMoveSwapOut"), null, () => { + this.moveInfoOverlay.show(allMoves[moveset[0]]); + ui.setModeWithoutClear(Mode.OPTION_SELECT, { options: moveset.map((m: Moves, i: number) => { const option: OptionSelectItem = { @@ -1065,8 +1078,11 @@ export default class StarterSelectUiHandler extends MessageUiHandler { handler: () => { ui.setMode(Mode.STARTER_SELECT).then(() => { ui.showText(`${i18next.t("starterSelectUiHandler:selectMoveSwapWith")} ${allMoves[m].name}.`, null, () => { + const possibleMoves = this.speciesStarterMoves.filter((sm: Moves) => sm !== m); + this.moveInfoOverlay.show(allMoves[possibleMoves[0]]); + ui.setModeWithoutClear(Mode.OPTION_SELECT, { - options: this.speciesStarterMoves.filter((sm: Moves) => sm !== m).map(sm => { + options: possibleMoves.map(sm => { // make an option for each available starter move const option = { label: allMoves[sm].name, @@ -1074,7 +1090,10 @@ export default class StarterSelectUiHandler extends MessageUiHandler { this.switchMoveHandler(i, sm, m); showSwapOptions(this.starterMoveset); return true; - } + }, + onHover: () => { + this.moveInfoOverlay.show(allMoves[sm]); + }, }; return option; }).concat({ @@ -1082,25 +1101,37 @@ export default class StarterSelectUiHandler extends MessageUiHandler { handler: () => { showSwapOptions(this.starterMoveset); return true; - } + }, + onHover: () => { + this.moveInfoOverlay.clear(); + }, }), + supportHover: true, maxOptions: 8, yOffset: 19 }); }); }); return true; - } + }, + onHover: () => { + this.moveInfoOverlay.show(allMoves[m]); + }, }; return option; }).concat({ label: i18next.t("menu:cancel"), handler: () => { + this.moveInfoOverlay.clear(); this.clearText(); ui.setMode(Mode.STARTER_SELECT); return true; - } + }, + onHover: () => { + this.moveInfoOverlay.clear(); + }, }), + supportHover: true, maxOptions: 8, yOffset: 19 });