Add setting for showing type effectiveness hints (#1061)

* type hints

* fix overwritten change

* don't set color to white, just leave it unchanged

* remove unrelated code

* don't show hints if no opponents, use type effectiveness instead of move effectiveness

* fix color not going back to white when new opponent is sent

* move effectiveness to move info container

* add effectiveness overlay, partial hints only show move effectiveness, improve colors

* lint

* docs

* remove full hints, move container to right of enemy info box

* hide effectiveness while flyout is visible

* move setting to display, use default style color instead of white
This commit is contained in:
Yentis 2024-06-08 21:33:13 +02:00 committed by GitHub
parent 5e5ece868c
commit 3f9eaf4a5d
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
7 changed files with 206 additions and 9 deletions

View File

@ -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;

View File

@ -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:

View File

@ -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) {

View File

@ -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<Setting> = [
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) {

View File

@ -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,

View File

@ -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;
}

View File

@ -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() {