Skip to content

Commit

Permalink
gamepad support
Browse files Browse the repository at this point in the history
  • Loading branch information
JosePedroDias committed Oct 6, 2024
1 parent b56e83f commit b7ede0f
Show file tree
Hide file tree
Showing 5 changed files with 222 additions and 28 deletions.
18 changes: 13 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,19 +6,26 @@ Implemented in vanilla js and rendered using canvas API.

## controls

### keyboard

- left, right - move pill sideways
- down - drop pill
- down - move pill 1 cell down
- space bar - drop pill
- z, x - rotate

### gamepad

- press 1 to define which button/axis to use for each action (see console for feedback)
- config is stored on local storage and reused on subsequent games

## references

- dr mario
- Dr Mario
- https://www.mariowiki.com/Dr._Mario
- https://www.mariomayhem.com/downloads/mario_instruction_booklets/Dr_Mario-NES.pdf
- canvas api
- JS APIs
- https://simon.html5.org/dump/html5-canvas-cheat-sheet.html

- https://developer.mozilla.org/en-US/docs/Web/API/Gamepad_API , https://developer.mozilla.org/en-US/docs/Web/API/Gamepad_API/Using_the_Gamepad_API

## TODO

Expand All @@ -27,7 +34,8 @@ Implemented in vanilla js and rendered using canvas API.
- hint leaving cells visually
- display next piece
- count viruses left
- gamepad support
- gamepad: show gamepad messages using a dom element instead of console
- keyboard: redefine keys
- twist from original game?
- actual sprites instead of geometric figures?
- port to go/nakama or use peerjs to do multiplayer
23 changes: 17 additions & 6 deletions constants.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -19,9 +19,20 @@ export const COLORS = [
'red',
];

export const KEY_LEFT = 'ArrowLeft';
export const KEY_RIGHT = 'ArrowRight';
export const KEY_DOWN = 'ArrowDown';
export const KEY_A = 'z';
export const KEY_B = 'x';
export const KEY_DROP = ' ';
export const KEY_LEFT = 'ArrowLeft';
export const KEY_RIGHT = 'ArrowRight';
export const KEY_DOWN = 'ArrowDown';
export const KEY_DROP = ' ';
export const KEY_ROT_CW = 'z';
export const KEY_ROT_CCW = 'x';
export const KEY_ROT_GP_REBIND = '1';
// TODO keyboard rebind

export const GP_LEFT = 'left';
export const GP_RIGHT = 'right';
export const GP_DOWN = 'down';
export const GP_DROP = 'drop';
export const GP_ROT_CW = 'rcw';
export const GP_ROT_CCW = 'rccw';

export const GP_ACTIONS = [ GP_LEFT, GP_RIGHT, GP_DOWN, GP_DROP, GP_ROT_CW, GP_ROT_CCW ];
61 changes: 45 additions & 16 deletions game.mjs
Original file line number Diff line number Diff line change
@@ -1,6 +1,11 @@
import { KEY_LEFT, KEY_RIGHT, KEY_DOWN, KEY_A, KEY_B, KEY_DROP, S } from './constants.mjs';
import {
KEY_LEFT, KEY_RIGHT, KEY_DOWN, KEY_DROP, KEY_ROT_CW, KEY_ROT_CCW, KEY_ROT_GP_REBIND,
GP_LEFT, GP_RIGHT, GP_DOWN, GP_DROP, GP_ROT_CW, GP_ROT_CCW,
S,
} from './constants.mjs';
import { createGame, moveLeft, moveRight, moveDown, drop, rotateCW, rotateCCW, applyPill } from './logic.mjs';
import { setupRender } from './render.mjs';
import { setupGamepad, rebindGamepad, getGamepadBindings, setGamepadBindings, subscribeToGamepadEvents, subscribeToGamepadBindingMessages } from './gamepad.mjs';

