[Feature] Move touch controls configuration (Reopened) (#3256)

* [Hotfix] Fix interactions of some moves not changing types (#3183)

* [Hotfix] Fix wild spawns not having their HA (#3190)

* [Hotfix] Allow to hatch pokemon with Hidden Ability again (#3222)

* chore: Update TNC links layout and position in index.html

* chore: Update TNC links font size in index.css (#3230)

* Move Touch Controls

* ConfigToolbar alignment

* Insert config toolbar on open, camel-case classes, hidden setting

* Better toolbar styling, fixed double configToolbar bug

* Fixed typedocs

---------

Co-authored-by: Adrian T. <68144167+torranx@users.noreply.github.com>
Co-authored-by: NightKev <34855794+DayKev@users.noreply.github.com>
Co-authored-by: Frederico Santos <frederico.f.santos@tecnico.ulisboa.pt>
This commit is contained in:
Tim Perdok 2024-08-10 15:17:04 +02:00 committed by GitHub
parent 638a0a66b5
commit 566cd80522
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
20 changed files with 794 additions and 204 deletions

301
index.css
View File

@ -1,16 +1,8 @@
/* Global */
:root {
--color-base: hsl(0, 0%, 55%);
--color-light: hsl(0, 0%, 90%);
--color-dark: hsl(0, 0%, 10%);
--controls-size: 10vh;
--text-shadow-size: 0.65vh;
}
@media (orientation: landscape) {
:root {
--controls-size: 20vh;
--text-shadow-size: 1.3vh;
}
}
html {
@ -43,33 +35,173 @@ body {
transform-origin: top !important;
}
#layout:fullscreen #dpad, #layout:fullscreen {
bottom: 6rem;
}
input:-internal-autofill-selected {
-webkit-background-clip: text;
background-clip: text;
}
/* Need adjust input font-size */
input {
font-size: 3.2rem;
}
.hidden {
display: none !important;
}
input:-internal-autofill-selected {
-webkit-background-clip: text;
background-clip: text;
}
/* Touch Controls: */
#touchControls {
--text-shadow-size: 0.65vh;
--controls-size: 10vh;
--touch-control-opacity: 0.6;
--controls-padding: 1rem;
--controls-size-with-padding: calc(var(--controls-size) + var(--controls-padding));
--control-group-extra-size: calc(var(--controls-size) * 0.8);
--control-group-extra-2-offset: calc(var(--controls-size-with-padding) + (var(--controls-size) - var(--control-group-extra-size)) / 2);
--control-group-extra-1-offset: calc(var(--controls-padding) + (var(--controls-size) - var(--control-group-extra-size)) / 2);
--small-control-size: calc(var(--controls-size) / 3);
--rect-control-size: calc(var(--controls-size) * 0.74);
font-family: 'emerald';
font-size: var(--controls-size);
text-shadow: var(--color-dark) var(--text-shadow-size) var(--text-shadow-size);
color: var(--color-light);
}
@media (orientation: landscape) {
#touchControls {
--controls-size: 20vh;
--text-shadow-size: 1.3vh;
--small-button-offset: 4vh;
}
}
#touchControls:not(.visible) {
display: none;
}
#dpad, #apad {
#touchControls .active {
opacity: var(--touch-control-opacity);
}
.control-group {
position: fixed;
bottom: 1rem;
display: flex;
flex-wrap: wrap;
justify-content: space-around;
align-items: center;
width: var(--controls-size);
}
.control-group-dpad {
width: calc(2 * var(--controls-size));
height: calc(2 * var(--controls-size));
}
.control-group-extra {
width: var(--control-group-extra-size);
height: var(--control-group-extra-size);
}
/* Hide buttons on specific UIs */
/* Show #cycleForm and #cycleShiny only on STARTER_SELECT and SETTINGS */
#touchControls:not([data-ui-mode='STARTER_SELECT']):not([data-ui-mode^='SETTINGS']) #apadCycleForm,
#touchControls:not([data-ui-mode='STARTER_SELECT']):not([data-ui-mode^='SETTINGS']) #apadCycleShiny {
display: none;
}
/* Show #apadInfo only in battle */
#touchControls:not([data-ui-mode='COMMAND']):not([data-ui-mode='FIGHT']):not([data-ui-mode='BALL']) #apadInfo {
display: none;
}
/* Show #apadInfo only in battle and target select */
#touchControls:not([data-ui-mode='COMMAND']):not([data-ui-mode='FIGHT']):not([data-ui-mode='BALL']):not([data-ui-mode='TARGET_SELECT']):not([data-ui-mode='MODIFIER_SELECT']) #apadStats {
display: none;
}
/* Show cycle buttons only on STARTER_SELECT and on touch configuration panel */
#touchControls:not(.config-mode):not([data-ui-mode='STARTER_SELECT']) #apadCycleNature,
#touchControls:not(.config-mode):not([data-ui-mode='STARTER_SELECT']) #apadCycleAbility,
#touchControls:not(.config-mode):not([data-ui-mode='STARTER_SELECT']) #apadCycleGender,
#touchControls:not(.config-mode):not([data-ui-mode='STARTER_SELECT']) #apadCycleVariant {
display: none;
}
/* Configuration toolbar */
#configToolbar {
width: 100%;
position: fixed;
top: 1rem;
left: 0;
z-index: 9;
user-select: none;
}
#configToolbar .column {
display: flex;
flex-direction: column;
align-items: center;
gap: 10%;
padding: 0 var(--controls-padding);
}
#configToolbar .button-row {
display: flex;
justify-content: space-evenly;
width: 100%;
}
#configToolbar .info-row {
display: flex;
justify-content: flex-start;
width: 100%;
}
#configToolbar .button {
z-index: 3;
background-color: var(--color-base);
display: flex;
justify-content: center;
align-items: center;
text-align: center;
border-radius: 10%;
height: var(--small-control-size);
font-size: var(--small-control-size);
border-radius: 8px;
padding: 2px 8px;
text-shadow: var(--color-dark) calc(var(--text-shadow-size) / 3) calc(var(--text-shadow-size) / 3);
}
@media (orientation: portrait) {
#dpad, #apad {
bottom: calc(1rem + env(safe-area-inset-bottom));
}
#configToolbar .button:active {
opacity: var(--touch-control-opacity)
}
#configToolbar .orientation-label {
font-size: var(--small-control-size);
text-shadow: var(--color-dark) calc(var(--text-shadow-size) / 3) calc(var(--text-shadow-size) / 3);
}
/* dpad */
#dpad {
left: 1rem;
}
#apad {
right: 1rem;
z-index: 3;
opacity: 0.8;
}
#dpad svg {
@ -78,114 +210,83 @@ input {
fill: var(--color-base);
}
#dpad svg rect {
opacity: 0.6;
}
/* apad buttons */
#apad > * {
width: var(--controls-size);
height: var(--controls-size);
}
#apad .apadBtn {
width: var(--controls-size);
height: var(--controls-size);
.apad-button {
background-color: var(--color-base);
border-radius: 50%;
display: flex;
justify-content: center;
align-items: center;
right: 0;
bottom: 0;
width: var(--controls-size);
height: var(--controls-size);
opacity: 0.8;
border-radius: 8px;
}
#apad .apadLabel {
font-family: 'emerald';
font-size: var(--controls-size);
text-shadow: var(--color-dark) var(--text-shadow-size) var(--text-shadow-size);
color: var(--color-light);
.apad-small {
width: var(--small-control-size);
height: var(--small-control-size);
}
.apad-label {
user-select: none;
height: 100%;
margin-right: -2px;
}
#apad .apadLabelSmall {
font-size: calc(var(--controls-size) / 3);
.apad-small > .apad-label {
font-size: var(--small-control-size);
text-shadow: var(--color-dark) calc(var(--text-shadow-size) / 3) calc(var(--text-shadow-size) / 3);
}
#apad #apadLabelAction, #apad #apadLabelCancel {
margin-left: calc(var(--controls-size) / 3);
line-height: 0.9;
}
#apad > :nth-child(2) {
position: relative;
right: var(--controls-size);
}
#apad .apadRectBtn {
position: relative;
.apad-rectangle {
text-align: center;
padding-right: 10%;
border-radius: 10%;
bottom: calc(var(--controls-size) * 0.05);
width: calc(var(--controls-size) * 0.6);
height: calc(var(--controls-size) * 0.3);
width: var(--rect-control-size);
height: var(--small-control-size);
}
#apad .apadSqBtn {
border-radius: 10%;
width: calc(var(--controls-size) * 0.3);
height: calc(var(--controls-size) * 0.3);
.apad-square {
width: var(--small-control-size);
height: var(--small-control-size);
}
#apad .apadBtnContainer {
position: relative;
display: flex;
.apad-circle {
width: var(--controls-size);
height: var(--controls-size);
border-radius: 50%;
}
#apad .apadRectBtnContainer {
flex-wrap: wrap;
margin-top: calc(var(--controls-size) * -0.8);
left: calc(var(--controls-size) * 0.175);
height: calc(var(--controls-size) * 0.8);
/* Defaults:*/
#control-group-dpad {
left: var(--controls-padding);
bottom: var(--controls-padding);
}
#apad .apadSqBtnContainer {
flex-wrap: wrap;
justify-content: space-evenly;
align-items: center;
margin-bottom: calc(var(--controls-size) * -0.8);
top: calc(var(--controls-size) * -0.9);
width: calc(var(--controls-size) * 0.8);
height: calc(var(--controls-size) * 0.8);
#control-group-action {
right: var(--controls-padding);
bottom: var(--controls-size-with-padding);
}
#apad .apadRectBtnContainer > #apadMenu {
align-self: flex-end;
#control-group-cancel {
right: var(--controls-size-with-padding);
bottom: var(--controls-padding);;
}
#apad .apadRectBtnContainer > .apadSqBtn:not(:first-child) {
margin-left: 10%;
#control-group-extra-1 {
right: var(--control-group-extra-1-offset);
bottom: var(--control-group-extra-1-offset);
}
#touchControls:not([data-ui-mode='STARTER_SELECT']):not([data-ui-mode='SETTINGS']):not([data-ui-mode='SETTINGS_DISPLAY']):not([data-ui-mode='SETTINGS_AUDIO']):not([data-ui-mode='SETTINGS_GAMEPAD']):not([data-ui-mode='SETTINGS_KEYBOARD']) #apad .apadRectBtnContainer > .apadSqBtn:not(.apadBattle),
#touchControls:not([data-ui-mode='STARTER_SELECT']):not([data-ui-mode='SETTINGS']):not([data-ui-mode='SETTINGS_DISPLAY']):not([data-ui-mode='SETTINGS_AUDIO']):not([data-ui-mode='SETTINGS_GAMEPAD']):not([data-ui-mode='SETTINGS_KEYBOARD']) #apad .apadSqBtnContainer > .apadSqBtn:not(.apadBattle)
{
display: none;
#control-group-extra-2 {
right: var(--control-group-extra-2-offset);
bottom: var(--control-group-extra-2-offset);
}
#touchControls:not([data-ui-mode='COMMAND']):not([data-ui-mode='FIGHT']):not([data-ui-mode='BALL']):not([data-ui-mode='TARGET_SELECT']):not([data-ui-mode='MODIFIER_SELECT']) #apad .apadBattle {
display: none;
}
#apad .apadRectBtnContainer + .apadSqBtnContainer {
top: calc(var(--controls-size) * -1.9);
left: calc(var(--controls-size) * -0.9);
}
#apad .apadBtnContainer .apadLabel {
margin-left: calc(var(--controls-size) / 12);
line-height: 0.8;
}
#dpad path:not(.active), #apad .apadBtn:not(.active) {
opacity: 0.6;
}
/* Layout */
#layout:fullscreen #dpad, #layout:fullscreen #apad {
bottom: 6rem;

