mirror of
https://github.com/AndrioCelos/TableturfBattleApp.git
synced 2026-08-22 02:44:01 -05:00
Enhance keyboard controls
The board can now receive focus. Hand buttons - arrows: move focus; Space/Enter: select card Board - arrows: move card; R: rotate clockwise; Shift+R: rotate anticlockwise; Space/Enter: submit play; Backspace/Escape: deselect card Either - p: toggle pass; s: toggle special attack
This commit is contained in:
@@ -103,7 +103,7 @@
|
||||
</div>
|
||||
</div>
|
||||
<div id="boardSection">
|
||||
<table id="gameBoard"></table>
|
||||
<table id="gameBoard" tabindex="0"></table>
|
||||
<div id="redrawModal" hidden>
|
||||
<div id="redrawBox">
|
||||
<p>Redraw your starting hand?</p>
|
||||
|
||||
@@ -14,13 +14,63 @@ class Board {
|
||||
highlightY = NaN;
|
||||
private highlightedCells: [x: number, y: number][] = [ ];
|
||||
|
||||
startSpaces: { x: number, y: number }[] = [ ];
|
||||
|
||||
onclick: ((x: number, y: number) => void) | null = null;
|
||||
oncancel: (() => void) | null = null;
|
||||
|
||||
constructor(table: HTMLTableElement) {
|
||||
this.table = table;
|
||||
table.addEventListener('mouseleave', _ => {
|
||||
if (this.autoHighlight) this.clearHighlight()
|
||||
});
|
||||
table.addEventListener('keydown', e => {
|
||||
if (this.autoHighlight) {
|
||||
switch (e.key) {
|
||||
case 'r':
|
||||
this.moveHighlight((x, y, r) => [x, y, r + 1], false);
|
||||
break;
|
||||
case 'R':
|
||||
this.moveHighlight((x, y, r) => [x, y, r - 1], false);
|
||||
break;
|
||||
case 'ArrowUp':
|
||||
this.moveHighlight((x, y, r) => [x, y - 1, r], true);
|
||||
break;
|
||||
case 'ArrowDown':
|
||||
this.moveHighlight((x, y, r) => [x, y + 1, r], true);
|
||||
break;
|
||||
case 'ArrowLeft':
|
||||
this.moveHighlight((x, y, r) => [x - 1, y, r], true);
|
||||
break;
|
||||
case 'ArrowRight':
|
||||
this.moveHighlight((x, y, r) => [x + 1, y, r], true);
|
||||
break;
|
||||
case 'Enter': case ' ':
|
||||
if (this.onclick)
|
||||
this.onclick(this.highlightX, this.highlightY);
|
||||
break;
|
||||
case 'Escape': case 'Backspace':
|
||||
if (this.oncancel)
|
||||
this.oncancel();
|
||||
break;
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
moveHighlight(move: (x: number, y: number, r: number) => [number, number, number], clamp: boolean) {
|
||||
if (this.playerIndex == null) return;
|
||||
if (this.highlightedCells.length == 0 || isNaN(this.highlightX) || isNaN(this.highlightY) || isNaN(this.cardRotation)) {
|
||||
const startSpace = this.startSpaces[this.playerIndex];
|
||||
[this.highlightX, this.highlightY, this.cardRotation] = [startSpace.x - 3, startSpace.y - 3, 0];
|
||||
} else {
|
||||
let [x, y, r] = move(this.highlightX, this.highlightY, this.cardRotation);
|
||||
let clampedPosition = clamp
|
||||
? this.cardPlaying!.clampPosition(x, y, this.grid.length, this.grid[0].length, r)
|
||||
: { x, y };
|
||||
[this.highlightX, this.highlightY, this.cardRotation] = [clampedPosition.x, clampedPosition.y, r];
|
||||
}
|
||||
this.refreshHighlight();
|
||||
}
|
||||
|
||||
checkMoveLegality(playerIndex: number, card: Card, x: number, y: number, rotation: number, isSpecialAttack: boolean): string | null {
|
||||
@@ -133,7 +183,7 @@ class Board {
|
||||
trs[y].appendChild(td);
|
||||
col.push(td);
|
||||
td.addEventListener('mousemove', e => {
|
||||
if (this.autoHighlight) {
|
||||
if (this.autoHighlight && this.cardPlaying != null) {
|
||||
const x = parseInt((e.target as HTMLTableCellElement).dataset.x!) - 3;
|
||||
const y = parseInt((e.target as HTMLTableCellElement).dataset.y!) - 3;
|
||||
if (x != this.highlightX || y != this.highlightY) {
|
||||
@@ -144,14 +194,14 @@ class Board {
|
||||
}
|
||||
});
|
||||
td.addEventListener('click', e => {
|
||||
if (this.autoHighlight && this.onclick) {
|
||||
if (this.autoHighlight && this.cardPlaying != null && this.onclick) {
|
||||
const x = parseInt((e.target as HTMLTableCellElement).dataset.x!) - 3;
|
||||
const y = parseInt((e.target as HTMLTableCellElement).dataset.y!) - 3;
|
||||
this.onclick(x, y);
|
||||
}
|
||||
});
|
||||
td.addEventListener('wheel', e => {
|
||||
if (this.autoHighlight) {
|
||||
if (this.autoHighlight && this.cardPlaying != null) {
|
||||
e.preventDefault();
|
||||
if (e.deltaY > 0) this.cardRotation++;
|
||||
else if (e.deltaY < 0) this.cardRotation--;
|
||||
|
||||
@@ -6,6 +6,11 @@ class Card {
|
||||
grid: Space[][];
|
||||
size: number;
|
||||
|
||||
private minX: number;
|
||||
private minY: number;
|
||||
private maxX: number;
|
||||
private maxY: number;
|
||||
|
||||
constructor(number: number, name: string, rarity: Rarity, specialCost: number, grid: Space[][]) {
|
||||
this.number = number;
|
||||
this.name = name;
|
||||
@@ -13,14 +18,23 @@ class Card {
|
||||
this.specialCost = specialCost;
|
||||
this.grid = grid;
|
||||
|
||||
let size = 0;
|
||||
let size = 0, minX = 3, minY = 3, maxX = 3, maxY = 3;
|
||||
for (let y = 0; y < 8; y++) {
|
||||
for (let x = 0; x < 8; x++) {
|
||||
if (grid[x][y] != Space.Empty)
|
||||
if (grid[x][y] != Space.Empty) {
|
||||
size++;
|
||||
minX = Math.min(minX, x);
|
||||
minY = Math.min(minY, y);
|
||||
maxX = Math.max(maxX, x);
|
||||
maxY = Math.max(maxY, y);
|
||||
}
|
||||
}
|
||||
}
|
||||
this.size = size;
|
||||
this.minX = minX;
|
||||
this.minY = minY;
|
||||
this.maxX = maxX;
|
||||
this.maxY = maxY;
|
||||
}
|
||||
|
||||
static fromJson(obj: any) {
|
||||
@@ -35,4 +49,17 @@ class Card {
|
||||
default: return this.grid[7 - y][x];
|
||||
}
|
||||
}
|
||||
|
||||
clampPosition(x: number, y: number, boardWidth: number, boardHeight: number, rotation: number) {
|
||||
let x1, y1, x2, y2;
|
||||
switch (rotation & 3) {
|
||||
case 0: x1 = this.minX; y1 = this.minY; x2 = this.maxX; y2 = this.maxY; break;
|
||||
case 1: x1 = 7 - this.maxY; y1 = this.minX; x2 = 7 - this.minY; y2 = this.maxX; break;
|
||||
case 2: x1 = 7 - this.maxX; y1 = 7 - this.maxY; x2 = 7 - this.minX; y2 = 7 - this.minY; break;
|
||||
default: x1 = this.minY; y1 = 7 - this.maxX; x2 = this.maxY; y2 = 7 - this.minX; break;
|
||||
}
|
||||
x = Math.min(Math.max(x, -x1), boardWidth - 1 - x2);
|
||||
y = Math.min(Math.max(y, -y1), boardHeight - 1 - y2);
|
||||
return { x, y };
|
||||
}
|
||||
}
|
||||
|
||||
@@ -28,8 +28,10 @@ for (let x = 0; x < 9; x++) {
|
||||
col.push(Space.Empty);
|
||||
cols.push(col);
|
||||
}
|
||||
cols[4][4] = Space.SpecialInactive2;
|
||||
board.startSpaces.push({ x: 4, y: 21 });
|
||||
board.startSpaces.push({ x: 4, y: 4 });
|
||||
cols[4][21] = Space.SpecialInactive1;
|
||||
cols[4][4] = Space.SpecialInactive2;
|
||||
board.resize(cols);
|
||||
|
||||
function loadPlayers(players: Player[]) {
|
||||
@@ -102,8 +104,9 @@ async function playInkAnimations(data: {
|
||||
const inkPlaced = new Set<number>();
|
||||
const placements = data.placements;
|
||||
board.clearHighlight();
|
||||
canPlay = false;
|
||||
board.cardPlaying = null;
|
||||
board.autoHighlight = false;
|
||||
canPlay = false;
|
||||
await delay(anySpecialAttacks ? 3000 : 1000);
|
||||
for (const placement of placements) {
|
||||
// Skip the delay when cards don't overlap.
|
||||
@@ -184,11 +187,12 @@ function updateHand(cards: any[]) {
|
||||
if (!currentGame?.me) return;
|
||||
currentGame.me.hand = cards.map(Card.fromJson);
|
||||
if (cards) {
|
||||
for (const card of currentGame.me.hand) {
|
||||
for (let i = 0; i < currentGame.me.hand.length; i++) {
|
||||
const card = currentGame.me.hand[i];
|
||||
const button = new CardButton('radio', card);
|
||||
button.inputElement.name = 'handCard';
|
||||
handButtons.push(button);
|
||||
button.inputElement.addEventListener('change', e => {
|
||||
button.inputElement.addEventListener('input', e => {
|
||||
if (button.checked) {
|
||||
for (const button2 of handButtons) {
|
||||
if (button2 != button)
|
||||
@@ -208,9 +212,40 @@ function updateHand(cards: any[]) {
|
||||
|
||||
board.autoHighlight = false;
|
||||
}
|
||||
} else {
|
||||
board.cardPlaying = card;
|
||||
if (isNaN(board.highlightX) || isNaN(board.highlightY)) {
|
||||
board.highlightX = board.startSpaces[board.playerIndex!].x - 3;
|
||||
board.highlightY = board.startSpaces[board.playerIndex!].y - 3;
|
||||
}
|
||||
board.cardRotation = 0;
|
||||
board.refreshHighlight();
|
||||
board.table.focus();
|
||||
}
|
||||
board.cardPlaying = card;
|
||||
board.cardRotation = 0;
|
||||
}
|
||||
});
|
||||
button.inputElement.addEventListener('keydown', e => {
|
||||
switch (e.key) {
|
||||
case 'ArrowUp':
|
||||
if (i >= 2)
|
||||
handButtons[i - 2].inputElement.focus();
|
||||
e.preventDefault();
|
||||
break;
|
||||
case 'ArrowDown':
|
||||
if (i < 2)
|
||||
handButtons[i + 2].inputElement.focus();
|
||||
e.preventDefault();
|
||||
break;
|
||||
case 'ArrowLeft':
|
||||
if (i % 2 != 0)
|
||||
handButtons[i - 1].inputElement.focus();
|
||||
e.preventDefault();
|
||||
break;
|
||||
case 'ArrowRight':
|
||||
if (i % 2 == 0)
|
||||
handButtons[i + 1].inputElement.focus();
|
||||
e.preventDefault();
|
||||
break;
|
||||
}
|
||||
});
|
||||
handContainer.appendChild(button.element);
|
||||
@@ -230,12 +265,13 @@ function redrawButton_click(e: MouseEvent) {
|
||||
redrawModal.hidden = true;
|
||||
}
|
||||
|
||||
passButton.addEventListener('change', e => {
|
||||
function passButton_input() {
|
||||
board.autoHighlight = !passButton.checked;
|
||||
if (passButton.checked) {
|
||||
specialButton.checked = false;
|
||||
board.cardPlaying = null;
|
||||
board.specialAttack = false;
|
||||
board.clearHighlight();
|
||||
for (const el of handButtons) {
|
||||
el.enabled = true;
|
||||
el.checked = false;
|
||||
@@ -245,8 +281,10 @@ passButton.addEventListener('change', e => {
|
||||
handButtons[i].enabled = canPlayCard[i];
|
||||
}
|
||||
}
|
||||
});
|
||||
specialButton.addEventListener('change', e => {
|
||||
}
|
||||
passButton.addEventListener('input', passButton_input);
|
||||
|
||||
function specialButton_input() {
|
||||
board.specialAttack = specialButton.checked;
|
||||
if (specialButton.checked) {
|
||||
passButton.checked = false;
|
||||
@@ -263,7 +301,8 @@ specialButton.addEventListener('change', e => {
|
||||
handButtons[i].checked = false;
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
specialButton.addEventListener('input', specialButton_input);
|
||||
|
||||
board.onclick = (x, y) => {
|
||||
if (board.cardPlaying == null || !currentGame?.me)
|
||||
@@ -308,6 +347,55 @@ board.onclick = (x, y) => {
|
||||
}
|
||||
};
|
||||
|
||||
board.oncancel = () => {
|
||||
board.cardPlaying = null;
|
||||
board.clearHighlight();
|
||||
for (const button of handButtons) {
|
||||
if (button.checked) {
|
||||
button.checked = false;
|
||||
button.inputElement.focus();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
function focusFirstEnabledHandCard() {
|
||||
const input = document.activeElement as HTMLInputElement;
|
||||
if (input.name == 'handCard') {
|
||||
if (input.disabled)
|
||||
input.checked = false;
|
||||
else {
|
||||
// An enabled hand button is already active; don't move the focus.
|
||||
return;
|
||||
}
|
||||
}
|
||||
const firstEnabledButton = handButtons.find(b => b.enabled);
|
||||
if (firstEnabledButton)
|
||||
firstEnabledButton.inputElement.focus();
|
||||
}
|
||||
|
||||
document.addEventListener('keydown', e => {
|
||||
if (!sections.get('game')!.hidden) {
|
||||
switch (e.key) {
|
||||
case 'p':
|
||||
if (!passButton.disabled) {
|
||||
passButton.checked = !passButton.checked;
|
||||
passButton_input();
|
||||
focusFirstEnabledHandCard();
|
||||
}
|
||||
e.preventDefault();
|
||||
break;
|
||||
case 's':
|
||||
if (!specialButton.disabled) {
|
||||
specialButton.checked = !specialButton.checked;
|
||||
specialButton_input();
|
||||
focusFirstEnabledHandCard();
|
||||
}
|
||||
e.preventDefault();
|
||||
break;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
document.getElementById('resultLeaveButton')!.addEventListener('click', e => {
|
||||
e.preventDefault();
|
||||
document.getElementById('preGameDefaultSection')!.hidden = false;
|
||||
|
||||
@@ -179,7 +179,7 @@ footer {
|
||||
z-index: -2;
|
||||
opacity: 0.25;
|
||||
}
|
||||
.card:hover:not(.checked):not(.disabled)::before {
|
||||
.card:is(:hover, :focus-within):not(.checked, .disabled)::before {
|
||||
content: '';
|
||||
background: grey;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user