Allow rewinding replays

This commit is contained in:
Andrio Celos 2022-12-26 13:27:25 +11:00
parent bb75c238a8
commit 42956e9e9e
6 changed files with 91 additions and 13 deletions

View File

@ -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>

View File

@ -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;
}
}

View File

@ -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;

View File

@ -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() {

View File

@ -1,4 +1,4 @@
interface Placement {
players: number[],
spacesAffected: { space: Point, newState: Space }[]
spacesAffected: { space: Point, newState: Space, oldState?: Space }[]
}

View File

@ -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)