View File

@ -64,54 +64,70 @@
<body>
<div id="app"></div>
<div id="touchControls">
<div id="dpad" class="unselectable">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 72 72">
<path id="dpadUp" data-key="UP" d="M48,5.8C48,2.5,45.4,0,42,0H29.9C26.6,0,24,2.4,24,5.8V24h24V5.8z" />
<path id="dpadRight" data-key="RIGHT" d="M66.2,24H48v24h18.2c3.3,0,5.8-2.7,5.8-6V29.9C72,26.5,69.5,24,66.2,24z" />
<path id="dpadDown" data-key="DOWN" d="M24,66.3c0,3.3,2.6,5.7,5.9,5.7H42c3.3,0,6-2.4,6-5.7V48H24V66.3z" />
<path id="dpadLeft" data-key="LEFT" d="M5.7,24C2.4,24,0,26.5,0,29.9V42c0,3.3,2.3,6,5.7,6H24V24H5.7z" />
<rect id="dpadCenter" x="24" y="24" width="24" height="24" />
</svg>
<div class="left">
<div id="control-group-dpad" class="control-group control-group-dpad">
<div id="dpad" data-control-key="DPAD">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 72 72">
<path id="dpadUp" data-key="UP"
d="M48,5.8C48,2.5,45.4,0,42,0H29.9C26.6,0,24,2.4,24,5.8V24h24V5.8z" />
<path id="dpadRight" data-key="RIGHT"
d="M66.2,24H48v24h18.2c3.3,0,5.8-2.7,5.8-6V29.9C72,26.5,69.5,24,66.2,24z" />
<path id="dpadDown" data-key="DOWN"
d="M24,66.3c0,3.3,2.6,5.7,5.9,5.7H42c3.3,0,6-2.4,6-5.7V48H24V66.3z" />
<path id="dpadLeft" data-key="LEFT"
d="M5.7,24C2.4,24,0,26.5,0,29.9V42c0,3.3,2.3,6,5.7,6H24V24H5.7z" />
<rect id="dpadCenter" x="24" y="24" width="24" height="24" />
</svg>
</div>
</div>
</div>
<div id="apad" class="unselectable">
<div id="apadAction" class="apadCircBtn apadBtn" data-key="ACTION">
<text id="apadLabelAction" class="apadLabel">A</text>
</div>
<div id="apadCancel" class="apadCircBtn apadBtn" data-key="CANCEL">
<text id="apadLabelCancel" class="apadLabel">B</text>
</div>
<div class="apadBtnContainer apadRectBtnContainer">
<div id="apadCycleShiny" class="apadSqBtn apadBtn" data-key="CYCLE_SHINY">
<text class="apadLabel apadLabelSmall">R</text>
</div>
<div id="apadCycleVariant" class="apadSqBtn apadBtn" data-key="V">
<text class="apadLabel apadLabelSmall">V</text>
</div>
<div id="apadStats" class="apadRectBtn apadBtn apadBattle" data-key="STATS">
<text class="apadLabel apadLabelSmall">C</text>
</div>
<div id="apadMenu" class="apadRectBtn apadBtn" data-key="MENU">
<text class="apadLabel apadLabelSmall">Menu</text>
<div class="right">
<div id="control-group-action" class="control-group">
<div id="apadAction" class="apad-button apad-circle" data-key="ACTION">
<span class="apad-label">A</span>
</div>
</div>
<div class="apadBtnContainer apadSqBtnContainer">
<div id="apadCycleForm" class="apadSqBtn apadBtn" data-key="CYCLE_FORM">
<text class="apadLabel apadLabelSmall">F</text>
</div>
<div id="apadCycleGender" class="apadSqBtn apadBtn" data-key="CYCLE_GENDER">
<text class="apadLabel apadLabelSmall">G</text>
</div>
<div id="apadCycleAbility" class="apadSqBtn apadBtn" data-key="CYCLE_ABILITY">
<text class="apadLabel apadLabelSmall">E</text>
</div>
<div id="apadCycleNature" class="apadSqBtn apadBtn" data-key="CYCLE_NATURE">
<text class="apadLabel apadLabelSmall">N</text>
</div>
<div id="apadInfo" class="apadRectBtn apadBtn apadBattle" data-key="V">
<text class="apadLabel apadLabelSmall">V</text>
<div id="control-group-cancel" class="control-group">
<div id="apadCancel" class="apad-button apad-circle" data-key="CANCEL">
<span class="apad-label">B</span>
</div>
</div>
<div id="control-group-extra-1" class="control-group control-group-extra">
<div id="apadCycleShiny" class="apad-button apad-square apad-small" data-key="CYCLE_SHINY">
<span class="apad-label">R</span>
</div>
<div id="apadCycleVariant" class="apad-button apad-square apad-small" data-key="V">
<span class="apad-label">V</span>
</div>
<div id="apadStats" class="apad-button apad-rectangle apad-small" data-key="STATS">
<span class="apad-label">C</span>
</div>
<div id="apadMenu" class="apad-button apad-rectangle apad-small" data-key="MENU">
<span class="apad-label">Menu</span>
</div>
</div>
<div id="control-group-extra-2" class="control-group control-group-extra">
<div id="apadCycleForm" class="apad-button apad-square apad-small" data-key="CYCLE_FORM">
<span class="apad-label">F</span>
</div>
<div id="apadCycleGender" class="apad-button apad-square apad-small" data-key="CYCLE_GENDER">
<span class="apad-label">G</span>
</div>
<div id="apadCycleAbility" class="apad-button apad-square apad-small" data-key="CYCLE_ABILITY">
<span class="apad-label">E</span>
</div>
<div id="apadCycleNature" class="apad-button apad-square apad-small" data-key="CYCLE_NATURE">
<span class="apad-label">N</span>
</div>
<div id="apadInfo" class="apad-button apad-rectangle apad-small" data-key="V">
<span class="apad-label">V</span>
</div>
</div>
</div>
</div>
<div id="tnc-links">

