Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
49 changes: 49 additions & 0 deletions src/main/missing-types.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,3 +29,52 @@ declare namespace JSX {
inputmode?: string;
}
}

/**
* This Gamepad API interface defines an individual gamepad or other controller, allowing access to
* information such as button presses, axis positions, and id. Available only in secure contexts.
*/
interface Gamepad {
readonly axes: ReadonlyArray<number>;
readonly buttons: ReadonlyArray<GamepadButton>;
readonly connected: boolean;
readonly hapticActuators: ReadonlyArray<GamepadHapticActuator>;
readonly id: string;
readonly index: number;
readonly mapping: GamepadMappingType;
readonly timestamp: DOMHighResTimeStamp;
}

declare var Gamepad: {
prototype: Gamepad;
new (): Gamepad;
};

/**
* An individual button of a gamepad or other controller, allowing access to the current state of
* different types of buttons available on the control device. Available only in secure contexts.
*/
interface GamepadButton {
readonly pressed: boolean;
readonly touched: boolean;
readonly value: number;
}

declare var GamepadButton: {
prototype: GamepadButton;
new (): GamepadButton;
};

/**
* This Gamepad API interface contains references to gamepads connected to the system, which is what
* the gamepad events Window.gamepadconnected and Window.gamepaddisconnected are fired in response to.
* Available only in secure contexts.
*/
interface GamepadEvent extends Event {
readonly gamepad: Gamepad;
}