let m, p, refresh;
let speedMs = 1500;
Expand All @@ -17,23 +22,23 @@ export async function play() {
mainEl.style.marginLeft = `-${S/2 * m.w}px`;
mainEl.style.marginTop = `-${S/2 * m.h}px`;

/*mainEl.addEventListener('click', (ev) => {
const geo = mainEl.getBoundingClientRect();
const x = Math.floor( (ev.clientX - geo.x) / S);
const y = Math.floor( (ev.clientY - geo.y) / S);
const pos = [x, y];
changeCell(pos);
});*/

document.addEventListener('keydown', (ev) => {
if (ev.altKey || ev.metaKey || ev.ctrlKey) return;
const key = ev.key;
if (key === KEY_LEFT) moveLeft(m, p);
else if (key === KEY_RIGHT) moveRight(m, p);
else if (key === KEY_DOWN) moveDown(m, p);
else if (key === KEY_DROP) drop(m, p);
else if (key === KEY_A) rotateCW(m, p);
else if (key === KEY_B) rotateCCW(m, p);
if (key === KEY_LEFT) moveLeft(m, p);
else if (key === KEY_RIGHT) moveRight(m, p);
else if (key === KEY_DOWN) moveDown(m, p);
else if (key === KEY_DROP) drop(m, p);
else if (key === KEY_ROT_CW) rotateCW(m, p);
else if (key === KEY_ROT_CCW) rotateCCW(m, p);
else if (key === KEY_ROT_GP_REBIND) {
rebindGamepad().then(() => {
console.warn('bindings complete');
try {
localStorage.setItem(GP_LS, JSON.stringify(getGamepadBindings()));
} catch (err) {}
});
}
else return;
ev.preventDefault();
ev.stopPropagation();
Expand All @@ -42,7 +47,6 @@ export async function play() {

refresh();


const onTick = () => {
if (isGameOver) {
window.alert('game over');
Expand All @@ -60,4 +64,29 @@ export async function play() {
};

onTick();

// gamepad wiring
const GP_LS = 'gamepad';
setupGamepad();

try {
let b = localStorage.getItem(GP_LS);
b = JSON.parse(b);
if (b) {
setGamepadBindings(b);
console.warn('gamepad bindings loaded');
}
} catch (err) {}

subscribeToGamepadEvents((action) => {
if (action === GP_LEFT) moveLeft(m, p);
else if (action === GP_RIGHT) moveRight(m, p);
else if (action === GP_DOWN) moveDown(m, p);
else if (action === GP_DROP) drop(m, p);
else if (action === GP_ROT_CW) rotateCW(m, p);
else if (action === GP_ROT_CCW) rotateCCW(m, p);
else return;
refresh();
});
subscribeToGamepadBindingMessages((m) => console.log(m));
}
138 changes: 138 additions & 0 deletions gamepad.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,138 @@
const speedMs = 1000 / 60;
let lastT = Date.now();

let state;
let bindings = new Map();
let inBindingMode = false;
let yetToBind = [];

import { GP_ACTIONS } from './constants.mjs';

let onGamepadEventHandler = () => {};
let onGamepadBindingMessage = () => {};

export function subscribeToGamepadEvents(fn) {
onGamepadEventHandler = fn;
}

export function subscribeToGamepadBindingMessages(fn) {
onGamepadBindingMessage = fn;
}

function onButton(i, isDown) {
const k = `b${i}`;
//console.warn(`button ${i}: ${isDown}`);
if (!isDown) return;
if (inBindingMode) {
const purpose = yetToBind.shift();
bindings.set(k, purpose);
onGamepadBindingMessage(`button #${i} -> ${purpose}`);
continueBinding();
} else {
const purpose = bindings.get(k);
if (purpose) {
//console.warn(k, purpose);
onGamepadEventHandler(purpose);
}
}
}

function onAxis(i, v) {
//console.warn(`axis ${i}: ${v}`);
if (!v) return;
const k = `a${i}${v}`;
//console.warn(`axis ${k}`);
if (inBindingMode) {
if (!bindings.get(k)) {
const purpose = yetToBind.shift();
bindings.set(k, purpose);
onGamepadBindingMessage(`axis ${k} -> ${purpose}`);
continueBinding();
}
} else {
const purpose = bindings.get(k);
if (purpose) {
//console.warn(k, purpose);
onGamepadEventHandler(purpose);
}
}
}

let bindingPromise, bindingResolve;

export function rebindGamepad() {
bindingPromise = new Promise((resolve) => {
bindingResolve = resolve;
});
bindings = new Map();
inBindingMode = true;
yetToBind = GP_ACTIONS.slice();
onGamepadBindingMessage(`define gamepad button/axis for action ${yetToBind[0]}...`);
return bindingPromise;
}

function continueBinding() {
if (yetToBind.length > 0) {
onGamepadBindingMessage(`define gamepad button/axis for action ${yetToBind[0]}...`);
} else {
inBindingMode = false;
bindingResolve();
}
}

export function getGamepadBindings() {
// TODO compat?
return Object.fromEntries(bindings);
}

export function setGamepadBindings(o) {
bindings = new Map(Object.entries(o));
}

const CUT1 = -0.5;
const CUT2 = 0.5;

function makeDiscrete(v) {
if (v < CUT1) return 'a';
if (v > CUT2) return 'b';
return '';
}

function readGamepad() {
const gp = navigator.getGamepads()[0];
if (!gp) return;

if (!state) {
state = {
buttons: [],
axes: [],
}
gp.buttons.forEach((b) => state.buttons.push(b.pressed));
//gp.axes.forEach((a) => state.axes.push(a));
gp.axes.forEach((a) => state.axes.push(makeDiscrete(a)));
} else {
gp.buttons.forEach((b, i) => {
const v = b.pressed;
if (v !== state.buttons[i]) onButton(i, v);
state.buttons[i] = v;
});
gp.axes.forEach((v, i) => {
v = makeDiscrete(v);
if (v !== state.axes[i]) onAxis(i, v);
state.axes[i] = v;
});
}
}

export function setupGamepad() {
const onTick = () => {
requestAnimationFrame(onTick);
const t = Date.now();
if (t - lastT >= speedMs) {
readGamepad();
lastT = t;
}
};

onTick();
}
10 changes: 9 additions & 1 deletion logic.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@ export function applyPill(m, p) {

// reset new pill
p.restore(randomPill());

return isPillColliding(m, p);
}

Expand Down Expand Up @@ -117,3 +117,11 @@ export function rotateCW(m, p) {
export function rotateCCW(m, p) {
rotate(m, p, false);
}

export function markCellsToDelete(m, p) {

}

export function removeMarkedCells(m, p) {

}

0 comments on commit b7ede0f

Please sign in to comment.