View File

@ -21,6 +21,7 @@ import {SettingKeyboard} from "#app/system/settings/settings-keyboard";
import TouchControl from "#app/touch-controls";
import { Button } from "#enums/buttons";
import { Device } from "#enums/devices";
import MoveTouchControlsHandler from "./ui/settings/move-touch-controls-handler";
export interface DeviceMapping {
[key: string]: number;
@ -105,6 +106,7 @@ export class InputsController {
public lastSource: string = "keyboard";
private inputInterval: NodeJS.Timeout[] = new Array();
private touchControls: TouchControl;
public moveTouchControlsHandler: MoveTouchControlsHandler;
/**
* Initializes a new instance of the game control system, setting up initial state and configurations.
@ -182,6 +184,7 @@ export class InputsController {
this.scene.input.keyboard?.on("keyup", this.keyboardKeyUp, this);
}
this.touchControls = new TouchControl(this.scene);
this.moveTouchControlsHandler = new MoveTouchControlsHandler(this.touchControls);
}
/**

View File

@ -97,5 +97,6 @@ export const settings: SimpleTranslationEntries = {
"controller": "Controller",
"gamepadSupport": "Controllerunterstützung",
"showBgmBar": "Musiknamen anzeigen",
"moveTouchControls": "Bewegung Touch Steuerung",
"shopOverlayOpacity": "Shop Overlay Deckkraft",
} as const;

View File

@ -97,5 +97,6 @@ export const settings: SimpleTranslationEntries = {
"controller": "Controller",
"gamepadSupport": "Gamepad Support",
"showBgmBar": "Show Music Names",
"moveTouchControls": "Move Touch Controls",
"shopOverlayOpacity": "Shop Overlay Opacity"
} as const;

View File

@ -97,5 +97,6 @@ export const settings: SimpleTranslationEntries = {
"controller": "Controller",
"gamepadSupport": "Gamepad Support",
"showBgmBar": "Show Music Names",
"moveTouchControls": "Move Touch Controls",
"shopOverlayOpacity": "Opacidad de la fase de compra"
} as const;

View File

@ -97,5 +97,6 @@ export const settings: SimpleTranslationEntries = {
"controller": "Controller",
"gamepadSupport": "Gamepad Support",
"showBgmBar": "Titre de la musique",
"moveTouchControls": "Déplacer les contrôles tactiles",
"shopOverlayOpacity": "Opacité boutique"
} as const;

View File

@ -97,5 +97,6 @@ export const settings: SimpleTranslationEntries = {
"controller": "Controller",
"gamepadSupport": "Supporto Gamepad",
"showBgmBar": "Mostra Nomi Musica",
"moveTouchControls": "Move Touch Controls",
"shopOverlayOpacity": "Opacità Finestra Negozio"
} as const;

View File

@ -97,5 +97,6 @@ export const settings: SimpleTranslationEntries = {
"controller": "컨트롤러",
"gamepadSupport": "게임패드 지원",
"showBgmBar": "BGM 제목 보여주기",
"moveTouchControls": "터치 컨트롤 이동",
"shopOverlayOpacity": "상점 오버레이 투명도"
} as const;

View File

@ -97,5 +97,6 @@ export const settings: SimpleTranslationEntries = {
"controller": "Controle",
"gamepadSupport": "Suporte para Controle",
"showBgmBar": "Exibir Nomes das Músicas",
"moveTouchControls": "Move Touch Controls",
"shopOverlayOpacity": "Opacidade da Loja"
} as const;

View File

@ -97,5 +97,6 @@ export const settings: SimpleTranslationEntries = {
"controller": "控制器",
"gamepadSupport": "手柄支持",
"showBgmBar": "显示音乐名称",
"moveTouchControls": "移动触摸控制",
"shopOverlayOpacity": "商店显示不透明度"
} as const;

View File

@ -97,5 +97,6 @@ export const settings: SimpleTranslationEntries = {
"controller": "控制器",
"gamepadSupport": "手柄支持",
"showBgmBar": "Show Music Names",
"moveTouchControls": "移動觸控控制",
"shopOverlayOpacity": "Shop Overlay Opacity"
} as const;

View File

@ -65,6 +65,10 @@ export interface Setting {
default: number
type: SettingType
requireReload?: boolean
/** Whether the setting can be activated or not */
activatable?: boolean
/** Determines whether the setting should be hidden from the UI */
isHidden?: () => boolean
}
/**
@ -106,6 +110,7 @@ export const SettingKeys = {
SE_Volume: "SE_VOLUME",
Music_Preference: "MUSIC_PREFERENCE",
Show_BGM_Bar: "SHOW_BGM_BAR",
Move_Touch_Controls: "MOVE_TOUCH_CONTROLS",
Shop_Overlay_Opacity: "SHOP_OVERLAY_OPACITY"
};
@ -550,6 +555,20 @@ export const Setting: Array<Setting> = [
type: SettingType.AUDIO,
requireReload: true
},
{
key: SettingKeys.Move_Touch_Controls,
label: i18next.t("settings:moveTouchControls"),
options: [
{
value: "Configure",
label: i18next.t("settings:change")
}
],
default: 0,
type: SettingType.GENERAL,
activatable: true,
isHidden: () => !hasTouchscreen()
},
{
key: SettingKeys.Shop_Overlay_Opacity,
label: i18next.t("settings:shopOverlayOpacity"),
@ -557,7 +576,7 @@ export const Setting: Array<Setting> = [
default: 7,
type: SettingType.DISPLAY,
requireReload: false
},
}
];
/**

View File

@ -1,50 +1,69 @@
<!DOCTYPE html><body>
<div id="touchControls">
<div id="dpad" class="unselectable">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 72 72">
<path id="dpadUp" data-key="UP" d="M48,5.8C48,2.5,45.4,0,42,0H29.9C26.6,0,24,2.4,24,5.8V24h24V5.8z" />
<path id="dpadRight" data-key="RIGHT" d="M66.2,24H48v24h18.2c3.3,0,5.8-2.7,5.8-6V29.9C72,26.5,69.5,24,66.2,24z" />
<path id="dpadDown" data-key="DOWN" d="M24,66.3c0,3.3,2.6,5.7,5.9,5.7H42c3.3,0,6-2.4,6-5.7V48H24V66.3z" />
<path id="dpadLeft" data-key="LEFT" d="M5.7,24C2.4,24,0,26.5,0,29.9V42c0,3.3,2.3,6,5.7,6H24V24H5.7z" />
<rect id="dpadCenter" x="24" y="24" width="24" height="24" />
</svg>
<div class="left">
<div id="control-group-dpad" class="control-group control-group-dpad">
<div id="dpad" data-control-key="DPAD">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 72 72">
<path id="dpadUp" data-key="UP"
d="M48,5.8C48,2.5,45.4,0,42,0H29.9C26.6,0,24,2.4,24,5.8V24h24V5.8z" />
<path id="dpadRight" data-key="RIGHT"
d="M66.2,24H48v24h18.2c3.3,0,5.8-2.7,5.8-6V29.9C72,26.5,69.5,24,66.2,24z" />
<path id="dpadDown" data-key="DOWN"
d="M24,66.3c0,3.3,2.6,5.7,5.9,5.7H42c3.3,0,6-2.4,6-5.7V48H24V66.3z" />
<path id="dpadLeft" data-key="LEFT"
d="M5.7,24C2.4,24,0,26.5,0,29.9V42c0,3.3,2.3,6,5.7,6H24V24H5.7z" />
<rect id="dpadCenter" x="24" y="24" width="24" height="24" />
</svg>
</div>
</div>
</div>
<div id="apad" class="unselectable">
<div id="apadAction" class="apadCircBtn apadBtn" data-key="ACTION">
<text id="apadLabelAction" class="apadLabel">A</text>
</div>
<div id="apadCancel" class="apadCircBtn apadBtn" data-key="CANCEL">
<text id="apadLabelCancel" class="apadLabel">B</text>
</div>
<div class="apadBtnContainer apadRectBtnContainer">
<div id="apadCycleShiny" class="apadSqBtn apadBtn" data-key="CYCLE_SHINY">
<text class="apadLabel apadLabelSmall">R</text>
</div>
<div id="apadCycleVariant" class="apadSqBtn apadBtn" data-key="CYCLE_VARIANT">
<text class="apadLabel apadLabelSmall">V</text>
</div>
<div id="apadStats" class="apadRectBtn apadBtn" data-key="STATS">
<text class="apadLabel apadLabelSmall">C</text>
</div>
<div id="apadMenu" class="apadRectBtn apadBtn" data-key="MENU">
<text class="apadLabel apadLabelSmall">Menu</text>
<div class="right">
<div id="control-group-action" class="control-group">
<div id="apadAction" class="apad-button apad-circle" data-key="ACTION">
<span class="apad-label">A</span>
</div>
</div>
<div class="apadBtnContainer apadSqBtnContainer">
<div id="apadCycleForm" class="apadSqBtn apadBtn" data-key="CYCLE_FORM">
<text class="apadLabel apadLabelSmall">F</text>
</div>
<div id="apadCycleGender" class="apadSqBtn apadBtn" data-key="CYCLE_GENDER">
<text class="apadLabel apadLabelSmall">G</text>
</div>
<div id="apadCycleAbility" class="apadSqBtn apadBtn" data-key="CYCLE_ABILITY">
<text class="apadLabel apadLabelSmall">E</text>
</div>
<div id="apadCycleNature" class="apadSqBtn apadBtn" data-key="CYCLE_NATURE">
<text class="apadLabel apadLabelSmall">N</text>
<div id="control-group-cancel" class="control-group">
<div id="apadCancel" class="apad-button apad-circle" data-key="CANCEL">
<span class="apad-label">B</span>
</div>
</div>
<div id="control-group-extra-1" class="control-group control-group-extra">
<div id="apadCycleShiny" class="apad-button apad-square apad-small" data-key="CYCLE_SHINY">
<span class="apad-label">R</span>
</div>
<div id="apadCycleVariant" class="apad-button apad-square apad-small" data-key="V">
<span class="apad-label">V</span>
</div>
<div id="apadStats" class="apad-button apad-rectangle apad-small" data-key="STATS">
<span class="apad-label">C</span>
</div>
<div id="apadMenu" class="apad-button apad-rectangle apad-small" data-key="MENU">
<span class="apad-label">Menu</span>
</div>
</div>
<div id="control-group-extra-2" class="control-group control-group-extra">
<div id="apadCycleForm" class="apad-button apad-square apad-small" data-key="CYCLE_FORM">
<span class="apad-label">F</span>
</div>
<div id="apadCycleGender" class="apad-button apad-square apad-small" data-key="CYCLE_GENDER">
<span class="apad-label">G</span>
</div>
<div id="apadCycleAbility" class="apad-button apad-square apad-small" data-key="CYCLE_ABILITY">
<span class="apad-label">E</span>
</div>
<div id="apadCycleNature" class="apad-button apad-square apad-small" data-key="CYCLE_NATURE">
<span class="apad-label">N</span>
</div>
<div id="apadInfo" class="apad-button apad-rectangle apad-small" data-key="V">
<span class="apad-label">V</span>
</div>
</div>
</div>
</div>
</body>

View File

@ -8,18 +8,38 @@ export default class TouchControl {
events: EventEmitter;
private buttonLock: string[] = new Array();
private inputInterval: NodeJS.Timeout[] = new Array();
/** Whether touch controls are disabled */
private disabled: boolean = false;
/** Whether the last touch event has finished before disabling */
private finishedLastTouch: boolean = false;
constructor(scene: BattleScene) {
this.events = scene.game.events;
this.init();
}
/**
* Disable touch controls
*/
disable() {
this.disabled = true;
this.finishedLastTouch = false;
}
/**
* Enable touch controls
*/
enable() {
this.disabled = false;
this.finishedLastTouch = false;
}
/**
* Initialize touch controls by binding keys to buttons.
*/
init() {
this.preventElementZoom(document.querySelector("#dpad"));
this.preventElementZoom(document.querySelector("#apad"));
document.querySelectorAll(".apad-button").forEach((element) => this.preventElementZoom(element as HTMLElement));
// Select all elements with the 'data-key' attribute and bind keys to them
for (const button of document.querySelectorAll("[data-key]")) {
// @ts-ignore - Bind the key to the button using the dataset key
@ -56,10 +76,14 @@ export default class TouchControl {
if (this.buttonLock.includes(key)) {
return;
}
this.simulateKeyboardEvent("keydown", key);
if (!this.simulateKeyboardEvent("keydown", key)) {
return;
}
clearInterval(this.inputInterval[key]);
this.inputInterval[key] = setInterval(() => {
this.simulateKeyboardEvent("keydown", key);
if (!this.simulateKeyboardEvent("keydown", key)) {
clearInterval(this.inputInterval[key]);
}
}, repeatInputDelayMillis);
this.buttonLock.push(key);
node.classList.add("active");
@ -67,11 +91,11 @@ export default class TouchControl {
}
touchButtonUp(node: HTMLElement, key: string, id: string) {
if (!this.buttonLock.includes(key)) {
if (!this.buttonLock.includes(key) || this.disabled && this.finishedLastTouch) {
return;
}
this.finishedLastTouch = true;
this.simulateKeyboardEvent("keyup", key);
node.classList.remove("active");
document.getElementById(id)?.classList.remove("active");
@ -81,18 +105,19 @@ export default class TouchControl {
}
/**
* Simulates a keyboard event on the canvas.
* Simulates a keyboard event on the canvas if the button is not disabled.
*
* @param eventType - The type of the keyboard event ('keydown' or 'keyup').
* @param key - The key to simulate.
*
* @returns Whether the simulation was successful.
* @remarks
* This function checks if the key exists in the Button enum. If it does, it retrieves the corresponding button
* and emits the appropriate event ('input_down' or 'input_up') based on the event type.
*/
simulateKeyboardEvent(eventType: string, key: string) {
if (!Button.hasOwnProperty(key)) {
return;
simulateKeyboardEvent(eventType: string, key: string): boolean {
console.log("simulateKeyboardEvent", eventType, key);
if (!Button.hasOwnProperty(key) || this.disabled) {
return false;
}
const button = Button[key];
@ -112,6 +137,7 @@ export default class TouchControl {
});
break;
}
return true;
}
/**

View File

@ -7,7 +7,7 @@ import { addWindow } from "../ui-theme";
import {Button} from "#enums/buttons";
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";
import { Setting, SettingKeys, SettingType } from "#app/system/settings/settings";
import i18next from "i18next";
@ -40,9 +40,9 @@ export default class AbstractSettingsUiHandler extends UiHandler {
protected settings: Array<Setting>;
protected localStorageKey: string;
constructor(scene: BattleScene, mode: Mode | null = null) {
constructor(scene: BattleScene, type: SettingType, mode: Mode | null = null) {
super(scene, mode);
this.settings = Setting.filter(s => s.type === type && !s?.isHidden?.());
this.reloadRequired = false;
this.rowsToDisplay = 8;
}
@ -264,6 +264,12 @@ export default class AbstractSettingsUiHandler extends UiHandler {
case Button.CYCLE_SHINY:
success = this.navigationContainer.navigate(button);
break;
case Button.ACTION:
const setting: Setting = this.settings[cursor];
if (setting?.activatable) {
success = this.activateSetting(setting);
}
break;
}
}
@ -275,6 +281,20 @@ export default class AbstractSettingsUiHandler extends UiHandler {
return success;
}
/**
* Activate the specified setting if it is activatable.
* @param setting The setting to activate.
* @returns Whether the setting was successfully activated.
*/
activateSetting(setting: Setting): boolean {
switch (setting.key) {
case SettingKeys.Move_Touch_Controls:
this.scene.inputController.moveTouchControlsHandler.enableConfigurationMode(this.getUi(), this.scene);
return true;
}
return false;
}
/**
* Set the cursor to the specified position.
*

View File

@ -0,0 +1,380 @@
import TouchControl from "#app/touch-controls.js";
import UI from "#app/ui/ui.js";
import { Scene } from "phaser";
export const TOUCH_CONTROL_POSITIONS_LANDSCAPE = "touchControlPositionsLandscape";
export const TOUCH_CONTROL_POSITIONS_PORTRAIT = "touchControlPositionsPortrait";
type ControlPosition = { id: string, x: number, y: number };
type ConfigurationEventListeners = {
"touchstart": EventListener[]
"touchmove": EventListener[]
"touchend": EventListener[]
};
type ToolbarRefs = {
toolbar: HTMLDivElement,
saveButton: HTMLDivElement
resetButton: HTMLDivElement
cancelButton: HTMLDivElement
};
/**
* Handles the dragging of touch controls around the screen.
*/
export default class MoveTouchControlsHandler {
/** The element that is currently being dragged */
private draggingElement: HTMLElement | null = null;
/**
* Whether the user is currently configuring the touch controls.
* When this is true, the touch controls can be dragged around the screen and the controls of the game are disabled.
*/
public inConfigurationMode: boolean;
/**
* The event listeners for the configuration mode.
* These are used to remove the event listeners when the configuration mode is disabled.
*/
private configurationEventListeners: ConfigurationEventListeners = {
"touchstart": [],
"touchmove": [],
"touchend": []
};
private overlay: Phaser.GameObjects.Container;
private isLandscapeMode: boolean = this.getScreenSize().width > this.getScreenSize().height;
private touchControls: TouchControl;
constructor(touchControls: TouchControl) {
this.touchControls = touchControls;
this.inConfigurationMode = false;
this.setPositions(this.getSavedPositionsOfCurrentOrientation() ?? []);
window.addEventListener("resize", (event) => {
const screenSize = this.getScreenSize();
if (screenSize.width > screenSize.height !== this.isLandscapeMode) {
this.changeOrientation(screenSize.width > screenSize.height);
}
});
}
/**
* Changes the state of the touch controls to the given orientation.
* @param isLandscapeMode Whether the screen is in landscape mode.
*/
private async changeOrientation(isLandscapeMode: boolean) {
this.isLandscapeMode = isLandscapeMode;
if (this.inConfigurationMode) {
const orientation = document.querySelector("#touchControls #orientation");
if (orientation) {
orientation.textContent = this.isLandscapeMode? "Landscape" : "Portrait";
}
}
const positions = this.getSavedPositionsOfCurrentOrientation() ?? [];
this.setPositions(positions);
}
private getScreenSize() {
return { width: window.screen.width, height: window.screen.height };
}
/**
* Creates the toolbar element for the configuration mode.
* @returns A new div element that contains the toolbar for the configuration mode.
*/
private createToolbarElement(): HTMLDivElement {
const toolbar = document.createElement("div");
toolbar.id = "configToolbar";
toolbar.innerHTML = `
<div class="column">
<div class="button-row">
<div id="resetButton" class="button">Reset</div>
<div id="saveButton" class="button">Save & close</div>
<div id="cancelButton" class="button">Cancel</div>
</div>
<div class="info-row">
<div class="orientation-label">
Orientation: <span id="orientation">${this.isLandscapeMode ? "Landscape" : "Portrait"}</span>
</div>
</div>
</div>
`;
return toolbar;
}
/**
* Initializes the toolbar of the configuration mode.
* Places its elements at the top of the touch controls and adds event listeners to them.
*/
private createToolbar() {
document.querySelector("#touchControls")?.prepend(this.createToolbarElement());
const refs = this.getConfigToolbarRefs();
if (!refs) {
return;
}
const { saveButton, resetButton, cancelButton } = refs;
saveButton.addEventListener("click", () => {
this.saveCurrentPositions();
this.disableConfigurationMode();
});
resetButton.addEventListener("click", () => {
this.resetPositions();
});
cancelButton.addEventListener("click", () => {
const positions = this.getSavedPositionsOfCurrentOrientation();
this.setPositions(positions);
this.disableConfigurationMode();
});
}
/**
* Returns the references to the elements of the configuration toolbar.
* @returns The references to the elements of the configuration toolbar
* or undefined if the elements can not be found (e.g. during tests)
*/
private getConfigToolbarRefs(): ToolbarRefs | undefined {
const toolbar = document.querySelector("#touchControls #configToolbar") as HTMLDivElement;
if (!toolbar) {
return;
}
return {
toolbar,
saveButton: toolbar.querySelector("#saveButton")!,
resetButton: toolbar.querySelector("#resetButton")!,
cancelButton: toolbar.querySelector("#cancelButton")!
};
}
/**
* Elements that are inside the left div are anchored to the left boundary of the screen.
* The x value of the positions are considered offsets to their respective boundaries.
* @param element Either an element in the left div or the right div.
* @returns Whether the given element is inside the left div.
*/
private isLeft = (element: HTMLElement) => document.querySelector("#touchControls .left")?.contains(element);
/**
* Start dragging the given button.
* @param controlGroup The button that is being dragged.
* @param touch The touch event that started the drag.
*/
private startDrag = (controlGroup: HTMLElement): void => {
this.draggingElement = controlGroup;
};
/**
* Drags the currently dragged element to the given touch position.
* @param touch The touch event that is currently happening.
* @param isLeft Whether the dragged element is a left button.
*/
private drag = (touch: Touch): void => {
if (!this.draggingElement) {
return;
}
const rect = this.draggingElement.getBoundingClientRect();
// Map the touch position to the center of the dragged element.
const xOffset = this.isLeft(this.draggingElement) ? touch.clientX - rect.width / 2 : window.innerWidth - touch.clientX - rect.width / 2;
const yOffset = window.innerHeight - touch.clientY - rect.height / 2;
this.setPosition(this.draggingElement, xOffset, yOffset);
};
/**
* Stops dragging the currently dragged element.
*/
private stopDrag = () => {
this.draggingElement = null;
};
/**
* Returns the current positions of all touch controls that have moved from their default positions of this orientation.
* @returns {ControlPosition[]} The current positions of all touch controls that have moved from their default positions of this orientation
*/
private getModifiedCurrentPositions(): ControlPosition[] {
return this.getControlGroupElements()
.filter((controlGroup: HTMLElement) => controlGroup.style.right || controlGroup.style.left)
.map((controlGroup: HTMLElement) => {
return {
id: controlGroup.id,
x: parseFloat(this.isLeft(controlGroup) ? controlGroup.style.left : controlGroup.style.right),
y: parseFloat(controlGroup.style.bottom),
};
});
}
/**
* Returns the key of the local storage for the control positions data of this orientation
*/
private getLocalStorageKey(): string {
return this.isLandscapeMode ? TOUCH_CONTROL_POSITIONS_LANDSCAPE : TOUCH_CONTROL_POSITIONS_PORTRAIT;
}
/**
* Returns the saved positions of the touch controls.
* Filters result by the given orientation.
* @returns The saved positions of the touch controls of this orientation
*/
private getSavedPositionsOfCurrentOrientation(): ControlPosition[] {
const positions = localStorage.getItem(this.getLocalStorageKey());
if (!positions) {
return [];
}
return JSON.parse(positions) as ControlPosition[];
}
/**
* Saves the current positions of the touch controls to the local storage.
*/
private saveCurrentPositions() {
const pos = this.getModifiedCurrentPositions();
localStorage.setItem(this.getLocalStorageKey(), JSON.stringify(pos));
}
/**
* Updates the positions of the touch controls.
* @param positions The new positions of the touch controls.
*/
private setPositions(positions: ControlPosition[]) {
this.resetPositions();
return positions.forEach((pos: ControlPosition) => {
const controlGroup = document.querySelector(`#${pos.id}`) as HTMLElement;
this.setPosition(controlGroup, pos.x, pos.y);
});
}
/**
* Sets a control element to the given position.
* The x values are either offsets to the left or right boundary of the screen, depending on the side of the element.
* E.g. For left elements, (0, 0) is the bottom left corner of the screen and
* for right elements, (0, 0) is the bottom right corner of the screen.
* @param controlElement
* @param x Either an offset to the left or right boundary of the screen.
* @param y An offset to the bottom boundary of the screen.
*/
private setPosition(controlElement: HTMLElement, x: number, y: number) {
const rect = controlElement.getBoundingClientRect();
const checkBound = (value: number, min: number, max: number) => Math.min(Math.max(value, min), max);
const { height, width } = this.getScreenSize();
x = checkBound(x, 0, width - rect.width);
y = checkBound(y, 0, height - rect.height);
if (this.isLeft(controlElement)) {
controlElement.style.left = `${x}px`;
} else {
controlElement.style.right = `${x}px`;
}
controlElement.style.bottom = `${y}px`;
}
/**
* Resets the positions of the touch controls to their default positions and clears the saved positions.
* Does not save the changes.
*/
private resetPositions() {
this.getControlGroupElements().forEach((controlGroup: HTMLDivElement) => {
controlGroup.style.removeProperty("left");
controlGroup.style.removeProperty("right");
controlGroup.style.removeProperty("bottom");
});
}
/**
* Returns all control groups of the touch controls.
* These are groups of buttons that can be dragged around the screen.
* @returns All control groups of the touch controls.
*/
private getControlGroupElements(): HTMLDivElement[] {
return [...document.querySelectorAll("#touchControls .control-group")] as HTMLDivElement[];
}
/**
* Creates the event listeners for the configuration mode.
* @param controlGroups The elements that can be dragged around the screen.
* @returns The event listeners for the configuration mode.
*/
private createConfigurationEventListeners(controlGroups: HTMLDivElement[]): ConfigurationEventListeners {
return {
"touchstart": controlGroups.map((element: HTMLDivElement) => {
const startDrag = () => this.startDrag(element);
element.addEventListener("touchstart", startDrag, { passive: true });
return startDrag;
}),
"touchmove": controlGroups.map(() => {
const drag = (event) => this.drag(event.touches[0]);
window.addEventListener("touchmove", drag, { passive: true });
return drag;
}),
"touchend": controlGroups.map(() => {
const stopDrag = () => this.stopDrag();
window.addEventListener("touchend", stopDrag, { passive: true });
return stopDrag;
})
};
}
/**
* Creates an overlay that covers the screen and allows the user to drag the touch controls around.
* Also enables the toolbar for saving, resetting, and canceling the changes.
* @param ui The UI of the game.
* @param scene The scene of the game.
*/
private createOverlay(ui: UI, scene: Scene) {
const container = new Phaser.GameObjects.Container(scene, 0, 0);
const overlay = new Phaser.GameObjects.Rectangle(scene, 0, 0, scene.game.canvas.width, scene.game.canvas.height, 0x000000, 0.5);
overlay.setInteractive();
container.add(overlay);
ui.add(container);
this.overlay = container;
// Display toolbar
document.querySelector("#touchControls")?.classList.add("config-mode");
}
/**
* Allows the user to configure the touch controls by dragging buttons around the screen.
* @param ui The UI of the game.
* @param scene The scene of the game.
*/
public enableConfigurationMode(ui: UI, scene: Scene) {
if (this.inConfigurationMode) {
return;
}
this.inConfigurationMode = true;
this.touchControls.disable();
this.createOverlay(ui, scene);
this.createToolbar();
// Create event listeners with a delay to prevent the touchstart event from being triggered immediately.
setTimeout(() => {
// Remember the event listeners so they can be removed later.
this.configurationEventListeners = this.createConfigurationEventListeners(this.getControlGroupElements());
}, 500);
}
/**
* Disables the configuration mode.
*/
public disableConfigurationMode() {
this.inConfigurationMode = false;
this.draggingElement = null;
// Remove event listeners
const { touchstart, touchmove, touchend } = this.configurationEventListeners;
this.getControlGroupElements().forEach((element, index) => element.removeEventListener("touchstart", touchstart[index]));
touchmove.forEach((listener) => window.removeEventListener("touchmove", listener));
touchend.forEach((listener) => window.removeEventListener("touchend", listener));
// Remove configuration toolbar
const toolbar = document.querySelector("#touchControls #configToolbar");
toolbar?.remove();
// Remove overlay
this.overlay?.destroy();
document.querySelector("#touchControls")?.classList.remove("config-mode");
this.touchControls.enable();
}
}

View File

@ -2,7 +2,7 @@ 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";
import { SettingType } from "#app/system/settings/settings";
export default class SettingsAudioUiHandler extends AbstractSettingsUiHandler {
/**
@ -12,9 +12,8 @@ export default class SettingsAudioUiHandler extends AbstractSettingsUiHandler {
* @param mode - The UI mode, optional.
*/
constructor(scene: BattleScene, mode: Mode | null = null) {
super(scene, mode);
super(scene, SettingType.AUDIO, mode);
this.title = "Audio";
this.settings = Setting.filter(s => s.type === SettingType.AUDIO);
this.localStorageKey = "settings";
this.rowsToDisplay = 4;
}

View File

@ -2,7 +2,7 @@ import BattleScene from "../../battle-scene";
import { Mode } from "../ui";
"#app/inputs-controller.js";
import AbstractSettingsUiHandler from "./abstract-settings-ui-handler";
import { Setting, SettingKeys, SettingType } from "#app/system/settings/settings";
import { SettingKeys, SettingType } from "#app/system/settings/settings";
export default class SettingsDisplayUiHandler extends AbstractSettingsUiHandler {
/**
@ -12,9 +12,8 @@ export default class SettingsDisplayUiHandler extends AbstractSettingsUiHandler
* @param mode - The UI mode, optional.
*/
constructor(scene: BattleScene, mode: Mode | null = null) {
super(scene, mode);
super(scene, SettingType.DISPLAY, mode);
this.title = "Display";
this.settings = Setting.filter(s => s.type === SettingType.DISPLAY);
/**
* Update to current language from default value.

View File

@ -1,5 +1,5 @@
import BattleScene from "../../battle-scene";
import {Setting, SettingType} from "../../system/settings/settings";
import { SettingType } from "../../system/settings/settings";
import { Mode } from "../ui";
import AbstractSettingsUiHandler from "./abstract-settings-ui-handler";
@ -11,9 +11,8 @@ export default class SettingsUiHandler extends AbstractSettingsUiHandler {
* @param mode - The UI mode, optional.
*/
constructor(scene: BattleScene, mode: Mode | null = null) {
super(scene, mode);
super(scene, SettingType.GENERAL, mode);
this.title = "General";
this.settings = Setting.filter(s => s.type === SettingType.GENERAL);
this.localStorageKey = "settings";
}
}