Skip to content

Commit d7a5209

Browse files
committed
Merge branch 'feature/gamepad'
2 parents 465e8d9 + bd9cd28 commit d7a5209

File tree

10 files changed

+204
-131
lines changed

10 files changed

+204
-131
lines changed

src/Game.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import SinglePlayer from "./nodes/SinglePlayer";
55
import Multiplayer from "./nodes/Multiplayer";
66
import Background from "./nodes/Background";
77
import { initSounds } from "./audio";
8-
import { inputs } from "./inputs";
8+
import { inputManager, inputs } from "./inputs";
99
import IdleMode from "./nodes/IdleMode";
1010
import { randomInt } from "mathjs";
1111
import { campaign } from "./levels";
@@ -46,6 +46,7 @@ export default class Game {
4646
const menu = new Menu(background);
4747
app.stage.addChild(background.view);
4848
app.ticker.add(background.tick);
49+
app.ticker.add(() => inputManager.tick());
4950

5051
let timeout: number;
5152
function setIdleTimeout() {

src/inputs.ts

Lines changed: 118 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -1,29 +1,120 @@
1-
export interface PlayerInput {
2-
left: string;
3-
right: string;
4-
up: string;
5-
down: string;
6-
flip: string;
7-
hold: string;
1+
import { difference } from "lodash-es";
2+
3+
export type Input = string;
4+
type Callback = (input: Input) => void;
5+
6+
// Singleton that unifies keyboard and gamepad inputs
7+
class InputManager {
8+
keydownCallbacks: Callback[] = [];
9+
keyupCallbacks: Callback[] = [];
10+
11+
// All currently pressed keys on gamepad
12+
gamepadPressed: Input[] = [];
13+
14+
constructor() {
15+
document.addEventListener("keydown", (e) => {
16+
for (let cb of this.keydownCallbacks) {
17+
cb(keyInputs[e.key]);
18+
}
19+
});
20+
document.addEventListener("keyup", (e) => {
21+
for (let cb of this.keyupCallbacks) {
22+
cb(keyInputs[e.key]);
23+
}
24+
});
25+
}
26+
27+
tick() {
28+
// On each tick, fire `keydown` events for each newly pressed gamepad button
29+
// and fire `keyup` events for each released button
30+
const gamepads = navigator.getGamepads();
31+
let newInputs: Input[] = [];
32+
if (gamepads[0]) {
33+
newInputs = newInputs.concat(
34+
this.getGamepadButtons("player1", gamepads[0])
35+
);
36+
}
37+
if (gamepads[1]) {
38+
newInputs = newInputs.concat(
39+
this.getGamepadButtons("player2", gamepads[1])
40+
);
41+
}
42+
for (let input of difference(this.gamepadPressed, newInputs)) {
43+
for (let cb of this.keyupCallbacks) {
44+
cb(input);
45+
}
46+
}
47+
for (let input of difference(newInputs, this.gamepadPressed)) {
48+
for (let cb of this.keydownCallbacks) {
49+
cb(input);
50+
}
51+
}
52+
53+
this.gamepadPressed = newInputs;
54+
}
55+
56+
getGamepadButtons(playerIndex: string, gp: Gamepad) {
57+
const buttons = [];
58+
if (gp.axes[0] < -0.5 || gp.buttons[14].pressed) {
59+
buttons.push(`${playerIndex}.left`);
60+
}
61+
if (gp.axes[0] > 0.5 || gp.buttons[15].pressed) {
62+
buttons.push(`${playerIndex}.right`);
63+
}
64+
if (gp.axes[1] < -0.5 || gp.buttons[12].pressed) {
65+
buttons.push(`${playerIndex}.up`);
66+
}
67+
if (gp.axes[1] > 0.5 || gp.buttons[13].pressed) {
68+
buttons.push(`${playerIndex}.down`);
69+
}
70+
if (gp.buttons[0].pressed) {
71+
buttons.push(`${playerIndex}.flip`);
72+
}
73+
if (gp.buttons[1].pressed) {
74+
buttons.push(`${playerIndex}.hold`);
75+
}
76+
return buttons;
77+
}
78+
79+
addKeydownListener(cb: Callback) {
80+
this.keydownCallbacks.push(cb);
81+
}
82+
83+
removeKeydownListener(cb: Callback) {
84+
const index = this.keydownCallbacks.indexOf(cb);
85+
if (index >= 0) {
86+
this.keydownCallbacks.splice(index, 1);
87+
}
88+
}
89+
90+
addKeyupListener(cb: Callback) {
91+
this.keyupCallbacks.push(cb);
92+
}
93+
94+
removeKeyupListener(cb: Callback) {
95+
const index = this.keydownCallbacks.indexOf(cb);
96+
if (index >= 0) {
97+
this.keyupCallbacks.splice(index, 1);
98+
}
99+
}
8100
}
9-
export const inputs = {
10-
refresh: "r",
11-
pause: "p",
12-
translate: "t",
13-
player1: {
14-
left: "a",
15-
right: "d",
16-
up: "w",
17-
down: "s",
18-
flip: "e",
19-
hold: "q",
20-
},
21-
player2: {
22-
left: "j",
23-
right: "l",
24-
up: "i",
25-
down: "k",
26-
flip: "o",
27-
hold: "u",
28-
},
101+
102+
export const inputManager = new InputManager();
103+
104+
const keyInputs: Record<string, string> = {
105+
r: "refresh",
106+
p: "pause",
107+
t: "translate",
108+
a: "player1.left",
109+
d: "player1.right",
110+
w: "player1.up",
111+
s: "player1.down",
112+
e: "player1.flip",
113+
q: "player1.hold",
114+
j: "player2.left",
115+
l: "player2.right",
116+
i: "player2.up",
117+
k: "player2.down",
118+
o: "player2.flip",
119+
u: "player2.hold",
29120
};

src/nodes/Credits.ts

Lines changed: 5 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import GameNode from "./GameNode";
55
import ministryLogoPath from "../assets/img/ministry-logo.png";
66
import imaginaryLogoPath from "../assets/img/imaginary-logo.png";
77
import mpiLogoPath from "../assets/img/mpi-logo.png";
8-
import { inputs } from "../inputs";
8+
import { inputManager, type Input } from "../inputs";
99
import { LayoutContainer } from "@pixi/layout/components";
1010
import { setI18nKey } from "../i18n";
1111

@@ -14,7 +14,7 @@ export default class Credits extends GameNode {
1414

1515
constructor() {
1616
super();
17-
document.addEventListener("keydown", this.handleKeyDown);
17+
inputManager.addKeydownListener(this.handleKeyDown);
1818
this.view.layout = {
1919
width: WIDTH,
2020
height: HEIGHT,
@@ -139,16 +139,9 @@ export default class Credits extends GameNode {
139139
return credit;
140140
}
141141

142-
handleKeyDown = (e: KeyboardEvent) => {
143-
if (
144-
[
145-
...Object.values(inputs.player1),
146-
...Object.values(inputs.player2),
147-
].includes(e.key)
148-
) {
149-
document.removeEventListener("keydown", this.handleKeyDown);
150-
this.onFinish?.();
151-
}
142+
handleKeyDown = (_input: Input) => {
143+
inputManager.removeKeydownListener(this.handleKeyDown);
144+
this.onFinish?.();
152145
};
153146

154147
async load() {

src/nodes/HighScoreScreen.ts

Lines changed: 5 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import { container } from "../util";
33
import GameNode from "./GameNode";
44
import { HEIGHT, TEXT_FONT, theme, WIDTH } from "../constants";
55
import { getScores, type Score } from "../storage";
6-
import { inputs } from "../inputs";
6+
import { inputManager, type Input } from "../inputs";
77
import { setI18nKey } from "../i18n";
88

99
// The high score screen displayed in the main menu
@@ -13,7 +13,7 @@ export default class HighScoreScreen extends GameNode {
1313

1414
constructor() {
1515
super();
16-
document.addEventListener("keydown", this.handleKeyDown);
16+
inputManager.addKeydownListener(this.handleKeyDown);
1717
const containerSize = HEIGHT * 0.75;
1818
this.view.position = { x: WIDTH / 2, y: HEIGHT / 2 };
1919
this.view.addChild(
@@ -71,15 +71,8 @@ export default class HighScoreScreen extends GameNode {
7171
this.view.addChild(nameText);
7272
}
7373

74-
handleKeyDown = (e: KeyboardEvent) => {
75-
if (
76-
[
77-
...Object.values(inputs.player1),
78-
...Object.values(inputs.player2),
79-
].includes(e.key)
80-
) {
81-
document.removeEventListener("keydown", this.handleKeyDown);
82-
this.onFinish?.();
83-
}
74+
handleKeyDown = (_input: Input) => {
75+
inputManager.removeKeydownListener(this.handleKeyDown);
76+
this.onFinish?.();
8477
};
8578
}

src/nodes/IdleMode.ts

Lines changed: 4 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import GameNode from "./GameNode";
44
import Player from "./Player";
55
import { campaign } from "../levels";
66
import Background from "./Background";
7-
import { inputs } from "../inputs";
7+
import { inputManager, inputs } from "../inputs";
88
import Countdown from "./Countdown";
99
import { type IMediaInstance } from "@pixi/sound";
1010
import { playSound } from "../audio";
@@ -25,7 +25,7 @@ export default class IdleMode extends GameNode {
2525
super();
2626
this.background = background;
2727
this.player = new Player({
28-
inputMap: inputs.player1,
28+
playerIndex: "player1",
2929
levels: campaign,
3030
startLevel,
3131
});
@@ -43,13 +43,8 @@ export default class IdleMode extends GameNode {
4343
}
4444

4545
async start() {
46-
// this.view.addChild(
47-
// new HTMLText({
48-
// text: "Press any button",
49-
// })
50-
// );
5146
this.view.addChild(this.player.view);
52-
document.addEventListener("keydown", this.handleKeyDown);
47+
inputManager.addKeydownListener(this.handleKeyDown);
5348
slideIn(
5449
this.player.view,
5550
new Point(WIDTH / 2 - this.player.view.width / 2, 0),
@@ -75,7 +70,7 @@ export default class IdleMode extends GameNode {
7570

7671
endIdle() {
7772
this.music?.stop();
78-
document.removeEventListener("keydown", this.handleKeyDown);
73+
inputManager.removeKeydownListener(this.handleKeyDown);
7974
this.player.destroy();
8075
this.onFinish?.();
8176
}

src/nodes/Menu.ts

Lines changed: 18 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { Container, Graphics, HTMLText } from "pixi.js";
22
import { HEIGHT, TEXT_FONT, theme, WIDTH } from "../constants";
33
import GameNode from "./GameNode";
4-
import { inputs } from "../inputs";
4+
import { inputManager, type Input } from "../inputs";
55
import { container } from "../util";
66
import { pulse } from "../animations";
77
import { playSound } from "../audio";
@@ -109,12 +109,12 @@ export default class Menu extends GameNode {
109109
pulse(this.optionTexts[index]);
110110
}
111111

112-
handleKeyDown = (e: KeyboardEvent) => {
112+
handleKeyDown = (input: Input) => {
113113
switch (this.state) {
114114
case "player-select": {
115-
switch (e.key) {
116-
case inputs.player1.flip:
117-
case inputs.player2.flip: {
115+
switch (input) {
116+
case "player1.flip":
117+
case "player2.flip": {
118118
playSound("clear");
119119
if (this.optionIndex === 2) {
120120
this.onHighScores?.();
@@ -126,14 +126,14 @@ export default class Menu extends GameNode {
126126
}
127127
break;
128128
}
129-
case inputs.player1.up:
130-
case inputs.player2.up: {
129+
case "player1.up":
130+
case "player2.up": {
131131
playSound("turn");
132132
this.setOptionIndex((this.optionIndex || options.length) - 1);
133133
break;
134134
}
135-
case inputs.player1.down:
136-
case inputs.player2.down: {
135+
case "player1.down":
136+
case "player2.down": {
137137
playSound("turn");
138138
this.setOptionIndex((this.optionIndex + 1) % options.length);
139139
break;
@@ -142,21 +142,21 @@ export default class Menu extends GameNode {
142142
break;
143143
}
144144
case "level-select": {
145-
switch (e.key) {
146-
case inputs.player1.flip:
147-
case inputs.player2.flip: {
145+
switch (input) {
146+
case "player1.flip":
147+
case "player2.flip": {
148148
playSound("clear");
149149
this.onStart?.(this.optionIndex + 1, this.level);
150150
break;
151151
}
152-
case inputs.player1.left:
153-
case inputs.player2.left: {
152+
case "player1.left":
153+
case "player2.left": {
154154
this.toggleLevel((this.level || campaign.length) - 1);
155155
break;
156156
}
157157

158-
case inputs.player1.right:
159-
case inputs.player2.right: {
158+
case "player1.right":
159+
case "player2.right": {
160160
this.toggleLevel((this.level + 1) % campaign.length);
161161
break;
162162
}
@@ -193,11 +193,11 @@ export default class Menu extends GameNode {
193193
show(parent: Container) {
194194
parent.addChild(this.view);
195195
this.showPlayerSelect();
196-
document.addEventListener("keydown", this.handleKeyDown);
196+
inputManager.addKeydownListener(this.handleKeyDown);
197197
}
198198

199199
hide() {
200200
this.view.parent.removeChild(this.view);
201-
document.removeEventListener("keydown", this.handleKeyDown);
201+
inputManager.removeKeydownListener(this.handleKeyDown);
202202
}
203203
}

0 commit comments

Comments
 (0)