Rework - Inputs management to include all gamepad mapping (#390)

* rework of the input handling, including different gamepad and keyboard

* rework of the input handling, including different gamepad and keyboard

* first version of a too complex inputHandler based on phaser3-merged-input

* removed useless control management and kept it simple for our use case, investigating to put out button_XX()

* renamed inputHandler to inputController

* aggregate directions and some action into a same method + fix menu return value

* added back repeated input feature on keeping down a key

* cleanup + return type

* fix submit/action doing two things simultaneously, still same behaviour as before

* extracted UI inputs out of battle-scene

* tab -> spaces

* tab -> spaces what about now github ?
This commit is contained in:
Greenlamp2 2024-05-03 18:59:10 +02:00 committed by GitHub
parent c44ec421b0
commit 70324c4159
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
33 changed files with 593 additions and 289 deletions

View File

@ -1,4 +1,4 @@
import Phaser, { Time } from 'phaser';
import Phaser from 'phaser';
import UI, { Mode } from './ui/ui';
import { NextEncounterPhase, NewBiomeEncounterPhase, SelectBiomePhase, MessagePhase, TurnInitPhase, ReturnPhase, LevelCapPhase, ShowTrainerPhase, LoginPhase, MovePhase, TitlePhase, SwitchPhase } from './phases';
import Pokemon, { PlayerPokemon, EnemyPokemon } from './field/pokemon';
@ -54,13 +54,14 @@ import CharSprite from './ui/char-sprite';
import DamageNumberHandler from './field/damage-number-handler';
import PokemonInfoContainer from './ui/pokemon-info-container';
import { biomeDepths } from './data/biomes';
import { initTouchControls } from './touch-controls';
import { UiTheme } from './enums/ui-theme';
import { SceneBase } from './scene-base';
import CandyBar from './ui/candy-bar';
import { Variant, variantData } from './data/variant';
import { Localizable } from './plugins/i18n';
import { STARTING_WAVE_OVERRIDE, OPP_SPECIES_OVERRIDE, SEED_OVERRIDE, STARTING_BIOME_OVERRIDE } from './overrides';
import {InputsController} from "./inputs-controller";
import {UiInputs} from "./ui-inputs";
export const bypassLogin = import.meta.env.VITE_BYPASS_LOGIN === "1";
@ -69,33 +70,12 @@ const DEBUG_RNG = false;
export const startingWave = STARTING_WAVE_OVERRIDE || 1;
const expSpriteKeys: string[] = [];
const repeatInputDelayMillis = 250;
export let starterColors: StarterColors;
interface StarterColors {
[key: string]: [string, string]
}
export enum Button {
UP,
DOWN,
LEFT,
RIGHT,
SUBMIT,
ACTION,
CANCEL,
MENU,
STATS,
CYCLE_SHINY,
CYCLE_FORM,
CYCLE_GENDER,
CYCLE_ABILITY,
CYCLE_NATURE,
CYCLE_VARIANT,
SPEED_UP,
SLOW_DOWN
}
export interface PokeballCounts {
[pb: string]: integer;
}
@ -104,6 +84,8 @@ export type AnySound = Phaser.Sound.WebAudioSound | Phaser.Sound.HTML5AudioSound
export default class BattleScene extends SceneBase {
public rexUI: UIPlugin;
public inputController: InputsController;
public uiInputs: UiInputs;
public sessionPlayTime: integer = null;
public masterVolume: number = 0.5;
@ -189,34 +171,6 @@ export default class BattleScene extends SceneBase {
private bgmResumeTimer: Phaser.Time.TimerEvent;
private bgmCache: Set<string> = new Set();
private playTimeTimer: Phaser.Time.TimerEvent;
private buttonKeys: Phaser.Input.Keyboard.Key[][];
private lastProcessedButtonPressTimes: Map<Button, number> = new Map();
// movementButtonLock ensures only a single movement key is firing repeated inputs
// (i.e. by holding down a button) at a time
private movementButtonLock: Button;
// using a dualshock controller as a map
private gamepadKeyConfig = {
[Button.UP]: 12, // up
[Button.DOWN]: 13, // down
[Button.LEFT]: 14, // left
[Button.RIGHT]: 15, // right
[Button.SUBMIT]: 17, // touchpad
[Button.ACTION]: 0, // X
[Button.CANCEL]: 1, // O
[Button.MENU]: 9, // options
[Button.STATS]: 8, // share
[Button.CYCLE_SHINY]: 5, // RB
[Button.CYCLE_FORM]: 4, // LB
[Button.CYCLE_GENDER]: 6, // LT
[Button.CYCLE_ABILITY]: 7, // RT
[Button.CYCLE_NATURE]: 2, // square
[Button.CYCLE_VARIANT]: 3, // triangle
[Button.SPEED_UP]: 10, // L3
[Button.SLOW_DOWN]: 11 // R3
};
public gamepadButtonStates: boolean[] = new Array(17).fill(false);
public rngCounter: integer = 0;
public rngSeedOverride: string = '';
@ -246,7 +200,9 @@ export default class BattleScene extends SceneBase {
this.load.atlas(key, `images/pokemon/${variant ? 'variant/' : ''}${experimental ? 'exp/' : ''}${atlasPath}.png`, `images/pokemon/${variant ? 'variant/' : ''}${experimental ? 'exp/' : ''}${atlasPath}.json`);
}
async preload() {
async preload() {
this.load.scenePlugin('inputController', InputsController);
this.load.scenePlugin('uiInputs', UiInputs);
if (DEBUG_RNG) {
const scene = this;
const originalRealInRange = Phaser.Math.RND.realInRange;
@ -260,7 +216,7 @@ export default class BattleScene extends SceneBase {
return ret;
};
}
populateAnims();
await this.initVariantData();
@ -273,8 +229,6 @@ export default class BattleScene extends SceneBase {
addUiThemeOverrides(this);
this.setupControls();
this.load.setBaseURL();
this.spritePipeline = new SpritePipeline(this.game);
@ -287,7 +241,6 @@ export default class BattleScene extends SceneBase {
}
update() {
this.checkInput();
this.ui?.update();
}
@ -606,42 +559,6 @@ export default class BattleScene extends SceneBase {
return true;
}
setupControls() {
const keyCodes = Phaser.Input.Keyboard.KeyCodes;
const keyConfig = {
[Button.UP]: [keyCodes.UP, keyCodes.W],
[Button.DOWN]: [keyCodes.DOWN, keyCodes.S],
[Button.LEFT]: [keyCodes.LEFT, keyCodes.A],
[Button.RIGHT]: [keyCodes.RIGHT, keyCodes.D],
[Button.SUBMIT]: [keyCodes.ENTER],
[Button.ACTION]: [keyCodes.SPACE, keyCodes.ENTER, keyCodes.Z],
[Button.CANCEL]: [keyCodes.BACKSPACE, keyCodes.X],
[Button.MENU]: [keyCodes.ESC, keyCodes.M],
[Button.STATS]: [keyCodes.SHIFT, keyCodes.C],
[Button.CYCLE_SHINY]: [keyCodes.R],
[Button.CYCLE_FORM]: [keyCodes.F],
[Button.CYCLE_GENDER]: [keyCodes.G],
[Button.CYCLE_ABILITY]: [keyCodes.E],
[Button.CYCLE_NATURE]: [keyCodes.N],
[Button.CYCLE_VARIANT]: [keyCodes.V],
[Button.SPEED_UP]: [keyCodes.PLUS],
[Button.SLOW_DOWN]: [keyCodes.MINUS]
};
const mobileKeyConfig = {};
this.buttonKeys = [];
for (let b of Utils.getEnumValues(Button)) {
const keys: Phaser.Input.Keyboard.Key[] = [];
if (keyConfig.hasOwnProperty(b)) {
for (let k of keyConfig[b])
keys.push(this.input.keyboard.addKey(k, false));
mobileKeyConfig[Button[b]] = keys[0];
}
this.buttonKeys[b] = keys;
}
initTouchControls(mobileKeyConfig);
}
getParty(): PlayerPokemon[] {
return this.party;
}
@ -1343,177 +1260,6 @@ export default class BattleScene extends SceneBase {
return biomes[Utils.randSeedInt(biomes.length)];
}
checkInput(): boolean {
let inputSuccess = false;
let vibrationLength = 0;
if (this.buttonJustPressed(Button.UP) || this.repeatInputDurationJustPassed(Button.UP)) {
inputSuccess = this.ui.processInput(Button.UP);
vibrationLength = 5;
this.setLastProcessedMovementTime(Button.UP)
} else if (this.buttonJustPressed(Button.DOWN) || this.repeatInputDurationJustPassed(Button.DOWN)) {
inputSuccess = this.ui.processInput(Button.DOWN);
vibrationLength = 5;
this.setLastProcessedMovementTime(Button.DOWN)
} else if (this.buttonJustPressed(Button.LEFT) || this.repeatInputDurationJustPassed(Button.LEFT)) {
inputSuccess = this.ui.processInput(Button.LEFT);
vibrationLength = 5;
this.setLastProcessedMovementTime(Button.LEFT)
} else if (this.buttonJustPressed(Button.RIGHT) || this.repeatInputDurationJustPassed(Button.RIGHT)) {
inputSuccess = this.ui.processInput(Button.RIGHT);
vibrationLength = 5;
this.setLastProcessedMovementTime(Button.RIGHT)
} else if (this.buttonJustPressed(Button.SUBMIT) || this.repeatInputDurationJustPassed(Button.SUBMIT)) {
inputSuccess = this.ui.processInput(Button.SUBMIT) || this.ui.processInput(Button.ACTION);
this.setLastProcessedMovementTime(Button.SUBMIT);
} else if (this.buttonJustPressed(Button.ACTION) || this.repeatInputDurationJustPassed(Button.ACTION)) {
inputSuccess = this.ui.processInput(Button.ACTION);
this.setLastProcessedMovementTime(Button.ACTION);
} else if (this.buttonJustPressed(Button.CANCEL)|| this.repeatInputDurationJustPassed(Button.CANCEL)) {
inputSuccess = this.ui.processInput(Button.CANCEL);
this.setLastProcessedMovementTime(Button.CANCEL);
} else if (this.buttonJustPressed(Button.MENU)) {
if (this.disableMenu)
return;
switch (this.ui?.getMode()) {
case Mode.MESSAGE:
if (!(this.ui.getHandler() as MessageUiHandler).pendingPrompt)
return;
case Mode.TITLE:
case Mode.COMMAND:
case Mode.FIGHT:
case Mode.BALL:
case Mode.TARGET_SELECT:
case Mode.SAVE_SLOT:
case Mode.PARTY:
case Mode.SUMMARY:
case Mode.STARTER_SELECT:
case Mode.CONFIRM:
case Mode.OPTION_SELECT:
this.ui.setOverlayMode(Mode.MENU);
inputSuccess = true;
break;
case Mode.MENU:
case Mode.SETTINGS:
case Mode.ACHIEVEMENTS:
this.ui.revertMode();
this.playSound('select');
inputSuccess = true;
break;
default:
return;
}
} else if (this.ui?.getHandler() instanceof StarterSelectUiHandler) {
if (this.buttonJustPressed(Button.CYCLE_SHINY)) {
inputSuccess = this.ui.processInput(Button.CYCLE_SHINY);
this.setLastProcessedMovementTime(Button.CYCLE_SHINY);
} else if (this.buttonJustPressed(Button.CYCLE_FORM)) {
inputSuccess = this.ui.processInput(Button.CYCLE_FORM);
this.setLastProcessedMovementTime(Button.CYCLE_FORM);
} else if (this.buttonJustPressed(Button.CYCLE_GENDER)) {
inputSuccess = this.ui.processInput(Button.CYCLE_GENDER);
this.setLastProcessedMovementTime(Button.CYCLE_GENDER);
} else if (this.buttonJustPressed(Button.CYCLE_ABILITY)) {
inputSuccess = this.ui.processInput(Button.CYCLE_ABILITY);
this.setLastProcessedMovementTime(Button.CYCLE_ABILITY);
} else if (this.buttonJustPressed(Button.CYCLE_NATURE)) {
inputSuccess = this.ui.processInput(Button.CYCLE_NATURE);
this.setLastProcessedMovementTime(Button.CYCLE_NATURE);
} else if (this.buttonJustPressed(Button.CYCLE_VARIANT)) {
inputSuccess = this.ui.processInput(Button.CYCLE_VARIANT);
this.setLastProcessedMovementTime(Button.CYCLE_VARIANT);
} else
return;
} else if (this.buttonJustPressed(Button.SPEED_UP)) {
if (this.gameSpeed < 5) {
this.gameData.saveSetting(Setting.Game_Speed, settingOptions[Setting.Game_Speed].indexOf(`${this.gameSpeed}x`) + 1);
if (this.ui?.getMode() === Mode.SETTINGS)
(this.ui.getHandler() as SettingsUiHandler).show([]);
}
} else if (this.buttonJustPressed(Button.SLOW_DOWN)) {
if (this.gameSpeed > 1) {
this.gameData.saveSetting(Setting.Game_Speed, Math.max(settingOptions[Setting.Game_Speed].indexOf(`${this.gameSpeed}x`) - 1, 0));
if (this.ui?.getMode() === Mode.SETTINGS)
(this.ui.getHandler() as SettingsUiHandler).show([]);
}
} else {
let pressed = false;
if (this.ui && (this.buttonJustReleased(Button.STATS) || (pressed = this.buttonJustPressed(Button.STATS)))) {
for (let p of this.getField().filter(p => p?.isActive(true)))
p.toggleStats(pressed);
if (pressed)
this.setLastProcessedMovementTime(Button.STATS);
} else
return;
}
if (inputSuccess && this.enableVibration && typeof navigator.vibrate !== 'undefined')
navigator.vibrate(vibrationLength || 10);
}
/**
* gamepadButtonJustDown returns true if @param button has just been pressed down
* or not. It will only return true once, until the key is released and pressed down
* again.
*/
gamepadButtonJustDown(button: Phaser.Input.Gamepad.Button): boolean {
if (!button || !this.gamepadSupport)
return false;
let ret = false;
if (button.pressed) {
if (!this.gamepadButtonStates[button.index])
ret = true;
this.gamepadButtonStates[button.index] = true;
} else
this.gamepadButtonStates[button.index] = false;
return ret;
}
buttonJustPressed(button: Button): boolean {
const gamepad = this.input.gamepad?.gamepads[0];
return this.buttonKeys[button].some(k => Phaser.Input.Keyboard.JustDown(k)) || this.gamepadButtonJustDown(gamepad?.buttons[this.gamepadKeyConfig[button]]);
}
/**
* gamepadButtonJustUp returns true if @param button has just been released
* or not. It will only return true once, until the key is released and pressed down
* again.
*/
gamepadButtonJustUp(button: Phaser.Input.Gamepad.Button): boolean {
if (!button || !this.gamepadSupport)
return false;
return !this.gamepadButtonStates[button.index];
}
buttonJustReleased(button: Button): boolean {
const gamepad = this.input.gamepad?.gamepads[0];
return this.buttonKeys[button].some(k => Phaser.Input.Keyboard.JustUp(k)) || this.gamepadButtonJustUp(gamepad?.buttons[this.gamepadKeyConfig[button]]);
}
/**
* repeatInputDurationJustPassed returns true if @param button has been held down long
* enough to fire a repeated input. A button must claim the movementButtonLock before
* firing a repeated input - this is to prevent multiple buttons from firing repeatedly.
*/
repeatInputDurationJustPassed(button: Button): boolean {
if (this.movementButtonLock !== null && this.movementButtonLock !== button) {
return false;
}
if (this.buttonKeys[button].every(k => k.isUp) && this.gamepadButtonStates.every(b => b == false)) {
this.movementButtonLock = null;
return false;
}
if (this.time.now - this.lastProcessedButtonPressTimes.get(button) >= repeatInputDelayMillis) {
return true;
}
}
setLastProcessedMovementTime(button: Button) {
this.lastProcessedButtonPressTimes.set(button, this.time.now);
this.movementButtonLock = button;
}
isBgmPlaying(): boolean {
return this.bgm && this.bgm.isPlaying;
}

View File

@ -0,0 +1,29 @@
/**
* Dualshock mapping
*/
const pad_dualshock = {
padID: 'Dualshock',
padType: 'Sony',
gamepadMapping: {
RC_S: 0,
RC_E: 1,
RC_W: 2,
RC_N: 3,
START: 9, // Options
SELECT: 8, // Share
LB: 4,
RB: 5,
LT: 6,
RT: 7,
LS: 10,
RS: 11,
LC_N: 12,
LC_S: 13,
LC_W: 14,
LC_E: 15,
MENU: 16,
TOUCH: 17
},
};
export default pad_dualshock;

View File

@ -0,0 +1,27 @@
/**
* Generic pad mapping
*/
const pad_generic = {
padID: 'Generic',
padType: 'generic',
gamepadMapping: {
RC_S: 0,
RC_E: 1,
RC_W: 2,
RC_N: 3,
START: 9,
SELECT: 8,
LB: 4,
RB: 5,
LT: 6,
RT: 7,
LS: 10,
RS: 11,
LC_N: 12,
LC_S: 13,
LC_W: 14,
LC_E: 15
},
};
export default pad_generic;

View File

@ -0,0 +1,23 @@
/**
* 081f-e401 - UnlicensedSNES
*/
const pad_unlicensedSNES = {
padID: '081f-e401',
padType: 'snes',
gamepadMapping : {
RC_S: 2,
RC_E: 1,
RC_W: 3,
RC_N: 0,
START: 9,
SELECT: 8,
LB: 4,
RB: 5,
LC_N: 12,
LC_S: 13,
LC_W: 14,
LC_E: 15
}
};
export default pad_unlicensedSNES;

View File

@ -0,0 +1,28 @@
/**
* Generic pad mapping
*/
const pad_xbox360 = {
padID: 'Xbox 360 controller (XInput STANDARD GAMEPAD)',
padType: 'xbox',
gamepadMapping: {
RC_S: 0,
RC_E: 1,
RC_W: 2,
RC_N: 3,
START: 9,
SELECT: 8,
LB: 4,
RB: 5,
LT: 6,
RT: 7,
LS: 10,
RS: 11,
LC_N: 12,
LC_S: 13,
LC_W: 14,
LC_E: 15,
MENU: 16
},
};
export default pad_xbox360;

266
src/inputs-controller.ts Normal file
View File

@ -0,0 +1,266 @@
import Phaser, {Time} from "phaser";
import * as Utils from "./utils";
import {initTouchControls} from './touch-controls';
import pad_generic from "#app/configs/pad_generic";
import pad_unlicensedSNES from "#app/configs/pad_unlicensedSNES";
import pad_xbox360 from "#app/configs/pad_xbox360";
import pad_dualshock from "#app/configs/pad_dualshock";
export enum Button {
UP,
DOWN,
LEFT,
RIGHT,
SUBMIT,
ACTION,
CANCEL,
MENU,
STATS,
CYCLE_SHINY,
CYCLE_FORM,
CYCLE_GENDER,
CYCLE_ABILITY,
CYCLE_NATURE,
CYCLE_VARIANT,
SPEED_UP,
SLOW_DOWN
}
const repeatInputDelayMillis = 250;
export class InputsController extends Phaser.Plugins.ScenePlugin {
private game: Phaser.Game;
private buttonKeys: Phaser.Input.Keyboard.Key[][];
private gamepads: Array<string> = new Array();
private scene: Phaser.Scene;
// buttonLock ensures only a single movement key is firing repeated inputs
// (i.e. by holding down a button) at a time
private buttonLock: Button;
private interactions: Map<Button, Map<string, boolean>> = new Map();
private time: Time;
constructor(scene: Phaser.Scene, pluginManager: Phaser.Plugins.PluginManager, pluginKey: string) {
super(scene, pluginManager, pluginKey);
this.game = pluginManager.game;
this.scene = scene;
this.time = this.scene.time;
this.buttonKeys = [];
for (const b of Utils.getEnumValues(Button)) {
this.interactions[b] = {
pressTime: false,
isPressed: false,
}
}
// We don't want the menu key to be repeated
delete this.interactions[Button.MENU];
}
boot() {
this.eventEmitter = this.systems.events;
this.events = new Phaser.Events.EventEmitter();
this.game.events.on(Phaser.Core.Events.STEP, this.update, this);
if (typeof this.systems.input.gamepad !== 'undefined') {
this.systems.input.gamepad.on('connected', function (thisGamepad) {
this.refreshGamepads();
this.setupGamepad(thisGamepad);
}, this);
// Check to see if the gamepad has already been setup by the browser
this.systems.input.gamepad.refreshPads();
if (this.systems.input.gamepad.total) {
this.refreshGamepads();
for (const thisGamepad of this.gamepads) {
this.systems.input.gamepad.emit('connected', thisGamepad);
}
}
this.systems.input.gamepad.on('down', this.gamepadButtonDown, this);
this.systems.input.gamepad.on('up', this.gamepadButtonUp, this);
}
// Keyboard
this.setupKeyboardControls();
}
update() {
for (const b of Utils.getEnumValues(Button)) {
if (!this.interactions.hasOwnProperty(b)) continue;
if (this.repeatInputDurationJustPassed(b)) {
this.events.emit('input_down', {
controller_type: 'keyboard',
button: b,
});
this.setLastProcessedMovementTime(b);
}
}
}
setupGamepad(thisGamepad): void {
let gamepadID = thisGamepad.id.toLowerCase();
const mappedPad = this.mapGamepad(gamepadID);
this.player = {
'mapping': mappedPad.gamepadMapping,
}
}
refreshGamepads(): void {
// Sometimes, gamepads are undefined. For some reason.
this.gamepads = this.systems.input.gamepad.gamepads.filter(function (el) {
return el != null;
});
for (const [index, thisGamepad] of this.gamepads.entries()) {
thisGamepad.index = index; // Overwrite the gamepad index, in case we had undefined gamepads earlier
}
}
getActionGamepadMapping() {
const gamepadMapping = {};
gamepadMapping[this.player.mapping.LC_N] = Button.UP;
gamepadMapping[this.player.mapping.LC_S] = Button.DOWN;
gamepadMapping[this.player.mapping.LC_W] = Button.LEFT;
gamepadMapping[this.player.mapping.LC_E] = Button.RIGHT;
gamepadMapping[this.player.mapping.TOUCH] = Button.SUBMIT;
gamepadMapping[this.player.mapping.RC_S] = Button.ACTION;
gamepadMapping[this.player.mapping.RC_E] = Button.CANCEL;
gamepadMapping[this.player.mapping.SELECT] = Button.STATS;
gamepadMapping[this.player.mapping.START] = Button.MENU;
gamepadMapping[this.player.mapping.RB] = Button.CYCLE_SHINY;
gamepadMapping[this.player.mapping.LB] = Button.CYCLE_FORM;
gamepadMapping[this.player.mapping.LT] = Button.CYCLE_GENDER;
gamepadMapping[this.player.mapping.RT] = Button.CYCLE_ABILITY;
gamepadMapping[this.player.mapping.RC_W] = Button.CYCLE_NATURE;
gamepadMapping[this.player.mapping.RC_N] = Button.CYCLE_VARIANT;
gamepadMapping[this.player.mapping.LS] = Button.SPEED_UP;
gamepadMapping[this.player.mapping.RS] = Button.SLOW_DOWN;
return gamepadMapping;
}
gamepadButtonDown(pad, button, value): void {
const actionMapping = this.getActionGamepadMapping();
const buttonDown = actionMapping.hasOwnProperty(button.index) && actionMapping[button.index];
if (buttonDown !== undefined) {
this.events.emit('input_down', {
controller_type: 'gamepad',
button: buttonDown,
});
this.setLastProcessedMovementTime(buttonDown);
}
}
gamepadButtonUp(pad, button, value): void {
const actionMapping = this.getActionGamepadMapping();
const buttonUp = actionMapping.hasOwnProperty(button.index) && actionMapping[button.index];
if (buttonUp !== undefined) {
this.events.emit('input_up', {
controller_type: 'gamepad',
button: buttonUp,
});
this.delLastProcessedMovementTime(buttonUp);
}
}
setupKeyboardControls(): void {
const keyCodes = Phaser.Input.Keyboard.KeyCodes;
const keyConfig = {
[Button.UP]: [keyCodes.UP, keyCodes.W],
[Button.DOWN]: [keyCodes.DOWN, keyCodes.S],
[Button.LEFT]: [keyCodes.LEFT, keyCodes.A],
[Button.RIGHT]: [keyCodes.RIGHT, keyCodes.D],
[Button.SUBMIT]: [keyCodes.ENTER],
[Button.ACTION]: [keyCodes.SPACE, keyCodes.Z],
[Button.CANCEL]: [keyCodes.BACKSPACE, keyCodes.X],
[Button.MENU]: [keyCodes.ESC, keyCodes.M],
[Button.STATS]: [keyCodes.SHIFT, keyCodes.C],
[Button.CYCLE_SHINY]: [keyCodes.R],
[Button.CYCLE_FORM]: [keyCodes.F],
[Button.CYCLE_GENDER]: [keyCodes.G],
[Button.CYCLE_ABILITY]: [keyCodes.E],
[Button.CYCLE_NATURE]: [keyCodes.N],
[Button.CYCLE_VARIANT]: [keyCodes.V],
[Button.SPEED_UP]: [keyCodes.PLUS],
[Button.SLOW_DOWN]: [keyCodes.MINUS]
};
const mobileKeyConfig = {};
for (const b of Utils.getEnumValues(Button)) {
const keys: Phaser.Input.Keyboard.Key[] = [];
if (keyConfig.hasOwnProperty(b)) {
for (let k of keyConfig[b])
keys.push(this.systems.input.keyboard.addKey(k, false));
mobileKeyConfig[Button[b]] = keys[0];
}
this.buttonKeys[b] = keys;
}
initTouchControls(mobileKeyConfig);
this.listenInputKeyboard();
}
listenInputKeyboard(): void {
this.buttonKeys.forEach((row, index) => {
for (const key of row) {
key.on('down', () => {
this.events.emit('input_down', {
controller_type: 'keyboard',
button: index,
});
this.setLastProcessedMovementTime(index);
});
key.on('up', () => {
this.events.emit('input_up', {
controller_type: 'keyboard',
button: index,
});
this.delLastProcessedMovementTime(index);
});
}
});
}
mapGamepad(id) {
id = id.toLowerCase();
let padConfig = pad_generic;
if (id.includes('081f') && id.includes('e401')) {
padConfig = pad_unlicensedSNES;
} else if (id.includes('xbox') && id.includes('360')) {
padConfig = pad_xbox360;
} else if (id.includes('054c')) {
padConfig = pad_dualshock;
}
return padConfig;
}
/**
* repeatInputDurationJustPassed returns true if @param button has been held down long
* enough to fire a repeated input. A button must claim the buttonLock before
* firing a repeated input - this is to prevent multiple buttons from firing repeatedly.
*/
repeatInputDurationJustPassed(button: Button): boolean {
if (this.buttonLock === null || this.buttonLock !== button) {
return false;
}
if (this.time.now - this.interactions[button].pressTime >= repeatInputDelayMillis) {
this.buttonLock = null;
return true;
}
}
setLastProcessedMovementTime(button: Button): void {
if (!this.interactions.hasOwnProperty(button)) return;
this.buttonLock = button;
this.interactions[button].pressTime = this.time.now;
}
delLastProcessedMovementTime(button: Button): void {
if (!this.interactions.hasOwnProperty(button)) return;
this.buttonLock = null;
this.interactions[button].pressTime = null;
}
}

159
src/ui-inputs.ts Normal file
View File

@ -0,0 +1,159 @@
import Phaser from "phaser";
import UI, {Mode} from "./ui/ui";
import {Button} from "#app/inputs-controller";
import MessageUiHandler from "#app/ui/message-ui-handler";
import StarterSelectUiHandler from "#app/ui/starter-select-ui-handler";
import {Setting, settingOptions} from "#app/system/settings";
import SettingsUiHandler from "#app/ui/settings-ui-handler";
export class UiInputs extends Phaser.Plugins.ScenePlugin {
private game: Phaser.Game;
private scene: Phaser.Scene;
private events;
constructor(scene: Phaser.Scene, pluginManager: Phaser.Plugins.PluginManager, pluginKey: string) {
super(scene, pluginManager, pluginKey);
this.game = pluginManager.game;
this.scene = scene;
this.events = this.scene.inputController.events
}
boot() {
this.listenInputs();
}
listenInputs(): void {
this.events.on('input_down', (event) => {
const actions = this.getActionsKeyDown();
if (!actions.hasOwnProperty(event.button)) return;
const [inputSuccess, vibrationLength] = actions[event.button]();
if (inputSuccess && this.scene.enableVibration && typeof navigator.vibrate !== 'undefined')
navigator.vibrate(vibrationLength);
}, this);
this.events.on('input_up', (event) => {
const actions = this.getActionsKeyUp();
if (!actions.hasOwnProperty(event.button)) return;
const [inputSuccess, vibrationLength] = actions[event.button]();
if (inputSuccess && this.scene.enableVibration && typeof navigator.vibrate !== 'undefined')
navigator.vibrate(vibrationLength);
}, this);
}
getActionsKeyDown() {
const actions = {};
actions[Button.UP] = () => this.buttonDirection(Button.UP);
actions[Button.DOWN] = () => this.buttonDirection(Button.DOWN);
actions[Button.LEFT] = () => this.buttonDirection(Button.LEFT);
actions[Button.RIGHT] = () => this.buttonDirection(Button.RIGHT);
actions[Button.SUBMIT] = () => this.buttonTouch();
actions[Button.ACTION] = () => this.buttonAb(Button.ACTION);
actions[Button.CANCEL] = () => this.buttonAb(Button.CANCEL);
actions[Button.MENU] = () => this.buttonMenu();
actions[Button.STATS] = () => this.buttonStats(true);
actions[Button.CYCLE_SHINY] = () => this.buttonCycleOption(Button.CYCLE_SHINY);
actions[Button.CYCLE_FORM] = () => this.buttonCycleOption(Button.CYCLE_FORM);
actions[Button.CYCLE_GENDER] = () => this.buttonCycleOption(Button.CYCLE_GENDER);
actions[Button.CYCLE_ABILITY] = () => this.buttonCycleOption(Button.CYCLE_ABILITY);
actions[Button.CYCLE_NATURE] = () => this.buttonCycleOption(Button.CYCLE_NATURE);
actions[Button.CYCLE_VARIANT] = () => this.buttonCycleOption(Button.CYCLE_VARIANT);
actions[Button.SPEED_UP] = () => this.buttonSpeedChange();
actions[Button.SLOW_DOWN] = () => this.buttonSpeedChange(false);
return actions;
}
getActionsKeyUp() {
const actions = {};
actions[Button.STATS] = () => this.buttonStats(false);
return actions;
}
buttonDirection(direction): Array<boolean | number> {
const inputSuccess = this.scene.ui.processInput(direction);
const vibrationLength = 5;
return [inputSuccess, vibrationLength];
}
buttonAb(button): Array<boolean | number> {
const inputSuccess = this.scene.ui.processInput(button);
return [inputSuccess, 0];
}
buttonTouch(): Array<boolean | number> {
const inputSuccess = this.scene.ui.processInput(Button.SUBMIT) || this.scene.ui.processInput(Button.ACTION);
return [inputSuccess, 0];
}
buttonStats(pressed = true): Array<boolean | number> {
if (pressed) {
for (let p of this.scene.getField().filter(p => p?.isActive(true)))
p.toggleStats(true);
} else {
for (let p of this.scene.getField().filter(p => p?.isActive(true)))
p.toggleStats(false);
}
return [true, 0];
}
buttonMenu(): Array<boolean | number> {
let inputSuccess;
if (this.scene.disableMenu)
return [true, 0];
switch (this.scene.ui?.getMode()) {
case Mode.MESSAGE:
if (!(this.scene.ui.getHandler() as MessageUiHandler).pendingPrompt)
return [true, 0];
case Mode.TITLE:
case Mode.COMMAND:
case Mode.FIGHT:
case Mode.BALL:
case Mode.TARGET_SELECT:
case Mode.SAVE_SLOT:
case Mode.PARTY:
case Mode.SUMMARY:
case Mode.STARTER_SELECT:
case Mode.CONFIRM:
case Mode.OPTION_SELECT:
this.scene.ui.setOverlayMode(Mode.MENU);
inputSuccess = true;
break;
case Mode.MENU:
case Mode.SETTINGS:
case Mode.ACHIEVEMENTS:
this.scene.ui.revertMode();
this.scene.playSound('select');
inputSuccess = true;
break;
default:
return [true, 0];
}
return [inputSuccess, 0];
}
buttonCycleOption(button): Array<boolean | number> {
let inputSuccess;
if (this.scene.ui?.getHandler() instanceof StarterSelectUiHandler) {
inputSuccess = this.scene.ui.processInput(button);
}
return [inputSuccess, 0];
}
buttonSpeedChange(up = true): Array<boolean | number> {
if (up) {
if (this.scene.gameSpeed < 5) {
this.scene.gameData.saveSetting(Setting.Game_Speed, settingOptions[Setting.Game_Speed].indexOf(`${this.scene.gameSpeed}x`) + 1);
if (this.scene.ui?.getMode() === Mode.SETTINGS)
(this.scene.ui.getHandler() as SettingsUiHandler).show([]);
}
return [0, 0];
}
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));
if (this.scene.ui?.getMode() === Mode.SETTINGS)
(this.scene.ui.getHandler() as SettingsUiHandler).show([]);
}
return [0, 0];
}
}

View File

@ -1,10 +1,11 @@
import BattleScene, { Button } from "../battle-scene";
import BattleScene from "../battle-scene";
import { TextStyle, addTextObject } from "./text";
import { Mode } from "./ui";
import UiHandler from "./ui-handler";
import { addWindow } from "./ui-theme";
import * as Utils from "../utils";
import { argbFromRgba } from "@material/material-color-utilities";
import {Button} from "#app/inputs-controller";
export interface OptionSelectConfig {
xOffset?: number;

View File

@ -1,9 +1,10 @@
import BattleScene, { Button } from "../battle-scene";
import BattleScene from "../battle-scene";
import { Achv, achvs } from "../system/achv";
import MessageUiHandler from "./message-ui-handler";
import { TextStyle, addTextObject } from "./text";
import { Mode } from "./ui";
import { addWindow } from "./ui-theme";
import {Button} from "#app/inputs-controller";
export default class AchvsUiHandler extends MessageUiHandler {
private achvsContainer: Phaser.GameObjects.Container;

View File

@ -1,6 +1,7 @@
import BattleScene, { Button } from "../battle-scene";
import BattleScene from "../battle-scene";
import { Mode } from "./ui";
import UiHandler from "./ui-handler";
import {Button} from "#app/inputs-controller";
export default abstract class AwaitableUiHandler extends UiHandler {
protected awaitingActionInput: boolean;

View File

@ -1,11 +1,12 @@
import { CommandPhase } from "../phases";
import BattleScene, { Button } from "../battle-scene";
import BattleScene from "../battle-scene";
import { getPokeballName } from "../data/pokeball";
import { addTextObject, TextStyle } from "./text";
import { Command } from "./command-ui-handler";
import { Mode } from "./ui";
import UiHandler from "./ui-handler";
import { addWindow } from "./ui-theme";
import {Button} from "#app/inputs-controller";
export default class BallUiHandler extends UiHandler {
private pokeballSelectContainer: Phaser.GameObjects.Container;

View File

@ -1,4 +1,4 @@
import BattleScene, { Button } from "../battle-scene";
import BattleScene from "../battle-scene";
import { addBBCodeTextObject, addTextObject, getTextColor, TextStyle } from "./text";
import { Mode } from "./ui";
import * as Utils from "../utils";
@ -6,6 +6,7 @@ import MessageUiHandler from "./message-ui-handler";
import { getStatName, Stat } from "../data/pokemon-stat";
import { addWindow } from "./ui-theme";
import BBCodeText from "phaser3-rex-plugins/plugins/bbcodetext";
import {Button} from "#app/inputs-controller";
export default class BattleMessageUiHandler extends MessageUiHandler {
private levelUpStatsContainer: Phaser.GameObjects.Container;

View File

@ -1,10 +1,11 @@
import { CommandPhase } from "../phases";
import BattleScene, { Button } from "../battle-scene";
import BattleScene from "../battle-scene";
import { addTextObject, TextStyle } from "./text";
import PartyUiHandler, { PartyUiMode } from "./party-ui-handler";
import { Mode } from "./ui";
import UiHandler from "./ui-handler";
import i18next from '../plugins/i18n';
import {Button} from "#app/inputs-controller";
export enum Command {
FIGHT = 0,

View File

@ -1,6 +1,7 @@
import BattleScene, { Button } from "../battle-scene";
import BattleScene from "../battle-scene";
import AbstractOptionSelectUiHandler, { OptionSelectConfig } from "./abstact-option-select-ui-handler";
import { Mode } from "./ui";
import {Button} from "#app/inputs-controller";
export default class ConfirmUiHandler extends AbstractOptionSelectUiHandler {
private switchCheck: boolean;

View File

@ -1,4 +1,4 @@
import BattleScene, { Button } from "../battle-scene";
import BattleScene from "../battle-scene";
import { Mode } from "./ui";
import { TextStyle, addTextObject, getEggTierTextTint } from "./text";
import MessageUiHandler from "./message-ui-handler";
@ -9,6 +9,7 @@ import { getPokemonSpecies } from "../data/pokemon-species";
import { addWindow } from "./ui-theme";
import { Tutorial, handleTutorial } from "../tutorial";
import { EggTier } from "../data/enums/egg-type";
import {Button} from "#app/inputs-controller";
const defaultText = 'Select a machine.';

View File

@ -1,7 +1,8 @@
import BattleScene, { Button } from "../battle-scene";
import BattleScene from "../battle-scene";
import { EggHatchPhase } from "../egg-hatch-phase";
import { Mode } from "./ui";
import UiHandler from "./ui-handler";
import {Button} from "#app/inputs-controller";
export default class EggHatchSceneHandler extends UiHandler {
public eggHatchContainer: Phaser.GameObjects.Container;

View File

@ -1,4 +1,4 @@
import BattleScene, { Button } from "../battle-scene";
import BattleScene from "../battle-scene";
import { Mode } from "./ui";
import PokemonIconAnimHandler, { PokemonIconAnimMode } from "./pokemon-icon-anim-handler";
import { TextStyle, addTextObject } from "./text";
@ -6,6 +6,7 @@ import MessageUiHandler from "./message-ui-handler";
import { EGG_SEED, Egg, GachaType, getEggGachaTypeDescriptor, getEggHatchWavesMessage, getEggDescriptor } from "../data/egg";
import * as Utils from "../utils";
import { addWindow } from "./ui-theme";
import {Button} from "#app/inputs-controller";
export default class EggListUiHandler extends MessageUiHandler {
private eggListContainer: Phaser.GameObjects.Container;

View File

@ -1,7 +1,8 @@
import BattleScene, { Button } from "../battle-scene";
import BattleScene from "../battle-scene";
import MessageUiHandler from "./message-ui-handler";
import { TextStyle, addTextObject } from "./text";
import { Mode } from "./ui";
import {Button} from "#app/inputs-controller";
export default class EvolutionSceneHandler extends MessageUiHandler {
public evolutionContainer: Phaser.GameObjects.Container;

View File

@ -1,4 +1,4 @@
import BattleScene, { Button } from "../battle-scene";
import BattleScene from "../battle-scene";
import { addTextObject, TextStyle } from "./text";
import { Type } from "../data/type";
import { Command } from "./command-ui-handler";
@ -8,6 +8,7 @@ import * as Utils from "../utils";
import { CommandPhase } from "../phases";
import { MoveCategory } from "#app/data/move.js";
import i18next from '../plugins/i18n';
import {Button} from "#app/inputs-controller";
export default class FightUiHandler extends UiHandler {
private movesContainer: Phaser.GameObjects.Container;

View File

@ -1,4 +1,4 @@
import BattleScene, { Button } from "../battle-scene";
import BattleScene from "../battle-scene";
import { ModalConfig, ModalUiHandler } from "./modal-ui-handler";
import { Mode } from "./ui";
import { TextStyle, addTextInputObject, addTextObject } from "./text";
@ -6,6 +6,7 @@ import { WindowVariant, addWindow } from "./ui-theme";
import InputText from "phaser3-rex-plugins/plugins/inputtext";
import * as Utils from "../utils";
import i18next from '../plugins/i18n';
import {Button} from "#app/inputs-controller";
export interface FormModalConfig extends ModalConfig {
errorMessage?: string;

View File

@ -1,4 +1,4 @@
import BattleScene, { Button } from "../battle-scene";
import BattleScene from "../battle-scene";
import { TextStyle, addTextObject, getTextColor } from "./text";
import { Mode } from "./ui";
import UiHandler from "./ui-handler";
@ -6,6 +6,7 @@ import { addWindow } from "./ui-theme";
import * as Utils from "../utils";
import { DexAttr, GameData } from "../system/game-data";
import { speciesStarters } from "../data/pokemon-species";
import {Button} from "#app/inputs-controller";
interface DisplayStat {
label?: string;

View File

@ -1,4 +1,4 @@
import BattleScene, { Button, bypassLogin } from "../battle-scene";
import BattleScene, { bypassLogin } from "../battle-scene";
import { TextStyle, addTextObject } from "./text";
import { Mode } from "./ui";
import * as Utils from "../utils";
@ -9,6 +9,7 @@ import { OptionSelectConfig, OptionSelectItem } from "./abstact-option-select-ui
import { Tutorial, handleTutorial } from "../tutorial";
import { updateUserInfo } from "../account";
import i18next from '../plugins/i18n';
import {Button} from "#app/inputs-controller";
export enum MenuOptions {
GAME_SETTINGS,

View File

@ -1,8 +1,9 @@
import BattleScene, { Button } from "../battle-scene";
import BattleScene from "../battle-scene";
import { TextStyle, addTextObject } from "./text";
import { Mode } from "./ui";
import UiHandler from "./ui-handler";
import { WindowVariant, addWindow } from "./ui-theme";
import {Button} from "#app/inputs-controller";
export interface ModalConfig {
buttonActions: Function[];

View File

@ -1,4 +1,4 @@
import BattleScene, { Button } from "../battle-scene";
import BattleScene from "../battle-scene";
import { getPlayerShopModifierTypeOptionsForWave, ModifierTypeOption } from "../modifier/modifier-type";
import { getPokeballAtlasKey, PokeballType } from "../data/pokeball";
import { addTextObject, getModifierTierTextTint, getTextColor, TextStyle } from "./text";
@ -6,6 +6,7 @@ import AwaitableUiHandler from "./awaitable-ui-handler";
import { Mode } from "./ui";
import { LockModifierTiersModifier, PokemonHeldItemModifier } from "../modifier/modifier";
import { handleTutorial, Tutorial } from "../tutorial";
import {Button} from "#app/inputs-controller";
export const SHOP_OPTIONS_ROW_LIMIT = 6;

View File

@ -1,5 +1,5 @@
import { CommandPhase } from "../phases";
import BattleScene, { Button } from "../battle-scene";
import BattleScene from "../battle-scene";
import { PlayerPokemon, PokemonMove } from "../field/pokemon";
import { addTextObject, TextStyle } from "./text";
import { Command } from "./command-ui-handler";
@ -16,6 +16,7 @@ import { pokemonEvolutions } from "../data/pokemon-evolutions";
import { addWindow } from "./ui-theme";
import { SpeciesFormChangeItemTrigger } from "../data/pokemon-forms";
import { getVariantTint } from "#app/data/variant";
import {Button} from "#app/inputs-controller";
const defaultMessage = 'Choose a Pokémon.';

View File

@ -1,4 +1,4 @@
import BattleScene, { Button } from "../battle-scene";
import BattleScene from "../battle-scene";
import { gameModes } from "../game-mode";
import { SessionSaveData } from "../system/game-data";
import { TextStyle, addTextObject } from "./text";
@ -8,6 +8,7 @@ import * as Utils from "../utils";
import PokemonData from "../system/pokemon-data";
import { PokemonHeldItemModifier } from "../modifier/modifier";
import MessageUiHandler from "./message-ui-handler";
import {Button} from "#app/inputs-controller";
const sessionSlotCount = 5;

View File

@ -1,10 +1,11 @@
import BattleScene, { Button } from "../battle-scene";
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 { Mode } from "./ui";
import UiHandler from "./ui-handler";
import { addWindow } from "./ui-theme";
import {Button} from "#app/inputs-controller";
export default class SettingsUiHandler extends UiHandler {
private settingsContainer: Phaser.GameObjects.Container;

View File

@ -1,4 +1,4 @@
import BattleScene, { Button, starterColors } from "../battle-scene";
import BattleScene, { starterColors } from "../battle-scene";
import PokemonSpecies, { allSpecies, getPokemonSpecies, getPokemonSpeciesForm, speciesStarters, starterPassiveAbilities } from "../data/pokemon-species";
import { Species } from "../data/enums/species";
import { TextStyle, addBBCodeTextObject, addTextObject } from "./text";
@ -27,6 +27,7 @@ import { argbFromRgba } from "@material/material-color-utilities";
import { OptionSelectItem } from "./abstact-option-select-ui-handler";
import { pokemonPrevolutions } from "#app/data/pokemon-evolutions";
import { Variant, getVariantTint } from "#app/data/variant";
import {Button} from "#app/inputs-controller";
export type StarterSelectCallback = (starters: Starter[]) => void;

View File

@ -1,4 +1,4 @@
import BattleScene, { Button } from "../battle-scene";
import BattleScene from "../battle-scene";
import { Mode } from "./ui";
import UiHandler from "./ui-handler";
import * as Utils from "../utils";
@ -17,6 +17,7 @@ import { Nature, getNatureStatMultiplier } from "../data/nature";
import { loggedInUser } from "../account";
import { PlayerGender } from "../system/game-data";
import { Variant, getVariantTint } from "#app/data/variant";
import {Button} from "#app/inputs-controller";
enum Page {
PROFILE,

View File

@ -1,10 +1,11 @@
import { BattlerIndex } from "../battle";
import BattleScene, { Button } from "../battle-scene";
import BattleScene from "../battle-scene";
import { Moves } from "../data/enums/moves";
import { Mode } from "./ui";
import UiHandler from "./ui-handler";
import * as Utils from "../utils";
import { getMoveTargets } from "../data/move";
import {Button} from "#app/inputs-controller";
export type TargetSelectCallback = (cursor: integer) => void;

View File

@ -1,6 +1,7 @@
import BattleScene, { Button } from "../battle-scene";
import BattleScene from "../battle-scene";
import { TextStyle, getTextColor } from "./text";
import UI, { Mode } from "./ui";
import {Button} from "#app/inputs-controller";
export default abstract class UiHandler {
protected scene: BattleScene;

View File

@ -1,4 +1,4 @@
import { Button, default as BattleScene } from '../battle-scene';
import { default as BattleScene } from '../battle-scene';
import UiHandler from './ui-handler';
import BattleMessageUiHandler from './battle-message-ui-handler';
import CommandUiHandler from './command-ui-handler';
@ -35,6 +35,7 @@ import SavingIconHandler from './saving-icon-handler';
import UnavailableModalUiHandler from './unavailable-modal-ui-handler';
import OutdatedModalUiHandler from './outdated-modal-ui-handler';
import SessionReloadModalUiHandler from './session-reload-modal-ui-handler';
import {Button} from "#app/inputs-controller";
export enum Mode {
MESSAGE,

View File

@ -1,9 +1,10 @@
import BattleScene, { Button } from "../battle-scene";
import BattleScene from "../battle-scene";
import { Voucher, getVoucherTypeIcon, getVoucherTypeName, vouchers } from "../system/voucher";
import MessageUiHandler from "./message-ui-handler";
import { TextStyle, addTextObject } from "./text";
import { Mode } from "./ui";
import { addWindow } from "./ui-theme";
import {Button} from "#app/inputs-controller";
const itemRows = 4;
const itemCols = 17;