diff --git a/src/battle-scene.ts b/src/battle-scene.ts index 9899935b9fa..848735e8f21 100644 --- a/src/battle-scene.ts +++ b/src/battle-scene.ts @@ -155,6 +155,13 @@ export default class BattleScene extends SceneBase { */ public battleStyle: integer = 0; + /** + * Defines whether or not to show type effectiveness hints + * - true: No hints + * - false: Show hints for moves + */ + public typeHints: boolean = false; + public disableMenu: boolean = false; public gameData: GameData; diff --git a/src/data/type.ts b/src/data/type.ts index b2bf8117249..c92416afca9 100644 --- a/src/data/type.ts +++ b/src/data/type.ts @@ -501,6 +501,52 @@ export function getTypeDamageMultiplier(attackType: integer, defType: integer): } } +/** + * Retrieve the color corresponding to a specific damage multiplier + * @returns A color or undefined if the default color should be used + */ +export function getTypeDamageMultiplierColor(multiplier: TypeDamageMultiplier, side: "defense" | "offense"): string | undefined { + if (side === "offense") { + switch (multiplier) { + case 0: + return "#929292"; + case 0.125: + return "#FF5500"; + case 0.25: + return "#FF7400"; + case 0.5: + return "#FE8E00"; + case 1: + return undefined; + case 2: + return "#4AA500"; + case 4: + return "#4BB400"; + case 8: + return "#52C200"; + } + } else if (side === "defense") { + switch (multiplier) { + case 0: + return "#B1B100"; + case 0.125: + return "#2DB4FF"; + case 0.25: + return "#00A4FF"; + case 0.5: + return "#0093FF"; + case 1: + return undefined; + case 2: + return "#FE8E00"; + case 4: + return "#FF7400"; + case 8: + return "#FF5500"; + } + } +} + export function getTypeRgb(type: Type): [ integer, integer, integer ] { switch (type) { case Type.NORMAL: diff --git a/src/field/pokemon.ts b/src/field/pokemon.ts index 8a3d21fd390..9a44f9ba144 100644 --- a/src/field/pokemon.ts +++ b/src/field/pokemon.ts @@ -1069,6 +1069,17 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container { return !this.isOfType(Type.FLYING, true, true) && !this.hasAbility(Abilities.LEVITATE); } + /** + * @returns The type damage multiplier or undefined if it's a status move + */ + getMoveEffectiveness(source: Pokemon, move: PokemonMove): TypeDamageMultiplier | undefined { + if (move.getMove().category === MoveCategory.STATUS) { + return undefined; + } + + return this.getAttackMoveEffectiveness(source, move); + } + getAttackMoveEffectiveness(source: Pokemon, pokemonMove: PokemonMove): TypeDamageMultiplier { const move = pokemonMove.getMove(); const typeless = move.hasAttr(TypelessAttr); @@ -1588,11 +1599,20 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container { return this.battleInfo.updateInfo(this, instant); } + /** + * Show or hide the type effectiveness multiplier window + * Passing undefined will hide the window + */ + updateEffectiveness(effectiveness?: string) { + this.battleInfo.updateEffectiveness(effectiveness); + } + toggleStats(visible: boolean): void { this.battleInfo.toggleStats(visible); } + toggleFlyout(visible: boolean): void { - this.battleInfo.flyoutMenu?.toggleFlyout(visible); + this.battleInfo.toggleFlyout(visible); } addExp(exp: integer) { diff --git a/src/system/settings/settings.ts b/src/system/settings/settings.ts index f5865c7ac9b..eb2b016b61e 100644 --- a/src/system/settings/settings.ts +++ b/src/system/settings/settings.ts @@ -64,6 +64,7 @@ export const SettingKeys = { Sprite_Set: "SPRITE_SET", Fusion_Palette_Swaps: "FUSION_PALETTE_SWAPS", Player_Gender: "PLAYER_GENDER", + Type_Hints: "TYPE_HINTS", Master_Volume: "MASTER_VOLUME", BGM_Volume: "BGM_VOLUME", SE_Volume: "SE_VOLUME", @@ -268,6 +269,13 @@ export const Setting: Array = [ default: 0, type: SettingType.DISPLAY }, + { + key: SettingKeys.Type_Hints, + label: "Type hints", + options: OFF_ON, + default: 0, + type: SettingType.DISPLAY + }, { key: SettingKeys.Master_Volume, label: "Master Volume", @@ -447,6 +455,9 @@ export function setSetting(scene: BattleScene, setting: string, value: integer): case SettingKeys.Vibration: scene.enableVibration = Setting[index].options[value] !== "Disabled" && hasTouchscreen(); break; + case SettingKeys.Type_Hints: + scene.typeHints = Setting[index].options[value] === "On"; + break; case SettingKeys.Language: if (value) { if (scene.ui) { diff --git a/src/ui/battle-flyout.ts b/src/ui/battle-flyout.ts index 9a9e3ef46a9..956ea65fd83 100644 --- a/src/ui/battle-flyout.ts +++ b/src/ui/battle-flyout.ts @@ -55,6 +55,9 @@ export default class BattleFlyout extends Phaser.GameObjects.Container { /** The array of {@linkcode MoveInfo} used to track moves for the {@linkcode Pokemon} linked to the flyout */ private moveInfo: MoveInfo[] = new Array(); + /** Current state of the flyout's visibility */ + public flyoutVisible: boolean = false; + // Stores callbacks in a variable so they can be unsubscribed from when destroyed private readonly onMoveUsedEvent = (event: Event) => this.onMoveUsed(event); private readonly onBerryUsedEvent = (event: Event) => this.onBerryUsed(event); @@ -170,6 +173,8 @@ export default class BattleFlyout extends Phaser.GameObjects.Container { /** Animates the flyout to either show or hide it by applying a fade and translation */ toggleFlyout(visible: boolean): void { + this.flyoutVisible = visible; + this.scene.tweens.add({ targets: this.flyoutParent, x: visible ? this.anchorX : this.anchorX - this.translationX, diff --git a/src/ui/battle-info.ts b/src/ui/battle-info.ts index 9fb81f89698..c246af73d07 100644 --- a/src/ui/battle-info.ts +++ b/src/ui/battle-info.ts @@ -9,6 +9,7 @@ import { Type, getTypeRgb } from "../data/type"; import { getVariantTint } from "#app/data/variant"; import { BattleStat } from "#app/data/battle-stat"; import BattleFlyout from "./battle-flyout"; +import { WindowVariant, addWindow } from "./ui-theme"; const battleStatOrder = [ BattleStat.ATK, BattleStat.DEF, BattleStat.SPATK, BattleStat.SPDEF, BattleStat.ACC, BattleStat.EVA, BattleStat.SPD ]; @@ -52,6 +53,13 @@ export default class BattleInfo extends Phaser.GameObjects.Container { private type3Icon: Phaser.GameObjects.Sprite; private expBar: Phaser.GameObjects.Image; + // #region Type effectiveness hint objects + private effectivenessContainer: Phaser.GameObjects.Container; + private effectivenessWindow: Phaser.GameObjects.NineSlice; + private effectivenessText: Phaser.GameObjects.Text; + private currentEffectiveness?: string; + // #endregion + public expMaskRect: Phaser.GameObjects.Graphics; private statsContainer: Phaser.GameObjects.Container; @@ -59,7 +67,7 @@ export default class BattleInfo extends Phaser.GameObjects.Container { private statValuesContainer: Phaser.GameObjects.Container; private statNumbers: Phaser.GameObjects.Sprite[]; - public flyoutMenu: BattleFlyout; + public flyoutMenu?: BattleFlyout; constructor(scene: Phaser.Scene, x: number, y: number, player: boolean) { super(scene, x, y); @@ -250,6 +258,19 @@ export default class BattleInfo extends Phaser.GameObjects.Container { this.type3Icon.setName("icon_type_3"); this.type3Icon.setOrigin(0, 0); this.add(this.type3Icon); + + if (!this.player) { + this.effectivenessContainer = this.scene.add.container(0, 0); + this.effectivenessContainer.setPositionRelative(this.type1Icon, 22, 4); + this.effectivenessContainer.setVisible(false); + this.add(this.effectivenessContainer); + + this.effectivenessText = addTextObject(this.scene, 5, 4.5, "", TextStyle.BATTLE_INFO); + this.effectivenessWindow = addWindow((this.scene as BattleScene), 0, 0, 0, 20, false, false, null, null, WindowVariant.XTHIN); + + this.effectivenessContainer.add(this.effectivenessWindow); + this.effectivenessContainer.add(this.effectivenessText); + } } initInfo(pokemon: Pokemon) { @@ -711,6 +732,39 @@ export default class BattleInfo extends Phaser.GameObjects.Container { }); } + /** + * Request the flyoutMenu to toggle if available and hides or shows the effectiveness window where necessary + */ + toggleFlyout(visible: boolean): void { + this.flyoutMenu?.toggleFlyout(visible); + + if (visible) { + this.effectivenessContainer?.setVisible(false); + } else { + this.updateEffectiveness(this.currentEffectiveness); + } + } + + /** + * Show or hide the type effectiveness multiplier window + * Passing undefined will hide the window + */ + updateEffectiveness(effectiveness?: string) { + if (this.player) { + return; + } + this.currentEffectiveness = effectiveness; + + if (!(this.scene as BattleScene).typeHints || effectiveness === undefined || this.flyoutMenu.flyoutVisible) { + this.effectivenessContainer.setVisible(false); + return; + } + + this.effectivenessText.setText(effectiveness); + this.effectivenessWindow.width = 10 + this.effectivenessText.displayWidth; + this.effectivenessContainer.setVisible(true); + } + getBaseY(): number { return this.baseY; } diff --git a/src/ui/fight-ui-handler.ts b/src/ui/fight-ui-handler.ts index acbf66b7075..f738fba5573 100644 --- a/src/ui/fight-ui-handler.ts +++ b/src/ui/fight-ui-handler.ts @@ -1,6 +1,6 @@ import BattleScene from "../battle-scene"; import { addTextObject, TextStyle } from "./text"; -import { Type } from "../data/type"; +import { getTypeDamageMultiplierColor, Type } from "../data/type"; import { Command } from "./command-ui-handler"; import { Mode } from "./ui"; import UiHandler from "./ui-handler"; @@ -9,6 +9,7 @@ import { CommandPhase } from "../phases"; import { MoveCategory } from "#app/data/move.js"; import i18next from "../plugins/i18n"; import {Button} from "../enums/buttons"; +import Pokemon, { PokemonMove } from "#app/field/pokemon.js"; export default class FightUiHandler extends UiHandler { private movesContainer: Phaser.GameObjects.Container; @@ -162,7 +163,8 @@ export default class FightUiHandler extends UiHandler { ui.add(this.cursorObj); } - const moveset = (this.scene.getCurrentPhase() as CommandPhase).getPokemon().getMoveset(); + const pokemon = (this.scene.getCurrentPhase() as CommandPhase).getPokemon(); + const moveset = pokemon.getMoveset(); const hasMove = cursor < moveset.length; @@ -179,6 +181,10 @@ export default class FightUiHandler extends UiHandler { this.ppText.setText(`${Utils.padInt(pp, 2, " ")}/${Utils.padInt(maxPP, 2, " ")}`); this.powerText.setText(`${power >= 0 ? power : "---"}`); this.accuracyText.setText(`${accuracy >= 0 ? accuracy : "---"}`); + + pokemon.getOpponents().forEach((opponent) => { + opponent.updateEffectiveness(this.getEffectivenessText(pokemon, opponent, pokemonMove)); + }); } this.typeIcon.setVisible(hasMove); @@ -195,17 +201,60 @@ export default class FightUiHandler extends UiHandler { return changed; } + /** + * Gets multiplier text for a pokemon's move against a specific opponent + * Returns undefined if it's a status move + */ + private getEffectivenessText(pokemon: Pokemon, opponent: Pokemon, pokemonMove: PokemonMove): string | undefined { + const effectiveness = opponent.getMoveEffectiveness(pokemon, pokemonMove); + if (effectiveness === undefined) { + return undefined; + } + + return `${effectiveness}x`; + } + displayMoves() { - const moveset = (this.scene.getCurrentPhase() as CommandPhase).getPokemon().getMoveset(); - for (let m = 0; m < 4; m++) { - const moveText = addTextObject(this.scene, m % 2 === 0 ? 0 : 100, m < 2 ? 0 : 16, "-", TextStyle.WINDOW); - if (m < moveset.length) { - moveText.setText(moveset[m].getName()); + const pokemon = (this.scene.getCurrentPhase() as CommandPhase).getPokemon(); + const moveset = pokemon.getMoveset(); + + for (let moveIndex = 0; moveIndex < 4; moveIndex++) { + const moveText = addTextObject(this.scene, moveIndex % 2 === 0 ? 0 : 100, moveIndex < 2 ? 0 : 16, "-", TextStyle.WINDOW); + + if (moveIndex < moveset.length) { + const pokemonMove = moveset[moveIndex]; + moveText.setText(pokemonMove.getName()); + moveText.setColor(this.getMoveColor(pokemon, pokemonMove) ?? moveText.style.color); } + this.movesContainer.add(moveText); } } + /** + * Returns a specific move's color based on its type effectiveness against opponents + * If there are multiple opponents, the highest effectiveness' color is returned + * @returns A color or undefined if the default color should be used + */ + private getMoveColor(pokemon: Pokemon, pokemonMove: PokemonMove): string | undefined { + if (!this.scene.typeHints) { + return undefined; + } + + const opponents = pokemon.getOpponents(); + if (opponents.length <= 0) { + return undefined; + } + + const moveColors = opponents.map((opponent) => { + return opponent.getMoveEffectiveness(pokemon, pokemonMove); + }).sort((a, b) => b - a).map((effectiveness) => { + return getTypeDamageMultiplierColor(effectiveness, "offense"); + }); + + return moveColors[0]; + } + clear() { super.clear(); this.clearMoves(); @@ -222,6 +271,11 @@ export default class FightUiHandler extends UiHandler { clearMoves() { this.movesContainer.removeAll(true); + + const opponents = (this.scene.getCurrentPhase() as CommandPhase).getPokemon().getOpponents(); + opponents.forEach((opponent) => { + opponent.updateEffectiveness(undefined); + }); } eraseCursor() {