declare var GamepadEvent: {
prototype: GamepadEvent;
new (type: string, eventInitDict: GamepadEventInit): GamepadEvent;
};
77 changes: 65 additions & 12 deletions src/main/services/preact-canvas/components/board/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,10 +15,11 @@ import { Animator } from "src/main/rendering/animator";
import { Renderer } from "src/main/rendering/renderer";
import { putCanvas } from "src/main/utils/canvas-pool";
import { cellFocusMode } from "src/main/utils/constants";
import { gamepad } from "src/main/utils/gamepad";
import { isFeaturePhone } from "src/main/utils/static-display";
import { bind } from "src/utils/bind";
import { StateChange } from "src/worker/gamelogic";
import { Cell } from "src/worker/gamelogic/types";
import { Cell, PlayMode } from "src/worker/gamelogic/types";
import { GameChangeCallback } from "../../index";
import {
board,
Expand All @@ -45,24 +46,17 @@ export interface Props {
renderer: Renderer;
animator: Animator;
dangerMode: boolean;
interactive: boolean;
gameChangeSubscribe: (f: GameChangeCallback) => void;
gameChangeUnsubscribe: (f: GameChangeCallback) => void;
onDangerModeChange: (v: boolean) => void;
}

interface State {
keyNavigation: boolean;
}

interface SetFocusOptions {
preventScroll?: boolean;
}

export default class Board extends Component<Props, State> {
state: State = {
keyNavigation: false
};

export default class Board extends Component<Props, {}> {
private _canvas?: HTMLCanvasElement;
private _table?: HTMLTableElement;
private _buttons: HTMLButtonElement[] = [];
Expand Down Expand Up @@ -101,12 +95,16 @@ export default class Board extends Component<Props, State> {

window.addEventListener("resize", this._onWindowResize);
window.addEventListener("keyup", this._onGlobalKeyUp);
gamepad.addButtonDownCallback(this._onGamepadButtonDown);
gamepad.addButtonPressCallback(this._onGamepadButtonPress);
}

componentWillUnmount() {
document.documentElement.classList.remove("in-game");
window.removeEventListener("resize", this._onWindowResize);
window.removeEventListener("keyup", this._onGlobalKeyUp);
gamepad.removeButtonDownCallback(this._onGamepadButtonDown);
gamepad.removeButtonPressCallback(this._onGamepadButtonPress);
this.props.gameChangeUnsubscribe(this._doManualDomHandling);
this.props.renderer.stop();
this.props.animator.stop();
Expand Down Expand Up @@ -144,6 +142,9 @@ export default class Board extends Component<Props, State> {

@bind
private _onGlobalKeyUp(event: KeyboardEvent) {
if (!this.props.interactive) {
return;
}
// This returns the focus to the board when one of these keys is pressed (on feature phones
// only). This means the user doesn't have to manually refocus the board.
if (
Expand All @@ -161,6 +162,57 @@ export default class Board extends Component<Props, State> {
}
}

@bind
private _onGamepadButtonDown(buttonId: number) {
if (!this.props.interactive) {
return;
}
switch (buttonId) {
case 0: // A
case 4: /* Left Shoulder Button */ {
const button = document.activeElement as HTMLButtonElement;
if (!button) {
return;
}
this.simulateClick(button);
break;
}
case 2: // X
case 5: /* Right Shoulder Button */ {
const button = document.activeElement as HTMLButtonElement;
if (!button) {
return;
}
this.simulateClick(button, true);
break;
}
case 1: // B
this._toggleDangerMode();
break;
}
}

@bind
private _onGamepadButtonPress(buttonId: number) {
if (!this.props.interactive) {
return;
}
switch (buttonId) {
case 15: // Right
this.moveFocusByKey(new Event(""), 1, 0);
break;
case 14: // Left
this.moveFocusByKey(new Event(""), -1, 0);
break;
case 12: // Up
this.moveFocusByKey(new Event(""), 0, -1);
break;
case 13: // Down
this.moveFocusByKey(new Event(""), 0, 1);
break;
}
}

@bind
private _doManualDomHandling(stateChange: StateChange) {
if (!stateChange.gridChanges) {
Expand Down Expand Up @@ -245,7 +297,8 @@ export default class Board extends Component<Props, State> {
const showFocusStyle =
button.classList.contains("focus-visible") ||
isFeaturePhone ||
cellFocusMode;
cellFocusMode ||
gamepad.isGamepadConnected;

if (!showFocusStyle) {
this.props.renderer.setFocus(-1, -1);
Expand Down Expand Up @@ -318,7 +371,7 @@ export default class Board extends Component<Props, State> {
}

@bind
private moveFocusByKey(event: KeyboardEvent, h: number, v: number) {
private moveFocusByKey(event: Event, h: number, v: number) {
event.stopPropagation();
event.preventDefault();

Expand Down
55 changes: 50 additions & 5 deletions src/main/services/preact-canvas/components/game/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import { GameChangeCallback } from "src/main/services/preact-canvas";
import { submitTime } from "src/main/services/state/best-times";
import { supportsVibration } from "src/main/services/state/vibration-preference";
import { vibrationLength } from "src/main/utils/constants";
import { gamepad } from "src/main/utils/gamepad";
import { isFeaturePhone } from "src/main/utils/static-display";
import { bind } from "src/utils/bind";
import { StateChange } from "src/worker/gamelogic";
Expand All @@ -33,6 +34,9 @@ import {
exitRow,
exitRowInner,
game as gameClass,
gamepadButton,
gamepadButtonA,
gamepadButtonB,
mainButton,
shortcutKey
} from "./style.css";
Expand All @@ -50,6 +54,7 @@ export interface Props {
useMotion: boolean;
bestTime?: number;
useVibration: boolean;
isGamepadConnected: boolean;
}

interface State {
Expand Down Expand Up @@ -96,7 +101,8 @@ export default class Game extends Component<Props, State> {
gameChangeUnsubscribe,
toRevealTotal,
useMotion,
bestTime: previousBestTime
bestTime: previousBestTime,
isGamepadConnected
}: Props,
{ playMode, toReveal, animator, renderer, completeTime, bestTime }: State
) {
Expand All @@ -123,6 +129,7 @@ export default class Game extends Component<Props, State> {
height={height}
mines={mines}
useMotion={this.props.useMotion}
isGamepadConnected={isGamepadConnected}
/>
) : renderer && animator ? (
[
Expand All @@ -132,6 +139,9 @@ export default class Game extends Component<Props, State> {
dangerMode={dangerMode}
animator={animator}
renderer={renderer}
interactive={
playMode === PlayMode.Pending || playMode === PlayMode.Playing
}
gameChangeSubscribe={gameChangeSubscribe}
gameChangeUnsubscribe={gameChangeUnsubscribe}
onCellClick={this.onCellClick}
Expand All @@ -150,10 +160,24 @@ export default class Game extends Component<Props, State> {
#
</span>
)}{" "}
{isGamepadConnected ? (
<span class={[gamepadButton, gamepadButtonA].join(" ")}>
A
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is going to be very specific to particular gamepads, right?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah. This is the key mapping for most Xbox derived gamepads, but will not match for JoyCons or Playstation controllers. I don't think the Gamepad API provides functionality to get the labels for each button, so this would have to be hardcoded based on gamepad vendor id / name most likely.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

hmm ok, I'll need to address that in some way before landing this

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sounds good. Just to clarify though: while the labels will not match (A on Xbox is X on PS), the actual input handling is correct. Pressing "X" on the PS controller will start a game etc.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It’s gonna be hard to find a perfect solution. We could add a map that maps from gamepad name to button config, and one fallback config...

</span>
) : (
""
)}{" "}
Try again
</button>
<button class={mainButton} onClick={this.onReset}>
{isFeaturePhone ? <span class={shortcutKey}>*</span> : ""}{" "}
{isGamepadConnected ? (
<span class={[gamepadButton, gamepadButtonB].join(" ")}>
B
</span>
) : (
""
)}{" "}
Main menu
</button>
</div>
Expand All @@ -175,11 +199,13 @@ export default class Game extends Component<Props, State> {
this.props.onDangerModeChange(true);
}
window.addEventListener("keyup", this.onKeyUp);
gamepad.addButtonDownCallback(this.onGamepadButtonDown);
}

componentWillUnmount() {
this.props.gameChangeUnsubscribe(this.onGameChange);
window.removeEventListener("keyup", this.onKeyUp);
gamepad.removeButtonDownCallback(this.onGamepadButtonDown);
}

componentDidUpdate(_: Props, previousState: State) {
Expand Down Expand Up @@ -230,12 +256,31 @@ export default class Game extends Component<Props, State> {

@bind
private onKeyUp(event: KeyboardEvent) {
if (event.key === "#") {
if (this.state.playMode === PlayMode.Lost) {
if (
this.state.playMode === PlayMode.Won ||
this.state.playMode === PlayMode.Lost
) {
if (event.key === "#") {
this.onRestart();
} else if (event.key === "*") {
this.onReset();
}
}
}

@bind
private onGamepadButtonDown(buttonId: number) {
if (
this.state.playMode === PlayMode.Won ||
this.state.playMode === PlayMode.Lost
) {
if (buttonId === 0) {
// A
this.onRestart();
} else if (buttonId === 1) {
// B
this.onReset();
}
} else if (event.key === "*") {
this.onReset();
}
}

Expand Down
12 changes: 12 additions & 0 deletions src/main/services/preact-canvas/components/game/style.css
Original file line number Diff line number Diff line change
Expand Up @@ -78,3 +78,15 @@
/** PostCSS adding classes in wrong order :( */
border-color: #000 !important;
}

.gamepad-button {
composes: gamepadbutton from "../../utils.css";
}

.gamepad-button-a {
color: var(--color-gamepad-a);
}

.gamepad-button-b {
color: var(--color-gamepad-b);
}
Loading