aboutsummaryrefslogtreecommitdiff
path: root/src/app/js/gamepad.js
blob: aa9e08917ff70b1348850bff2c16950a7c197716 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
const popups = require("./popups");
const launcher = require("./launcher");
const navigate = require("./navigate");

window.addEventListener("gamepadconnected", (e) => {
	console.log("Gamepad connected:", e.gamepad.id);
}, false)

window.addEventListener("gamepaddisconnected", (e) => {
	console.log("Gamepad disconnected:", e.gamepad.id);
}, false)

// this contains the names/directions of axes and IDs that have
// previously been pressed, if it is found that these were recently
// pressed in the next iteration of the `setInterval()` below than the
// iteration is skipped
//
// the value of each item is equivalent to the amount of iterations to
// wait, so `up: 3` will cause it to wait 3 iterations, before `up` can
// be pressed again
let delay_press = {};

let held_buttons = {};

setInterval(() => {
	let gamepads = navigator.getGamepads();

	// this has a list of all the directions that the `.axes[]` are
	// pointing in, letting us navigate in that direction
	let directions = {}

	// keeps track of which buttons `delay_press` that have already been
	// lowered, that way we can lower the ones that haven't been lowered
	// through a button press
	let lowered_delay = [];

	// is the select/accept button being held
	let selecting = false;

	for (let i in gamepads) {
		if (! gamepads[i]) {continue}
		// every other `.axes[]` element is a different coordinate, each
		// analog stick has 2 elements in `.axes[]`, the first one is
		// the x coordinate, second is the y coordinate
		//
		// so we use this to keep track of which coordinate we're
		// currently on, and thereby the direction of the float inside
		// `.axes[i]`
		let coord = "x";
		let deadzone = 0.5;

		for (let ii = 0; ii < gamepads[i].axes.length; ii++) {
			let value = gamepads[i].axes[ii];

			// check if we're beyond the deadzone in both the negative
			// and positive direction, and then using `coord` add a
			// direction to `directions`
			if (value < -deadzone) {
				if (coord == "y") {
					directions.up = true;
				} else {
					directions.left = true;
				}
			} else if (value > deadzone) {
				if (coord == "y") {
					directions.down = true;
				} else {
					directions.right = true;
				}
			}

			// flip `coord`
			if (coord == "x") {
				coord = "y";
			} else {
				coord = "x";
			}
		}

		// only support "standard" button layouts/mappings
		//
		// TODO: for anybody reading this in the future, the support
		// for other mappings is something that's on the table,
		// however, due to not having all the hardware in the world,
		// this will have to be up to someone else
		if (gamepads[i].mapping != "standard") {
			continue;
		}

		for (let ii = 0; ii < gamepads[i].buttons.length; ii++) {
			if (! gamepads[i].buttons[ii].pressed) {
				held_buttons[ii] = false;
				continue;
			}

			// a list of known combinations of buttons for the most
			// common brands out there, more should possibly be added
			let brands = {
				"Xbox": {
					accept: 0,
					cancel: 1
				},
				"Nintendo": {
					accept: 1,
					cancel: 0
				},
				"PlayStation": {
					accept: 0,
					cancel: 1
				}
			}

			// this is the most common setup, to my understanding, with
			// the exception of third party Nintendo controller, may
			// need to be adjusted in the future
			let buttons = {
				accept: 0,
				cancel: 1
			}

			// set `cancel` and `accept` accordingly to the ID of the
			// gamepad, if its a known brand
			for (let brand in brands) {
				// unknown brand
				if (! gamepads[i].id.includes(brand)) {
					continue;
				}

				// set buttons according to brand
				buttons = brands[brand];
				break;
			}

			// if the button that's being pressed is the "accept"
			// button, then we set `selecting` to `true`, this is done
			// before we check for the button delay so that holding the
			// button keeps the selection in place, until the button is
			// no longer pressed
			if (ii == buttons.accept) {
				selecting = true;
			}

			// if this button is still delayed, we lower the delay and
			// then go to the next button
			if (delay_press[ii]) {
				delay_press[ii]--;
				lowered_delay.push(ii);
				continue;
			}

			// add delay to this button, so it doesn't get clicked
			// immediately again after this
			delay_press[ii] = 3;

			if (held_buttons[ii]) {
				continue;
			}

			held_buttons[ii] = true;

			// interpret `ii` as a specific button/action, using the
			// standard IDs: https://w3c.github.io/gamepad/#remapping
			switch(ii) {
				// change active section
				case 4: launcher.relative_section("left"); break;
				case 5: launcher.relative_section("right"); break;

				// navigate selection
				case 12: navigate.move("up"); break;
				case 13: navigate.move("down"); break;
				case 14: navigate.move("left"); break;
				case 15: navigate.move("right"); break;

				// click selected element
				case buttons.accept: navigate.select(); break;

				// close last opened popup
				case buttons.cancel: popups.hide_last(); break;
			}
		}
	}

	for (let i in directions) {
		if (directions[i] === true) {
			// if this direction is still delayed, we lower the delay,
			// and then go to the next direction
			if (delay_press[i]) {
				delay_press[i]--;
				lowered_delay.push(i);
				continue;
			}

			// move in the direction
			navigate.move(i);

			// add delay to this direction, to prevent it from being
			// triggered immediately again
			delay_press[i] = 3;
		}
	}

	// run through buttons that have or have had a delay
	for (let i in delay_press) {
		// if a button has a delay, and it hasn't already been lowered,
		// then we lower it
		if (delay_press[i] && ! lowered_delay.includes(i)) {
			delay_press[i]--;
		}
	}

	let selection_el = document.getElementById("selection");

	// add `.selecting` to `#selection` depending on whether
	// `selecting`, is set or not
	if (selecting) {
		selection_el.classList.add("controller-selecting");
	} else {
		selection_el.classList.remove("controller-selecting");
	}
}, 50)


