mirror of
https://github.com/AndrioCelos/TableturfBattleApp.git
synced 2026-04-25 08:04:33 -05:00
Allow rewinding replays
This commit is contained in:
parent
bb75c238a8
commit
42956e9e9e
|
|
@ -155,6 +155,7 @@
|
|||
<a id="resultLeaveButton" href="#">Leave game</a>
|
||||
</div>
|
||||
<div id="replayControls" hidden>
|
||||
<button id="replayPreviousButton">Previous turn</button>
|
||||
<button id="replayNextButton">Next turn</button>
|
||||
<a id="replayLeaveButton" href="#">Exit replay</a>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -342,7 +342,7 @@ class Board {
|
|||
}
|
||||
} else {
|
||||
if (this.grid[x][y] < Space.SpecialInactive1) // Ink spaces can't overlap special spaces from larger cards.
|
||||
placement.spacesAffected.push({ space: point, newState: this.grid[x][y] = (Space.Ink1 | i) });
|
||||
placement.spacesAffected.push({ space: point, oldState: this.grid[x][y], newState: this.grid[x][y] = (Space.Ink1 | i) });
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
|
@ -356,7 +356,7 @@ class Board {
|
|||
// If a special space overlaps an ink space, overwrite it.
|
||||
e.newState = this.grid[x][y] = (Space.SpecialInactive1 | i);
|
||||
} else
|
||||
placement.spacesAffected.push({ space: point, newState: this.grid[x][y] = (Space.SpecialInactive1 | i) });
|
||||
placement.spacesAffected.push({ space: point, oldState: this.grid[x][y], newState: this.grid[x][y] = (Space.SpecialInactive1 | i) });
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -17,6 +17,7 @@ let currentGame: {
|
|||
let enterGameTimeout: number | null = null;
|
||||
let currentReplay: {
|
||||
turns: Move[][],
|
||||
placements: { placements: Placement[], specialSpacesActivated: Point[] }[],
|
||||
initialDrawOrder: number[][],
|
||||
drawOrder: number[][]
|
||||
} | null = null;
|
||||
|
|
|
|||
|
|
@ -20,6 +20,8 @@ const resultContainer = document.getElementById('resultContainer')!;
|
|||
const resultElement = document.getElementById('result')!;
|
||||
const replayControls = document.getElementById('replayControls')!;
|
||||
const replayNextButton = document.getElementById('replayNextButton')!;
|
||||
const replayPreviousButton = document.getElementById('replayPreviousButton')!;
|
||||
let replayAnimationAbortController: AbortController | null = null;
|
||||
|
||||
const shareReplayLinkButton = document.getElementById('shareReplayLinkButton') as HTMLButtonElement;
|
||||
let canShareReplay = false;
|
||||
|
|
@ -62,10 +64,21 @@ function initReplay() {
|
|||
}
|
||||
|
||||
replayNextButton.addEventListener('click', _ => {
|
||||
if (currentGame == null || currentReplay == null) return;
|
||||
if (currentGame == null || currentReplay == null || currentGame.turnNumber > 12) return;
|
||||
|
||||
if (replayAnimationAbortController) {
|
||||
replayAnimationAbortController.abort();
|
||||
replayAnimationAbortController = null;
|
||||
turnNumberLabel.setTurnNumber(currentGame.turnNumber);
|
||||
board.refresh();
|
||||
for (let i = 0; i < currentGame.players.length; i++) {
|
||||
updateStats(i);
|
||||
}
|
||||
}
|
||||
|
||||
const moves = currentReplay.turns[currentGame.turnNumber - 1];
|
||||
const result = board.makePlacements(moves);
|
||||
currentReplay.placements.push(result);
|
||||
|
||||
let anySpecialAttacks = false;
|
||||
// Show the cards that were played.
|
||||
|
|
@ -77,6 +90,7 @@ replayNextButton.addEventListener('click', _ => {
|
|||
const button = new CardButton('checkbox', move.card);
|
||||
if ((move as PlayMove).isSpecialAttack) {
|
||||
anySpecialAttacks = true;
|
||||
player.specialPoints -= (move as PlayMove).card.specialCost;
|
||||
button.element.classList.add('specialAttack');
|
||||
} else if (move.isPass) {
|
||||
player.passes++;
|
||||
|
|
@ -99,8 +113,9 @@ replayNextButton.addEventListener('click', _ => {
|
|||
}
|
||||
currentGame.turnNumber++;
|
||||
|
||||
replayAnimationAbortController = new AbortController();
|
||||
(async () => {
|
||||
await playInkAnimations({ game: { state: GameState.Ongoing, board: null, turnNumber: currentGame.turnNumber, players: currentGame.players }, placements: result.placements, specialSpacesActivated: result.specialSpacesActivated }, anySpecialAttacks);
|
||||
await playInkAnimations({ game: { state: GameState.Ongoing, board: null, turnNumber: currentGame.turnNumber, players: currentGame.players }, placements: result.placements, specialSpacesActivated: result.specialSpacesActivated }, anySpecialAttacks, replayAnimationAbortController.signal);
|
||||
turnNumberLabel.setTurnNumber(currentGame.turnNumber);
|
||||
clearPlayContainers();
|
||||
if (currentGame.turnNumber > 12) {
|
||||
|
|
@ -113,6 +128,55 @@ replayNextButton.addEventListener('click', _ => {
|
|||
})();
|
||||
});
|
||||
|
||||
replayPreviousButton.addEventListener('click', _ => {
|
||||
if (currentGame == null || currentReplay == null) return;
|
||||
|
||||
replayAnimationAbortController?.abort();
|
||||
replayAnimationAbortController = null;
|
||||
|
||||
const result = currentReplay.placements.pop();
|
||||
if (!result) return;
|
||||
|
||||
clearPlayContainers();
|
||||
for (let i = 0; i < currentGame.players.length; i++) {
|
||||
const el = playerBars[i].resultElement;
|
||||
el.innerText = '';
|
||||
}
|
||||
|
||||
// Unwind the turn.
|
||||
for (const p of result.specialSpacesActivated) {
|
||||
const space = board.grid[p.x][p.y];
|
||||
const player2 = currentGame.players[space & 3];
|
||||
player2.specialPoints--;
|
||||
player2.totalSpecialPoints--;
|
||||
board.grid[p.x][p.y] &= ~4;
|
||||
}
|
||||
currentGame.turnNumber--;
|
||||
|
||||
for (let i = result.placements.length - 1; i >= 0; i--) {
|
||||
const placement = result.placements[i];
|
||||
for (const p of placement.spacesAffected) {
|
||||
if (p.oldState == undefined) throw new TypeError('oldState missing');
|
||||
board.grid[p.space.x][p.space.y] = p.oldState;
|
||||
}
|
||||
}
|
||||
|
||||
gamePage.classList.remove('gameEnded');
|
||||
turnNumberLabel.setTurnNumber(currentGame.turnNumber);
|
||||
board.refresh();
|
||||
|
||||
for (let i = 0; i < currentGame.players.length; i++) {
|
||||
const move = currentReplay.turns[currentGame.turnNumber - 1][i];
|
||||
if (move.isPass) {
|
||||
currentGame.players[i].passes--;
|
||||
currentGame.players[i].specialPoints--;
|
||||
currentGame.players[i].totalSpecialPoints--;
|
||||
} else if ((move as PlayMove).isSpecialAttack)
|
||||
currentGame.players[i].specialPoints += (move as PlayMove).card.specialCost;
|
||||
updateStats(i);
|
||||
}
|
||||
});
|
||||
|
||||
function loadPlayers(players: Player[]) {
|
||||
gamePage.dataset.players = players.length.toString();
|
||||
for (let i = 0; i < players.length; i++) {
|
||||
|
|
@ -184,19 +248,19 @@ async function playInkAnimations(data: {
|
|||
game: { state: GameState, board: Space[][] | null, turnNumber: number, players: Player[] },
|
||||
placements: Placement[],
|
||||
specialSpacesActivated: Point[]
|
||||
}, anySpecialAttacks: boolean) {
|
||||
}, anySpecialAttacks: boolean, abortSignal?: AbortSignal) {
|
||||
const inkPlaced = new Set<number>();
|
||||
const placements = data.placements;
|
||||
board.clearHighlight();
|
||||
board.cardPlaying = null;
|
||||
board.autoHighlight = false;
|
||||
canPlay = false;
|
||||
await delay(anySpecialAttacks ? 3000 : 1000);
|
||||
await delay(anySpecialAttacks ? 3000 : 1000, abortSignal);
|
||||
for (const placement of placements) {
|
||||
// Skip the delay when cards don't overlap.
|
||||
if (placement.spacesAffected.find(p => inkPlaced.has(p.space.y * 37 + p.space.x))) {
|
||||
inkPlaced.clear();
|
||||
await delay(1000);
|
||||
await delay(1000, abortSignal);
|
||||
}
|
||||
|
||||
for (const p of placement.spacesAffected) {
|
||||
|
|
@ -204,23 +268,23 @@ async function playInkAnimations(data: {
|
|||
board.setDisplayedSpace(p.space.x, p.space.y, p.newState);
|
||||
}
|
||||
}
|
||||
await delay(1000);
|
||||
await delay(1000, abortSignal);
|
||||
|
||||
// Show special spaces.
|
||||
if (data.game.board)
|
||||
board.grid = data.game.board;
|
||||
board.refresh();
|
||||
if (data.specialSpacesActivated.length > 0)
|
||||
await delay(1000); // Delay if we expect that this changed the board.
|
||||
await delay(1000, abortSignal); // Delay if we expect that this changed the board.
|
||||
for (let i = 0; i < data.game.players.length; i++) {
|
||||
playerBars[i].specialPoints = data.game.players[i].specialPoints;
|
||||
playerBars[i].pointsDelta = board.getScore(i) - playerBars[i].points;
|
||||
}
|
||||
await delay(1000);
|
||||
await delay(1000, abortSignal);
|
||||
for (let i = 0; i < data.game.players.length; i++) {
|
||||
updateStats(i);
|
||||
}
|
||||
await delay(1000);
|
||||
await delay(1000, abortSignal);
|
||||
}
|
||||
|
||||
function showResult() {
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
interface Placement {
|
||||
players: number[],
|
||||
spacesAffected: { space: Point, newState: Space }[]
|
||||
spacesAffected: { space: Point, newState: Space, oldState?: Space }[]
|
||||
}
|
||||
|
|
|
|||
|
|
@ -14,7 +14,19 @@ const decks: Deck[] = [ new Deck('Starter Deck', [ 6, 34, 159, 13, 45, 137, 22,
|
|||
let selectedDeck: Deck | null = null;
|
||||
let deckModified = false;
|
||||
|
||||
function delay(ms: number) { return new Promise(resolve => setTimeout(() => resolve(null), ms)); }
|
||||
function delay(ms: number, abortSignal?: AbortSignal) {
|
||||
return new Promise((resolve, reject) => {
|
||||
if (abortSignal?.aborted) {
|
||||
reject(new DOMException('Operation cancelled', 'AbortError'));
|
||||
return;
|
||||
}
|
||||
const timeout = setTimeout(() => resolve(null), ms);
|
||||
abortSignal?.addEventListener('abort', _ => {
|
||||
clearTimeout(timeout);
|
||||
reject(new DOMException('Operation cancelled', 'AbortError'));
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function onInitialise(callback: () => void) {
|
||||
if (initialised)
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user