Add ability to spectate

This commit is contained in:
Andrio Celos
2023-03-07 14:15:55 +11:00
parent 3c0b3d3b3e
commit e735de5bfa
5 changed files with 202 additions and 92 deletions

View File

@@ -15,13 +15,13 @@ const gameDeckButton = document.getElementById('gameDeckButton') as HTMLButtonEl
const handContainer = document.getElementById('handContainer')!;
const redrawModal = document.getElementById('redrawModal')!;
const playControls = document.getElementById('playControls')!;
const resultContainer = document.getElementById('resultContainer')!;
const resultElement = document.getElementById('result')!;
const replayControls = document.getElementById('replayControls')!;
const gameControls = document.getElementById('gameControls')!;
const playRow = document.getElementById('playRow')!;
const spectatorRow = document.getElementById('spectatorRow')!;
const replayNextButton = document.getElementById('replayNextButton')!;
const replayPreviousButton = document.getElementById('replayPreviousButton')!;
const replayFlipBox = document.getElementById('replayFlipBox') as HTMLInputElement;
const flipLabel = document.getElementById('flipLabel') as HTMLLabelElement;
const flipBox = document.getElementById('flipBox') as HTMLInputElement;
let replayAnimationAbortController: AbortController | null = null;
const shareReplayLinkButton = document.getElementById('shareReplayLinkButton') as HTMLButtonElement;
@@ -68,25 +68,56 @@ cols[4][21] = Space.SpecialInactive1;
cols[4][4] = Space.SpecialInactive2;
board.resize(cols);
function initGame() {
function clear() {
testMode = false;
gamePage.classList.remove('deckTest');
playControls.hidden = false;
resultContainer.hidden = true;
replayControls.hidden = true;
gameButtonsContainer.hidden = false;
gamePage.classList.remove('gameEnded');
gameControls.hidden = true;
handContainer.hidden = true;
playRow.hidden = true;
spectatorRow.hidden = true;
replayPreviousButton.hidden = true;
replayNextButton.hidden = true;
shareReplayLinkButton.hidden = true;
flipLabel.hidden = false;
flipBox.checked = false;
gameButtonsContainer.hidden = true;
testControls.hidden = true;
for (const playerBar of playerBars) {
playerBar.resultElement.classList.remove('win');
playerBar.resultElement.classList.remove('lose');
playerBar.resultElement.classList.remove('draw');
playerBar.resultElement.innerText = '';
}
}
function initGame() {
clear();
gameControls.hidden = false;
handContainer.hidden = false;
playRow.hidden = false;
gameButtonsContainer.hidden = false;
showPage('game');
}
function initSpectator() {
clear();
gameControls.hidden = false;
spectatorRow.hidden = false;
flipLabel.hidden = false;
flipBox.checked = false;
gameButtonsContainer.hidden = false;
showPage('game');
}
function initReplay() {
testMode = false;
gamePage.classList.remove('deckTest');
playControls.hidden = true;
resultContainer.hidden = true;
replayControls.hidden = false;
gameButtonsContainer.hidden = true;
testControls.hidden = true;
clear();
gameControls.hidden = false;
spectatorRow.hidden = false;
replayPreviousButton.hidden = false;
replayNextButton.hidden = false;
flipLabel.hidden = false;
flipBox.checked = false;
canPlay = false;
showPage('game');
clearPlayContainers();
@@ -108,11 +139,10 @@ function initTest(stage: Stage) {
testPlacements.splice(0);
clearChildren(testPlacementList);
gamePage.classList.add('deckTest');
gamePage.classList.remove('gameEnded');
gamePage.dataset.myPlayerIndex = '0';
gamePage.dataset.uiBaseColourIsSpecialColour = 'true';
playControls.hidden = true;
resultContainer.hidden = true;
replayControls.hidden = true;
gameControls.hidden = true;
gameButtonsContainer.hidden = false;
testControls.hidden = false;
clearPlayContainers();
@@ -242,8 +272,10 @@ function undoTurn(turn: PlacementResults) {
board.refresh();
}
replayFlipBox.addEventListener('input', _ => {
if (currentGame == null || currentReplay == null) return;
flipBox.addEventListener('input', flipBox_input);
function flipBox_input(e: Event) {
if (currentGame == null) return;
if (replayAnimationAbortController) {
replayAnimationAbortController.abort();
replayAnimationAbortController = null;
@@ -253,11 +285,11 @@ replayFlipBox.addEventListener('input', _ => {
updateStats(i);
}
}
board.flip = replayFlipBox.checked;
board.flip = (e.target as HTMLInputElement).checked;
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));
@@ -496,8 +528,13 @@ function showResult() {
}
if (!currentReplay) {
playControls.hidden = true;
resultContainer.hidden = false;
handContainer.hidden = true;
playRow.hidden = true;
spectatorRow.hidden = false;
replayPreviousButton.hidden = true;
replayNextButton.hidden = true;
shareReplayLinkButton.hidden = false;
flipLabel.hidden = true;
canShareReplay = navigator.canShare && navigator.canShare({ url: window.location.href, title: 'Tableturf Battle Replay' });
shareReplayLinkButton.innerText = canShareReplay ? 'Share replay link' : 'Copy replay link';
}
@@ -767,7 +804,7 @@ gameDeckButton.addEventListener('click', toggleShowDeck);
showDeckCloseButton.addEventListener('click', toggleShowDeck);
document.addEventListener('keydown', e => {
if (!pages.get('game')!.hidden && !playControls.hidden) {
if (!pages.get('game')!.hidden && !playRow.hidden) {
switch (e.key) {
case 'p':
if (passButton.enabled) {
@@ -808,24 +845,19 @@ shareReplayLinkButton.addEventListener('click', _ => {
if (canShareReplay) {
navigator.share({ url: url.href, title: 'Tableturf Battle Replay' });
} else {
navigator.clipboard.writeText(window.location.toString()).then(() => shareLinkButton.innerText = 'Copied');
navigator.clipboard.writeText(url.href).then(() => shareReplayLinkButton.innerText = 'Copied');
}
}
});
req.send();
});
document.getElementById('resultLeaveButton')!.addEventListener('click', e => {
e.preventDefault();
clearPreGameForm(true);
showPage('preGame');
newGameButton.focus();
});
document.getElementById('replayLeaveButton')!.addEventListener('click', e => {
function leaveButton_click(e: MouseEvent) {
e.preventDefault();
clearPreGameForm(true);
showPage('preGame');
newGameButton.focus();
currentReplay = null;
});
}
document.getElementById('leaveButton')!.addEventListener('click', leaveButton_click);

View File

@@ -34,10 +34,11 @@ maxPlayersBox.addEventListener('change', () => {
preGameForm.addEventListener('submit', e => {
e.preventDefault();
if (e.submitter?.id == 'newGameButton' || (e.submitter?.id == 'preGameImplicitSubmitButton' && !gameIDBox.value)) {
const name = nameBox.value;
window.localStorage.setItem('name', name);
const name = nameBox.value;
window.localStorage.setItem('name', name);
if (e.submitter?.id == 'newGameButton' || (e.submitter?.id == 'preGameImplicitSubmitButton' && !gameIDBox.value)) {
let request = new XMLHttpRequest();
request.open('POST', `${config.apiBaseUrl}/games/new`);
request.addEventListener('load', () => {
@@ -64,16 +65,32 @@ preGameForm.addEventListener('submit', e => {
data.append('maxPlayers', maxPlayersBox.value);
request.send(data.toString());
setLoadingMessage('Creating a room...');
} else if (e.submitter?.id?.startsWith('spectate')) {
spectate(false);
} else {
const name = nameBox.value;
window.localStorage.setItem('name', name);
tryJoinGame(name, gameIDBox.value, false);
}
});
function tryJoinGame(name: string, idOrUrl: string, fromInitialLoad: boolean) {
const m = /(?:^|[#/])([0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})$/i.exec(idOrUrl);
if (!m) {
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;
}
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);
@@ -83,7 +100,6 @@ function tryJoinGame(name: string, idOrUrl: string, fromInitialLoad: boolean) {
}
return;
}
const gameID = m[1];
if (!fromInitialLoad)
setGameUrl(gameID);

View File

@@ -29,6 +29,10 @@ function delay(ms: number, abortSignal?: AbortSignal) {
});
}
/**
* Schedules the specified callback to run when game data is initialised, or runs it synchronously if already initialised.
* Only one method may be scheduled this way.
*/
function onInitialise(callback: () => void) {
if (initialised)
callback();
@@ -125,17 +129,19 @@ function onGameStateChange(game: any, playerData: PlayerData | null) {
case GameState.Redraw:
case GameState.Ongoing:
case GameState.Ended:
if (playerData)
updateHand(playerData);
board.autoHighlight = false;
redrawModal.hidden = true;
initGame();
if (playerData) {
updateHand(playerData);
initGame();
} else
initSpectator();
gameButtonsContainer.hidden = currentGame.me == null || game.state == GameState.Ended;
switch (game.state) {
case GameState.Redraw:
redrawModal.hidden = false;
redrawModal.hidden = currentGame.me == null || currentGame.players[currentGame.me.playerIndex].isReady;
turnNumberLabel.setTurnNumber(null);
canPlay = false;
break;
@@ -317,7 +323,7 @@ function setupWebSocket(gameID: string, myPlayerIndex: number | null) {
(async () => {
await playInkAnimations(payload.data, anySpecialAttacks);
updateHand(payload.playerData);
if (payload.playerData) updateHand(payload.playerData);
turnNumberLabel.setTurnNumber(payload.data.game.turnNumber);
clearPlayContainers();
if (payload.event == 'gameEnd') {
@@ -352,6 +358,8 @@ function processUrl() {
}
stopEditingDeck();
errorDialog.close();
currentGame = null;
currentReplay = null;
if (location.pathname.endsWith('/deckeditor') || location.hash == '#deckeditor')
onInitialise(showDeckList);
else {
@@ -376,13 +384,48 @@ function processUrl() {
return true;
}
function parseGameID(s: string) {
const m = /(?:^|[#/])([0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})$/i.exec(s);
return m ? m[1] : null;
}
function presetGameID(url: string) {
document.getElementById('preGameDefaultSection')!.hidden = true;
document.getElementById('preGameJoinSection')!.hidden = false;
(document.getElementById('gameIDBox') as HTMLInputElement).value = url;
showPage('preGame');
const gameID = parseGameID(url);
if (!gameID) {
joinGameError('Invalid game ID or link.', true);
return;
}
onInitialise(() => {
if (playerName)
tryJoinGame(playerName, url, true);
let request = new XMLHttpRequest();
request.open('GET', `${config.apiBaseUrl}/games/${gameID}/playerData` + (clientToken ? `?clientToken=${clientToken}` : ''));
request.addEventListener('load', () => {
switch (request.status) {
case 200:
let response = JSON.parse(request.responseText);
if (response.playerData) {
// We are already in the game; go to the game page immediately.
onInitialise(() => getGameInfo(gameID, response.playerData.playerIndex));
} else {
// We're not already in the game; offer the option to join or spectate.
document.getElementById('preGameDefaultSection')!.hidden = true;
document.getElementById('preGameJoinSection')!.hidden = false;
(document.getElementById('gameIDBox') as HTMLInputElement).value = gameID;
setLoadingMessage(null);
}
break;
case 404: joinGameError('The room was not found.', true); break;
default: joinGameError('Unable to join the room.', true); break;
}
});
request.addEventListener('error', () => {
joinGameError('Unable to join the room.', true);
});
request.send();
setLoadingMessage('Checking room info...');
});
}