diff --git a/TableturfBattleClient/index.html b/TableturfBattleClient/index.html
index 4cf4027..e6d2824 100644
--- a/TableturfBattleClient/index.html
+++ b/TableturfBattleClient/index.html
@@ -163,7 +163,7 @@
-
+
diff --git a/TableturfBattleClient/src/GameVariables.ts b/TableturfBattleClient/src/GameVariables.ts
index ad12230..961838e 100644
--- a/TableturfBattleClient/src/GameVariables.ts
+++ b/TableturfBattleClient/src/GameVariables.ts
@@ -18,8 +18,8 @@ let enterGameTimeout: number | null = null;
let currentReplay: {
turns: Move[][],
placements: PlacementResults[],
- initialDrawOrder: number[][],
- drawOrder: number[][]
+ replayPlayerData: ReplayPlayerData[],
+ watchingPlayer: number
} | null = null;
const playerList = document.getElementById('playerList')!;
diff --git a/TableturfBattleClient/src/Pages/GamePage.ts b/TableturfBattleClient/src/Pages/GamePage.ts
index 85670be..d35129b 100644
--- a/TableturfBattleClient/src/Pages/GamePage.ts
+++ b/TableturfBattleClient/src/Pages/GamePage.ts
@@ -20,8 +20,7 @@ const playRow = document.getElementById('playRow')!;
const spectatorRow = document.getElementById('spectatorRow')!;
const replayNextButton = document.getElementById('replayNextButton')!;
const replayPreviousButton = document.getElementById('replayPreviousButton')!;
-const flipLabel = document.getElementById('flipLabel') as HTMLLabelElement;
-const flipBox = document.getElementById('flipBox') as HTMLInputElement;
+const flipButton = document.getElementById('flipButton') as HTMLButtonElement;
let replayAnimationAbortController: AbortController | null = null;
const shareReplayLinkButton = document.getElementById('shareReplayLinkButton') as HTMLButtonElement;
@@ -70,6 +69,7 @@ board.resize(cols);
function clear() {
testMode = false;
+ gamePage.classList.remove('replay');
gamePage.classList.remove('deckTest');
gamePage.classList.remove('gameEnded');
gameControls.hidden = true;
@@ -79,8 +79,7 @@ function clear() {
replayPreviousButton.hidden = true;
replayNextButton.hidden = true;
shareReplayLinkButton.hidden = true;
- flipLabel.hidden = false;
- flipBox.checked = false;
+ flipButton.hidden = false;
gameButtonsContainer.hidden = true;
testControls.hidden = true;
for (const playerBar of playerBars) {
@@ -104,27 +103,32 @@ function initSpectator() {
clear();
gameControls.hidden = false;
spectatorRow.hidden = false;
- flipLabel.hidden = false;
- flipBox.checked = false;
+ flipButton.hidden = false;
gameButtonsContainer.hidden = false;
showPage('game');
}
function initReplay() {
clear();
+ gamePage.classList.add('replay');
gameControls.hidden = false;
+ handContainer.hidden = false;
spectatorRow.hidden = false;
replayPreviousButton.hidden = false;
replayNextButton.hidden = false;
- flipLabel.hidden = false;
- flipBox.checked = false;
+ flipButton.hidden = false;
+ gameButtonsContainer.hidden = false;
canPlay = false;
showPage('game');
clearPlayContainers();
- turnNumberLabel.setTurnNumber(1);
+ turnNumberLabel.setTurnNumber(null);
+ replayUpdateHand();
}
function initTest(stage: Stage) {
+ clear();
+ testMode = true;
+ gamePage.classList.add('deckTest');
currentGame = { id: 'test', maxPlayers: 2, players: [ ], webSocket: null, turnNumber: 1, me: { playerIndex: 0, move: null, deck: null, hand: null, cardsUsed: [ ] } };
board.resize(stage.copyGrid());
const startSpaces = stage.getStartSpaces(2);
@@ -135,14 +139,10 @@ function initTest(stage: Stage) {
for (var o of playerBars)
o.element.hidden = true;
- testMode = true;
testPlacements.splice(0);
clearChildren(testPlacementList);
- gamePage.classList.add('deckTest');
- gamePage.classList.remove('gameEnded');
gamePage.dataset.myPlayerIndex = '0';
gamePage.dataset.uiBaseColourIsSpecialColour = 'true';
- gameControls.hidden = true;
gameButtonsContainer.hidden = false;
testControls.hidden = false;
clearPlayContainers();
@@ -159,6 +159,7 @@ replayNextButton.addEventListener('click', _ => {
if (currentGame == null || currentReplay == null || currentGame.turnNumber > 12) return;
if (replayAnimationAbortController) {
+ replayUpdateHand();
replayAnimationAbortController.abort();
replayAnimationAbortController = null;
turnNumberLabel.setTurnNumber(currentGame.turnNumber);
@@ -168,86 +169,105 @@ replayNextButton.addEventListener('click', _ => {
}
}
- 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.
clearPlayContainers();
- for (let i = 0; i < currentGame.players.length; i++) {
- const player = currentGame.players[i];
-
- const move = moves[i];
- 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++;
- player.specialPoints++;
- const el = document.createElement('div');
- el.className = 'passLabel';
- el.innerText = 'Pass';
- button.element.appendChild(el);
- }
- button.inputElement.hidden = true;
- playContainers[i].append(button.element);
- }
-
- for (const p of result.specialSpacesActivated) {
- const space = board.grid[p.x][p.y];
- const player2 = currentGame.players[space & 3];
- player2.specialPoints++;
- player2.totalSpecialPoints++;
- }
- 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, replayAnimationAbortController.signal);
+ if (currentGame.turnNumber == 0) {
+ // Show redraw decisions.
+ currentGame.turnNumber++;
turnNumberLabel.setTurnNumber(currentGame.turnNumber);
- clearPlayContainers();
- if (currentGame.turnNumber > 12) {
- gameButtonsContainer.hidden = true;
- passButton.enabled = false;
- specialButton.enabled = false;
- gamePage.classList.add('gameEnded');
- showResult();
+ replayUpdateHand();
+ } else {
+ 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.
+ const handButton = handButtons.find(b => b.card.number == moves[currentReplay!.watchingPlayer].card.number);
+ if (handButton) handButton.checked = true;
+ for (let i = 0; i < currentGame.players.length; i++) {
+ const player = currentGame.players[i];
+
+ const move = moves[i];
+ 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++;
+ player.specialPoints++;
+ const el = document.createElement('div');
+ el.className = 'passLabel';
+ el.innerText = 'Pass';
+ button.element.appendChild(el);
+ }
+ button.inputElement.hidden = true;
+ playContainers[i].append(button.element);
}
- })();
+
+ for (const p of result.specialSpacesActivated) {
+ const space = board.grid[p.x][p.y];
+ const player2 = currentGame.players[space & 3];
+ player2.specialPoints++;
+ player2.totalSpecialPoints++;
+ }
+ 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, replayAnimationAbortController.signal);
+ turnNumberLabel.setTurnNumber(currentGame.turnNumber);
+ clearPlayContainers();
+ if (currentGame.turnNumber > 12) {
+ gameButtonsContainer.hidden = true;
+ passButton.enabled = false;
+ specialButton.enabled = false;
+ gamePage.classList.add('gameEnded');
+ showResult();
+ } else
+ replayUpdateHand();
+ })();
+ }
});
replayPreviousButton.addEventListener('click', _ => {
- if (currentGame == null || currentReplay == null) return;
+ if (currentGame == null || currentReplay == null || currentGame.turnNumber == 0) return;
replayAnimationAbortController?.abort();
replayAnimationAbortController = null;
- const result = currentReplay.placements.pop();
- if (!result) return;
+ if (currentGame.turnNumber > 1) {
+ 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 = '';
+ clearPlayContainers();
+ for (let i = 0; i < currentGame.players.length; i++) {
+ const el = playerBars[i].resultElement;
+ el.innerText = '';
+ }
+
+ undoTurn(result);
}
-
- undoTurn(result);
- currentGame!.turnNumber--;
+ currentGame.turnNumber--;
gamePage.classList.remove('gameEnded');
- turnNumberLabel.setTurnNumber(currentGame!.turnNumber);
+ handContainer.hidden = false;
+ gameButtonsContainer.hidden = false;
- 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--;
- } else if ((move as PlayMove).isSpecialAttack)
- currentGame.players[i].specialPoints += (move as PlayMove).card.specialCost;
- updateStats(i);
- }
+ if (currentGame.turnNumber > 0) {
+ turnNumberLabel.setTurnNumber(currentGame.turnNumber);
+ 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--;
+ } else if ((move as PlayMove).isSpecialAttack)
+ currentGame.players[i].specialPoints += (move as PlayMove).card.specialCost;
+ updateStats(i);
+ }
+ } else
+ turnNumberLabel.setTurnNumber(null);
+
+ replayUpdateHand();
});
function undoTurn(turn: PlacementResults) {
@@ -272,9 +292,7 @@ function undoTurn(turn: PlacementResults) {
board.refresh();
}
-flipBox.addEventListener('input', flipBox_input);
-
-function flipBox_input(e: Event) {
+flipButton.addEventListener('click', () => {
if (currentGame == null) return;
if (replayAnimationAbortController) {
replayAnimationAbortController.abort();
@@ -285,11 +303,20 @@ function flipBox_input(e: Event) {
updateStats(i);
}
}
- board.flip = (e.target as HTMLInputElement).checked;
+ if (currentReplay) {
+ currentReplay.watchingPlayer++;
+ if (currentReplay.watchingPlayer >= currentGame.players.length)
+ currentReplay.watchingPlayer = 0;
+ gamePage.dataset.myPlayerIndex = currentReplay.watchingPlayer.toString();
+ board.flip = currentReplay.watchingPlayer % 2 != 0;
+ clearShowDeck();
+ replayUpdateHand();
+ } else
+ board.flip = !board.flip;
if (board.flip) gamePage.classList.add('boardFlipped');
else gamePage.classList.remove('boardFlipped');
board.resize();
-}
+});
function addTestDeckCard(card: Card) {
const button = new CardButton('radio', cardDatabase.get(card.number));
@@ -527,14 +554,14 @@ function showResult() {
}
}
+ handContainer.hidden = true;
if (!currentReplay) {
- handContainer.hidden = true;
playRow.hidden = true;
spectatorRow.hidden = false;
replayPreviousButton.hidden = true;
replayNextButton.hidden = true;
shareReplayLinkButton.hidden = false;
- flipLabel.hidden = true;
+ flipButton.hidden = true;
canShareReplay = navigator.canShare && navigator.canShare({ url: window.location.href, title: 'Tableturf Battle Replay' });
shareReplayLinkButton.innerText = canShareReplay ? 'Share replay link' : 'Copy replay link';
}
@@ -547,14 +574,9 @@ function clearShowDeck() {
showDeckButtons.splice(0);
}
-function updateHand(playerData: PlayerData) {
- for (const button of handButtons) {
- handContainer.removeChild(button.element);
- }
- handButtons.splice(0);
-
+function populateShowDeck(deck: Card[]) {
if (showDeckButtons.length == 0) {
- for (const card of playerData.deck!) {
+ for (const card of deck) {
const button = new CardButton('checkbox', card);
button.inputElement.hidden = true;
showDeckButtons.push(button);
@@ -564,6 +586,15 @@ function updateHand(playerData: PlayerData) {
showDeckListElement.appendChild(li);
}
}
+}
+
+function updateHand(playerData: PlayerData) {
+ for (const button of handButtons) {
+ handContainer.removeChild(button.element);
+ }
+ handButtons.splice(0);
+
+ populateShowDeck(playerData.deck!);
for (const button of showDeckButtons) {
const li = button.element.parentElement!;
@@ -643,6 +674,52 @@ function updateHand(playerData: PlayerData) {
}
}
+function replayUpdateHand() {
+ if (currentGame == null || currentReplay == null) return;
+
+ for (const button of handButtons) {
+ handContainer.removeChild(button.element);
+ }
+ handButtons.splice(0);
+
+ const playerData = currentReplay.replayPlayerData[currentReplay.watchingPlayer];
+ populateShowDeck(playerData.deck);
+ for (const b of showDeckButtons)
+ b.element.parentElement!.className = '';
+
+ let indices;
+ if (currentGame.turnNumber == 0) {
+ indices = playerData.initialDrawOrder;
+ } else {
+ indices = playerData.drawOrder.slice(0, 4);
+ for (let i = 0; i < currentGame.turnNumber - 1; i++) {
+ const move = currentReplay.turns[i][currentReplay.watchingPlayer];
+ let j = indices.findIndex(k => playerData.deck[k].number == move.card.number);
+ if (j < 0) j = indices.findIndex(k => k < 0 || k >= 15);
+ if (j >= 0) {
+ showDeckButtons[indices[j]].element.parentElement!.className = 'used';
+ if (i < 11)
+ indices[j] = playerData.drawOrder[4 + i];
+ }
+ }
+ }
+
+ for (let i = 0; i < showDeckButtons.length; i++) {
+ const li = showDeckButtons[i].element.parentElement!;
+ if (!li.className)
+ li.className = indices.includes(i) ? 'inHand' : 'unused';
+ }
+
+ for (let i = 0; i < 4; i++) {
+ if (indices[i] >= 15) continue; // Accounts for an old bug in the server that corrupted the initialDrawOrder replay fields.
+ const card = playerData.deck[indices[i]];
+ const button = new CardButton('radio', card);
+ button.inputElement.hidden = true;
+ handButtons.push(button);
+ handContainer.appendChild(button.element);
+ }
+}
+
(document.getElementById('redrawNoButton') as HTMLButtonElement).addEventListener('click', redrawButton_click);
(document.getElementById('redrawYesButton') as HTMLButtonElement).addEventListener('click', redrawButton_click);
function redrawButton_click(e: MouseEvent) {
@@ -804,9 +881,10 @@ gameDeckButton.addEventListener('click', toggleShowDeck);
showDeckCloseButton.addEventListener('click', toggleShowDeck);
document.addEventListener('keydown', e => {
- if (!pages.get('game')!.hidden && !playRow.hidden) {
+ if (!pages.get('game')!.hidden) {
switch (e.key) {
case 'p':
+ if (playRow.hidden) return;
if (passButton.enabled) {
passButton.checked = !passButton.checked;
passButton_input();
@@ -815,6 +893,7 @@ document.addEventListener('keydown', e => {
e.preventDefault();
break;
case 's':
+ if (playRow.hidden) return;
if (specialButton.enabled) {
specialButton.checked = !specialButton.checked;
specialButton_input();
@@ -823,7 +902,8 @@ document.addEventListener('keydown', e => {
e.preventDefault();
break;
case 'd':
- if (!gameButtonsContainer.hidden) toggleShowDeck();
+ if (testMode || gameButtonsContainer.hidden) return;
+ toggleShowDeck();
e.preventDefault();
break;
}
diff --git a/TableturfBattleClient/src/Pages/PreGamePage.ts b/TableturfBattleClient/src/Pages/PreGamePage.ts
index 0b7d256..bc98e84 100644
--- a/TableturfBattleClient/src/Pages/PreGamePage.ts
+++ b/TableturfBattleClient/src/Pages/PreGamePage.ts
@@ -230,7 +230,7 @@ function loadReplay(base64: string) {
let pos = 2;
const players = [ ];
- currentReplay = { turns: [ ], placements: [ ], drawOrder: [ ], initialDrawOrder: [ ] };
+ currentReplay = { turns: [ ], placements: [ ], replayPlayerData: [ ], watchingPlayer: 0 };
for (let i = 0; i < numPlayers; i++) {
const len = dataView.getUint8(pos + 34);
const player: Player = {
@@ -245,8 +245,12 @@ function loadReplay(base64: string) {
};
players.push(player);
+ const deck = [ ];
const initialDrawOrder = [ ];
const drawOrder = [ ];
+ for (let j = 0; j < 15; j++) {
+ deck.push(cardDatabase.get(dataView.getUint8(pos + 9 + j)));
+ }
for (let j = 0; j < 2; j++) {
initialDrawOrder.push(dataView.getUint8(pos + 24 + j) & 0xF);
initialDrawOrder.push(dataView.getUint8(pos + 24 + j) >> 4 & 0xF);
@@ -258,9 +262,7 @@ function loadReplay(base64: string) {
else
drawOrder.push(dataView.getUint8(pos + 26 + j) >> 4 & 0xF);
}
- currentReplay.initialDrawOrder.push(initialDrawOrder);
- currentReplay.drawOrder.push(drawOrder);
-
+ currentReplay.replayPlayerData.push({ deck, initialDrawOrder, drawOrder });
pos += 35 + len;
}
@@ -287,7 +289,7 @@ function loadReplay(base64: string) {
me: null,
players: players,
maxPlayers: numPlayers,
- turnNumber: 1,
+ turnNumber: 0,
webSocket: null
};
diff --git a/TableturfBattleClient/src/PlayerData.ts b/TableturfBattleClient/src/PlayerData.ts
index b5a1843..b622108 100644
--- a/TableturfBattleClient/src/PlayerData.ts
+++ b/TableturfBattleClient/src/PlayerData.ts
@@ -5,3 +5,9 @@ interface PlayerData {
cardsUsed: number[];
move: Move | null;
}
+
+interface ReplayPlayerData {
+ deck: Card[],
+ initialDrawOrder: number[],
+ drawOrder: number[]
+}
diff --git a/TableturfBattleClient/tableturf.css b/TableturfBattleClient/tableturf.css
index 274bc52..d0c2721 100644
--- a/TableturfBattleClient/tableturf.css
+++ b/TableturfBattleClient/tableturf.css
@@ -47,6 +47,11 @@ body {
--player-ui-highlight-colour: var(--player-special-colour);
--player-ui-highlight2-colour: var(--player-special-accent-colour);
}
+#gamePage[data-my-player-index="0"], .playContainer[data-index="0"] {
+ --player-primary-colour: var(--primary-colour-1);
+ --player-special-colour: var(--special-colour-1);
+ --player-special-accent-colour: var(--special-accent-colour-1);
+}
#gamePage[data-my-player-index="1"], .playContainer[data-index="1"] {
--player-primary-colour: var(--primary-colour-2);
--player-special-colour: var(--special-colour-2);
@@ -748,6 +753,9 @@ label[for="flipBox"] {
#gamePage.deckTest #gameDeckButton {
display: none;
}
+#gamePage.replay :is(#rotateLeftButton, #rotateRightButton) {
+ display: none;
+}
#boardSection {
grid-column: board-column;