let can_keyboard_navigate = (e) => {
	// quite empty right now, might add more in the future, these are
	// just element selectors where movement with the keyboard is off
	let ignore_on_focus = [
		"input",
		"select"
	]

	// check for whether the active element is one that matches
	// something in `ignore_on_focus`
	for (let i = 0; i < ignore_on_focus.length; i++) {
		if (! document.activeElement.matches(ignore_on_focus)) {
			// active element does not match to `ignore_on_focus[i]`
			continue;
		}

		// if the key that's being pressed is "Escape" then we unfocus
		// to the currently focused active element, this lets you go
		// into an input, and then exit it as well
		if (e.key == "Escape") {
			document.activeElement.blur();
		}

		return false;
	}

	// check if there's already an active selection
	if (document.querySelector(".active-selection")) {
		// this is a list of keys where this keyboard event will be
		// cancelled on, this prevents key events from being sent to
		// element, but still lets you type
		let cancel_keys = [
			"Space", "Enter",
			"ArrowUp", "ArrowDown",
			"ArrowLeft", "ArrowRight"
		]

		// cancel this keyboard event if `e.key` is inside `cancel_keys`
		if (cancel_keys.includes(e.code)) {
			e.preventDefault();
		}
	}

	return true;
}

window.addEventListener("keydown", (e) => {
	// do nothing if we cant navigate
	if (! can_keyboard_navigate(e)) {
		return;
	}

	let select = () => {
		// do nothing if this is a repeat key press
		if (e.repeat) {return}

		// select `.active-selection`
		navigate.select();

		// add `.keyboard-selecting` to `#selection`
		document.getElementById("selection")
			.classList.add("keyboard-selecting");
	}

	// perform the relevant action for the key that was pressed
	switch(e.code) {
		// select
		case "Space": return select();
		case "Enter": return select();

		// move selection
		case "ArrowUp": return navigate.move("up")
		case "ArrowDown": return navigate.move("down")
		case "ArrowLeft": return navigate.move("left")
		case "ArrowRight": return navigate.move("right")
	}
})

window.addEventListener("keyup", (e) => {
	if (! can_keyboard_navigate(e)) {
		return;
	}

	let selection_el = document.getElementById("selection");

	// perform the relevant action for the key that was pressed
	switch(e.code) {
		case "KeyQ": launcher.relative_section("left"); break;
		case "KeyE": launcher.relative_section("right"); break;

		case "Space": return selection_el
				.classList.remove("keyboard-selecting");

		case "Enter": return selection_el
				.classList.remove("keyboard-selecting");
	}
})