Settings Refactor (#1771)

This commit is contained in:
Matthew 2024-06-03 19:57:47 -04:00 committed by GitHub
parent 763d2bfeeb
commit 69da96d543
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
25 changed files with 1487 additions and 1326 deletions

View File

@ -1,5 +1,5 @@
import {Button} from "#app/enums/buttons";
import {SettingKeyboard} from "#app/system/settings-keyboard";
import {SettingKeyboard} from "#app/system/settings/settings-keyboard";
const cfg_keyboard_qwerty = {
padID: "default",

View File

@ -1,4 +1,4 @@
import {SettingGamepad} from "../../system/settings-gamepad";
import {SettingGamepad} from "../../system/settings/settings-gamepad";
import {Button} from "../../enums/buttons";
/**

View File

@ -1,4 +1,4 @@
import {SettingGamepad} from "../../system/settings-gamepad";
import {SettingGamepad} from "../../system/settings/settings-gamepad";
import {Button} from "../../enums/buttons";
/**

View File

@ -1,4 +1,4 @@
import {SettingGamepad} from "#app/system/settings-gamepad";
import {SettingGamepad} from "#app/system/settings/settings-gamepad.js";
import {Button} from "#app/enums/buttons";
/**

View File

@ -1,4 +1,4 @@
import {SettingGamepad} from "../../system/settings-gamepad";
import {SettingGamepad} from "../../system/settings/settings-gamepad";
import {Button} from "../../enums/buttons";
/**

View File

@ -1,4 +1,4 @@
import {SettingGamepad} from "../../system/settings-gamepad";
import {SettingGamepad} from "../../system/settings/settings-gamepad";
import {Button} from "#app/enums/buttons";
/**

View File

@ -19,8 +19,8 @@ import {
getIconForLatestInput, swap,
} from "#app/configs/inputs/configHandler";
import BattleScene from "./battle-scene";
import {SettingGamepad} from "#app/system/settings-gamepad";
import {SettingKeyboard} from "#app/system/settings-keyboard";
import {SettingGamepad} from "#app/system/settings/settings-gamepad.js";
import {SettingKeyboard} from "#app/system/settings/settings-keyboard";
export interface DeviceMapping {
[key: string]: number;

View File

@ -48,7 +48,7 @@ import { addPokeballCaptureStars, addPokeballOpenParticles } from "./field/anims
import { SpeciesFormChangeActiveTrigger, SpeciesFormChangeManualTrigger, SpeciesFormChangeMoveLearnedTrigger, SpeciesFormChangePostMoveTrigger, SpeciesFormChangePreMoveTrigger } from "./data/pokemon-forms";
import { battleSpecDialogue, getCharVariantFromDialogue, miscDialogue } from "./data/dialogue";
import ModifierSelectUiHandler, { SHOP_OPTIONS_ROW_LIMIT } from "./ui/modifier-select-ui-handler";
import { Setting } from "./system/settings";
import { SettingKeys } from "./system/settings/settings";
import { Tutorial, handleTutorial } from "./tutorial";
import { TerrainType } from "./data/terrain";
import { OptionSelectConfig, OptionSelectItem } from "./ui/abstact-option-select-ui-handler";
@ -477,7 +477,7 @@ export class SelectGenderPhase extends Phase {
label: i18next.t("menu:boy"),
handler: () => {
this.scene.gameData.gender = PlayerGender.MALE;
this.scene.gameData.saveSetting(Setting.Player_Gender, 0);
this.scene.gameData.saveSetting(SettingKeys.Player_Gender, 0);
this.scene.gameData.saveSystem().then(() => this.end());
return true;
}
@ -486,7 +486,7 @@ export class SelectGenderPhase extends Phase {
label: i18next.t("menu:girl"),
handler: () => {
this.scene.gameData.gender = PlayerGender.FEMALE;
this.scene.gameData.saveSetting(Setting.Player_Gender, 1);
this.scene.gameData.saveSetting(SettingKeys.Player_Gender, 1);
this.scene.gameData.saveSystem().then(() => this.end());
return true;
}

View File

@ -13,7 +13,7 @@ import { GameModes, gameModes } from "../game-mode";
import { BattleType } from "../battle";
import TrainerData from "./trainer-data";
import { trainerConfigs } from "../data/trainer-config";
import { Setting, setSetting, settingDefaults } from "./settings";
import { SettingKeys, resetSettings, setSetting } from "./settings/settings";
import { achvs } from "./achv";
import EggData from "./egg-data";
import { Egg } from "../data/egg";
@ -30,9 +30,10 @@ import { allMoves } from "../data/move";
import { TrainerVariant } from "../field/trainer";
import { OutdatedPhase, ReloadSessionPhase } from "#app/phases";
import { Variant, variantData } from "#app/data/variant";
import {setSettingGamepad, SettingGamepad, settingGamepadDefaults} from "./settings-gamepad";
import {setSettingKeyboard, SettingKeyboard, settingKeyboardDefaults} from "#app/system/settings-keyboard";
import {setSettingGamepad, SettingGamepad, settingGamepadDefaults} from "./settings/settings-gamepad";
import {setSettingKeyboard, SettingKeyboard} from "#app/system/settings/settings-keyboard";
import { TerrainChangedEvent, WeatherChangedEvent } from "#app/field/arena-events.js";
import { Device } from "#app/enums/devices.js";
const saveKey = "x0i2O7WRiANTqPmZ"; // Temporary; secure encryption is not yet necessary
@ -402,7 +403,7 @@ export class GameData {
this.gender = systemData.gender;
this.saveSetting(Setting.Player_Gender, systemData.gender === PlayerGender.FEMALE ? 1 : 0);
this.saveSetting(SettingKeys.Player_Gender, systemData.gender === PlayerGender.FEMALE ? 1 : 0);
const initStarterData = !systemData.starterData;
@ -568,19 +569,21 @@ export class GameData {
}
}
public saveSetting(setting: Setting, valueIndex: integer): boolean {
/**
* Saves a setting to localStorage
* @param setting string ideally of SettingKeys
* @param valueIndex index of the setting's option
* @returns true
*/
public saveSetting(setting: string, valueIndex: integer): boolean {
let settings: object = {};
if (localStorage.hasOwnProperty("settings")) {
settings = JSON.parse(localStorage.getItem("settings"));
}
setSetting(this.scene, setting as Setting, valueIndex);
setSetting(this.scene, setting, valueIndex);
Object.keys(settingDefaults).forEach(s => {
if (s === setting) {
settings[s] = valueIndex;
}
});
settings[setting] = valueIndex;
localStorage.setItem("settings", JSON.stringify(settings));
@ -653,61 +656,36 @@ export class GameData {
* to update the specified setting with the new value. Finally, it saves the updated settings back
* to localStorage and returns `true` to indicate success.
*/
public saveGamepadSetting(setting: SettingGamepad, valueIndex: integer): boolean {
let settingsGamepad: object = {}; // Initialize an empty object to hold the gamepad settings
public saveControlSetting(device: Device, localStoragePropertyName: string, setting: SettingGamepad|SettingKeyboard, settingDefaults, valueIndex: integer): boolean {
let settingsControls: object = {}; // Initialize an empty object to hold the gamepad settings
if (localStorage.hasOwnProperty("settingsGamepad")) { // Check if 'settingsGamepad' exists in localStorage
settingsGamepad = JSON.parse(localStorage.getItem("settingsGamepad")); // Parse the existing 'settingsGamepad' from localStorage
if (localStorage.hasOwnProperty(localStoragePropertyName)) { // Check if 'settingsControls' exists in localStorage
settingsControls = JSON.parse(localStorage.getItem(localStoragePropertyName)); // Parse the existing 'settingsControls' from localStorage
}
if (device === Device.GAMEPAD) {
setSettingGamepad(this.scene, setting as SettingGamepad, valueIndex); // Set the gamepad setting in the current scene
} else if (device === Device.KEYBOARD) {
setSettingKeyboard(this.scene, setting as SettingKeyboard, valueIndex); // Set the keyboard setting in the current scene
}
Object.keys(settingGamepadDefaults).forEach(s => { // Iterate over the default gamepad settings
Object.keys(settingDefaults).forEach(s => { // Iterate over the default gamepad settings
if (s === setting) {// If the current setting matches, update its value
settingsGamepad[s] = valueIndex;
settingsControls[s] = valueIndex;
}
});
localStorage.setItem("settingsGamepad", JSON.stringify(settingsGamepad)); // Save the updated gamepad settings back to localStorage
localStorage.setItem(localStoragePropertyName, JSON.stringify(settingsControls)); // Save the updated gamepad settings back to localStorage
return true; // Return true to indicate the operation was successful
}
/**
* Saves a keyboard setting to localStorage.
*
* @param setting - The keyboard setting to save.
* @param valueIndex - The index of the value to set for the keyboard setting.
* @returns `true` if the setting is successfully saved.
*
* @remarks
* This method initializes an empty object for keyboard settings if none exist in localStorage.
* It then updates the setting in the current scene and iterates over the default keyboard settings
* to update the specified setting with the new value. Finally, it saves the updated settings back
* to localStorage and returns `true` to indicate success.
* Loads Settings from local storage if available
* @returns true if succesful, false if not
*/
public saveKeyboardSetting(setting: SettingKeyboard, valueIndex: integer): boolean {
let settingsKeyboard: object = {}; // Initialize an empty object to hold the keyboard settings
if (localStorage.hasOwnProperty("settingsKeyboard")) { // Check if 'settingsKeyboard' exists in localStorage
settingsKeyboard = JSON.parse(localStorage.getItem("settingsKeyboard")); // Parse the existing 'settingsKeyboard' from localStorage
}
setSettingKeyboard(this.scene, setting as SettingKeyboard, valueIndex); // Set the keyboard setting in the current scene
Object.keys(settingKeyboardDefaults).forEach(s => { // Iterate over the default keyboard settings
if (s === setting) {// If the current setting matches, update its value
settingsKeyboard[s] = valueIndex;
}
});
localStorage.setItem("settingsKeyboard", JSON.stringify(settingsKeyboard)); // Save the updated keyboard settings back to localStorage
return true; // Return true to indicate the operation was successful
}
private loadSettings(): boolean {
Object.values(Setting).map(setting => setting as Setting).forEach(setting => setSetting(this.scene, setting, settingDefaults[setting]));
resetSettings(this.scene);
if (!localStorage.hasOwnProperty("settings")) {
return false;
@ -716,7 +694,7 @@ export class GameData {
const settings = JSON.parse(localStorage.getItem("settings"));
for (const setting of Object.keys(settings)) {
setSetting(this.scene, setting as Setting, settings[setting]);
setSetting(this.scene, setting, settings[setting]);
}
}

View File

@ -1,279 +0,0 @@
import { Mode } from "#app/ui/ui";
import i18next from "i18next";
import BattleScene from "../battle-scene";
import { hasTouchscreen } from "../touch-controls";
import { updateWindowType } from "../ui/ui-theme";
import { PlayerGender } from "./game-data";
import { CandyUpgradeNotificationChangedEvent } from "#app/battle-scene-events.js";
import { MoneyFormat } from "../enums/money-format";
import SettingsUiHandler from "#app/ui/settings/settings-ui-handler";
export enum Setting {
Game_Speed = "GAME_SPEED",
Master_Volume = "MASTER_VOLUME",
BGM_Volume = "BGM_VOLUME",
SE_Volume = "SE_VOLUME",
Language = "LANGUAGE",
Damage_Numbers = "DAMAGE_NUMBERS",
UI_Theme = "UI_THEME",
Window_Type = "WINDOW_TYPE",
Tutorials = "TUTORIALS",
Enable_Retries = "ENABLE_RETRIES",
Skip_Seen_Dialogues = "SKIP_SEEN_DIALOGUES",
Candy_Upgrade_Notification = "CANDY_UPGRADE_NOTIFICATION",
Candy_Upgrade_Display = "CANDY_UPGRADE_DISPLAY",
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",
HP_Bar_Speed = "HP_BAR_SPEED",
Fusion_Palette_Swaps = "FUSION_PALETTE_SWAPS",
Player_Gender = "PLAYER_GENDER",
Touch_Controls = "TOUCH_CONTROLS",
Vibration = "VIBRATION"
}
export interface SettingOptions {
[key: string]: string[]
}
export interface SettingDefaults {
[key: string]: integer
}
export const settingOptions: SettingOptions = {
[Setting.Game_Speed]: ["1x", "1.25x", "1.5x", "2x", "2.5x", "3x", "4x", "5x"],
[Setting.Master_Volume]: new Array(11).fill(null).map((_, i) => i ? (i * 10).toString() : "Mute"),
[Setting.BGM_Volume]: new Array(11).fill(null).map((_, i) => i ? (i * 10).toString() : "Mute"),
[Setting.SE_Volume]: new Array(11).fill(null).map((_, i) => i ? (i * 10).toString() : "Mute"),
[Setting.Language]: ["English", "Change"],
[Setting.Damage_Numbers]: ["Off", "Simple", "Fancy"],
[Setting.UI_Theme]: ["Default", "Legacy"],
[Setting.Window_Type]: new Array(5).fill(null).map((_, i) => (i + 1).toString()),
[Setting.Tutorials]: ["Off", "On"],
[Setting.Enable_Retries]: ["Off", "On"],
[Setting.Skip_Seen_Dialogues]: ["Off", "On"],
[Setting.Candy_Upgrade_Notification]: ["Off", "Passives Only", "On"],
[Setting.Candy_Upgrade_Display]: ["Icon", "Animation"],
[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"],
[Setting.HP_Bar_Speed]: ["Normal", "Fast", "Faster", "Instant"],
[Setting.Fusion_Palette_Swaps]: ["Off", "On"],
[Setting.Player_Gender]: ["Boy", "Girl"],
[Setting.Touch_Controls]: ["Auto", "Disabled"],
[Setting.Vibration]: ["Auto", "Disabled"]
};
export const settingDefaults: SettingDefaults = {
[Setting.Game_Speed]: 3,
[Setting.Master_Volume]: 5,
[Setting.BGM_Volume]: 10,
[Setting.SE_Volume]: 10,
[Setting.Language]: 0,
[Setting.Damage_Numbers]: 0,
[Setting.UI_Theme]: 0,
[Setting.Window_Type]: 0,
[Setting.Tutorials]: 1,
[Setting.Enable_Retries]: 0,
[Setting.Skip_Seen_Dialogues]: 0,
[Setting.Candy_Upgrade_Notification]: 0,
[Setting.Candy_Upgrade_Display]: 0,
[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,
[Setting.HP_Bar_Speed]: 0,
[Setting.Fusion_Palette_Swaps]: 1,
[Setting.Player_Gender]: 0,
[Setting.Touch_Controls]: 0,
[Setting.Vibration]: 0
};
export const reloadSettings: Setting[] = [Setting.UI_Theme, Setting.Language, Setting.Sprite_Set, Setting.Candy_Upgrade_Display];
export function setSetting(scene: BattleScene, setting: Setting, value: integer): boolean {
switch (setting) {
case Setting.Game_Speed:
scene.gameSpeed = parseFloat(settingOptions[setting][value].replace("x", ""));
break;
case Setting.Master_Volume:
scene.masterVolume = value ? parseInt(settingOptions[setting][value]) * 0.01 : 0;
scene.updateSoundVolume();
break;
case Setting.BGM_Volume:
scene.bgmVolume = value ? parseInt(settingOptions[setting][value]) * 0.01 : 0;
scene.updateSoundVolume();
break;
case Setting.SE_Volume:
scene.seVolume = value ? parseInt(settingOptions[setting][value]) * 0.01 : 0;
scene.updateSoundVolume();
break;
case Setting.Damage_Numbers:
scene.damageNumbersMode = value;
break;
case Setting.UI_Theme:
scene.uiTheme = value;
break;
case Setting.Window_Type:
updateWindowType(scene, parseInt(settingOptions[setting][value]));
break;
case Setting.Tutorials:
scene.enableTutorials = settingOptions[setting][value] === "On";
break;
case Setting.Enable_Retries:
scene.enableRetries = settingOptions[setting][value] === "On";
break;
case Setting.Candy_Upgrade_Notification:
if (scene.candyUpgradeNotification === value) {
break;
}
scene.candyUpgradeNotification = value;
scene.eventTarget.dispatchEvent(new CandyUpgradeNotificationChangedEvent(value));
break;
case Setting.Candy_Upgrade_Display:
scene.candyUpgradeDisplay = value;
case Setting.Money_Format:
switch (settingOptions[setting][value]) {
case "Normal":
scene.moneyFormat = MoneyFormat.NORMAL;
break;
case "Abbreviated":
scene.moneyFormat = MoneyFormat.ABBREVIATED;
break;
}
scene.updateMoneyText(false);
break;
case Setting.Sprite_Set:
scene.experimentalSprites = !!value;
if (value) {
scene.initExpSprites();
}
break;
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;
case Setting.EXP_Gains_Speed:
scene.expGainsSpeed = value;
break;
case Setting.EXP_Party_Display:
scene.expParty = value;
break;
case Setting.HP_Bar_Speed:
scene.hpBarSpeed = value;
break;
case Setting.Fusion_Palette_Swaps:
scene.fusionPaletteSwaps = !!value;
break;
case Setting.Player_Gender:
if (scene.gameData) {
const female = settingOptions[setting][value] === "Girl";
scene.gameData.gender = female ? PlayerGender.FEMALE : PlayerGender.MALE;
scene.trainer.setTexture(scene.trainer.texture.key.replace(female ? "m" : "f", female ? "f" : "m"));
} else {
return false;
}
break;
case Setting.Touch_Controls:
scene.enableTouchControls = settingOptions[setting][value] !== "Disabled" && hasTouchscreen();
const touchControls = document.getElementById("touchControls");
if (touchControls) {
touchControls.classList.toggle("visible", scene.enableTouchControls);
}
break;
case Setting.Vibration:
scene.enableVibration = settingOptions[setting][value] !== "Disabled" && hasTouchscreen();
break;
case Setting.Skip_Seen_Dialogues:
scene.skipSeenDialogues = settingOptions[setting][value] === "On";
break;
case Setting.Language:
if (value) {
if (scene.ui) {
const cancelHandler = () => {
scene.ui.revertMode();
(scene.ui.getHandler() as SettingsUiHandler).setOptionCursor(Object.values(Setting).indexOf(Setting.Language), 0, true);
};
const changeLocaleHandler = (locale: string): boolean => {
try {
i18next.changeLanguage(locale);
localStorage.setItem("prLang", locale);
cancelHandler();
// Reload the whole game to apply the new locale since also some constants are translated
window.location.reload();
return true;
} catch (error) {
console.error("Error changing locale:", error);
return false;
}
};
scene.ui.setOverlayMode(Mode.OPTION_SELECT, {
options: [
{
label: "English",
handler: () => changeLocaleHandler("en")
},
{
label: "Español",
handler: () => changeLocaleHandler("es")
},
{
label: "Italiano",
handler: () => changeLocaleHandler("it")
},
{
label: "Français",
handler: () => changeLocaleHandler("fr")
},
{
label: "Deutsch",
handler: () => changeLocaleHandler("de")
},
{
label: "Português (BR)",
handler: () => changeLocaleHandler("pt_BR")
},
{
label: "简体中文",
handler: () => changeLocaleHandler("zh_CN")
},
{
label: "繁體中文",
handler: () => changeLocaleHandler("zh_TW")
},
{
label: "한국어",
handler: () => changeLocaleHandler("ko")
},
{
label: "Cancel",
handler: () => cancelHandler()
}
],
maxOptions: 7
});
return false;
}
}
break;
}
return true;
}

View File

@ -1,10 +1,9 @@
import BattleScene from "../battle-scene";
import {SettingDefaults, SettingOptions} from "./settings";
import SettingsGamepadUiHandler from "../ui/settings/settings-gamepad-ui-handler";
import {Mode} from "../ui/ui";
import {truncateString} from "../utils";
import {Button} from "../enums/buttons";
import {SettingKeyboard} from "#app/system/settings-keyboard";
import BattleScene from "../../battle-scene";
import SettingsGamepadUiHandler from "../../ui/settings/settings-gamepad-ui-handler";
import {Mode} from "../../ui/ui";
import {truncateString} from "../../utils";
import {Button} from "../../enums/buttons";
import {SettingKeyboard} from "#app/system/settings/settings-keyboard";
export enum SettingGamepad {
Controller = "CONTROLLER",
@ -28,29 +27,31 @@ export enum SettingGamepad {
Button_Submit = "BUTTON_SUBMIT",
}
export const settingGamepadOptions: SettingOptions = {
const pressAction = "Press action to assign";
export const settingGamepadOptions = {
[SettingGamepad.Controller]: ["Default", "Change"],
[SettingGamepad.Gamepad_Support]: ["Auto", "Disabled"],
[SettingGamepad.Button_Up]: [`KEY ${Button.UP.toString()}`, "Press action to assign"],
[SettingGamepad.Button_Down]: [`KEY ${Button.DOWN.toString()}`, "Press action to assign"],
[SettingGamepad.Button_Left]: [`KEY ${Button.LEFT.toString()}`, "Press action to assign"],
[SettingGamepad.Button_Right]: [`KEY ${Button.RIGHT.toString()}`, "Press action to assign"],
[SettingGamepad.Button_Action]: [`KEY ${Button.ACTION.toString()}`, "Press action to assign"],
[SettingGamepad.Button_Cancel]: [`KEY ${Button.CANCEL.toString()}`, "Press action to assign"],
[SettingGamepad.Button_Menu]: [`KEY ${Button.MENU.toString()}`, "Press action to assign"],
[SettingGamepad.Button_Stats]: [`KEY ${Button.STATS.toString()}`, "Press action to assign"],
[SettingGamepad.Button_Cycle_Form]: [`KEY ${Button.CYCLE_FORM.toString()}`, "Press action to assign"],
[SettingGamepad.Button_Cycle_Shiny]: [`KEY ${Button.CYCLE_SHINY.toString()}`, "Press action to assign"],
[SettingGamepad.Button_Cycle_Gender]: [`KEY ${Button.CYCLE_GENDER.toString()}`, "Press action to assign"],
[SettingGamepad.Button_Cycle_Ability]: [`KEY ${Button.CYCLE_ABILITY.toString()}`, "Press action to assign"],
[SettingGamepad.Button_Cycle_Nature]: [`KEY ${Button.CYCLE_NATURE.toString()}`, "Press action to assign"],
[SettingGamepad.Button_Cycle_Variant]: [`KEY ${Button.V.toString()}`, "Press action to assign"],
[SettingGamepad.Button_Speed_Up]: [`KEY ${Button.SPEED_UP.toString()}`, "Press action to assign"],
[SettingGamepad.Button_Slow_Down]: [`KEY ${Button.SLOW_DOWN.toString()}`, "Press action to assign"],
[SettingGamepad.Button_Submit]: [`KEY ${Button.SUBMIT.toString()}`, "Press action to assign"],
[SettingGamepad.Button_Up]: [`KEY ${Button.UP.toString()}`, pressAction],
[SettingGamepad.Button_Down]: [`KEY ${Button.DOWN.toString()}`, pressAction],
[SettingGamepad.Button_Left]: [`KEY ${Button.LEFT.toString()}`, pressAction],
[SettingGamepad.Button_Right]: [`KEY ${Button.RIGHT.toString()}`, pressAction],
[SettingGamepad.Button_Action]: [`KEY ${Button.ACTION.toString()}`, pressAction],
[SettingGamepad.Button_Cancel]: [`KEY ${Button.CANCEL.toString()}`, pressAction],
[SettingGamepad.Button_Menu]: [`KEY ${Button.MENU.toString()}`, pressAction],
[SettingGamepad.Button_Stats]: [`KEY ${Button.STATS.toString()}`, pressAction],
[SettingGamepad.Button_Cycle_Form]: [`KEY ${Button.CYCLE_FORM.toString()}`, pressAction],
[SettingGamepad.Button_Cycle_Shiny]: [`KEY ${Button.CYCLE_SHINY.toString()}`, pressAction],
[SettingGamepad.Button_Cycle_Gender]: [`KEY ${Button.CYCLE_GENDER.toString()}`, pressAction],
[SettingGamepad.Button_Cycle_Ability]: [`KEY ${Button.CYCLE_ABILITY.toString()}`, pressAction],
[SettingGamepad.Button_Cycle_Nature]: [`KEY ${Button.CYCLE_NATURE.toString()}`, pressAction],
[SettingGamepad.Button_Cycle_Variant]: [`KEY ${Button.V.toString()}`, pressAction],
[SettingGamepad.Button_Speed_Up]: [`KEY ${Button.SPEED_UP.toString()}`, pressAction],
[SettingGamepad.Button_Slow_Down]: [`KEY ${Button.SLOW_DOWN.toString()}`, pressAction],
[SettingGamepad.Button_Submit]: [`KEY ${Button.SUBMIT.toString()}`, pressAction],
};
export const settingGamepadDefaults: SettingDefaults = {
export const settingGamepadDefaults = {
[SettingGamepad.Controller]: 0,
[SettingGamepad.Gamepad_Support]: 0,
[SettingGamepad.Button_Up]: 0,

View File

@ -1,4 +1,3 @@
import {SettingDefaults, SettingOptions} from "#app/system/settings";
import {Button} from "#app/enums/buttons";
import BattleScene from "#app/battle-scene";
import {Mode} from "#app/ui/ui";
@ -42,46 +41,47 @@ export enum SettingKeyboard {
Alt_Button_Submit = "ALT_BUTTON_SUBMIT",
}
export const settingKeyboardOptions: SettingOptions = {
// [SettingKeyboard.Default_Layout]: ['Default'],
[SettingKeyboard.Button_Up]: [`KEY ${Button.UP.toString()}`, "Press action to assign"],
[SettingKeyboard.Button_Down]: [`KEY ${Button.DOWN.toString()}`, "Press action to assign"],
[SettingKeyboard.Alt_Button_Up]: [`KEY ${Button.UP.toString()}`, "Press action to assign"],
[SettingKeyboard.Button_Left]: [`KEY ${Button.LEFT.toString()}`, "Press action to assign"],
[SettingKeyboard.Button_Right]: [`KEY ${Button.RIGHT.toString()}`, "Press action to assign"],
[SettingKeyboard.Button_Action]: [`KEY ${Button.ACTION.toString()}`, "Press action to assign"],
[SettingKeyboard.Button_Menu]: [`KEY ${Button.MENU.toString()}`, "Press action to assign"],
[SettingKeyboard.Button_Submit]: [`KEY ${Button.SUBMIT.toString()}`, "Press action to assign"],
const pressAction = "Press action to assign";
[SettingKeyboard.Alt_Button_Down]: [`KEY ${Button.DOWN.toString()}`, "Press action to assign"],
[SettingKeyboard.Alt_Button_Left]: [`KEY ${Button.LEFT.toString()}`, "Press action to assign"],
[SettingKeyboard.Alt_Button_Right]: [`KEY ${Button.RIGHT.toString()}`, "Press action to assign"],
[SettingKeyboard.Alt_Button_Action]: [`KEY ${Button.ACTION.toString()}`, "Press action to assign"],
[SettingKeyboard.Button_Cancel]: [`KEY ${Button.CANCEL.toString()}`, "Press action to assign"],
[SettingKeyboard.Alt_Button_Cancel]: [`KEY ${Button.CANCEL.toString()}`, "Press action to assign"],
[SettingKeyboard.Alt_Button_Menu]: [`KEY ${Button.MENU.toString()}`, "Press action to assign"],
[SettingKeyboard.Button_Stats]: [`KEY ${Button.STATS.toString()}`, "Press action to assign"],
[SettingKeyboard.Alt_Button_Stats]: [`KEY ${Button.STATS.toString()}`, "Press action to assign"],
[SettingKeyboard.Button_Cycle_Form]: [`KEY ${Button.CYCLE_FORM.toString()}`, "Press action to assign"],
[SettingKeyboard.Alt_Button_Cycle_Form]: [`KEY ${Button.CYCLE_FORM.toString()}`, "Press action to assign"],
[SettingKeyboard.Button_Cycle_Shiny]: [`KEY ${Button.CYCLE_SHINY.toString()}`, "Press action to assign"],
[SettingKeyboard.Alt_Button_Cycle_Shiny]: [`KEY ${Button.CYCLE_SHINY.toString()}`, "Press action to assign"],
[SettingKeyboard.Button_Cycle_Gender]: [`KEY ${Button.CYCLE_GENDER.toString()}`, "Press action to assign"],
[SettingKeyboard.Alt_Button_Cycle_Gender]: [`KEY ${Button.CYCLE_GENDER.toString()}`, "Press action to assign"],
[SettingKeyboard.Button_Cycle_Ability]: [`KEY ${Button.CYCLE_ABILITY.toString()}`, "Press action to assign"],
[SettingKeyboard.Alt_Button_Cycle_Ability]: [`KEY ${Button.CYCLE_ABILITY.toString()}`, "Press action to assign"],
[SettingKeyboard.Button_Cycle_Nature]: [`KEY ${Button.CYCLE_NATURE.toString()}`, "Press action to assign"],
[SettingKeyboard.Alt_Button_Cycle_Nature]: [`KEY ${Button.CYCLE_NATURE.toString()}`, "Press action to assign"],
[SettingKeyboard.Button_Cycle_Variant]: [`KEY ${Button.V.toString()}`, "Press action to assign"],
[SettingKeyboard.Alt_Button_Cycle_Variant]: [`KEY ${Button.V.toString()}`, "Press action to assign"],
[SettingKeyboard.Button_Speed_Up]: [`KEY ${Button.SPEED_UP.toString()}`, "Press action to assign"],
[SettingKeyboard.Alt_Button_Speed_Up]: [`KEY ${Button.SPEED_UP.toString()}`, "Press action to assign"],
[SettingKeyboard.Button_Slow_Down]: [`KEY ${Button.SLOW_DOWN.toString()}`, "Press action to assign"],
[SettingKeyboard.Alt_Button_Slow_Down]: [`KEY ${Button.SLOW_DOWN.toString()}`, "Press action to assign"],
[SettingKeyboard.Alt_Button_Submit]: [`KEY ${Button.SUBMIT.toString()}`, "Press action to assign"],
export const settingKeyboardOptions = {
// [SettingKeyboard.Default_Layout]: ['Default'],
[SettingKeyboard.Button_Up]: [`KEY ${Button.UP.toString()}`, pressAction],
[SettingKeyboard.Button_Down]: [`KEY ${Button.DOWN.toString()}`, pressAction],
[SettingKeyboard.Alt_Button_Up]: [`KEY ${Button.UP.toString()}`, pressAction],
[SettingKeyboard.Button_Left]: [`KEY ${Button.LEFT.toString()}`, pressAction],
[SettingKeyboard.Button_Right]: [`KEY ${Button.RIGHT.toString()}`, pressAction],
[SettingKeyboard.Button_Action]: [`KEY ${Button.ACTION.toString()}`, pressAction],
[SettingKeyboard.Button_Menu]: [`KEY ${Button.MENU.toString()}`, pressAction],
[SettingKeyboard.Button_Submit]: [`KEY ${Button.SUBMIT.toString()}`, pressAction],
[SettingKeyboard.Alt_Button_Down]: [`KEY ${Button.DOWN.toString()}`, pressAction],
[SettingKeyboard.Alt_Button_Left]: [`KEY ${Button.LEFT.toString()}`, pressAction],
[SettingKeyboard.Alt_Button_Right]: [`KEY ${Button.RIGHT.toString()}`, pressAction],
[SettingKeyboard.Alt_Button_Action]: [`KEY ${Button.ACTION.toString()}`, pressAction],
[SettingKeyboard.Button_Cancel]: [`KEY ${Button.CANCEL.toString()}`, pressAction],
[SettingKeyboard.Alt_Button_Cancel]: [`KEY ${Button.CANCEL.toString()}`, pressAction],
[SettingKeyboard.Alt_Button_Menu]: [`KEY ${Button.MENU.toString()}`, pressAction],
[SettingKeyboard.Button_Stats]: [`KEY ${Button.STATS.toString()}`, pressAction],
[SettingKeyboard.Alt_Button_Stats]: [`KEY ${Button.STATS.toString()}`, pressAction],
[SettingKeyboard.Button_Cycle_Form]: [`KEY ${Button.CYCLE_FORM.toString()}`, pressAction],
[SettingKeyboard.Alt_Button_Cycle_Form]: [`KEY ${Button.CYCLE_FORM.toString()}`, pressAction],
[SettingKeyboard.Button_Cycle_Shiny]: [`KEY ${Button.CYCLE_SHINY.toString()}`, pressAction],
[SettingKeyboard.Alt_Button_Cycle_Shiny]: [`KEY ${Button.CYCLE_SHINY.toString()}`, pressAction],
[SettingKeyboard.Button_Cycle_Gender]: [`KEY ${Button.CYCLE_GENDER.toString()}`, pressAction],
[SettingKeyboard.Alt_Button_Cycle_Gender]: [`KEY ${Button.CYCLE_GENDER.toString()}`, pressAction],
[SettingKeyboard.Button_Cycle_Ability]: [`KEY ${Button.CYCLE_ABILITY.toString()}`, pressAction],
[SettingKeyboard.Alt_Button_Cycle_Ability]: [`KEY ${Button.CYCLE_ABILITY.toString()}`, pressAction],
[SettingKeyboard.Button_Cycle_Nature]: [`KEY ${Button.CYCLE_NATURE.toString()}`, pressAction],
[SettingKeyboard.Alt_Button_Cycle_Nature]: [`KEY ${Button.CYCLE_NATURE.toString()}`, pressAction],
[SettingKeyboard.Button_Cycle_Variant]: [`KEY ${Button.V.toString()}`, pressAction],
[SettingKeyboard.Alt_Button_Cycle_Variant]: [`KEY ${Button.V.toString()}`, pressAction],
[SettingKeyboard.Button_Speed_Up]: [`KEY ${Button.SPEED_UP.toString()}`, pressAction],
[SettingKeyboard.Alt_Button_Speed_Up]: [`KEY ${Button.SPEED_UP.toString()}`, pressAction],
[SettingKeyboard.Button_Slow_Down]: [`KEY ${Button.SLOW_DOWN.toString()}`, pressAction],
[SettingKeyboard.Alt_Button_Slow_Down]: [`KEY ${Button.SLOW_DOWN.toString()}`, pressAction],
[SettingKeyboard.Alt_Button_Submit]: [`KEY ${Button.SUBMIT.toString()}`, pressAction],
};
export const settingKeyboardDefaults: SettingDefaults = {
export const settingKeyboardDefaults = {
// [SettingKeyboard.Default_Layout]: 0,
[SettingKeyboard.Button_Up]: 0,
[SettingKeyboard.Button_Down]: 0,

View File

@ -0,0 +1,453 @@
import { Mode } from "#app/ui/ui";
import i18next from "i18next";
import BattleScene from "../../battle-scene";
import { hasTouchscreen } from "../../touch-controls";
import { updateWindowType } from "../../ui/ui-theme";
import { PlayerGender } from "../game-data";
import { CandyUpgradeNotificationChangedEvent } from "#app/battle-scene-events.js";
import { MoneyFormat } from "../../enums/money-format";
import SettingsUiHandler from "#app/ui/settings/settings-ui-handler";
const MUTE = "Mute";
const VOLUME_OPTIONS = new Array(11).fill(null).map((_, i) => i ? (i * 10).toString() : MUTE);
const OFF_ON = ["Off", "On"];
const AUTO_DISABLED = ["Auto", "Disabled"];
/**
* Types for helping separate settings to different menus
*/
export enum SettingType {
GENERAL,
ACCESSIBILITY
}
export interface Setting {
key: string
label: string
options: Array<string>
default: number
type: SettingType
requireReload?: boolean
}
/**
* Setting Keys for existing settings
* to be used when trying to find or update Settings
*/
export const SettingKeys = {
Game_Speed: "GAME_SPEED",
Master_Volume: "MASTER_VOLUME",
BGM_Volume: "BGM_VOLUME",
SE_Volume: "SE_VOLUME",
Language: "LANGUAGE",
Damage_Numbers: "DAMAGE_NUMBERS",
UI_Theme: "UI_THEME",
Window_Type: "WINDOW_TYPE",
Tutorials: "TUTORIALS",
Enable_Retries: "ENABLE_RETRIES",
Skip_Seen_Dialogues: "SKIP_SEEN_DIALOGUES",
Candy_Upgrade_Notification: "CANDY_UPGRADE_NOTIFICATION",
Candy_Upgrade_Display: "CANDY_UPGRADE_DISPLAY",
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",
HP_Bar_Speed: "HP_BAR_SPEED",
Fusion_Palette_Swaps: "FUSION_PALETTE_SWAPS",
Player_Gender: "PLAYER_GENDER",
Touch_Controls: "TOUCH_CONTROLS",
Vibration: "VIBRATION"
};
/**
* All Settings not related to controls
*/
export const Setting: Array<Setting> = [
{
key: SettingKeys.Game_Speed,
label: "Game Speed",
options: ["1x", "1.25x", "1.5x", "2x", "2.5x", "3x", "4x", "5x"],
default: 3,
type: SettingType.GENERAL
},
{
key: SettingKeys.Master_Volume,
label: "Master Volume",
options: VOLUME_OPTIONS,
default: 5,
type: SettingType.GENERAL
},
{
key: SettingKeys.BGM_Volume,
label: "BGM Volume",
options: VOLUME_OPTIONS,
default: 10,
type: SettingType.GENERAL
},
{
key: SettingKeys.SE_Volume,
label: "SE Volume",
options: VOLUME_OPTIONS,
default: 10,
type: SettingType.GENERAL
},
{
key: SettingKeys.Language,
label: "Language",
options: ["English", "Change"],
default: 0,
type: SettingType.GENERAL,
requireReload: true
},
{
key: SettingKeys.Damage_Numbers,
label: "Damage Numbers",
options: ["Off", "Simple", "Fancy"],
default: 0,
type: SettingType.GENERAL
},
{
key: SettingKeys.UI_Theme,
label: "UI Theme",
options: ["Default", "Legacy"],
default: 0,
type: SettingType.GENERAL,
requireReload: true
},
{
key: SettingKeys.Window_Type,
label: "Window Type",
options: new Array(5).fill(null).map((_, i) => (i + 1).toString()),
default: 0,
type: SettingType.GENERAL
},
{
key: SettingKeys.Tutorials,
label: "Tutorials",
options: OFF_ON,
default: 1,
type: SettingType.GENERAL
},
{
key: SettingKeys.Enable_Retries,
label: "Enable Retries",
options: OFF_ON,
default: 0,
type: SettingType.ACCESSIBILITY
},
{
key: SettingKeys.Skip_Seen_Dialogues,
label: "Skip Seen Dialogues",
options: OFF_ON,
default: 0,
type: SettingType.GENERAL
},
{
key: SettingKeys.Candy_Upgrade_Notification,
label: "Candy Upgrade Notification",
options: ["Off", "Passives Only", "On"],
default: 0,
type: SettingType.ACCESSIBILITY
},
{
key: SettingKeys.Candy_Upgrade_Display,
label: "Candy Upgrade Display",
options: ["Icon", "Animation"],
default: 0,
type: SettingType.ACCESSIBILITY,
requireReload: true
},
{
key: SettingKeys.Money_Format,
label: "Money Format",
options: ["Normal", "Abbreviated"],
default: 0,
type: SettingType.ACCESSIBILITY
},
{
key: SettingKeys.Sprite_Set,
label: "Sprite Set",
options: ["Consistent", "Mixed Animated"],
default: 0,
type: SettingType.GENERAL,
requireReload: true
},
{
key: SettingKeys.Move_Animations,
label: "Move Animations",
options: OFF_ON,
default: 1,
type: SettingType.GENERAL
},
{
key: SettingKeys.Show_Moveset_Flyout,
label: "Show Moveset Flyout",
options: OFF_ON,
default: 1,
type: SettingType.ACCESSIBILITY
},
{
key: SettingKeys.Show_Stats_on_Level_Up,
label: "Show Stats on Level Up",
options: OFF_ON,
default: 1,
type: SettingType.GENERAL
},
{
key: SettingKeys.EXP_Gains_Speed,
label: "EXP Gains Speed",
options: ["Normal", "Fast", "Faster", "Skip"],
default: 0,
type: SettingType.GENERAL
},
{
key: SettingKeys.EXP_Party_Display,
label: "EXP Party Display",
options: ["Normal", "Level Up Notification", "Skip"],
default: 0,
type: SettingType.GENERAL
},
{
key: SettingKeys.HP_Bar_Speed,
label: "HP Bar Speed",
options: ["Normal", "Fast", "Faster", "Skip"],
default: 0,
type: SettingType.GENERAL
},
{
key: SettingKeys.Fusion_Palette_Swaps,
label: "Fusion Palette Swaps",
options: OFF_ON,
default: 1,
type: SettingType.GENERAL
},
{
key: SettingKeys.Player_Gender,
label: "Player Gender",
options: ["Boy", "Girl"],
default: 0,
type: SettingType.GENERAL
},
{
key: SettingKeys.Touch_Controls,
label: "Touch Controls",
options: AUTO_DISABLED,
default: 0,
type: SettingType.GENERAL
},
{
key: SettingKeys.Vibration,
label: "Vibration",
options: AUTO_DISABLED,
default: 0,
type: SettingType.GENERAL
}
];
/**
* Return the index of a Setting
* @param key SettingKey
* @returns index or -1 if doesn't exist
*/
export function settingIndex(key: string) {
return Setting.findIndex(s => s.key === key);
}
/**
* Resets all settings to their defaults
* @param scene current BattleScene
*/
export function resetSettings(scene: BattleScene) {
Setting.forEach(s => setSetting(scene, s.key, s.default));
}
/**
* Updates a setting for current BattleScene
* @param scene current BattleScene
* @param setting string ideally from SettingKeys
* @param value value to update setting with
* @returns true if successful, false if not
*/
export function setSetting(scene: BattleScene, setting: string, value: integer): boolean {
const index: number = settingIndex(setting);
if ( index === -1) {
return false;
}
switch (Setting[index].key) {
case SettingKeys.Game_Speed:
scene.gameSpeed = parseFloat(Setting[index].options[value].replace("x", ""));
break;
case SettingKeys.Master_Volume:
scene.masterVolume = value ? parseInt(Setting[index].options[value]) * 0.01 : 0;
scene.updateSoundVolume();
break;
case SettingKeys.BGM_Volume:
scene.bgmVolume = value ? parseInt(Setting[index].options[value]) * 0.01 : 0;
scene.updateSoundVolume();
break;
case SettingKeys.SE_Volume:
scene.seVolume = value ? parseInt(Setting[index].options[value]) * 0.01 : 0;
scene.updateSoundVolume();
break;
case SettingKeys.Damage_Numbers:
scene.damageNumbersMode = value;
break;
case SettingKeys.UI_Theme:
scene.uiTheme = value;
break;
case SettingKeys.Window_Type:
updateWindowType(scene, parseInt(Setting[index].options[value]));
break;
case SettingKeys.Tutorials:
scene.enableTutorials = Setting[index].options[value] === "On";
break;
case SettingKeys.Enable_Retries:
scene.enableRetries = Setting[index].options[value] === "On";
break;
case SettingKeys.Skip_Seen_Dialogues:
scene.skipSeenDialogues = Setting[index].options[value] === "On";
break;
case SettingKeys.Candy_Upgrade_Notification:
if (scene.candyUpgradeNotification === value) {
break;
}
scene.candyUpgradeNotification = value;
scene.eventTarget.dispatchEvent(new CandyUpgradeNotificationChangedEvent(value));
break;
case SettingKeys.Candy_Upgrade_Display:
scene.candyUpgradeDisplay = value;
case SettingKeys.Money_Format:
switch (Setting[index].options[value]) {
case "Normal":
scene.moneyFormat = MoneyFormat.NORMAL;
break;
case "Abbreviated":
scene.moneyFormat = MoneyFormat.ABBREVIATED;
break;
}
scene.updateMoneyText(false);
break;
case SettingKeys.Sprite_Set:
scene.experimentalSprites = !!value;
if (value) {
scene.initExpSprites();
}
break;
case SettingKeys.Move_Animations:
scene.moveAnimations = Setting[index].options[value] === "On";
break;
case SettingKeys.Show_Moveset_Flyout:
scene.showMovesetFlyout = Setting[index].options[value] === "On";
break;
case SettingKeys.Show_Stats_on_Level_Up:
scene.showLevelUpStats = Setting[index].options[value] === "On";
break;
case SettingKeys.EXP_Gains_Speed:
scene.expGainsSpeed = value;
break;
case SettingKeys.EXP_Party_Display:
scene.expParty = value;
break;
case SettingKeys.HP_Bar_Speed:
scene.hpBarSpeed = value;
break;
case SettingKeys.Fusion_Palette_Swaps:
scene.fusionPaletteSwaps = !!value;
break;
case SettingKeys.Player_Gender:
if (scene.gameData) {
const female = Setting[index].options[value] === "Girl";
scene.gameData.gender = female ? PlayerGender.FEMALE : PlayerGender.MALE;
scene.trainer.setTexture(scene.trainer.texture.key.replace(female ? "m" : "f", female ? "f" : "m"));
} else {
return false;
}
break;
case SettingKeys.Touch_Controls:
scene.enableTouchControls = Setting[index].options[value] !== "Disabled" && hasTouchscreen();
const touchControls = document.getElementById("touchControls");
if (touchControls) {
touchControls.classList.toggle("visible", scene.enableTouchControls);
}
break;
case SettingKeys.Vibration:
scene.enableVibration = Setting[index].options[value] !== "Disabled" && hasTouchscreen();
break;
case SettingKeys.Language:
if (value) {
if (scene.ui) {
const cancelHandler = () => {
scene.ui.revertMode();
const languageSetting = Setting.find(setting => setting.key === SettingKeys.Language);
(scene.ui.getHandler() as SettingsUiHandler).setOptionCursor(Setting.indexOf(languageSetting), 0, true);
};
const changeLocaleHandler = (locale: string): boolean => {
try {
i18next.changeLanguage(locale);
localStorage.setItem("prLang", locale);
cancelHandler();
// Reload the whole game to apply the new locale since also some constants are translated
window.location.reload();
return true;
} catch (error) {
console.error("Error changing locale:", error);
return false;
}
};
scene.ui.setOverlayMode(Mode.OPTION_SELECT, {
options: [
{
label: "English",
handler: () => changeLocaleHandler("en")
},
{
label: "Español",
handler: () => changeLocaleHandler("es")
},
{
label: "Italiano",
handler: () => changeLocaleHandler("it")
},
{
label: "Français",
handler: () => changeLocaleHandler("fr")
},
{
label: "Deutsch",
handler: () => changeLocaleHandler("de")
},
{
label: "Português (BR)",
handler: () => changeLocaleHandler("pt_BR")
},
{
label: "简体中文",
handler: () => changeLocaleHandler("zh_CN")
},
{
label: "繁體中文",
handler: () => changeLocaleHandler("zh_TW")
},
{
label: "한국어",
handler: () => changeLocaleHandler("ko")
},
{
label: "Cancel",
handler: () => cancelHandler()
}
],
maxOptions: 7
});
return false;
}
}
break;
}
return true;
}

View File

@ -3,7 +3,7 @@ import {
getSettingNameWithKeycode
} from "#app/configs/inputs/configHandler";
import {expect} from "vitest";
import {SettingKeyboard} from "#app/system/settings-keyboard";
import {SettingKeyboard} from "#app/system/settings/settings-keyboard";
export class InGameManip {
private config;

View File

@ -8,7 +8,7 @@ import {
assign,
getSettingNameWithKeycode, canIAssignThisKey, canIDeleteThisKey, canIOverrideThisSetting
} from "#app/configs/inputs/configHandler";
import {SettingKeyboard} from "#app/system/settings-keyboard";
import {SettingKeyboard} from "#app/system/settings/settings-keyboard";
export class MenuManip {
private config;

View File

@ -10,7 +10,7 @@ import {InGameManip} from "#app/test/helpers/inGameManip";
import {Device} from "#app/enums/devices";
import {InterfaceConfig} from "#app/inputs-controller";
import cfg_keyboard_qwerty from "#app/configs/inputs/cfg_keyboard_qwerty";
import {SettingKeyboard} from "#app/system/settings-keyboard";
import {SettingKeyboard} from "#app/system/settings/settings-keyboard";
describe("Test Rebinding", () => {

View File

@ -3,12 +3,13 @@ import {Mode} from "./ui/ui";
import {InputsController} from "./inputs-controller";
import MessageUiHandler from "./ui/message-ui-handler";
import StarterSelectUiHandler from "./ui/starter-select-ui-handler";
import {Setting, settingOptions} from "./system/settings";
import {Setting, SettingKeys, settingIndex} from "./system/settings/settings";
import SettingsUiHandler from "./ui/settings/settings-ui-handler";
import {Button} from "./enums/buttons";
import SettingsGamepadUiHandler from "./ui/settings/settings-gamepad-ui-handler";
import SettingsKeyboardUiHandler from "#app/ui/settings/settings-keyboard-ui-handler";
import BattleScene from "./battle-scene";
import SettingsAccessibilityUiHandler from "./ui/settings/settings-accessiblity-ui-handler";
type ActionKeys = Record<Button, () => void>;
@ -161,7 +162,7 @@ export class UiInputs {
}
buttonCycleOption(button: Button): void {
const whitelist = [StarterSelectUiHandler, SettingsUiHandler, SettingsGamepadUiHandler, SettingsKeyboardUiHandler];
const whitelist = [StarterSelectUiHandler, SettingsUiHandler, SettingsAccessibilityUiHandler, SettingsGamepadUiHandler, SettingsKeyboardUiHandler];
const uiHandler = this.scene.ui?.getHandler();
if (whitelist.some(handler => uiHandler instanceof handler)) {
this.scene.ui.processInput(button);
@ -171,17 +172,14 @@ export class UiInputs {
}
buttonSpeedChange(up = true): void {
if (up) {
if (this.scene.gameSpeed < 5) {
this.scene.gameData.saveSetting(Setting.Game_Speed, settingOptions[Setting.Game_Speed].indexOf(`${this.scene.gameSpeed}x`) + 1);
const settingGameSpeed = settingIndex(SettingKeys.Game_Speed);
if (up && this.scene.gameSpeed < 5) {
this.scene.gameData.saveSetting(SettingKeys.Game_Speed, Setting[settingGameSpeed].options.indexOf(`${this.scene.gameSpeed}x`) + 1);
if (this.scene.ui?.getMode() === Mode.SETTINGS) {
(this.scene.ui.getHandler() as SettingsUiHandler).show([]);
}
}
return;
}
if (this.scene.gameSpeed > 1) {
this.scene.gameData.saveSetting(Setting.Game_Speed, Math.max(settingOptions[Setting.Game_Speed].indexOf(`${this.scene.gameSpeed}x`) - 1, 0));
} else if (!up && this.scene.gameSpeed > 1) {
this.scene.gameData.saveSetting(SettingKeys.Game_Speed, Math.max(Setting[settingGameSpeed].options.indexOf(`${this.scene.gameSpeed}x`) - 1, 0));
if (this.scene.ui?.getMode() === Mode.SETTINGS) {
(this.scene.ui.getHandler() as SettingsUiHandler).show([]);
}

View File

@ -0,0 +1,660 @@
import UiHandler from "../ui-handler";
import BattleScene from "../../battle-scene";
import {Mode} from "../ui";
import {InterfaceConfig} from "../../inputs-controller";
import {addWindow} from "../ui-theme";
import {addTextObject, TextStyle} from "../text";
import {Button} from "../../enums/buttons";
import {getIconWithSettingName} from "#app/configs/inputs/configHandler";
import NavigationMenu, {NavigationManager} from "#app/ui/settings/navigationMenu";
import { Device } from "#app/enums/devices.js";
export interface InputsIcons {
[key: string]: Phaser.GameObjects.Sprite;
}
export interface LayoutConfig {
optionsContainer: Phaser.GameObjects.Container;
inputsIcons: InputsIcons;
settingLabels: Phaser.GameObjects.Text[];
optionValueLabels: Phaser.GameObjects.Text[][];
optionCursors: integer[];
keys: string[];
bindingSettings: Array<String>;
}
/**
* Abstract class for handling UI elements related to control settings.
*/
export default abstract class AbstractControlSettingsUiHandler extends UiHandler {
protected settingsContainer: Phaser.GameObjects.Container;
protected optionsContainer: Phaser.GameObjects.Container;
protected navigationContainer: NavigationMenu;
protected scrollCursor: integer;
protected optionCursors: integer[];
protected cursorObj: Phaser.GameObjects.NineSlice;
protected optionsBg: Phaser.GameObjects.NineSlice;
protected actionsBg: Phaser.GameObjects.NineSlice;
protected settingLabels: Phaser.GameObjects.Text[];
protected optionValueLabels: Phaser.GameObjects.Text[][];
// layout will contain the 3 Gamepad tab for each config - dualshock, xbox, snes
protected layout: Map<string, LayoutConfig> = new Map<string, LayoutConfig>();
// Will contain the input icons from the selected layout
protected inputsIcons: InputsIcons;
protected navigationIcons: InputsIcons;
// list all the setting keys used in the selected layout (because dualshock has more buttons than xbox)
protected keys: Array<String>;
// Store the specific settings related to key bindings for the current gamepad configuration.
protected bindingSettings: Array<String>;
protected setting;
protected settingBlacklisted;
protected settingDeviceDefaults;
protected settingDeviceOptions;
protected configs;
protected commonSettingsCount;
protected textureOverride;
protected titleSelected;
protected localStoragePropertyName;
protected rowsToDisplay: number;
protected device: Device;
abstract saveSettingToLocalStorage(setting, cursor): void;
abstract setSetting(scene: BattleScene, setting, value: integer): boolean;
/**
* Constructor for the AbstractSettingsUiHandler.
*
* @param scene - The BattleScene instance.
* @param mode - The UI mode.
*/
constructor(scene: BattleScene, mode?: Mode) {
super(scene, mode);
this.rowsToDisplay = 8;
}
getLocalStorageSetting(): object {
// Retrieve the settings from local storage or use an empty object if none exist.
const settings: object = localStorage.hasOwnProperty(this.localStoragePropertyName) ? JSON.parse(localStorage.getItem(this.localStoragePropertyName)) : {};
return settings;
}
/**
* Setup UI elements.
*/
setup() {
const ui = this.getUi();
this.navigationIcons = {};
this.settingsContainer = this.scene.add.container(1, -(this.scene.game.canvas.height / 6) + 1);
this.settingsContainer.setInteractive(new Phaser.Geom.Rectangle(0, 0, this.scene.game.canvas.width / 6, this.scene.game.canvas.height / 6), Phaser.Geom.Rectangle.Contains);
this.navigationContainer = new NavigationMenu(this.scene, 0, 0);
this.optionsBg = addWindow(this.scene, 0, this.navigationContainer.height, (this.scene.game.canvas.width / 6) - 2, (this.scene.game.canvas.height / 6) - 16 - this.navigationContainer.height - 2);
this.optionsBg.setOrigin(0, 0);
this.actionsBg = addWindow(this.scene, 0, (this.scene.game.canvas.height / 6) - this.navigationContainer.height, (this.scene.game.canvas.width / 6) - 2, 22);
this.actionsBg.setOrigin(0, 0);
const iconAction = this.scene.add.sprite(0, 0, "keyboard");
iconAction.setOrigin(0, -0.1);
iconAction.setPositionRelative(this.actionsBg, this.navigationContainer.width - 32, 4);
this.navigationIcons["BUTTON_ACTION"] = iconAction;
const actionText = addTextObject(this.scene, 0, 0, "Action", TextStyle.SETTINGS_LABEL);
actionText.setOrigin(0, 0.15);
actionText.setPositionRelative(iconAction, -actionText.width/6-2, 0);
const iconCancel = this.scene.add.sprite(0, 0, "keyboard");
iconCancel.setOrigin(0, -0.1);
iconCancel.setPositionRelative(this.actionsBg, this.navigationContainer.width - 100, 4);
this.navigationIcons["BUTTON_CANCEL"] = iconCancel;
const cancelText = addTextObject(this.scene, 0, 0, "Cancel", TextStyle.SETTINGS_LABEL);
cancelText.setOrigin(0, 0.15);
cancelText.setPositionRelative(iconCancel, -cancelText.width/6-2, 0);
const iconReset = this.scene.add.sprite(0, 0, "keyboard");
iconReset.setOrigin(0, -0.1);
iconReset.setPositionRelative(this.actionsBg, this.navigationContainer.width - 180, 4);
this.navigationIcons["BUTTON_HOME"] = iconReset;
const resetText = addTextObject(this.scene, 0, 0, "Reset all", TextStyle.SETTINGS_LABEL);
resetText.setOrigin(0, 0.15);
resetText.setPositionRelative(iconReset, -resetText.width/6-2, 0);
this.settingsContainer.add(this.optionsBg);
this.settingsContainer.add(this.actionsBg);
this.settingsContainer.add(this.navigationContainer);
this.settingsContainer.add(iconAction);
this.settingsContainer.add(iconCancel);
this.settingsContainer.add(iconReset);
this.settingsContainer.add(actionText);
this.settingsContainer.add(cancelText);
this.settingsContainer.add(resetText);
/// Initialize a new configuration "screen" for each type of gamepad.
for (const config of this.configs) {
// Create a map to store layout settings based on the pad type.
this.layout[config.padType] = new Map();
// Create a container for gamepad options in the scene, initially hidden.
const optionsContainer = this.scene.add.container(0, 0);
optionsContainer.setVisible(false);
// Gather all binding settings from the configuration.
const bindingSettings = Object.keys(config.settings);
// Array to hold labels for different settings such as 'Controller', 'Gamepad Support', etc.
const settingLabels: Phaser.GameObjects.Text[] = [];
// Array to hold options for each setting, e.g., 'Auto', 'Disabled'.
const optionValueLabels: Phaser.GameObjects.GameObject[][] = [];
// Object to store sprites for each button configuration.
const inputsIcons: InputsIcons = {};
// Fetch common setting keys such as 'Controller' and 'Gamepad Support' from gamepad settings.
const commonSettingKeys = Object.keys(this.setting).slice(0, this.commonSettingsCount).map(key => this.setting[key]);
// Combine common and specific bindings into a single array.
const specificBindingKeys = [...commonSettingKeys, ...Object.keys(config.settings)];
// Fetch default values for these settings and prepare to highlight selected options.
const optionCursors = Object.values(Object.keys(this.settingDeviceDefaults).filter(s => specificBindingKeys.includes(s)).map(k => this.settingDeviceDefaults[k]));
// Filter out settings that are not relevant to the current gamepad configuration.
const settingFiltered = Object.keys(this.setting).filter(_key => specificBindingKeys.includes(this.setting[_key]));
// Loop through the filtered settings to manage display and options.
settingFiltered.forEach((setting, s) => {
// Convert the setting key from format 'Key_Name' to 'Key name' for display.
const settingName = setting.replace(/\_/g, " ");
// Create and add a text object for the setting name to the scene.
const isLock = this.settingBlacklisted.includes(this.setting[setting]);
const labelStyle = isLock ? TextStyle.SETTINGS_LOCKED : TextStyle.SETTINGS_LABEL;
settingLabels[s] = addTextObject(this.scene, 8, 28 + s * 16, settingName, labelStyle);
settingLabels[s].setOrigin(0, 0);
optionsContainer.add(settingLabels[s]);
// Initialize an array to store the option labels for this setting.
const valueLabels: Phaser.GameObjects.GameObject[] = [];
// Process each option for the current setting.
for (const [o, option] of this.settingDeviceOptions[this.setting[setting]].entries()) {
// Check if the current setting is for binding keys.
if (bindingSettings.includes(this.setting[setting])) {
// Create a label for non-null options, typically indicating actionable options like 'change'.
if (o) {
const valueLabel = addTextObject(this.scene, 0, 0, isLock ? "" : option, TextStyle.WINDOW);
valueLabel.setOrigin(0, 0);
optionsContainer.add(valueLabel);
valueLabels.push(valueLabel);
continue;
}
// For null options, add an icon for the key.
const icon = this.scene.add.sprite(0, 0, this.textureOverride ? this.textureOverride : config.padType);
icon.setOrigin(0, -0.15);
inputsIcons[this.setting[setting]] = icon;
optionsContainer.add(icon);
valueLabels.push(icon);
continue;
}
// For regular settings like 'Gamepad support', create a label and determine if it is selected.
const valueLabel = addTextObject(this.scene, 0, 0, option, this.settingDeviceDefaults[this.setting[setting]] === o ? TextStyle.SETTINGS_SELECTED : TextStyle.WINDOW);
valueLabel.setOrigin(0, 0);
optionsContainer.add(valueLabel);
//if a setting has 2 options, valueLabels will be an array of 2 elements
valueLabels.push(valueLabel);
}
// Collect all option labels for this setting into the main array.
optionValueLabels.push(valueLabels);
// Calculate the total width of all option labels within a specific setting
// This is achieved by summing the width of each option label
const totalWidth = optionValueLabels[s].map((o) => (o as Phaser.GameObjects.Text).width).reduce((total, width) => total += width, 0);
// Define the minimum width for a label, ensuring it's at least 78 pixels wide or the width of the setting label plus some padding
const labelWidth = Math.max(130, settingLabels[s].displayWidth + 8);
// Calculate the total available space for placing option labels next to their setting label
// We reserve space for the setting label and then distribute the remaining space evenly
const totalSpace = (300 - labelWidth) - totalWidth / 6;
// Calculate the spacing between options based on the available space divided by the number of gaps between labels
const optionSpacing = Math.floor(totalSpace / (optionValueLabels[s].length - 1));
// Initialize xOffset to zero, which will be used to position each option label horizontally
let xOffset = 0;
// Start positioning each option label one by one
for (const value of optionValueLabels[s]) {
// Set the option label's position right next to the setting label, adjusted by xOffset
(value as Phaser.GameObjects.Text).setPositionRelative(settingLabels[s], labelWidth + xOffset, 0);
// Move the xOffset to the right for the next label, ensuring each label is spaced evenly
xOffset += (value as Phaser.GameObjects.Text).width / 6 + optionSpacing;
}
});
// Assigning the newly created components to the layout map under the specific gamepad type.
this.layout[config.padType].optionsContainer = optionsContainer; // Container for this pad's options.
this.layout[config.padType].inputsIcons = inputsIcons; // Icons for each input specific to this pad.
this.layout[config.padType].settingLabels = settingLabels; // Text labels for each setting available on this pad.
this.layout[config.padType].optionValueLabels = optionValueLabels; // Labels for values corresponding to each setting.
this.layout[config.padType].optionCursors = optionCursors; // Cursors to navigate through the options.
this.layout[config.padType].keys = specificBindingKeys; // Keys that identify each setting specifically bound to this pad.
this.layout[config.padType].bindingSettings = bindingSettings; // Settings that define how the keys are bound.
// Add the options container to the overall settings container to be displayed in the UI.
this.settingsContainer.add(optionsContainer);
}
// Add the settings container to the UI.
ui.add(this.settingsContainer);
// Initially hide the settings container until needed (e.g., when a gamepad is connected or a button is pressed).
this.settingsContainer.setVisible(false);
}
/**
* Get the active configuration.
*
* @returns The active configuration for current device
*/
getActiveConfig(): InterfaceConfig {
return this.scene.inputController.getActiveConfig(this.device);
}
/**
* Update the bindings for the current active device configuration.
*/
updateBindings(): void {
// Hide the options container for all layouts to reset the UI visibility.
Object.keys(this.layout).forEach((key) => this.layout[key].optionsContainer.setVisible(false));
// Fetch the active gamepad configuration from the input controller.
const activeConfig = this.getActiveConfig();
// Set the UI layout for the active configuration. If unsuccessful, exit the function early.
if (!this.setLayout(activeConfig)) {
return;
}
// Retrieve the gamepad settings from local storage or use an empty object if none exist.
const settings: object = this.getLocalStorageSetting();
// Update the cursor for each key based on the stored settings or default cursors.
this.keys.forEach((key, index) => {
this.setOptionCursor(index, settings.hasOwnProperty(key as string) ? settings[key as string] : this.optionCursors[index]);
});
// If the active configuration has no custom bindings set, exit the function early.
// by default, if custom does not exists, a default is assigned to it
// it only means the gamepad is not yet initalized
if (!activeConfig.custom) {
return;
}
// For each element in the binding settings, update the icon according to the current assignment.
for (const elm of this.bindingSettings) {
const icon = getIconWithSettingName(activeConfig, elm);
if (icon) {
this.inputsIcons[elm as string].setFrame(icon);
this.inputsIcons[elm as string].alpha = 1;
} else {
this.inputsIcons[elm as string].alpha = 0;
}
}
// Set the cursor and scroll cursor to their initial positions.
this.setCursor(this.cursor);
this.setScrollCursor(this.scrollCursor);
}
updateNavigationDisplay() {
const specialIcons = {
"BUTTON_HOME": "HOME.png",
"BUTTON_DELETE": "DEL.png",
};
for (const settingName of Object.keys(this.navigationIcons)) {
if (Object.keys(specialIcons).includes(settingName)) {
this.navigationIcons[settingName].setTexture("keyboard");
this.navigationIcons[settingName].setFrame(specialIcons[settingName]);
this.navigationIcons[settingName].alpha = 1;
continue;
}
const icon = this.scene.inputController?.getIconForLatestInputRecorded(settingName);
if (icon) {
const type = this.scene.inputController?.getLastSourceType();
this.navigationIcons[settingName].setTexture(type);
this.navigationIcons[settingName].setFrame(icon);
this.navigationIcons[settingName].alpha = 1;
} else {
this.navigationIcons[settingName].alpha = 0;
}
}
}
/**
* Show the UI with the provided arguments.
*
* @param args - Arguments to be passed to the show method.
* @returns `true` if successful.
*/
show(args: any[]): boolean {
super.show(args);
this.updateNavigationDisplay();
NavigationManager.getInstance().updateIcons();
// Update the bindings for the current active gamepad configuration.
this.updateBindings();
// Make the settings container visible to the user.
this.settingsContainer.setVisible(true);
// Reset the scroll cursor to the top of the settings container.
this.resetScroll();
// Move the settings container to the end of the UI stack to ensure it is displayed on top.
this.getUi().moveTo(this.settingsContainer, this.getUi().length - 1);
// Hide any tooltips that might be visible before showing the settings container.
this.getUi().hideTooltip();
// Return true to indicate the UI was successfully shown.
return true;
}
/**
* Set the UI layout for the active device configuration.
*
* @param activeConfig - The active device configuration.
* @returns `true` if the layout was successfully applied, otherwise `false`.
*/
setLayout(activeConfig: InterfaceConfig): boolean {
// Check if there is no active configuration (e.g., no gamepad connected).
if (!activeConfig) {
// Retrieve the layout for when no gamepads are connected.
const layout = this.layout["noGamepads"];
// Make the options container visible to show message.
layout.optionsContainer.setVisible(true);
// Return false indicating the layout application was not successful due to lack of gamepad.
return false;
}
// Extract the type of the gamepad from the active configuration.
const configType = activeConfig.padType;
// Retrieve the layout settings based on the type of the gamepad.
const layout = this.layout[configType];
// Update the main controller with configuration details from the selected layout.
this.keys = layout.keys;
this.optionsContainer = layout.optionsContainer;
this.optionsContainer.setVisible(true);
this.settingLabels = layout.settingLabels;
this.optionValueLabels = layout.optionValueLabels;
this.optionCursors = layout.optionCursors;
this.inputsIcons = layout.inputsIcons;
this.bindingSettings = layout.bindingSettings;
// Return true indicating the layout was successfully applied.
return true;
}
/**
* Process the input for the given button.
*
* @param button - The button to process.
* @returns `true` if the input was processed successfully.
*/
processInput(button: Button): boolean {
const ui = this.getUi();
// Defines the maximum number of rows that can be displayed on the screen.
let success = false;
this.updateNavigationDisplay();
// Handle the input based on the button pressed.
if (button === Button.CANCEL) {
// Handle cancel button press, reverting UI mode to previous state.
success = true;
NavigationManager.getInstance().reset();
this.scene.ui.revertMode();
} else {
const cursor = this.cursor + this.scrollCursor; // Calculate the absolute cursor position.
const setting = this.setting[Object.keys(this.setting)[cursor]];
switch (button) {
case Button.ACTION:
if (!this.optionCursors || !this.optionValueLabels) {
return;
}
if (this.settingBlacklisted.includes(setting) || !setting.includes("BUTTON_")) {
success = false;
} else {
success = this.setSetting(this.scene, setting, 1);
}
break;
case Button.UP: // Move up in the menu.
if (!this.optionValueLabels) {
return false;
}
if (cursor) { // If not at the top, move the cursor up.
if (this.cursor) {
success = this.setCursor(this.cursor - 1);
} else {// If at the top of the visible items, scroll up.
success = this.setScrollCursor(this.scrollCursor - 1);
}
} else {
// When at the top of the menu and pressing UP, move to the bottommost item.
// First, set the cursor to the last visible element, preparing for the scroll to the end.
const successA = this.setCursor(this.rowsToDisplay - 1);
// Then, adjust the scroll to display the bottommost elements of the menu.
const successB = this.setScrollCursor(this.optionValueLabels.length - this.rowsToDisplay);
success = successA && successB; // success is just there to play the little validation sound effect
}
break;
case Button.DOWN: // Move down in the menu.
if (!this.optionValueLabels) {
return false;
}
if (cursor < this.optionValueLabels.length - 1) {
if (this.cursor < this.rowsToDisplay - 1) {
success = this.setCursor(this.cursor + 1);
} else if (this.scrollCursor < this.optionValueLabels.length - this.rowsToDisplay) {
success = this.setScrollCursor(this.scrollCursor + 1);
}
} else {
// When at the bottom of the menu and pressing DOWN, move to the topmost item.
// First, set the cursor to the first visible element, resetting the scroll to the top.
const successA = this.setCursor(0);
// Then, reset the scroll to start from the first element of the menu.
const successB = this.setScrollCursor(0);
success = successA && successB; // Indicates a successful cursor and scroll adjustment.
}
break;
case Button.LEFT: // Move selection left within the current option set.
if (!this.optionCursors || !this.optionValueLabels) {
return;
}
if (this.settingBlacklisted.includes(setting) || setting.includes("BUTTON_")) {
success = false;
} else if (this.optionCursors[cursor]) {
success = this.setOptionCursor(cursor, this.optionCursors[cursor] - 1, true);
}
break;
case Button.RIGHT: // Move selection right within the current option set.
if (!this.optionCursors || !this.optionValueLabels) {
return;
}
if (this.settingBlacklisted.includes(setting) || setting.includes("BUTTON_")) {
success = false;
} else if (this.optionCursors[cursor] < this.optionValueLabels[cursor].length - 1) {
success = this.setOptionCursor(cursor, this.optionCursors[cursor] + 1, true);
}
break;
case Button.CYCLE_FORM:
case Button.CYCLE_SHINY:
success = this.navigationContainer.navigate(button);
break;
}
}
// If a change occurred, play the selection sound.
if (success) {
ui.playSelect();
}
return success; // Return whether the input resulted in a successful action.
}
resetScroll() {
this.cursorObj?.destroy();
this.cursorObj = null;
this.cursor = null;
this.setCursor(0);
this.setScrollCursor(0);
this.updateSettingsScroll();
}
/**
* Set the cursor to the specified position.
*
* @param cursor - The cursor position to set.
* @returns `true` if the cursor was set successfully.
*/
setCursor(cursor: integer): boolean {
const ret = super.setCursor(cursor);
// If the optionsContainer is not initialized, return the result from the parent class directly.
if (!this.optionsContainer) {
return ret;
}
// Check if the cursor object exists, if not, create it.
if (!this.cursorObj) {
this.cursorObj = this.scene.add.nineslice(0, 0, "summary_moves_cursor", null, (this.scene.game.canvas.width / 6) - 10, 16, 1, 1, 1, 1);
this.cursorObj.setOrigin(0, 0); // Set the origin to the top-left corner.
this.optionsContainer.add(this.cursorObj); // Add the cursor to the options container.
}
// Update the position of the cursor object relative to the options background based on the current cursor and scroll positions.
this.cursorObj.setPositionRelative(this.optionsBg, 4, 4 + (this.cursor + this.scrollCursor) * 16);
return ret; // Return the result from the parent class's setCursor method.
}
/**
* Set the scroll cursor to the specified position.
*
* @param scrollCursor - The scroll cursor position to set.
* @returns `true` if the scroll cursor was set successfully.
*/
setScrollCursor(scrollCursor: integer): boolean {
// Check if the new scroll position is the same as the current one; if so, do not update.
if (scrollCursor === this.scrollCursor) {
return false;
}
// Update the internal scroll cursor state
this.scrollCursor = scrollCursor;
// Apply the new scroll position to the settings UI.
this.updateSettingsScroll();
// Reset the cursor to its current position to adjust its visibility after scrolling.
this.setCursor(this.cursor);
return true; // Return true to indicate the scroll cursor was successfully updated.
}
/**
* Set the option cursor to the specified position.
*
* @param settingIndex - The index of the setting.
* @param cursor - The cursor position to set.
* @param save - Whether to save the setting to local storage.
* @returns `true` if the option cursor was set successfully.
*/
setOptionCursor(settingIndex: integer, cursor: integer, save?: boolean): boolean {
// Retrieve the specific setting using the settingIndex from the settingDevice enumeration.
const setting = this.setting[Object.keys(this.setting)[settingIndex]];
// Get the current cursor position for this setting.
const lastCursor = this.optionCursors[settingIndex];
// Check if the setting is not part of the bindings (i.e., it's a regular setting).
if (!this.bindingSettings.includes(setting) && !setting.includes("BUTTON_")) {
// Get the label of the last selected option and revert its color to the default.
const lastValueLabel = this.optionValueLabels[settingIndex][lastCursor];
lastValueLabel.setColor(this.getTextColor(TextStyle.WINDOW));
lastValueLabel.setShadowColor(this.getTextColor(TextStyle.WINDOW, true));
// Update the cursor for the setting to the new position.
this.optionCursors[settingIndex] = cursor;
// Change the color of the new selected option to indicate it's selected.
const newValueLabel = this.optionValueLabels[settingIndex][cursor];
newValueLabel.setColor(this.getTextColor(TextStyle.SETTINGS_SELECTED));
newValueLabel.setShadowColor(this.getTextColor(TextStyle.SETTINGS_SELECTED, true));
}
// If the save flag is set, save the setting to local storage
if (save) {
this.saveSettingToLocalStorage(setting, cursor);
}
return true; // Return true to indicate the cursor was successfully updated.
}
/**
* Update the scroll position of the settings UI.
*/
updateSettingsScroll(): void {
// Return immediately if the options container is not initialized.
if (!this.optionsContainer) {
return;
}
// Set the vertical position of the options container based on the current scroll cursor, multiplying by the item height.
this.optionsContainer.setY(-16 * this.scrollCursor);
// Iterate over all setting labels to update their visibility.
for (let s = 0; s < this.settingLabels.length; s++) {
// Determine if the current setting should be visible based on the scroll position.
const visible = s >= this.scrollCursor && s < this.scrollCursor + this.rowsToDisplay;
// Set the visibility of the setting label and its corresponding options.
this.settingLabels[s].setVisible(visible);
for (const option of this.optionValueLabels[s]) {
option.setVisible(visible);
}
}
}
/**
* Clear the UI elements and state.
*/
clear(): void {
super.clear();
// Hide the settings container to remove it from the view.
this.settingsContainer.setVisible(false);
// Remove the cursor from the UI.
this.eraseCursor();
}
/**
* Erase the cursor from the UI.
*/
eraseCursor(): void {
// Check if a cursor object exists.
if (this.cursorObj) {
this.cursorObj.destroy();
} // Destroy the cursor object to clean up resources.
// Set the cursor object reference to null to fully dereference it.
this.cursorObj = null;
}
}

View File

@ -1,107 +1,74 @@
import UiHandler from "../ui-handler";
import BattleScene from "../../battle-scene";
import { hasTouchscreen, isMobile } from "../../touch-controls";
import { TextStyle, addTextObject } from "../text";
import { Mode } from "../ui";
import {InterfaceConfig} from "../../inputs-controller";
import UiHandler from "../ui-handler";
import { addWindow } from "../ui-theme";
import {addTextObject, TextStyle} from "../text";
import {Button} from "../../enums/buttons";
import {getIconWithSettingName} from "#app/configs/inputs/configHandler";
import {InputsIcons} from "#app/ui/settings/abstract-control-settings-ui-handler.js";
import NavigationMenu, {NavigationManager} from "#app/ui/settings/navigationMenu";
import { Setting, SettingKeys } from "#app/system/settings/settings";
export interface InputsIcons {
[key: string]: Phaser.GameObjects.Sprite;
}
export interface LayoutConfig {
optionsContainer: Phaser.GameObjects.Container;
inputsIcons: InputsIcons;
settingLabels: Phaser.GameObjects.Text[];
optionValueLabels: Phaser.GameObjects.Text[][];
optionCursors: integer[];
keys: string[];
bindingSettings: Array<String>;
}
/**
* Abstract class for handling UI elements related to settings.
*/
export default abstract class AbstractSettingsUiUiHandler extends UiHandler {
protected settingsContainer: Phaser.GameObjects.Container;
protected optionsContainer: Phaser.GameObjects.Container;
protected navigationContainer: NavigationMenu;
export default class AbstractSettingsUiHandler extends UiHandler {
private settingsContainer: Phaser.GameObjects.Container;
private optionsContainer: Phaser.GameObjects.Container;
private navigationContainer: NavigationMenu;
protected scrollCursor: integer;
protected optionCursors: integer[];
protected cursorObj: Phaser.GameObjects.NineSlice;
private scrollCursor: integer;
protected optionsBg: Phaser.GameObjects.NineSlice;
protected actionsBg: Phaser.GameObjects.NineSlice;
private optionsBg: Phaser.GameObjects.NineSlice;
protected settingLabels: Phaser.GameObjects.Text[];
protected optionValueLabels: Phaser.GameObjects.Text[][];
private optionCursors: integer[];
private settingLabels: Phaser.GameObjects.Text[];
private optionValueLabels: Phaser.GameObjects.Text[][];
// layout will contain the 3 Gamepad tab for each config - dualshock, xbox, snes
protected layout: Map<string, LayoutConfig> = new Map<string, LayoutConfig>();
// Will contain the input icons from the selected layout
protected inputsIcons: InputsIcons;
protected navigationIcons: InputsIcons;
// list all the setting keys used in the selected layout (because dualshock has more buttons than xbox)
protected keys: Array<String>;
// Store the specific settings related to key bindings for the current gamepad configuration.
protected bindingSettings: Array<String>;
private cursorObj: Phaser.GameObjects.NineSlice;
protected settingDevice;
protected settingBlacklisted;
protected settingDeviceDefaults;
protected settingDeviceOptions;
protected configs;
protected commonSettingsCount;
protected textureOverride;
protected titleSelected;
protected localStoragePropertyName;
protected rowsToDisplay: number;
private reloadSettings: Array<Setting>;
private reloadRequired: boolean;
private rowsToDisplay: number;
abstract getLocalStorageSetting(): object;
abstract navigateMenuLeft(): boolean;
abstract navigateMenuRight(): boolean;
abstract saveSettingToLocalStorage(setting, cursor): void;
abstract getActiveConfig(): InterfaceConfig;
abstract setSetting(scene: BattleScene, setting, value: integer): boolean;
protected title: string;
protected settings: Array<Setting>;
protected localStorageKey: string;
/**
* Constructor for the AbstractSettingsUiUiHandler.
*
* @param scene - The BattleScene instance.
* @param mode - The UI mode.
*/
constructor(scene: BattleScene, mode?: Mode) {
super(scene, mode);
this.reloadRequired = false;
this.rowsToDisplay = 8;
}
/**
* Setup UI elements.
* Setup UI elements
*/
setup() {
const ui = this.getUi();
this.navigationIcons = {};
this.settingsContainer = this.scene.add.container(1, -(this.scene.game.canvas.height / 6) + 1);
this.settingsContainer.setInteractive(new Phaser.Geom.Rectangle(0, 0, this.scene.game.canvas.width / 6, this.scene.game.canvas.height / 6), Phaser.Geom.Rectangle.Contains);
this.settingsContainer.setInteractive(new Phaser.Geom.Rectangle(0, 0, this.scene.game.canvas.width / 6, this.scene.game.canvas.height / 6 - 20), Phaser.Geom.Rectangle.Contains);
this.navigationIcons = {};
this.navigationContainer = new NavigationMenu(this.scene, 0, 0);
this.optionsBg = addWindow(this.scene, 0, this.navigationContainer.height, (this.scene.game.canvas.width / 6) - 2, (this.scene.game.canvas.height / 6) - 16 - this.navigationContainer.height - 2);
this.optionsBg.setOrigin(0, 0);
this.actionsBg = addWindow(this.scene, 0, (this.scene.game.canvas.height / 6) - this.navigationContainer.height, (this.scene.game.canvas.width / 6) - 2, 22);
this.actionsBg.setOrigin(0, 0);
const actionsBg = addWindow(this.scene, 0, (this.scene.game.canvas.height / 6) - this.navigationContainer.height, (this.scene.game.canvas.width / 6) - 2, 22);
actionsBg.setOrigin(0, 0);
const iconAction = this.scene.add.sprite(0, 0, "keyboard");
iconAction.setOrigin(0, -0.1);
iconAction.setPositionRelative(this.actionsBg, this.navigationContainer.width - 32, 4);
iconAction.setPositionRelative(actionsBg, this.navigationContainer.width - 32, 4);
this.navigationIcons["BUTTON_ACTION"] = iconAction;
const actionText = addTextObject(this.scene, 0, 0, "Action", TextStyle.SETTINGS_LABEL);
@ -110,207 +77,81 @@ export default abstract class AbstractSettingsUiUiHandler extends UiHandler {
const iconCancel = this.scene.add.sprite(0, 0, "keyboard");
iconCancel.setOrigin(0, -0.1);
iconCancel.setPositionRelative(this.actionsBg, this.navigationContainer.width - 100, 4);
iconCancel.setPositionRelative(actionsBg, this.navigationContainer.width - 100, 4);
this.navigationIcons["BUTTON_CANCEL"] = iconCancel;
const cancelText = addTextObject(this.scene, 0, 0, "Cancel", TextStyle.SETTINGS_LABEL);
cancelText.setOrigin(0, 0.15);
cancelText.setPositionRelative(iconCancel, -cancelText.width/6-2, 0);
const iconReset = this.scene.add.sprite(0, 0, "keyboard");
iconReset.setOrigin(0, -0.1);
iconReset.setPositionRelative(this.actionsBg, this.navigationContainer.width - 180, 4);
this.navigationIcons["BUTTON_HOME"] = iconReset;
this.optionsContainer = this.scene.add.container(0, 0);
const resetText = addTextObject(this.scene, 0, 0, "Reset all", TextStyle.SETTINGS_LABEL);
resetText.setOrigin(0, 0.15);
resetText.setPositionRelative(iconReset, -resetText.width/6-2, 0);
this.settingLabels = [];
this.optionValueLabels = [];
this.settingsContainer.add(this.optionsBg);
this.settingsContainer.add(this.actionsBg);
this.settingsContainer.add(this.navigationContainer);
this.settingsContainer.add(iconAction);
this.settingsContainer.add(iconCancel);
this.settingsContainer.add(iconReset);
this.settingsContainer.add(actionText);
this.settingsContainer.add(cancelText);
this.settingsContainer.add(resetText);
this.reloadSettings = this.settings.filter(s => s?.requireReload);
/// Initialize a new configuration "screen" for each type of gamepad.
for (const config of this.configs) {
// Create a map to store layout settings based on the pad type.
this.layout[config.padType] = new Map();
// Create a container for gamepad options in the scene, initially hidden.
const optionsContainer = this.scene.add.container(0, 0);
optionsContainer.setVisible(false);
// Gather all binding settings from the configuration.
const bindingSettings = Object.keys(config.settings);
// Array to hold labels for different settings such as 'Controller', 'Gamepad Support', etc.
const settingLabels: Phaser.GameObjects.Text[] = [];
// Array to hold options for each setting, e.g., 'Auto', 'Disabled'.
const optionValueLabels: Phaser.GameObjects.GameObject[][] = [];
// Object to store sprites for each button configuration.
const inputsIcons: InputsIcons = {};
// Fetch common setting keys such as 'Controller' and 'Gamepad Support' from gamepad settings.
const commonSettingKeys = Object.keys(this.settingDevice).slice(0, this.commonSettingsCount).map(key => this.settingDevice[key]);
// Combine common and specific bindings into a single array.
const specificBindingKeys = [...commonSettingKeys, ...Object.keys(config.settings)];
// Fetch default values for these settings and prepare to highlight selected options.
const optionCursors = Object.values(Object.keys(this.settingDeviceDefaults).filter(s => specificBindingKeys.includes(s)).map(k => this.settingDeviceDefaults[k]));
// Filter out settings that are not relevant to the current gamepad configuration.
const settingFiltered = Object.keys(this.settingDevice).filter(_key => specificBindingKeys.includes(this.settingDevice[_key]));
// Loop through the filtered settings to manage display and options.
settingFiltered.forEach((setting, s) => {
// Convert the setting key from format 'Key_Name' to 'Key name' for display.
const settingName = setting.replace(/\_/g, " ");
// Create and add a text object for the setting name to the scene.
const isLock = this.settingBlacklisted.includes(this.settingDevice[setting]);
const labelStyle = isLock ? TextStyle.SETTINGS_LOCKED : TextStyle.SETTINGS_LABEL;
settingLabels[s] = addTextObject(this.scene, 8, 28 + s * 16, settingName, labelStyle);
settingLabels[s].setOrigin(0, 0);
optionsContainer.add(settingLabels[s]);
// Initialize an array to store the option labels for this setting.
const valueLabels: Phaser.GameObjects.GameObject[] = [];
// Process each option for the current setting.
for (const [o, option] of this.settingDeviceOptions[this.settingDevice[setting]].entries()) {
// Check if the current setting is for binding keys.
if (bindingSettings.includes(this.settingDevice[setting])) {
// Create a label for non-null options, typically indicating actionable options like 'change'.
if (o) {
const valueLabel = addTextObject(this.scene, 0, 0, isLock ? "" : option, TextStyle.WINDOW);
valueLabel.setOrigin(0, 0);
optionsContainer.add(valueLabel);
valueLabels.push(valueLabel);
continue;
this.settings
.forEach((setting, s) => {
let settingName = setting.label;
if (setting?.requireReload) {
settingName += " (Requires Reload)";
}
// For null options, add an icon for the key.
const icon = this.scene.add.sprite(0, 0, this.textureOverride ? this.textureOverride : config.padType);
icon.setOrigin(0, -0.15);
inputsIcons[this.settingDevice[setting]] = icon;
optionsContainer.add(icon);
valueLabels.push(icon);
continue;
}
// For regular settings like 'Gamepad support', create a label and determine if it is selected.
const valueLabel = addTextObject(this.scene, 0, 0, option, this.settingDeviceDefaults[this.settingDevice[setting]] === o ? TextStyle.SETTINGS_SELECTED : TextStyle.WINDOW);
this.settingLabels[s] = addTextObject(this.scene, 8, 28 + s * 16, settingName, TextStyle.SETTINGS_LABEL);
this.settingLabels[s].setOrigin(0, 0);
this.optionsContainer.add(this.settingLabels[s]);
this.optionValueLabels.push(setting.options.map((option, o) => {
const valueLabel = addTextObject(this.scene, 0, 0, option, setting.default === o ? TextStyle.SETTINGS_SELECTED : TextStyle.WINDOW);
valueLabel.setOrigin(0, 0);
optionsContainer.add(valueLabel);
this.optionsContainer.add(valueLabel);
//if a setting has 2 options, valueLabels will be an array of 2 elements
valueLabels.push(valueLabel);
}
// Collect all option labels for this setting into the main array.
optionValueLabels.push(valueLabels);
return valueLabel;
}));
// Calculate the total width of all option labels within a specific setting
// This is achieved by summing the width of each option label
const totalWidth = optionValueLabels[s].map((o) => (o as Phaser.GameObjects.Text).width).reduce((total, width) => total += width, 0);
const totalWidth = this.optionValueLabels[s].map(o => o.width).reduce((total, width) => total += width, 0);
// Define the minimum width for a label, ensuring it's at least 78 pixels wide or the width of the setting label plus some padding
const labelWidth = Math.max(130, settingLabels[s].displayWidth + 8);
const labelWidth = Math.max(78, this.settingLabels[s].displayWidth + 8);
// Calculate the total available space for placing option labels next to their setting label
// We reserve space for the setting label and then distribute the remaining space evenly
const totalSpace = (300 - labelWidth) - totalWidth / 6;
// Calculate the spacing between options based on the available space divided by the number of gaps between labels
const optionSpacing = Math.floor(totalSpace / (optionValueLabels[s].length - 1));
const optionSpacing = Math.floor(totalSpace / (this.optionValueLabels[s].length - 1));
// Initialize xOffset to zero, which will be used to position each option label horizontally
let xOffset = 0;
// Start positioning each option label one by one
for (const value of optionValueLabels[s]) {
// Set the option label's position right next to the setting label, adjusted by xOffset
(value as Phaser.GameObjects.Text).setPositionRelative(settingLabels[s], labelWidth + xOffset, 0);
// Move the xOffset to the right for the next label, ensuring each label is spaced evenly
xOffset += (value as Phaser.GameObjects.Text).width / 6 + optionSpacing;
for (const value of this.optionValueLabels[s]) {
value.setPositionRelative(this.settingLabels[s], labelWidth + xOffset, 0);
xOffset += value.width / 6 + optionSpacing;
}
});
// Assigning the newly created components to the layout map under the specific gamepad type.
this.layout[config.padType].optionsContainer = optionsContainer; // Container for this pad's options.
this.layout[config.padType].inputsIcons = inputsIcons; // Icons for each input specific to this pad.
this.layout[config.padType].settingLabels = settingLabels; // Text labels for each setting available on this pad.
this.layout[config.padType].optionValueLabels = optionValueLabels; // Labels for values corresponding to each setting.
this.layout[config.padType].optionCursors = optionCursors; // Cursors to navigate through the options.
this.layout[config.padType].keys = specificBindingKeys; // Keys that identify each setting specifically bound to this pad.
this.layout[config.padType].bindingSettings = bindingSettings; // Settings that define how the keys are bound.
this.optionCursors = this.settings.map(setting => setting.default);
this.settingsContainer.add(this.optionsBg);
this.settingsContainer.add(this.navigationContainer);
this.settingsContainer.add(actionsBg);
this.settingsContainer.add(this.optionsContainer);
this.settingsContainer.add(iconAction);
this.settingsContainer.add(iconCancel);
this.settingsContainer.add(actionText);
this.settingsContainer.add(cancelText);
// Add the options container to the overall settings container to be displayed in the UI.
this.settingsContainer.add(optionsContainer);
}
// Add the settings container to the UI.
ui.add(this.settingsContainer);
// Initially hide the settings container until needed (e.g., when a gamepad is connected or a button is pressed).
this.setCursor(0);
this.setScrollCursor(0);
this.settingsContainer.setVisible(false);
}
/**
* Update the bindings for the current active device configuration.
*/
updateBindings(): void {
// Hide the options container for all layouts to reset the UI visibility.
Object.keys(this.layout).forEach((key) => this.layout[key].optionsContainer.setVisible(false));
// Fetch the active gamepad configuration from the input controller.
const activeConfig = this.getActiveConfig();
// Set the UI layout for the active configuration. If unsuccessful, exit the function early.
if (!this.setLayout(activeConfig)) {
return;
}
// Retrieve the gamepad settings from local storage or use an empty object if none exist.
const settings: object = this.getLocalStorageSetting();
// Update the cursor for each key based on the stored settings or default cursors.
this.keys.forEach((key, index) => {
this.setOptionCursor(index, settings.hasOwnProperty(key as string) ? settings[key as string] : this.optionCursors[index]);
});
// If the active configuration has no custom bindings set, exit the function early.
// by default, if custom does not exists, a default is assigned to it
// it only means the gamepad is not yet initalized
if (!activeConfig.custom) {
return;
}
// For each element in the binding settings, update the icon according to the current assignment.
for (const elm of this.bindingSettings) {
const icon = getIconWithSettingName(activeConfig, elm);
if (icon) {
this.inputsIcons[elm as string].setFrame(icon);
this.inputsIcons[elm as string].alpha = 1;
} else {
this.inputsIcons[elm as string].alpha = 0;
}
}
// Set the cursor and scroll cursor to their initial positions.
this.setCursor(this.cursor);
this.setScrollCursor(this.scrollCursor);
}
updateNavigationDisplay() {
const specialIcons = {
"BUTTON_HOME": "HOME.png",
"BUTTON_DELETE": "DEL.png",
};
for (const settingName of Object.keys(this.navigationIcons)) {
if (Object.keys(specialIcons).includes(settingName)) {
if (settingName === "BUTTON_HOME") {
this.navigationIcons[settingName].setTexture("keyboard");
this.navigationIcons[settingName].setFrame(specialIcons[settingName]);
this.navigationIcons[settingName].setFrame("HOME.png");
this.navigationIcons[settingName].alpha = 1;
continue;
}
@ -324,6 +165,7 @@ export default abstract class AbstractSettingsUiUiHandler extends UiHandler {
this.navigationIcons[settingName].alpha = 0;
}
}
NavigationManager.getInstance().updateIcons();
}
/**
@ -334,102 +176,50 @@ export default abstract class AbstractSettingsUiUiHandler extends UiHandler {
*/
show(args: any[]): boolean {
super.show(args);
this.updateNavigationDisplay();
NavigationManager.getInstance().updateIcons();
// Update the bindings for the current active gamepad configuration.
this.updateBindings();
// Make the settings container visible to the user.
this.settingsContainer.setVisible(true);
// Reset the scroll cursor to the top of the settings container.
this.resetScroll();
const settings: object = localStorage.hasOwnProperty(this.localStorageKey) ? JSON.parse(localStorage.getItem(this.localStorageKey)) : {};
this.settings.forEach((setting, s) => this.setOptionCursor(s, settings.hasOwnProperty(setting.key) ? settings[setting.key] : this.settings[s].default));
this.settingsContainer.setVisible(true);
this.setCursor(0);
// Move the settings container to the end of the UI stack to ensure it is displayed on top.
this.getUi().moveTo(this.settingsContainer, this.getUi().length - 1);
// Hide any tooltips that might be visible before showing the settings container.
this.getUi().hideTooltip();
// Return true to indicate the UI was successfully shown.
return true;
}
/**
* Set the UI layout for the active device configuration.
* Processes input from a specified button.
* This method handles navigation through a UI menu, including movement through menu items
* and handling special actions like cancellation. Each button press may adjust the cursor
* position or the menu scroll, and plays a sound effect if the action was successful.
*
* @param activeConfig - The active device configuration.
* @returns `true` if the layout was successfully applied, otherwise `false`.
*/
setLayout(activeConfig: InterfaceConfig): boolean {
// Check if there is no active configuration (e.g., no gamepad connected).
if (!activeConfig) {
// Retrieve the layout for when no gamepads are connected.
const layout = this.layout["noGamepads"];
// Make the options container visible to show message.
layout.optionsContainer.setVisible(true);
// Return false indicating the layout application was not successful due to lack of gamepad.
return false;
}
// Extract the type of the gamepad from the active configuration.
const configType = activeConfig.padType;
// Retrieve the layout settings based on the type of the gamepad.
const layout = this.layout[configType];
// Update the main controller with configuration details from the selected layout.
this.keys = layout.keys;
this.optionsContainer = layout.optionsContainer;
this.optionsContainer.setVisible(true);
this.settingLabels = layout.settingLabels;
this.optionValueLabels = layout.optionValueLabels;
this.optionCursors = layout.optionCursors;
this.inputsIcons = layout.inputsIcons;
this.bindingSettings = layout.bindingSettings;
// Return true indicating the layout was successfully applied.
return true;
}
/**
* Process the input for the given button.
*
* @param button - The button to process.
* @returns `true` if the input was processed successfully.
* @param button - The button pressed by the user.
* @returns `true` if the action associated with the button was successfully processed, `false` otherwise.
*/
processInput(button: Button): boolean {
const ui = this.getUi();
// Defines the maximum number of rows that can be displayed on the screen.
let success = false;
this.updateNavigationDisplay();
// Handle the input based on the button pressed.
let success = false;
if (button === Button.CANCEL) {
// Handle cancel button press, reverting UI mode to previous state.
success = true;
NavigationManager.getInstance().reset();
// Reverts UI to its previous state on cancel.
this.scene.ui.revertMode();
} else {
const cursor = this.cursor + this.scrollCursor; // Calculate the absolute cursor position.
const setting = this.settingDevice[Object.keys(this.settingDevice)[cursor]];
const cursor = this.cursor + this.scrollCursor;
switch (button) {
case Button.ACTION:
if (!this.optionCursors || !this.optionValueLabels) {
return;
}
if (this.settingBlacklisted.includes(setting) || !setting.includes("BUTTON_")) {
success = false;
} else {
success = this.setSetting(this.scene, setting, 1);
}
break;
case Button.UP: // Move up in the menu.
if (!this.optionValueLabels) {
return false;
}
if (cursor) { // If not at the top, move the cursor up.
case Button.UP:
if (cursor) {
if (this.cursor) {
success = this.setCursor(this.cursor - 1);
} else {// If at the top of the visible items, scroll up.
} else {
success = this.setScrollCursor(this.scrollCursor - 1);
}
} else {
@ -441,12 +231,9 @@ export default abstract class AbstractSettingsUiUiHandler extends UiHandler {
success = successA && successB; // success is just there to play the little validation sound effect
}
break;
case Button.DOWN: // Move down in the menu.
if (!this.optionValueLabels) {
return false;
}
case Button.DOWN:
if (cursor < this.optionValueLabels.length - 1) {
if (this.cursor < this.rowsToDisplay - 1) {
if (this.cursor < this.rowsToDisplay - 1) {// if the visual cursor is in the frame of 0 to 8
success = this.setCursor(this.cursor + 1);
} else if (this.scrollCursor < this.optionValueLabels.length - this.rowsToDisplay) {
success = this.setScrollCursor(this.scrollCursor + 1);
@ -460,23 +247,14 @@ export default abstract class AbstractSettingsUiUiHandler extends UiHandler {
success = successA && successB; // Indicates a successful cursor and scroll adjustment.
}
break;
case Button.LEFT: // Move selection left within the current option set.
if (!this.optionCursors || !this.optionValueLabels) {
return;
}
if (this.settingBlacklisted.includes(setting) || setting.includes("BUTTON_")) {
success = false;
} else if (this.optionCursors[cursor]) {
case Button.LEFT:
if (this.optionCursors[cursor]) {// Moves the option cursor left, if possible.
success = this.setOptionCursor(cursor, this.optionCursors[cursor] - 1, true);
}
break;
case Button.RIGHT: // Move selection right within the current option set.
if (!this.optionCursors || !this.optionValueLabels) {
return;
}
if (this.settingBlacklisted.includes(setting) || setting.includes("BUTTON_")) {
success = false;
} else if (this.optionCursors[cursor] < this.optionValueLabels[cursor].length - 1) {
case Button.RIGHT:
// Moves the option cursor right, if possible.
if (this.optionCursors[cursor] < this.optionValueLabels[cursor].length - 1) {
success = this.setOptionCursor(cursor, this.optionCursors[cursor] + 1, true);
}
break;
@ -487,21 +265,12 @@ export default abstract class AbstractSettingsUiUiHandler extends UiHandler {
}
}
// If a change occurred, play the selection sound.
// Plays a select sound effect if an action was successfully processed.
if (success) {
ui.playSelect();
}
return success; // Return whether the input resulted in a successful action.
}
resetScroll() {
this.cursorObj?.destroy();
this.cursorObj = null;
this.cursor = null;
this.setCursor(0);
this.setScrollCursor(0);
this.updateSettingsScroll();
return success;
}
/**
@ -512,46 +281,16 @@ export default abstract class AbstractSettingsUiUiHandler extends UiHandler {
*/
setCursor(cursor: integer): boolean {
const ret = super.setCursor(cursor);
// If the optionsContainer is not initialized, return the result from the parent class directly.
if (!this.optionsContainer) {
return ret;
}
// Check if the cursor object exists, if not, create it.
if (!this.cursorObj) {
this.cursorObj = this.scene.add.nineslice(0, 0, "summary_moves_cursor", null, (this.scene.game.canvas.width / 6) - 10, 16, 1, 1, 1, 1);
this.cursorObj.setOrigin(0, 0); // Set the origin to the top-left corner.
this.optionsContainer.add(this.cursorObj); // Add the cursor to the options container.
this.cursorObj.setOrigin(0, 0);
this.optionsContainer.add(this.cursorObj);
}
// Update the position of the cursor object relative to the options background based on the current cursor and scroll positions.
this.cursorObj.setPositionRelative(this.optionsBg, 4, 4 + (this.cursor + this.scrollCursor) * 16);
return ret; // Return the result from the parent class's setCursor method.
}
/**
* Set the scroll cursor to the specified position.
*
* @param scrollCursor - The scroll cursor position to set.
* @returns `true` if the scroll cursor was set successfully.
*/
setScrollCursor(scrollCursor: integer): boolean {
// Check if the new scroll position is the same as the current one; if so, do not update.
if (scrollCursor === this.scrollCursor) {
return false;
}
// Update the internal scroll cursor state
this.scrollCursor = scrollCursor;
// Apply the new scroll position to the settings UI.
this.updateSettingsScroll();
// Reset the cursor to its current position to adjust its visibility after scrolling.
this.setCursor(this.cursor);
return true; // Return true to indicate the scroll cursor was successfully updated.
return ret;
}
/**
@ -563,54 +302,63 @@ export default abstract class AbstractSettingsUiUiHandler extends UiHandler {
* @returns `true` if the option cursor was set successfully.
*/
setOptionCursor(settingIndex: integer, cursor: integer, save?: boolean): boolean {
// Retrieve the specific setting using the settingIndex from the settingDevice enumeration.
const setting = this.settingDevice[Object.keys(this.settingDevice)[settingIndex]];
const setting = this.settings[settingIndex];
if (setting.key === SettingKeys.Touch_Controls && cursor && hasTouchscreen() && isMobile()) {
this.getUi().playError();
return false;
}
// Get the current cursor position for this setting.
const lastCursor = this.optionCursors[settingIndex];
// Check if the setting is not part of the bindings (i.e., it's a regular setting).
if (!this.bindingSettings.includes(setting) && !setting.includes("BUTTON_")) {
// Get the label of the last selected option and revert its color to the default.
const lastValueLabel = this.optionValueLabels[settingIndex][lastCursor];
lastValueLabel.setColor(this.getTextColor(TextStyle.WINDOW));
lastValueLabel.setShadowColor(this.getTextColor(TextStyle.WINDOW, true));
// Update the cursor for the setting to the new position.
this.optionCursors[settingIndex] = cursor;
// Change the color of the new selected option to indicate it's selected.
const newValueLabel = this.optionValueLabels[settingIndex][cursor];
newValueLabel.setColor(this.getTextColor(TextStyle.SETTINGS_SELECTED));
newValueLabel.setShadowColor(this.getTextColor(TextStyle.SETTINGS_SELECTED, true));
}
// If the save flag is set, save the setting to local storage
if (save) {
this.saveSettingToLocalStorage(setting, cursor);
this.scene.gameData.saveSetting(setting.key, cursor);
if (this.reloadSettings.includes(setting)) {
this.reloadRequired = true;
}
}
return true; // Return true to indicate the cursor was successfully updated.
return true;
}
/**
* Set the scroll cursor to the specified position.
*
* @param scrollCursor - The scroll cursor position to set.
* @returns `true` if the scroll cursor was set successfully.
*/
setScrollCursor(scrollCursor: integer): boolean {
if (scrollCursor === this.scrollCursor) {
return false;
}
this.scrollCursor = scrollCursor;
this.updateSettingsScroll();
this.setCursor(this.cursor);
return true;
}
/**
* Update the scroll position of the settings UI.
*/
updateSettingsScroll(): void {
// Return immediately if the options container is not initialized.
if (!this.optionsContainer) {
return;
}
// Set the vertical position of the options container based on the current scroll cursor, multiplying by the item height.
this.optionsContainer.setY(-16 * this.scrollCursor);
// Iterate over all setting labels to update their visibility.
for (let s = 0; s < this.settingLabels.length; s++) {
// Determine if the current setting should be visible based on the scroll position.
const visible = s >= this.scrollCursor && s < this.scrollCursor + this.rowsToDisplay;
// Set the visibility of the setting label and its corresponding options.
this.settingLabels[s].setVisible(visible);
for (const option of this.optionValueLabels[s]) {
option.setVisible(visible);
@ -621,27 +369,23 @@ export default abstract class AbstractSettingsUiUiHandler extends UiHandler {
/**
* Clear the UI elements and state.
*/
clear(): void {
clear() {
super.clear();
// Hide the settings container to remove it from the view.
this.settingsContainer.setVisible(false);
// Remove the cursor from the UI.
this.eraseCursor();
if (this.reloadRequired) {
this.reloadRequired = false;
this.scene.reset(true, false, true);
}
}
/**
* Erase the cursor from the UI.
*/
eraseCursor(): void {
// Check if a cursor object exists.
eraseCursor() {
if (this.cursorObj) {
this.cursorObj.destroy();
} // Destroy the cursor object to clean up resources.
// Set the cursor object reference to null to fully dereference it.
}
this.cursorObj = null;
}
}

View File

@ -1,10 +1,13 @@
import BattleScene from "#app/battle-scene";
import {Mode} from "#app/ui/ui";
import {InputsIcons} from "#app/ui/settings/abstract-settings-ui-handler";
import {InputsIcons} from "#app/ui/settings/abstract-control-settings-ui-handler.js";
import {addTextObject, setTextStyle, TextStyle} from "#app/ui/text";
import {addWindow} from "#app/ui/ui-theme";
import {Button} from "#app/enums/buttons";
const LEFT = "LEFT";
const RIGHT = "RIGHT";
/**
* Manages navigation and menus tabs within the setting menu.
*/
@ -24,14 +27,16 @@ export class NavigationManager {
constructor() {
this.modes = [
Mode.SETTINGS,
Mode.SETTINGS_ACCESSIBILITY,
Mode.SETTINGS_GAMEPAD,
Mode.SETTINGS_KEYBOARD,
];
this.labels = ["General", "Gamepad", "Keyboard"];
this.labels = ["General", "Accessibility", "Gamepad", "Keyboard"];
}
public reset() {
this.selectedMode = Mode.SETTINGS;
this.updateNavigationMenus();
}
/**
@ -46,32 +51,20 @@ export class NavigationManager {
}
/**
* Navigates to the previous mode in the modes array.
* @param scene The current BattleScene instance.
* Navigates modes based on given direction
* @param scene The current BattleScene instance
* @param direction LEFT or RIGHT
*/
public navigateLeft(scene) {
public navigate(scene, direction) {
const pos = this.modes.indexOf(this.selectedMode);
const maxPos = this.modes.length - 1;
if (pos === 0) {
const increment = direction === LEFT ? -1 : 1;
if (pos === 0 && direction === LEFT) {
this.selectedMode = this.modes[maxPos];
} else {
this.selectedMode = this.modes[pos - 1];
}
scene.ui.setMode(this.selectedMode);
this.updateNavigationMenus();
}
/**
* Navigates to the next mode in the modes array.
* @param scene The current BattleScene instance.
*/
public navigateRight(scene) {
const pos = this.modes.indexOf(this.selectedMode);
const maxPos = this.modes.length - 1;
if (pos === maxPos) {
} else if (pos === maxPos && direction === RIGHT) {
this.selectedMode = this.modes[0];
} else {
this.selectedMode = this.modes[pos + 1];
this.selectedMode = this.modes[pos + increment];
}
scene.ui.setMode(this.selectedMode);
this.updateNavigationMenus();
@ -204,13 +197,11 @@ export default class NavigationMenu extends Phaser.GameObjects.Container {
const navigationManager = NavigationManager.getInstance();
switch (button) {
case Button.CYCLE_FORM:
navigationManager.navigateLeft(this.scene);
navigationManager.navigate(this.scene, LEFT);
return true;
break;
case Button.CYCLE_SHINY:
navigationManager.navigateRight(this.scene);
navigationManager.navigate(this.scene, RIGHT);
return true;
break;
}
return false;
}

View File

@ -0,0 +1,20 @@
import BattleScene from "../../battle-scene";
import { Mode } from "../ui";
"#app/inputs-controller.js";
import AbstractSettingsUiHandler from "./abstract-settings-ui-handler";
import { Setting, SettingType } from "#app/system/settings/settings";
export default class SettingsAccessibilityUiHandler extends AbstractSettingsUiHandler {
/**
* Creates an instance of SettingsGamepadUiHandler.
*
* @param scene - The BattleScene instance.
* @param mode - The UI mode, optional.
*/
constructor(scene: BattleScene, mode?: Mode) {
super(scene, mode);
this.title = "Accessibility";
this.settings = Setting.filter(s => s.type === SettingType.ACCESSIBILITY);
this.localStorageKey = "settings";
}
}

View File

@ -7,22 +7,22 @@ import {
settingGamepadBlackList,
settingGamepadDefaults,
settingGamepadOptions
} from "../../system/settings-gamepad";
} from "../../system/settings/settings-gamepad";
import pad_xbox360 from "#app/configs/inputs/pad_xbox360";
import pad_dualshock from "#app/configs/inputs/pad_dualshock";
import pad_unlicensedSNES from "#app/configs/inputs/pad_unlicensedSNES";
import {InterfaceConfig} from "#app/inputs-controller";
import AbstractSettingsUiUiHandler from "#app/ui/settings/abstract-settings-ui-handler";
import AbstractControlSettingsUiHandler from "#app/ui/settings/abstract-control-settings-ui-handler.js";
import {Device} from "#app/enums/devices";
import {truncateString} from "#app/utils";
/**
* Class representing the settings UI handler for gamepads.
*
* @extends AbstractSettingsUiUiHandler
* @extends AbstractControlSettingsUiHandler
*/
export default class SettingsGamepadUiHandler extends AbstractSettingsUiUiHandler {
export default class SettingsGamepadUiHandler extends AbstractControlSettingsUiHandler {
/**
* Creates an instance of SettingsGamepadUiHandler.
@ -33,18 +33,17 @@ export default class SettingsGamepadUiHandler extends AbstractSettingsUiUiHandle
constructor(scene: BattleScene, mode?: Mode) {
super(scene, mode);
this.titleSelected = "Gamepad";
this.settingDevice = SettingGamepad;
this.setting = SettingGamepad;
this.settingDeviceDefaults = settingGamepadDefaults;
this.settingDeviceOptions = settingGamepadOptions;
this.configs = [pad_xbox360, pad_dualshock, pad_unlicensedSNES];
this.commonSettingsCount = 2;
this.localStoragePropertyName = "settingsGamepad";
this.settingBlacklisted = settingGamepadBlackList;
this.device = Device.GAMEPAD;
}
setSetting(scene: BattleScene, setting, value: integer): boolean {
return setSettingGamepad(scene, setting, value);
}
setSetting = setSettingGamepad;
/**
* Setup UI elements.
@ -65,26 +64,6 @@ export default class SettingsGamepadUiHandler extends AbstractSettingsUiUiHandle
this.layout["noGamepads"].label = label;
}
/**
* Get the active configuration.
*
* @returns The active gamepad configuration.
*/
getActiveConfig(): InterfaceConfig {
return this.scene.inputController.getActiveConfig(Device.GAMEPAD);
}
/**
* Get the gamepad settings from local storage.
*
* @returns The gamepad settings from local storage.
*/
getLocalStorageSetting(): object {
// Retrieve the gamepad settings from local storage or use an empty object if none exist.
const settings: object = localStorage.hasOwnProperty("settingsGamepad") ? JSON.parse(localStorage.getItem("settingsGamepad")) : {};
return settings;
}
/**
* Set the layout for the active configuration.
*
@ -105,27 +84,6 @@ export default class SettingsGamepadUiHandler extends AbstractSettingsUiUiHandle
return super.setLayout(activeConfig);
}
/**
* Navigate to the left menu tab.
*
* @returns `true` indicating the navigation was successful.
*/
navigateMenuLeft(): boolean {
this.scene.ui.setMode(Mode.SETTINGS);
return true;
}
/**
* Navigate to the right menu tab.
*
* @returns `true` indicating the navigation was successful.
*/
navigateMenuRight(): boolean {
this.scene.ui.setMode(Mode.SETTINGS_KEYBOARD);
return true;
}
/**
* Update the display of the chosen gamepad.
*/
@ -135,11 +93,11 @@ export default class SettingsGamepadUiHandler extends AbstractSettingsUiUiHandle
this.resetScroll();
// Iterate over the keys in the settingDevice enumeration.
for (const [index, key] of Object.keys(this.settingDevice).entries()) {
const setting = this.settingDevice[key]; // Get the actual setting value using the key.
for (const [index, key] of Object.keys(this.setting).entries()) {
const setting = this.setting[key]; // Get the actual setting value using the key.
// Check if the current setting corresponds to the controller setting.
if (setting === this.settingDevice.Controller) {
if (setting === this.setting.Controller) {
// Iterate over all layouts excluding the 'noGamepads' special case.
for (const _key of Object.keys(this.layout)) {
if (_key === "noGamepads") {
@ -157,12 +115,12 @@ export default class SettingsGamepadUiHandler extends AbstractSettingsUiUiHandle
/**
* Save the setting to local storage.
*
* @param setting - The setting to save.
* @param settingName - The setting to save.
* @param cursor - The cursor position to save.
*/
saveSettingToLocalStorage(setting, cursor): void {
if (this.settingDevice[setting] !== this.settingDevice.Controller) {
this.scene.gameData.saveGamepadSetting(setting, cursor);
saveSettingToLocalStorage(settingName, cursor): void {
if (this.setting[settingName] !== this.setting.Controller) {
this.scene.gameData.saveControlSetting(this.device, this.localStoragePropertyName, settingName, this.settingDeviceDefaults, cursor);
}
}
}

View File

@ -7,9 +7,9 @@ import {
settingKeyboardBlackList,
settingKeyboardDefaults,
settingKeyboardOptions
} from "#app/system/settings-keyboard";
} from "#app/system/settings/settings-keyboard";
import {reverseValueToKeySetting, truncateString} from "#app/utils";
import AbstractSettingsUiUiHandler from "#app/ui/settings/abstract-settings-ui-handler";
import AbstractControlSettingsUiHandler from "#app/ui/settings/abstract-control-settings-ui-handler.js";
import {InterfaceConfig} from "#app/inputs-controller";
import {addTextObject, TextStyle} from "#app/ui/text";
import {deleteBind} from "#app/configs/inputs/configHandler";
@ -19,9 +19,9 @@ import {NavigationManager} from "#app/ui/settings/navigationMenu";
/**
* Class representing the settings UI handler for keyboards.
*
* @extends AbstractSettingsUiUiHandler
* @extends AbstractControlSettingsUiHandler
*/
export default class SettingsKeyboardUiHandler extends AbstractSettingsUiUiHandler {
export default class SettingsKeyboardUiHandler extends AbstractControlSettingsUiHandler {
/**
* Creates an instance of SettingsKeyboardUiHandler.
*
@ -31,7 +31,7 @@ export default class SettingsKeyboardUiHandler extends AbstractSettingsUiUiHandl
constructor(scene: BattleScene, mode?: Mode) {
super(scene, mode);
this.titleSelected = "Keyboard";
this.settingDevice = SettingKeyboard;
this.setting = SettingKeyboard;
this.settingDeviceDefaults = settingKeyboardDefaults;
this.settingDeviceOptions = settingKeyboardOptions;
this.configs = [cfg_keyboard_qwerty];
@ -39,6 +39,7 @@ export default class SettingsKeyboardUiHandler extends AbstractSettingsUiUiHandl
this.textureOverride = "keyboard";
this.localStoragePropertyName = "settingsKeyboard";
this.settingBlacklisted = settingKeyboardBlackList;
this.device = Device.KEYBOARD;
const deleteEvent = scene.input.keyboard.addKey(Phaser.Input.Keyboard.KeyCodes.DELETE);
const restoreDefaultEvent = scene.input.keyboard.addKey(Phaser.Input.Keyboard.KeyCodes.HOME);
@ -46,9 +47,7 @@ export default class SettingsKeyboardUiHandler extends AbstractSettingsUiUiHandl
restoreDefaultEvent.on("up", this.onHomeDown, this);
}
setSetting(scene: BattleScene, setting, value: integer): boolean {
return setSettingKeyboard(scene, setting, value);
}
setSetting = setSettingKeyboard;
/**
* Setup UI elements.
@ -114,26 +113,6 @@ export default class SettingsKeyboardUiHandler extends AbstractSettingsUiUiHandl
}
}
/**
* Get the active configuration.
*
* @returns The active keyboard configuration.
*/
getActiveConfig(): InterfaceConfig {
return this.scene.inputController.getActiveConfig(Device.KEYBOARD);
}
/**
* Get the keyboard settings from local storage.
*
* @returns The keyboard settings from local storage.
*/
getLocalStorageSetting(): object {
// Retrieve the gamepad settings from local storage or use an empty object if none exist.
const settings: object = localStorage.hasOwnProperty("settingsKeyboard") ? JSON.parse(localStorage.getItem("settingsKeyboard")) : {};
return settings;
}
/**
* Set the layout for the active configuration.
*
@ -154,26 +133,6 @@ export default class SettingsKeyboardUiHandler extends AbstractSettingsUiUiHandl
return super.setLayout(activeConfig);
}
/**
* Navigate to the left menu tab.
*
* @returns `true` indicating the navigation was successful.
*/
navigateMenuLeft(): boolean {
this.scene.ui.setMode(Mode.SETTINGS_GAMEPAD);
return true;
}
/**
* Navigate to the right menu tab.
*
* @returns `true` indicating the navigation was successful.
*/
navigateMenuRight(): boolean {
this.scene.ui.setMode(Mode.SETTINGS);
return true;
}
/**
* Update the display of the chosen keyboard layout.
*/
@ -182,11 +141,11 @@ export default class SettingsKeyboardUiHandler extends AbstractSettingsUiUiHandl
this.updateBindings();
// Iterate over the keys in the settingDevice enumeration.
for (const [index, key] of Object.keys(this.settingDevice).entries()) {
const setting = this.settingDevice[key]; // Get the actual setting value using the key.
for (const [index, key] of Object.keys(this.setting).entries()) {
const setting = this.setting[key]; // Get the actual setting value using the key.
// Check if the current setting corresponds to the layout setting.
if (setting === this.settingDevice.Default_Layout) {
if (setting === this.setting.Default_Layout) {
// Iterate over all layouts excluding the 'noGamepads' special case.
for (const _key of Object.keys(this.layout)) {
if (_key === "noKeyboard") {
@ -217,8 +176,8 @@ export default class SettingsKeyboardUiHandler extends AbstractSettingsUiUiHandl
* @param cursor - The cursor position to save.
*/
saveSettingToLocalStorage(settingName, cursor): void {
if (this.settingDevice[settingName] !== this.settingDevice.Default_Layout) {
this.scene.gameData.saveKeyboardSetting(settingName, cursor);
if (this.setting[settingName] !== this.setting.Default_Layout) {
this.scene.gameData.saveControlSetting(this.device, this.localStoragePropertyName, settingName, this.settingDeviceDefaults, cursor);
}
}
}

View File

@ -1,345 +1,19 @@
import BattleScene from "../../battle-scene";
import {Setting, reloadSettings, settingDefaults, settingOptions} from "../../system/settings";
import { hasTouchscreen, isMobile } from "../../touch-controls";
import { TextStyle, addTextObject } from "../text";
import {Setting, SettingType} from "../../system/settings/settings";
import { Mode } from "../ui";
import UiHandler from "../ui-handler";
import { addWindow } from "../ui-theme";
import {Button} from "../../enums/buttons";
import {InputsIcons} from "#app/ui/settings/abstract-settings-ui-handler";
import NavigationMenu, {NavigationManager} from "#app/ui/settings/navigationMenu";
export default class SettingsUiHandler extends UiHandler {
private settingsContainer: Phaser.GameObjects.Container;
private optionsContainer: Phaser.GameObjects.Container;
private navigationContainer: NavigationMenu;
private scrollCursor: integer;
private optionsBg: Phaser.GameObjects.NineSlice;
private optionCursors: integer[];
private settingLabels: Phaser.GameObjects.Text[];
private optionValueLabels: Phaser.GameObjects.Text[][];
protected navigationIcons: InputsIcons;
private cursorObj: Phaser.GameObjects.NineSlice;
private reloadRequired: boolean;
private reloadI18n: boolean;
private rowsToDisplay: number;
import AbstractSettingsUiHandler from "./abstract-settings-ui-handler";
export default class SettingsUiHandler extends AbstractSettingsUiHandler {
/**
* Creates an instance of SettingsGamepadUiHandler.
*
* @param scene - The BattleScene instance.
* @param mode - The UI mode, optional.
*/
constructor(scene: BattleScene, mode?: Mode) {
super(scene, mode);
this.reloadRequired = false;
this.reloadI18n = false;
this.rowsToDisplay = 8;
}
setup() {
const ui = this.getUi();
this.settingsContainer = this.scene.add.container(1, -(this.scene.game.canvas.height / 6) + 1);
this.settingsContainer.setInteractive(new Phaser.Geom.Rectangle(0, 0, this.scene.game.canvas.width / 6, this.scene.game.canvas.height / 6 - 20), Phaser.Geom.Rectangle.Contains);
this.navigationIcons = {};
this.navigationContainer = new NavigationMenu(this.scene, 0, 0);
this.optionsBg = addWindow(this.scene, 0, this.navigationContainer.height, (this.scene.game.canvas.width / 6) - 2, (this.scene.game.canvas.height / 6) - 16 - this.navigationContainer.height - 2);
this.optionsBg.setOrigin(0, 0);
const actionsBg = addWindow(this.scene, 0, (this.scene.game.canvas.height / 6) - this.navigationContainer.height, (this.scene.game.canvas.width / 6) - 2, 22);
actionsBg.setOrigin(0, 0);
const iconAction = this.scene.add.sprite(0, 0, "keyboard");
iconAction.setOrigin(0, -0.1);
iconAction.setPositionRelative(actionsBg, this.navigationContainer.width - 32, 4);
this.navigationIcons["BUTTON_ACTION"] = iconAction;
const actionText = addTextObject(this.scene, 0, 0, "Action", TextStyle.SETTINGS_LABEL);
actionText.setOrigin(0, 0.15);
actionText.setPositionRelative(iconAction, -actionText.width/6-2, 0);
const iconCancel = this.scene.add.sprite(0, 0, "keyboard");
iconCancel.setOrigin(0, -0.1);
iconCancel.setPositionRelative(actionsBg, this.navigationContainer.width - 100, 4);
this.navigationIcons["BUTTON_CANCEL"] = iconCancel;
const cancelText = addTextObject(this.scene, 0, 0, "Cancel", TextStyle.SETTINGS_LABEL);
cancelText.setOrigin(0, 0.15);
cancelText.setPositionRelative(iconCancel, -cancelText.width/6-2, 0);
this.optionsContainer = this.scene.add.container(0, 0);
this.settingLabels = [];
this.optionValueLabels = [];
Object.keys(Setting).forEach((setting, s) => {
let settingName = setting.replace(/\_/g, " ");
if (reloadSettings.includes(Setting[setting])) {
settingName += " (Requires Reload)";
}
this.settingLabels[s] = addTextObject(this.scene, 8, 28 + s * 16, settingName, TextStyle.SETTINGS_LABEL);
this.settingLabels[s].setOrigin(0, 0);
this.optionsContainer.add(this.settingLabels[s]);
this.optionValueLabels.push(settingOptions[Setting[setting]].map((option, o) => {
const valueLabel = addTextObject(this.scene, 0, 0, option, settingDefaults[Setting[setting]] === o ? TextStyle.SETTINGS_SELECTED : TextStyle.WINDOW);
valueLabel.setOrigin(0, 0);
this.optionsContainer.add(valueLabel);
return valueLabel;
}));
const totalWidth = this.optionValueLabels[s].map(o => o.width).reduce((total, width) => total += width, 0);
const labelWidth = Math.max(78, this.settingLabels[s].displayWidth + 8);
const totalSpace = (300 - labelWidth) - totalWidth / 6;
const optionSpacing = Math.floor(totalSpace / (this.optionValueLabels[s].length - 1));
let xOffset = 0;
for (const value of this.optionValueLabels[s]) {
value.setPositionRelative(this.settingLabels[s], labelWidth + xOffset, 0);
xOffset += value.width / 6 + optionSpacing;
}
});
this.optionCursors = Object.values(settingDefaults);
this.settingsContainer.add(this.optionsBg);
this.settingsContainer.add(this.navigationContainer);
this.settingsContainer.add(actionsBg);
this.settingsContainer.add(this.optionsContainer);
this.settingsContainer.add(iconAction);
this.settingsContainer.add(iconCancel);
this.settingsContainer.add(actionText);
this.settingsContainer.add(cancelText);
ui.add(this.settingsContainer);
this.setCursor(0);
this.setScrollCursor(0);
this.settingsContainer.setVisible(false);
}
updateBindings(): void {
for (const settingName of Object.keys(this.navigationIcons)) {
if (settingName === "BUTTON_HOME") {
this.navigationIcons[settingName].setTexture("keyboard");
this.navigationIcons[settingName].setFrame("HOME.png");
this.navigationIcons[settingName].alpha = 1;
continue;
}
const icon = this.scene.inputController?.getIconForLatestInputRecorded(settingName);
if (icon) {
const type = this.scene.inputController?.getLastSourceType();
this.navigationIcons[settingName].setTexture(type);
this.navigationIcons[settingName].setFrame(icon);
this.navigationIcons[settingName].alpha = 1;
} else {
this.navigationIcons[settingName].alpha = 0;
}
}
NavigationManager.getInstance().updateIcons();
}
show(args: any[]): boolean {
super.show(args);
this.updateBindings();
const settings: object = localStorage.hasOwnProperty("settings") ? JSON.parse(localStorage.getItem("settings")) : {};
Object.keys(settingDefaults).forEach((setting, s) => this.setOptionCursor(s, settings.hasOwnProperty(setting) ? settings[setting] : settingDefaults[setting]));
this.settingsContainer.setVisible(true);
this.setCursor(0);
this.getUi().moveTo(this.settingsContainer, this.getUi().length - 1);
this.getUi().hideTooltip();
return true;
}
/**
* Processes input from a specified button.
* This method handles navigation through a UI menu, including movement through menu items
* and handling special actions like cancellation. Each button press may adjust the cursor
* position or the menu scroll, and plays a sound effect if the action was successful.
*
* @param button - The button pressed by the user.
* @returns `true` if the action associated with the button was successfully processed, `false` otherwise.
*/
processInput(button: Button): boolean {
const ui = this.getUi();
// Defines the maximum number of rows that can be displayed on the screen.
let success = false;
if (button === Button.CANCEL) {
success = true;
NavigationManager.getInstance().reset();
// Reverts UI to its previous state on cancel.
this.scene.ui.revertMode();
} else {
const cursor = this.cursor + this.scrollCursor;
switch (button) {
case Button.UP:
if (cursor) {
if (this.cursor) {
success = this.setCursor(this.cursor - 1);
} else {
success = this.setScrollCursor(this.scrollCursor - 1);
}
} else {
// When at the top of the menu and pressing UP, move to the bottommost item.
// First, set the cursor to the last visible element, preparing for the scroll to the end.
const successA = this.setCursor(this.rowsToDisplay - 1);
// Then, adjust the scroll to display the bottommost elements of the menu.
const successB = this.setScrollCursor(this.optionValueLabels.length - this.rowsToDisplay);
success = successA && successB; // success is just there to play the little validation sound effect
}
break;
case Button.DOWN:
if (cursor < this.optionValueLabels.length - 1) {
if (this.cursor < this.rowsToDisplay - 1) {// if the visual cursor is in the frame of 0 to 8
success = this.setCursor(this.cursor + 1);
} else if (this.scrollCursor < this.optionValueLabels.length - this.rowsToDisplay) {
success = this.setScrollCursor(this.scrollCursor + 1);
}
} else {
// When at the bottom of the menu and pressing DOWN, move to the topmost item.
// First, set the cursor to the first visible element, resetting the scroll to the top.
const successA = this.setCursor(0);
// Then, reset the scroll to start from the first element of the menu.
const successB = this.setScrollCursor(0);
success = successA && successB; // Indicates a successful cursor and scroll adjustment.
}
break;
case Button.LEFT:
if (this.optionCursors[cursor]) {// Moves the option cursor left, if possible.
success = this.setOptionCursor(cursor, this.optionCursors[cursor] - 1, true);
}
break;
case Button.RIGHT:
// Moves the option cursor right, if possible.
if (this.optionCursors[cursor] < this.optionValueLabels[cursor].length - 1) {
success = this.setOptionCursor(cursor, this.optionCursors[cursor] + 1, true);
}
break;
case Button.CYCLE_FORM:
case Button.CYCLE_SHINY:
success = this.navigationContainer.navigate(button);
break;
}
}
// Plays a select sound effect if an action was successfully processed.
if (success) {
ui.playSelect();
}
return success;
}
setCursor(cursor: integer): boolean {
const ret = super.setCursor(cursor);
if (!this.cursorObj) {
this.cursorObj = this.scene.add.nineslice(0, 0, "summary_moves_cursor", null, (this.scene.game.canvas.width / 6) - 10, 16, 1, 1, 1, 1);
this.cursorObj.setOrigin(0, 0);
this.optionsContainer.add(this.cursorObj);
}
this.cursorObj.setPositionRelative(this.optionsBg, 4, 4 + (this.cursor + this.scrollCursor) * 16);
return ret;
}
setOptionCursor(settingIndex: integer, cursor: integer, save?: boolean): boolean {
const setting = Setting[Object.keys(Setting)[settingIndex]];
if (setting === Setting.Touch_Controls && cursor && hasTouchscreen() && isMobile()) {
this.getUi().playError();
return false;
}
const lastCursor = this.optionCursors[settingIndex];
const lastValueLabel = this.optionValueLabels[settingIndex][lastCursor];
lastValueLabel.setColor(this.getTextColor(TextStyle.WINDOW));
lastValueLabel.setShadowColor(this.getTextColor(TextStyle.WINDOW, true));
this.optionCursors[settingIndex] = cursor;
const newValueLabel = this.optionValueLabels[settingIndex][cursor];
newValueLabel.setColor(this.getTextColor(TextStyle.SETTINGS_SELECTED));
newValueLabel.setShadowColor(this.getTextColor(TextStyle.SETTINGS_SELECTED, true));
if (save) {
this.scene.gameData.saveSetting(setting, cursor);
if (reloadSettings.includes(setting)) {
this.reloadRequired = true;
if (setting === Setting.Language) {
this.reloadI18n = true;
}
}
}
return true;
}
setScrollCursor(scrollCursor: integer): boolean {
if (scrollCursor === this.scrollCursor) {
return false;
}
this.scrollCursor = scrollCursor;
this.updateSettingsScroll();
this.setCursor(this.cursor);
return true;
}
updateSettingsScroll(): void {
this.optionsContainer.setY(-16 * this.scrollCursor);
for (let s = 0; s < this.settingLabels.length; s++) {
const visible = s >= this.scrollCursor && s < this.scrollCursor + this.rowsToDisplay;
this.settingLabels[s].setVisible(visible);
for (const option of this.optionValueLabels[s]) {
option.setVisible(visible);
}
}
}
clear() {
super.clear();
this.settingsContainer.setVisible(false);
this.eraseCursor();
if (this.reloadRequired) {
this.reloadRequired = false;
this.scene.reset(true, false, true);
}
}
eraseCursor() {
if (this.cursorObj) {
this.cursorObj.destroy();
}
this.cursorObj = null;
this.title = "General";
this.settings = Setting.filter(s => s.type === SettingType.GENERAL);
this.localStorageKey = "settings";
}
}

View File

@ -42,6 +42,7 @@ import {PlayerGender} from "#app/system/game-data";
import GamepadBindingUiHandler from "./settings/gamepad-binding-ui-handler";
import SettingsKeyboardUiHandler from "#app/ui/settings/settings-keyboard-ui-handler";
import KeyboardBindingUiHandler from "#app/ui/settings/keyboard-binding-ui-handler";
import SettingsAccessibilityUiHandler from "./settings/settings-accessiblity-ui-handler";
export enum Mode {
MESSAGE,
@ -62,6 +63,7 @@ export enum Mode {
MENU,
MENU_OPTION_SELECT,
SETTINGS,
SETTINGS_ACCESSIBILITY,
SETTINGS_GAMEPAD,
GAMEPAD_BINDING,
SETTINGS_KEYBOARD,
@ -99,6 +101,7 @@ const noTransitionModes = [
Mode.GAMEPAD_BINDING,
Mode.KEYBOARD_BINDING,
Mode.SETTINGS,
Mode.SETTINGS_ACCESSIBILITY,
Mode.SETTINGS_GAMEPAD,
Mode.SETTINGS_KEYBOARD,
Mode.ACHIEVEMENTS,
@ -151,6 +154,7 @@ export default class UI extends Phaser.GameObjects.Container {
new MenuUiHandler(scene),
new OptionSelectUiHandler(scene, Mode.MENU_OPTION_SELECT),
new SettingsUiHandler(scene),
new SettingsAccessibilityUiHandler(scene),
new SettingsGamepadUiHandler(scene),
new GamepadBindingUiHandler(scene),
new SettingsKeyboardUiHandler(scene),