Minor fixes and refactoring

This commit is contained in:
Andrio Celos 2023-06-21 22:35:01 +10:00
parent 7a24364c7a
commit d042d0e79b
6 changed files with 302 additions and 299 deletions

View File

@ -1,5 +1,3 @@
/// <reference path="../CardList.ts"/>
const deckNameLabel2 = document.getElementById('deckName2')!;
const deckEditSize = document.getElementById('deckEditSize')!;
const deckCardListEdit = document.getElementById('deckCardListEdit')!;
@ -17,6 +15,82 @@ const deckEditCardButtons: (CardButton | HTMLLabelElement)[] = [ ];
let selectedDeckCardIndex: number | null = null;
function deckEditInitCardDatabase(cards: Card[]) {
for (const card of cards) {
const button = new CardButton('radio', card);
button.inputElement.name = 'deckEditorCardList';
cardList.add(button);
button.inputElement.addEventListener('input', () => {
if (button.inputElement.checked) {
for (const button2 of cardList.cardButtons) {
if (button2 != button)
button2.checked = false;
}
if (selectedDeckCardIndex == null) return;
const oldButton = deckEditCardButtons[selectedDeckCardIndex];
const button3 = createDeckEditCardButton(selectedDeckCardIndex, card.number);
button3.checked = true;
const oldElement = (oldButton as CardButton).element ?? (oldButton as Element);
deckCardListEdit.insertBefore(button3.element, oldElement);
deckCardListEdit.removeChild(oldElement);
deckEditCardButtons[selectedDeckCardIndex] = button3;
deckEditUpdateSize();
cardList.listElement.parentElement!.classList.remove('selecting');
if (!deckModified) {
deckModified = true;
window.addEventListener('beforeunload', onBeforeUnload_deckEditor);
}
}
});
addTestCard(card);
}
}
function deckEditInitStageDatabase(stages: Stage[]) {
for (const stage of stages) {
const button = new StageButton(stage);
testStageButtons.push(button);
button.inputElement.name = 'stage';
button.inputElement.addEventListener('input', () => {
if (button.inputElement.checked) {
stageRandomLabel.classList.remove('checked');
for (const button2 of testStageButtons) {
if (button2 != button)
button2.element.classList.remove('checked');
}
clearChildren(testDeckList);
testDeckCardButtons.splice(0);
if (editingDeck) {
for (const el of deckEditCardButtons) {
const card = (el as CardButton).card;
if (card) {
addTestDeckCard(card);
}
}
} else if (selectedDeck) {
for (const cardNumber of selectedDeck.cards) {
if (cardNumber > 0 && cardNumber <= cardDatabase.cards!.length) {
addTestDeckCard(cardDatabase.get(cardNumber));
}
}
}
testStageSelectionDialog.close();
initTest(stage);
}
});
button.setStartSpaces(2);
testStageSelectionList.appendChild(button.element);
}
}
function editDeck() {
if (selectedDeck == null) return;
@ -124,42 +198,6 @@ function deckEditUpdateSize() {
deckEditSize.innerText = size.toString();
}
function initCardDatabase(cards: Card[]) {
for (const card of cards) {
const button = new CardButton('radio', card);
button.inputElement.name = 'deckEditorCardList';
cardList.add(button);
button.inputElement.addEventListener('input', () => {
if (button.inputElement.checked) {
for (const button2 of cardList.cardButtons) {
if (button2 != button)
button2.checked = false;
}
if (selectedDeckCardIndex == null) return;
const oldButton = deckEditCardButtons[selectedDeckCardIndex];
const button3 = createDeckEditCardButton(selectedDeckCardIndex, card.number);
button3.checked = true;
const oldElement = (oldButton as CardButton).element ?? (oldButton as Element);
deckCardListEdit.insertBefore(button3.element, oldElement);
deckCardListEdit.removeChild(oldElement);
deckEditCardButtons[selectedDeckCardIndex] = button3;
deckEditUpdateSize();
cardList.listElement.parentElement!.classList.remove('selecting');
if (!deckModified) {
deckModified = true;
window.addEventListener('beforeunload', onBeforeUnload_deckEditor);
}
}
});
addTestCard(card);
}
}
deckCardListBackButton.addEventListener('click', e => {
e.preventDefault();
for (const o of deckEditCardButtons) {
@ -186,44 +224,6 @@ function onBeforeUnload_deckEditor(e: BeforeUnloadEvent) {
return 'You have unsaved changes to your deck that will be lost.';
}
function addTestStage(stage: Stage) {
const button = new StageButton(stage);
testStageButtons.push(button);
button.inputElement.name = 'stage';
button.inputElement.addEventListener('input', () => {
if (button.inputElement.checked) {
stageRandomLabel.classList.remove('checked');
for (const button2 of testStageButtons) {
if (button2 != button)
button2.element.classList.remove('checked');
}
clearChildren(testDeckList);
testDeckCardButtons.splice(0);
if (editingDeck) {
for (const el of deckEditCardButtons) {
const card = (el as CardButton).card;
if (card) {
addTestDeckCard(card);
}
}
} else if (selectedDeck) {
for (const cardNumber of selectedDeck.cards) {
if (cardNumber > 0 && cardNumber <= cardDatabase.cards!.length) {
addTestDeckCard(cardDatabase.get(cardNumber));
}
}
}
testStageSelectionDialog.close();
initTest(stage);
}
});
button.setStartSpaces(2);
testStageSelectionList.appendChild(button.element);
}
deckTestButton.addEventListener('click', _ => {
for (const button of testStageButtons)
button.checked = false;

View File

@ -1,5 +1,3 @@
/// <reference path="../TimeLabel.ts"/>
const gamePage = document.getElementById('gamePage')!;
const board = new Board(document.getElementById('gameBoard') as HTMLTableElement);
const turnNumberLabel = new TurnNumberLabel(document.getElementById('turnNumberContainer')!, document.getElementById('turnNumberLabel')!);
@ -91,6 +89,7 @@ function clear() {
}
}
/** Shows the game page for playing in a game. */
function initGame() {
clear();
gameControls.hidden = false;
@ -100,6 +99,7 @@ function initGame() {
showPage('game');
}
/** Shows the game page for spectating a game. */
function initSpectator() {
clear();
gameControls.hidden = false;
@ -109,6 +109,7 @@ function initSpectator() {
showPage('game');
}
/** Shows the game page for viewing a replay. */
function initReplay() {
clear();
gamePage.classList.add('replay');
@ -127,6 +128,7 @@ function initReplay() {
replayUpdateHand();
}
/** Shows the game page for deck editor testing. */
function initTest(stage: Stage) {
clear();
testMode = true;
@ -148,6 +150,7 @@ function initTest(stage: Stage) {
gameButtonsContainer.hidden = false;
testControls.hidden = false;
clearPlayContainers();
timeLabel.hide();
turnNumberLabel.setTurnNumber(null);
showPage('game');
@ -274,6 +277,7 @@ replayPreviousButton.addEventListener('click', _ => {
replayUpdateHand();
});
/** Undoes the last turn of the current replay, returning the view to the game state before that turn. */
function undoTurn(turn: PlacementResults) {
for (const p of turn.specialSpacesActivated) {
const space = board.grid[p.x][p.y];
@ -334,6 +338,7 @@ function addTestDeckCard(card: Card) {
if (button2 != button)
button2.element.classList.remove('checked');
}
board.autoHighlight = true;
board.cardPlaying = card;
if (isNaN(board.highlightX) || isNaN(board.highlightY)) {
board.highlightX = board.startSpaces[board.playerIndex!].x - (board.flip ? 4 : 3);
@ -356,6 +361,7 @@ function addTestCard(card: Card) {
if (button2 != button)
button2.element.classList.remove('checked');
}
board.autoHighlight = true;
board.cardPlaying = card;
if (isNaN(board.highlightX) || isNaN(board.highlightY)) {
board.highlightX = board.startSpaces[board.playerIndex!].x - (board.flip ? 4 : 3);
@ -413,6 +419,7 @@ document.getElementById('testAllCardsMobileButton')!.addEventListener('click', _
testCardListBackdrop.hidden = false;
});
/** Updates the game view with received player data. */
function loadPlayers(players: Player[]) {
gamePage.dataset.players = players.length.toString();
const scores = board.getScores();
@ -442,6 +449,7 @@ function updateStats(playerIndex: number, scores: number[]) {
playerBars[playerIndex].statPassesElement.innerText = currentGame.players[playerIndex].passes.toString();
}
/** Shows the ready indication for the specified player. */
function showReady(playerIndex: number) {
const el = document.createElement('div');
el.className = 'cardBack';
@ -455,14 +463,13 @@ function clearPlayContainers() {
}
}
function setupControlsForPlay() {
/** Clears the game page controls for a new turn. */
function resetPlayControls() {
passButton.checked = false;
specialButton.checked = false;
board.specialAttack = false;
board.cardPlaying = null;
if (canPlay && currentGame?.me?.hand != null) {
passButton.enabled = true;
for (let i = 0; i < 4; i++) {
canPlayCard[i] = board.canPlayCard(currentGame.me.playerIndex, currentGame.me.hand[i], false);
canPlayCardAsSpecialAttack[i] = currentGame.players[currentGame.me.playerIndex].specialPoints >= currentGame.me.hand[i].specialCost
@ -470,16 +477,29 @@ function setupControlsForPlay() {
handButtons[i].enabled = canPlayCard[i];
}
passButton.enabled = true;
specialButton.enabled = canPlayCardAsSpecialAttack.includes(true);
board.autoHighlight = true;
focusFirstEnabledHandCard();
} else {
for (const button of handButtons) {
button.enabled = false;
lockGamePage();
if (currentGame?.me?.move) {
for (const button of handButtons)
button.checked = button.card.number == currentGame.me.move.card.number;
passButton.checked = currentGame.me.move.isPass;
specialButton.checked = (currentGame.me.move as PlayMove).isSpecialAttack;
}
}
}
/** Disables controls on the game page after a play is submitted. */
function lockGamePage() {
canPlay = false;
board.autoHighlight = false;
for (const el of handButtons) el.enabled = false;
passButton.enabled = false;
specialButton.enabled = false;
}
}
async function playInkAnimations(data: {
game: { state: GameState, board: Space[][] | null, turnNumber: number, players: Player[] },
@ -596,7 +616,8 @@ function populateShowDeck(deck: Card[]) {
}
}
function updateHand(playerData: PlayerData) {
/** Handles an update to the player's hand and/or deck during a game. */
function updateHandAndDeck(playerData: PlayerData) {
for (const button of handButtons) {
handContainer.removeChild(button.element);
}
@ -629,19 +650,9 @@ function updateHand(playerData: PlayerData) {
}
if (passButton.checked) {
if (canPlay) {
canPlay = false;
timeLabel.faded = true;
// Send the play to the server.
let req = new XMLHttpRequest();
req.open('POST', `${config.apiBaseUrl}/games/${currentGame!.id}/play`);
req.addEventListener('error', () => communicationError());
let data = new URLSearchParams();
data.append('clientToken', clientToken);
data.append('cardNumber', card.number.toString());
data.append('isPass', 'true');
req.send(data.toString());
board.autoHighlight = false;
lockGamePage();
sendPlay({ clientToken, cardNumber: card.number.toString(), isPass: 'true' });
}
} else {
board.cardPlaying = card;
@ -683,6 +694,7 @@ function updateHand(playerData: PlayerData) {
}
}
/** Handles an update to the player's hand and/or deck during a replay. */
function replayUpdateHand() {
if (currentGame == null || currentReplay == null) return;
@ -781,6 +793,25 @@ function specialButton_input() {
}
specialButton.input.addEventListener('input', specialButton_input);
function sendPlay(data: { clientToken: string, [k: string]: string }) {
if (!currentGame) throw new Error('No game in progress.');
let req = new XMLHttpRequest();
req.open('POST', `${config.apiBaseUrl}/games/${currentGame.id}/play`);
req.addEventListener('load', _ => {
if (req.status != 204) {
alert(`The server rejected the play. This is probably a bug.\n${req.responseText}`);
canPlay = true;
board.autoHighlight = true;
for (let i = 0; i < handButtons.length; i++) handButtons[i].enabled = passButton.checked || (specialButton.checked ? canPlayCardAsSpecialAttack : canPlayCard)[i];
passButton.enabled = true;
specialButton.enabled = canPlayCardAsSpecialAttack.includes(true);
board.clearHighlight();
}
});
req.addEventListener('error', () => communicationError());
req.send(new URLSearchParams(data).toString());
}
board.onsubmit = (x, y) => {
if (board.cardPlaying == null || !currentGame?.me)
return;
@ -813,29 +844,25 @@ board.onsubmit = (x, y) => {
board.cardPlaying = null;
testUndoButton.disabled = false;
} else if (canPlay) {
canPlay = false;
timeLabel.faded = true;
// Send the play to the server.
lockGamePage();
let req = new XMLHttpRequest();
req.open('POST', `${config.apiBaseUrl}/games/${currentGame.id}/play`);
req.addEventListener('load', e => {
req.addEventListener('load', _ => {
if (req.status != 204) {
alert(req.responseText);
board.clearHighlight();
board.autoHighlight = true;
}
});
req.addEventListener('error', () => communicationError());
let data = new URLSearchParams();
data.append('clientToken', clientToken);
data.append('cardNumber', board.cardPlaying.number.toString());
data.append('isSpecialAttack', specialButton.checked.toString());
data.append('x', board.highlightX.toString());
data.append('y', board.highlightY.toString());
data.append('r', board.cardRotation.toString());
req.send(data.toString());
board.autoHighlight = false;
sendPlay({
clientToken,
cardNumber: board.cardPlaying.number.toString(),
isSpecialAttack: specialButton.checked.toString(),
x: board.highlightX.toString(),
y: board.highlightY.toString(),
r: board.cardRotation.toString()
});
}
};
@ -846,6 +873,7 @@ board.oncancel = () => {
if (button.checked) {
button.checked = false;
button.inputElement.focus();
break;
}
}
};
@ -864,11 +892,7 @@ timeLabel.ontimeout = () => {
let req = new XMLHttpRequest();
req.open('POST', `${config.apiBaseUrl}/games/${currentGame!.id}/redraw`);
req.addEventListener('error', () => communicationError());
let data = new URLSearchParams();
data.append('clientToken', clientToken);
data.append('redraw', 'false');
data.append('isTimeout', 'true');
req.send(data.toString());
req.send(new URLSearchParams({ clientToken, redraw: 'false', isTimeout: 'true' }).toString());
redrawModal.hidden = true;
} else {
// When time runs out, automatically discard the largest card in the player's hand.
@ -877,24 +901,10 @@ timeLabel.ontimeout = () => {
if (handButtons[i].card.size > button.card.size)
button = handButtons[i];
}
for (const el of handButtons) {
el.enabled = false;
el.checked = false;
}
for (const el of handButtons) el.checked = false;
button.checked = true;
canPlay = false;
// Send the play to the server.
let req = new XMLHttpRequest();
req.open('POST', `${config.apiBaseUrl}/games/${currentGame!.id}/play`);
req.addEventListener('error', () => communicationError());
let data = new URLSearchParams();
data.append('clientToken', clientToken);
data.append('cardNumber', button.card.number.toString());
data.append('isPass', 'true');
data.append('isTimeout', 'true');
req.send(data.toString());
board.autoHighlight = false;
lockGamePage();
sendPlay({ clientToken, cardNumber: button.card.number.toString(), isPass: 'true', isTimeout: 'true' });
}
};

View File

@ -1,6 +1,3 @@
/// <reference path="../CardDatabase.ts"/>
/// <reference path="../StageDatabase.ts"/>
const stageButtons: StageButton[] = [ ];
const shareLinkButton = document.getElementById('shareLinkButton') as HTMLButtonElement;
const showQrCodeButton = document.getElementById('showQrCodeButton') as HTMLButtonElement;
@ -27,6 +24,27 @@ let lobbyShareData: ShareData | null;
let selectedStageButton = null as StageButton | null;
function lobbyInitStageDatabase(stages: Stage[]) {
const stageList = document.getElementById('stageList')!;
for (const stage of stages) {
const button = new StageButton(stage);
stageButtons.push(button);
button.inputElement.name = 'stage';
button.inputElement.addEventListener('input', () => {
if (button.inputElement.checked) {
stageRandomLabel.classList.remove('checked');
for (const button2 of stageButtons) {
if (button2 != button)
button2.element.classList.remove('checked');
}
}
});
button.setStartSpaces(2);
stageList.appendChild(button.element);
}
document.getElementById('stageListLoadingSection')!.hidden = true;
}
function initLobbyPage(url: string) {
stageSelectionFormSubmitButton.disabled = false;
stageSelectionFormLoadingSection.hidden = true;
@ -199,28 +217,6 @@ deckSelectionForm.addEventListener('submit', e => {
}
});
function initStageDatabase(stages: Stage[]) {
const stageList = document.getElementById('stageList')!;
for (const stage of stages) {
const button = new StageButton(stage);
stageButtons.push(button);
button.inputElement.name = 'stage';
button.inputElement.addEventListener('input', () => {
if (button.inputElement.checked) {
stageRandomLabel.classList.remove('checked');
for (const button2 of stageButtons) {
if (button2 != button)
button2.element.classList.remove('checked');
}
}
});
button.setStartSpaces(2);
stageList.appendChild(button.element);
addTestStage(stage);
}
document.getElementById('stageListLoadingSection')!.hidden = true;
}
stageRandomButton.addEventListener('input', () => {
if (stageRandomButton.checked) {
stageRandomLabel.classList.add('checked');

View File

@ -64,6 +64,20 @@ gameSetupForm.addEventListener('submit', e => {
createRoom(true);
});
function uiParseGameID(s: string, fromInitialLoad: boolean) {
const gameID = parseGameID(s);
if (!gameID) {
alert("Invalid game ID or link");
if (fromInitialLoad)
clearPreGameForm(true);
else {
gameIDBox.focus();
gameIDBox.setSelectionRange(0, gameIDBox.value.length);
}
}
return gameID;
}
function createRoom(useOptionsForm: boolean) {
const name = nameBox.value;
let request = new XMLHttpRequest();
@ -99,40 +113,21 @@ function createRoom(useOptionsForm: boolean) {
}
function spectate(fromInitialLoad: boolean) {
const gameID = parseGameID(gameIDBox.value);
if (!gameID) {
alert("Invalid game ID or link");
if (fromInitialLoad)
clearPreGameForm(true);
else {
gameIDBox.focus();
gameIDBox.setSelectionRange(0, gameIDBox.value.length);
}
return;
}
const gameID = uiParseGameID(gameIDBox.value, fromInitialLoad);
if (!gameID) return;
setGameUrl(gameID);
getGameInfo(gameID, null);
}
function tryJoinGame(name: string, idOrUrl: string, fromInitialLoad: boolean) {
const gameID = parseGameID(idOrUrl);
if (!gameID) {
alert("Invalid game ID or link");
if (fromInitialLoad)
clearPreGameForm(true);
else {
gameIDBox.focus();
gameIDBox.setSelectionRange(0, gameIDBox.value.length);
}
return;
}
const gameID = uiParseGameID(idOrUrl, fromInitialLoad);
if (!gameID) return;
if (!fromInitialLoad)
setGameUrl(gameID);
setGameUrl(gameID);
let request = new XMLHttpRequest();
request.open('POST', `${config.apiBaseUrl}/games/${gameID}/join`);
request.addEventListener('load', e => {
request.addEventListener('load', () => {
if (request.status == 200) {
let response = JSON.parse(request.responseText);
if (!clientToken)
@ -152,10 +147,7 @@ function tryJoinGame(name: string, idOrUrl: string, fromInitialLoad: boolean) {
request.addEventListener('error', () => {
joinGameError('Unable to join the room.', fromInitialLoad);
});
let data = new URLSearchParams();
data.append('name', name);
data.append('clientToken', clientToken);
request.send(data.toString());
request.send(new URLSearchParams({ name, clientToken }).toString());
setLoadingMessage('Joining the game...');
}
@ -178,7 +170,7 @@ function getGameInfo(gameID: string, myPlayerIndex: number | null) {
showDeckButtons.splice(0);
clearShowDeck();
myPlayerIndex = setupWebSocket(gameID, myPlayerIndex);
setupWebSocket(gameID);
}
function backPreGameForm(updateUrl: boolean) {
@ -238,108 +230,15 @@ preGameReplayButton.addEventListener('click', e => {
loadReplay(m[1]);
});
function loadReplay(base64: string) {
if (stageDatabase.stages == null)
throw new Error('Game data not loaded');
base64 = base64.replaceAll('-', '+');
base64 = base64.replaceAll('_', '/');
const bytes = Base64.base64DecToArr(base64);
const dataView = new DataView(bytes.buffer);
if (dataView.getUint8(0) != 1) {
alert('Unknown replay data version');
return;
}
const n = dataView.getUint8(1);
const stage = stageDatabase.stages[n & 0x1F];
const numPlayers = n >> 5;
let pos = 2;
const players = [ ];
currentReplay = { turns: [ ], placements: [ ], replayPlayerData: [ ], watchingPlayer: 0 };
for (let i = 0; i < numPlayers; i++) {
const len = dataView.getUint8(pos + 34);
const player: Player = {
name: new TextDecoder().decode(new DataView(bytes.buffer, pos + 35, len)),
specialPoints: 0,
isReady: false,
colour: { r: dataView.getUint8(pos + 0), g: dataView.getUint8(pos + 1), b: dataView.getUint8(pos + 2) },
specialColour: { r: dataView.getUint8(pos + 3), g: dataView.getUint8(pos + 4), b: dataView.getUint8(pos + 5) },
specialAccentColour: { r: dataView.getUint8(pos + 6), g: dataView.getUint8(pos + 7), b: dataView.getUint8(pos + 8) },
totalSpecialPoints: 0,
passes: 0
};
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);
}
for (let j = 0; j < 8; j++) {
drawOrder.push(dataView.getUint8(pos + 26 + j) & 0xF);
if (j == 7)
player.uiBaseColourIsSpecialColour = (dataView.getUint8(pos + 26 + j) & 0x80) != 0;
else
drawOrder.push(dataView.getUint8(pos + 26 + j) >> 4 & 0xF);
}
currentReplay.replayPlayerData.push({ deck, initialDrawOrder, drawOrder });
pos += 35 + len;
}
for (let i = 0; i < 12; i++) {
const turn = [ ]
for (let j = 0; j < numPlayers; j++) {
const cardNumber = dataView.getUint8(pos);
const b = dataView.getUint8(pos + 1);
const x = dataView.getInt8(pos + 2);
const y = dataView.getInt8(pos + 3);
if (b & 0x80)
turn.push({ card: cardDatabase.get(cardNumber), isPass: true, isTimeout: (b & 0x20) != 0 });
else {
const move: PlayMove = { card: cardDatabase.get(cardNumber), isPass: false, isTimeout: (b & 0x20) != 0, x, y, rotation: b & 0x03, isSpecialAttack: (b & 0x40) != 0 };
turn.push(move);
}
pos += 4;
}
currentReplay.turns.push(turn);
}
currentGame = {
id: 'replay',
state: GameState.Redraw,
me: null,
players: players,
maxPlayers: numPlayers,
turnNumber: 0,
turnTimeLimit: null,
turnTimeLeft: null,
webSocket: null
};
board.resize(stage.copyGrid());
const startSpaces = stage.getStartSpaces(numPlayers);
for (let i = 0; i < numPlayers; i++)
board.grid[startSpaces[i].x][startSpaces[i].y] = Space.SpecialInactive1 | i;
board.refresh();
loadPlayers(players);
setUrl(`replay/${encodeToUrlSafeBase64(bytes)}`)
initReplay();
}
let playerName = localStorage.getItem('name');
(document.getElementById('nameBox') as HTMLInputElement).value = playerName || '';
let settingUrl = false;
window.addEventListener('popstate', e => {
window.addEventListener('popstate', () => {
if (!settingUrl)
processUrl();
});
if (!canPushState)
preGameDeckEditorButton.href = '#deckeditor';
setLoadingMessage('Loading game data...');

View File

@ -1,5 +1,3 @@
/// <reference path="Pages/PreGamePage.ts"/>
declare var baseUrl: string;
const errorDialog = document.getElementById('errorDialog') as HTMLDialogElement;
@ -41,6 +39,14 @@ function onInitialise(callback: () => void) {
initialiseCallback = callback;
}
function initCardDatabase(cards: Card[]) {
deckEditInitCardDatabase(cards);
}
function initStageDatabase(stages: Stage[]) {
lobbyInitStageDatabase(stages);
deckEditInitStageDatabase(stages);
}
// Pages
const pages = new Map<string, HTMLDivElement>();
for (var id of [ 'noJS', 'preGame', 'lobby', 'game', 'deckList', 'deckEdit' ]) {
@ -137,7 +143,7 @@ function onGameStateChange(game: any, playerData: PlayerData | null) {
board.autoHighlight = false;
redrawModal.hidden = true;
if (playerData) {
updateHand(playerData);
updateHandAndDeck(playerData);
initGame();
} else
initSpectator();
@ -161,7 +167,7 @@ function onGameStateChange(game: any, playerData: PlayerData | null) {
canPlay = currentGame.me != null && !currentGame.players[currentGame.me.playerIndex].isReady;
timeLabel.faded = !canPlay;
timeLabel.paused = false;
setupControlsForPlay();
resetPlayControls();
break;
case GameState.Ended:
clearConfirmLeavingGame();
@ -201,7 +207,7 @@ function playerDataReviver(key: string, value: any) {
: value;
}
function setupWebSocket(gameID: string, myPlayerIndex: number | null) {
function setupWebSocket(gameID: string) {
const webSocket = new WebSocket(`${config.apiBaseUrl.replace(/(http)(s)?\:\/\//, 'ws$2://')}/websocket?gameID=${gameID}&clientToken=${clientToken}`);
webSocket.addEventListener('open', _ => {
enterGameTimeout = setTimeout(() => {
@ -355,13 +361,13 @@ function setupWebSocket(gameID: string, myPlayerIndex: number | null) {
button.inputElement.hidden = true;
playContainers[i].append(button.element);
}
timeLabel.paused = true;
if (payload.data.game.turnTimeLeft)
timeLabel.setTime(payload.data.game.turnTimeLeft);
(async () => {
timeLabel.paused = true;
if (payload.data.game.turnTimeLeft)
timeLabel.setTime(payload.data.game.turnTimeLeft);
await playInkAnimations(payload.data, anySpecialAttacks);
if (payload.playerData) updateHand(payload.playerData);
if (payload.playerData) updateHandAndDeck(payload.playerData);
turnNumberLabel.setTurnNumber(payload.data.game.turnNumber);
clearPlayContainers();
if (payload.event == 'gameEnd') {
@ -374,9 +380,9 @@ function setupWebSocket(gameID: string, myPlayerIndex: number | null) {
timeLabel.hide();
showResult();
} else {
canPlay = myPlayerIndex != null;
canPlay = currentGame.me != null;
board.autoHighlight = canPlay;
setupControlsForPlay();
resetPlayControls();
timeLabel.faded = !canPlay;
timeLabel.paused = false;
if (payload.data.game.turnTimeLeft)
@ -389,12 +395,106 @@ function setupWebSocket(gameID: string, myPlayerIndex: number | null) {
}
});
webSocket.addEventListener('close', webSocket_close);
return myPlayerIndex;
}
function webSocket_close() {
communicationError();
}
function loadReplay(base64: string) {
if (stageDatabase.stages == null)
throw new Error('Game data not loaded');
base64 = base64.replaceAll('-', '+');
base64 = base64.replaceAll('_', '/');
const bytes = Base64.base64DecToArr(base64);
const dataView = new DataView(bytes.buffer);
if (dataView.getUint8(0) != 1) {
alert('Unknown replay data version');
return;
}
const n = dataView.getUint8(1);
const stage = stageDatabase.stages[n & 0x1F];
const numPlayers = n >> 5;
let pos = 2;
const players = [ ];
currentReplay = { turns: [ ], placements: [ ], replayPlayerData: [ ], watchingPlayer: 0 };
for (let i = 0; i < numPlayers; i++) {
const len = dataView.getUint8(pos + 34);
const player: Player = {
name: new TextDecoder().decode(new DataView(bytes.buffer, pos + 35, len)),
specialPoints: 0,
isReady: false,
colour: { r: dataView.getUint8(pos + 0), g: dataView.getUint8(pos + 1), b: dataView.getUint8(pos + 2) },
specialColour: { r: dataView.getUint8(pos + 3), g: dataView.getUint8(pos + 4), b: dataView.getUint8(pos + 5) },
specialAccentColour: { r: dataView.getUint8(pos + 6), g: dataView.getUint8(pos + 7), b: dataView.getUint8(pos + 8) },
totalSpecialPoints: 0,
passes: 0
};
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);
}
for (let j = 0; j < 8; j++) {
drawOrder.push(dataView.getUint8(pos + 26 + j) & 0xF);
if (j == 7)
player.uiBaseColourIsSpecialColour = (dataView.getUint8(pos + 26 + j) & 0x80) != 0;
else
drawOrder.push(dataView.getUint8(pos + 26 + j) >> 4 & 0xF);
}
currentReplay.replayPlayerData.push({ deck, initialDrawOrder, drawOrder });
pos += 35 + len;
}
for (let i = 0; i < 12; i++) {
const turn = [ ];
for (let j = 0; j < numPlayers; j++) {
const cardNumber = dataView.getUint8(pos);
const b = dataView.getUint8(pos + 1);
const x = dataView.getInt8(pos + 2);
const y = dataView.getInt8(pos + 3);
if (b & 0x80)
turn.push({ card: cardDatabase.get(cardNumber), isPass: true, isTimeout: (b & 0x20) != 0 });
else {
const move: PlayMove = { card: cardDatabase.get(cardNumber), isPass: false, isTimeout: (b & 0x20) != 0, x, y, rotation: b & 0x03, isSpecialAttack: (b & 0x40) != 0 };
turn.push(move);
}
pos += 4;
}
currentReplay.turns.push(turn);
}
currentGame = {
id: 'replay',
state: GameState.Redraw,
me: null,
players: players,
maxPlayers: numPlayers,
turnNumber: 0,
turnTimeLimit: null,
turnTimeLeft: null,
webSocket: null
};
board.resize(stage.copyGrid());
const startSpaces = stage.getStartSpaces(numPlayers);
for (let i = 0; i < numPlayers; i++)
board.grid[startSpaces[i].x][startSpaces[i].y] = Space.SpecialInactive1 | i;
board.refresh();
loadPlayers(players);
setUrl(`replay/${encodeToUrlSafeBase64(bytes)}`)
initReplay();
}
function setConfirmLeavingGame() {
if (!shouldConfirmLeavingGame) {
shouldConfirmLeavingGame = true;
@ -549,7 +649,5 @@ Promise.all([ cardDatabase.loadAsync().then(initCardDatabase), stageDatabase.loa
communicationError('Unable to load game data from the server.', false);
});
if (!canPushState)
preGameDeckEditorButton.href = '#deckeditor';
showPage('preGame');
processUrl();

View File

@ -574,7 +574,7 @@ dialog::backdrop {
:is(#playRow, #testControlsHeader) > label:hover { background: var(--player-ui-highlight-colour); }
:is(#playRow, #testControlsHeader) > label:focus-within { outline: 2px solid var(--player-ui-highlight2-colour); }
:is(#playRow, #testControlsHeader) > label:is(:active, .checked) { background: var(--player-ui-highlight2-colour); }
:is(#playRow, #testControlsHeader) > label.disabled { background: grey; }
:is(#playRow, #testControlsHeader) > label.disabled:not(.checked) { background: grey; }
#spectatorRow:not([hidden]) {
display: grid;