mirror of
https://github.com/AndrioCelos/TableturfBattleApp.git
synced 2026-07-11 23:34:03 -05:00
Multiple-game sets
This allows any number of games to be played in a room. Each player bar will show the number of games that player has won. If a goal win count is selected in More Options, the set will end when someone reaches that many wins. Otherwise, players can start a new game after the game ends.
This commit is contained in:
parent
91044db4c1
commit
dcb669490c
|
|
@ -55,7 +55,7 @@
|
|||
<a id="preGameHelpButton" href="help">Help</a>
|
||||
</p>
|
||||
<div id="preGameLoadingSection" class="loadingContainer" hidden>
|
||||
<div class="loadingSpinner"><div></div><div></div></div> <span id="preGameLoadingLabel"></span>
|
||||
<div class="loadingSpinner"></div> <span id="preGameLoadingLabel"></span>
|
||||
</div>
|
||||
</form>
|
||||
<footer>
|
||||
|
|
@ -79,13 +79,13 @@
|
|||
<div class="submitButtonContainer">
|
||||
<button type="submit" id="submitStageButton">Submit</button>
|
||||
<div class="loadingContainer">
|
||||
<div class="loadingSpinner"><div></div><div></div></div>
|
||||
<div class="loadingSpinner"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div id="stageList">
|
||||
<button type="button" id="stageRandomButton" class="stageRandom">Random</button>
|
||||
</div>
|
||||
<div id="stageListLoadingSection"><div class="loadingSpinner"><div></div><div></div></div> Loading stages...</div>
|
||||
<div id="stageListLoadingSection"><div class="loadingSpinner"></div> Loading stages...</div>
|
||||
</form>
|
||||
</section>
|
||||
<div id="lobbyDeckSection" hidden>
|
||||
|
|
@ -94,7 +94,7 @@
|
|||
<div class="submitButtonContainer">
|
||||
<button type="submit" id="submitDeckButton" disabled>Submit</button>
|
||||
<div class="loadingContainer">
|
||||
<div class="loadingSpinner"><div></div><div></div></div>
|
||||
<div class="loadingSpinner"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div id="lobbyDeckList" class="deckList"></div>
|
||||
|
|
@ -111,6 +111,7 @@
|
|||
<div id="gamePage" hidden>
|
||||
<div class="playerBar" data-index="1">
|
||||
<div class="result"></div>
|
||||
<div class="wins" title="Battles won">0</div>
|
||||
<div class="name">Player 2</div>
|
||||
<div class="specialPoints"> </div>
|
||||
<div class="playerStats">
|
||||
|
|
@ -126,6 +127,7 @@
|
|||
</div>
|
||||
<div class="playerBar" data-index="3">
|
||||
<div class="result"></div>
|
||||
<div class="wins" title="Battles won">0</div>
|
||||
<div class="name">Player 4</div>
|
||||
<div class="specialPoints"> </div>
|
||||
<div class="playerStats">
|
||||
|
|
@ -243,20 +245,51 @@
|
|||
<div id="handContainer"></div>
|
||||
|
||||
<div id="playRow">
|
||||
<button type="button" id="passButton">Pass</button>
|
||||
<button type="button" id="specialButton">Special Attack</button>
|
||||
<button type="button" id="passButton" class="gameButton">Pass</button>
|
||||
<button type="button" id="specialButton" class="gameButton">Special Attack</button>
|
||||
</div>
|
||||
<div id="spectatorRow">
|
||||
<a id="leaveButton" href="#">Exit</a>
|
||||
<button id="replayPreviousButton">Previous turn</button>
|
||||
<button id="replayNextButton">Next turn</button>
|
||||
<button id="shareReplayLinkButton">Copy replay link</button>
|
||||
<button id="flipButton">Change view</button>
|
||||
<a id="leaveButton" href="#">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-arrow-left" viewBox="0 0 16 16">
|
||||
<path fill-rule="evenodd" d="M15 8a.5.5 0 0 0-.5-.5H2.707l3.147-3.146a.5.5 0 1 0-.708-.708l-4 4a.5.5 0 0 0 0 .708l4 4a.5.5 0 0 0 .708-.708L2.707 8.5H14.5A.5.5 0 0 0 15 8z"/>
|
||||
</svg>
|
||||
Exit
|
||||
</a>
|
||||
<button id="replayPreviousGameButton" class="gameButton replayButton">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-skip-backward-fill" viewBox="0 0 16 16">
|
||||
<path d="M.5 3.5A.5.5 0 0 0 0 4v8a.5.5 0 0 0 1 0V8.753l6.267 3.636c.54.313 1.233-.066 1.233-.697v-2.94l6.267 3.636c.54.314 1.233-.065 1.233-.696V4.308c0-.63-.693-1.01-1.233-.696L8.5 7.248v-2.94c0-.63-.692-1.01-1.233-.696L1 7.248V4a.5.5 0 0 0-.5-.5z"/>
|
||||
</svg>
|
||||
Previous game
|
||||
</button>
|
||||
<button id="replayPreviousButton" class="gameButton replayButton">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-rewind-fill" viewBox="0 0 16 16">
|
||||
<path d="M8.404 7.304a.802.802 0 0 0 0 1.392l6.363 3.692c.52.302 1.233-.043 1.233-.696V4.308c0-.653-.713-.998-1.233-.696L8.404 7.304Z"/>
|
||||
<path d="M.404 7.304a.802.802 0 0 0 0 1.392l6.363 3.692c.52.302 1.233-.043 1.233-.696V4.308c0-.653-.713-.998-1.233-.696L.404 7.304Z"/>
|
||||
</svg>
|
||||
Previous turn
|
||||
</button>
|
||||
<button id="replayNextButton" class="gameButton replayButton">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-fast-forward-fill" viewBox="0 0 16 16">
|
||||
<path d="M7.596 7.304a.802.802 0 0 1 0 1.392l-6.363 3.692C.713 12.69 0 12.345 0 11.692V4.308c0-.653.713-.998 1.233-.696l6.363 3.692Z"/>
|
||||
<path d="M15.596 7.304a.802.802 0 0 1 0 1.392l-6.363 3.692C8.713 12.69 8 12.345 8 11.692V4.308c0-.653.713-.998 1.233-.696l6.363 3.692Z"/>
|
||||
</svg>
|
||||
Next turn
|
||||
</button>
|
||||
<button id="replayNextGameButton" class="gameButton replayButton">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-skip-forward-fill" viewBox="0 0 16 16">
|
||||
<path d="M15.5 3.5a.5.5 0 0 1 .5.5v8a.5.5 0 0 1-1 0V8.753l-6.267 3.636c-.54.313-1.233-.066-1.233-.697v-2.94l-6.267 3.636C.693 12.703 0 12.324 0 11.693V4.308c0-.63.693-1.01 1.233-.696L7.5 7.248v-2.94c0-.63.693-1.01 1.233-.696L15 7.248V4a.5.5 0 0 1 .5-.5z"/>
|
||||
</svg>
|
||||
Next game
|
||||
</button>
|
||||
<button id="shareReplayLinkButton" class="gameButton">Copy replay link</button>
|
||||
<button id="flipButton" class="gameButton">Change view</button>
|
||||
</div>
|
||||
<button id="nextGameButton" class="gameButton">Next game</button>
|
||||
</div>
|
||||
|
||||
<div class="playerBar" data-index="2">
|
||||
<div class="result"></div>
|
||||
<div class="wins" title="Battles won">0</div>
|
||||
<div class="name">Player 3</div>
|
||||
<div class="specialPoints"> </div>
|
||||
<div class="playerStats">
|
||||
|
|
@ -272,6 +305,7 @@
|
|||
</div>
|
||||
<div class="playerBar" data-index="0">
|
||||
<div class="result"></div>
|
||||
<div class="wins" title="Battles won">0</div>
|
||||
<div class="name">Player 1</div>
|
||||
<div class="specialPoints"> </div>
|
||||
<div class="playerStats">
|
||||
|
|
@ -290,15 +324,15 @@
|
|||
<div id="showDeck">
|
||||
<ul id="showDeckList"></ul>
|
||||
<div>
|
||||
<button type="button" id="showDeckCloseButton">Close</button>
|
||||
<button type="button" id="showDeckCloseButton" class="gameButton">Close</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="testControls">
|
||||
<div id="testControlsHeader" class="testControls">
|
||||
<button type="button" id="testDeckButton">Deck</button>
|
||||
<button type="button" id="testAllCardsButton">All cards</button>
|
||||
<button type="button" id="testDeckButton" class="gameButton">Deck</button>
|
||||
<button type="button" id="testAllCardsButton" class="gameButton">All cards</button>
|
||||
</div>
|
||||
<div id="testDeckContainer" class="testControls">
|
||||
<button id="testAllCardsMobileButton">All cards</button>
|
||||
|
|
@ -315,8 +349,8 @@
|
|||
<div id="testAllCardsList" class="cardListGrid"></div>
|
||||
</div>
|
||||
<div id="testControlsFooter" class="testControls">
|
||||
<button type="button" id="testUndoButton">Undo</button>
|
||||
<button type="button" id="testBackButton">End test</button>
|
||||
<button type="button" id="testUndoButton" class="gameButton">Undo</button>
|
||||
<button type="button" id="testBackButton" class="gameButton">End test</button>
|
||||
</div>
|
||||
</div>
|
||||
<div id="testCardListBackdrop" hidden></div>
|
||||
|
|
@ -508,6 +542,18 @@
|
|||
<span id="turnTimeLimitUnit">seconds</span>
|
||||
</label>
|
||||
</p>
|
||||
<p>
|
||||
<label for="goalWinCountBox">
|
||||
Format:
|
||||
<select id="goalWinCountBox">
|
||||
<option value="">Single game</option>
|
||||
<option value="1">First to 1 (replay draws)</option>
|
||||
<option value="2">First to 2</option>
|
||||
<option value="3">First to 3</option>
|
||||
<option value="4">First to 4</option>
|
||||
</select>
|
||||
</label>
|
||||
</p>
|
||||
<p>
|
||||
<button type="submit" id="gameSetupSubmitButton">Create room</button>
|
||||
<button type="submit">Back</button>
|
||||
|
|
|
|||
|
|
@ -1,7 +1,9 @@
|
|||
enum GameState {
|
||||
WaitingForPlayers,
|
||||
Preparing,
|
||||
ChoosingStage,
|
||||
ChoosingDeck,
|
||||
Redraw,
|
||||
Ongoing,
|
||||
Ended
|
||||
GameEnded,
|
||||
SetEnded
|
||||
}
|
||||
|
|
|
|||
|
|
@ -13,15 +13,26 @@ let currentGame: {
|
|||
turnNumber: number,
|
||||
turnTimeLimit: number | null,
|
||||
turnTimeLeft: number | null,
|
||||
goalWinCount: number | null,
|
||||
/** The WebSocket used for receiving game events, or null if not yet connected. */
|
||||
webSocket: WebSocket | null
|
||||
} | null = null;
|
||||
|
||||
let enterGameTimeout: number | null = null;
|
||||
let currentReplay: {
|
||||
gameNumber: number,
|
||||
games: {
|
||||
stage: Stage,
|
||||
playerData: {
|
||||
deck: Card[],
|
||||
initialDrawOrder: number[],
|
||||
drawOrder: number[],
|
||||
won: boolean
|
||||
}[],
|
||||
turns: Move[][],
|
||||
}[],
|
||||
turns: Move[][],
|
||||
placements: PlacementResults[],
|
||||
replayPlayerData: ReplayPlayerData[],
|
||||
watchingPlayer: number
|
||||
} | null = null;
|
||||
|
||||
|
|
|
|||
|
|
@ -19,8 +19,12 @@ const redrawModal = document.getElementById('redrawModal')!;
|
|||
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 leaveButton = document.getElementById('leaveButton') as HTMLLinkElement;
|
||||
const replayPreviousGameButton = CheckButton.fromId('replayPreviousGameButton');
|
||||
const replayNextGameButton = CheckButton.fromId('replayNextGameButton');
|
||||
const replayPreviousButton = CheckButton.fromId('replayPreviousButton');
|
||||
const replayNextButton = CheckButton.fromId('replayNextButton');
|
||||
const nextGameButton = CheckButton.fromId('nextGameButton');
|
||||
const flipButton = document.getElementById('flipButton') as HTMLButtonElement;
|
||||
let replayAnimationAbortController: AbortController | null = null;
|
||||
|
||||
|
|
@ -83,8 +87,12 @@ function clear() {
|
|||
handContainer.hidden = true;
|
||||
playRow.hidden = true;
|
||||
spectatorRow.hidden = true;
|
||||
replayPreviousButton.hidden = true;
|
||||
replayNextButton.hidden = true;
|
||||
leaveButton.hidden = false;
|
||||
replayPreviousGameButton.buttonElement.hidden = true;
|
||||
replayNextGameButton.buttonElement.hidden = true;
|
||||
replayPreviousButton.buttonElement.hidden = true;
|
||||
replayNextButton.buttonElement.hidden = true;
|
||||
nextGameButton.buttonElement.hidden = true;
|
||||
shareReplayLinkButton.hidden = true;
|
||||
flipButton.hidden = false;
|
||||
gameButtonsContainer.hidden = true;
|
||||
|
|
@ -124,16 +132,20 @@ function initReplay() {
|
|||
gameControls.hidden = false;
|
||||
handContainer.hidden = false;
|
||||
spectatorRow.hidden = false;
|
||||
replayPreviousButton.hidden = false;
|
||||
replayNextButton.hidden = false;
|
||||
replayPreviousGameButton.buttonElement.hidden = false;
|
||||
replayNextGameButton.buttonElement.hidden = false;
|
||||
replayPreviousButton.buttonElement.hidden = false;
|
||||
replayNextButton.buttonElement.hidden = false;
|
||||
flipButton.hidden = false;
|
||||
gameButtonsContainer.hidden = false;
|
||||
gamePage.dataset.myPlayerIndex = '0';
|
||||
gamePage.dataset.uiBaseColourIsSpecialColour = 'true';
|
||||
canPlay = false;
|
||||
showPage('game');
|
||||
clearPlayContainers();
|
||||
timeLabel.hide();
|
||||
turnNumberLabel.setTurnNumber(null);
|
||||
replayUpdateHand();
|
||||
replaySwitchGame(0);
|
||||
}
|
||||
|
||||
/** Shows the game page for deck editor testing. */
|
||||
|
|
@ -141,7 +153,7 @@ function initTest(stage: Stage) {
|
|||
clear();
|
||||
testMode = true;
|
||||
gamePage.classList.add('deckTest');
|
||||
currentGame = { id: 'test', state: GameState.Ongoing, maxPlayers: 2, players: [ ], webSocket: null, turnNumber: 1, turnTimeLimit: null, turnTimeLeft: null, me: { playerIndex: 0, move: null, deck: null, hand: null, cardsUsed: [ ] } };
|
||||
currentGame = { id: 'test', state: GameState.Ongoing, maxPlayers: 2, players: [ ], webSocket: null, turnNumber: 1, turnTimeLimit: null, turnTimeLeft: null, goalWinCount: null, me: { playerIndex: 0, move: null, deck: null, hand: null, cardsUsed: [ ] } };
|
||||
board.resize(stage.copyGrid());
|
||||
const startSpaces = stage.getStartSpaces(2);
|
||||
board.startSpaces = startSpaces;
|
||||
|
|
@ -169,8 +181,9 @@ function initTest(stage: Stage) {
|
|||
testAllCardsList.clearFilter();
|
||||
}
|
||||
|
||||
replayNextButton.addEventListener('click', _ => {
|
||||
if (currentGame == null || currentReplay == null || currentGame.turnNumber > 12) return;
|
||||
replayNextButton.buttonElement.addEventListener('click', _ => {
|
||||
if (currentGame == null || currentReplay == null || currentGame.state == GameState.GameEnded || currentGame.state == GameState.SetEnded)
|
||||
return;
|
||||
|
||||
if (replayAnimationAbortController) {
|
||||
replayUpdateHand();
|
||||
|
|
@ -187,9 +200,16 @@ replayNextButton.addEventListener('click', _ => {
|
|||
clearPlayContainers();
|
||||
if (currentGame.turnNumber == 0) {
|
||||
// Show redraw decisions.
|
||||
replayPreviousButton.enabled = true;
|
||||
currentGame.state = GameState.Ongoing;
|
||||
currentGame.turnNumber++;
|
||||
turnNumberLabel.setTurnNumber(currentGame.turnNumber);
|
||||
replayUpdateHand();
|
||||
} else if (currentGame.turnNumber > 12) {
|
||||
currentGame.state = currentReplay.gameNumber + 1 >= currentReplay.games.length ? GameState.SetEnded : GameState.GameEnded;
|
||||
gameButtonsContainer.hidden = true;
|
||||
gamePage.classList.add('gameEnded');
|
||||
showResult();
|
||||
} else {
|
||||
const moves = currentReplay.turns[currentGame.turnNumber - 1];
|
||||
const result = board.makePlacements(moves);
|
||||
|
|
@ -234,9 +254,8 @@ replayNextButton.addEventListener('click', _ => {
|
|||
turnNumberLabel.setTurnNumber(currentGame.turnNumber);
|
||||
clearPlayContainers();
|
||||
if (currentGame.turnNumber > 12) {
|
||||
currentGame.state = currentReplay.gameNumber + 1 >= currentReplay.games.length ? GameState.SetEnded : GameState.GameEnded;
|
||||
gameButtonsContainer.hidden = true;
|
||||
passButton.enabled = false;
|
||||
specialButton.enabled = false;
|
||||
gamePage.classList.add('gameEnded');
|
||||
showResult();
|
||||
} else
|
||||
|
|
@ -245,12 +264,20 @@ replayNextButton.addEventListener('click', _ => {
|
|||
}
|
||||
});
|
||||
|
||||
replayPreviousButton.addEventListener('click', _ => {
|
||||
replayPreviousButton.buttonElement.addEventListener('click', _ => {
|
||||
if (currentGame == null || currentReplay == null || currentGame.turnNumber == 0) return;
|
||||
|
||||
replayAnimationAbortController?.abort();
|
||||
replayAnimationAbortController = null;
|
||||
|
||||
if (currentGame.state == GameState.GameEnded || currentGame.state == GameState.SetEnded) {
|
||||
for (let i = 0; i < currentGame.players.length; i++) {
|
||||
if (currentReplay.games[currentReplay.gameNumber].playerData[i].won)
|
||||
currentGame.players[i].gamesWon--;
|
||||
playerBars[i].wins = currentGame.players[i].gamesWon;
|
||||
}
|
||||
}
|
||||
|
||||
if (currentGame.turnNumber > 1) {
|
||||
const result = currentReplay.placements.pop();
|
||||
if (!result) return;
|
||||
|
|
@ -264,11 +291,13 @@ replayPreviousButton.addEventListener('click', _ => {
|
|||
undoTurn(result);
|
||||
}
|
||||
currentGame.turnNumber--;
|
||||
replayNextButton.enabled = true;
|
||||
gamePage.classList.remove('gameEnded');
|
||||
handContainer.hidden = false;
|
||||
gameButtonsContainer.hidden = false;
|
||||
|
||||
if (currentGame.turnNumber > 0) {
|
||||
currentGame.state = GameState.Ongoing;
|
||||
turnNumberLabel.setTurnNumber(currentGame.turnNumber);
|
||||
const scores = board.getScores();
|
||||
for (let i = 0; i < currentGame.players.length; i++) {
|
||||
|
|
@ -280,8 +309,11 @@ replayPreviousButton.addEventListener('click', _ => {
|
|||
currentGame.players[i].specialPoints += (move as PlayMove).card.specialCost;
|
||||
updateStats(i, scores);
|
||||
}
|
||||
} else
|
||||
} else {
|
||||
currentGame.state = GameState.Redraw;
|
||||
replayPreviousButton.enabled = false;
|
||||
turnNumberLabel.setTurnNumber(null);
|
||||
}
|
||||
|
||||
replayUpdateHand();
|
||||
});
|
||||
|
|
@ -309,6 +341,57 @@ function undoTurn(turn: PlacementResults) {
|
|||
board.refresh();
|
||||
}
|
||||
|
||||
function replaySwitchGame(gameNumber: number) {
|
||||
if (!currentGame || !currentReplay)
|
||||
throw new Error('Not in a replay.');
|
||||
if (gameNumber < 0 || gameNumber >= currentReplay.games.length)
|
||||
throw new RangeError(`No game number ${gameNumber} in replay.`);
|
||||
|
||||
replayAnimationAbortController?.abort();
|
||||
replayAnimationAbortController = null;
|
||||
|
||||
for (let i = 0; i < currentGame.players.length; i++) {
|
||||
currentGame.players[i].specialPoints = 0;
|
||||
currentGame.players[i].totalSpecialPoints = 0;
|
||||
currentGame.players[i].passes = 0;
|
||||
currentGame.players[i].gamesWon = 0;
|
||||
for (let j = 0; j < gameNumber; j++) {
|
||||
if (currentReplay.games[j].playerData[i].won)
|
||||
currentGame.players[i].gamesWon++;
|
||||
}
|
||||
playerBars[i].wins = currentGame.players[i].gamesWon;
|
||||
}
|
||||
|
||||
currentReplay.gameNumber = gameNumber;
|
||||
currentReplay.turns = currentReplay.games[gameNumber].turns;
|
||||
currentReplay.placements.splice(0);
|
||||
currentGame.state = GameState.Ongoing;
|
||||
currentGame.turnNumber = 0;
|
||||
clearPlayContainers();
|
||||
gamePage.classList.remove('gameEnded');
|
||||
replayPreviousGameButton.enabled = gameNumber > 0;
|
||||
replayNextGameButton.enabled = gameNumber + 1 < currentReplay.games.length;
|
||||
replayPreviousButton.enabled = false;
|
||||
replayNextButton.enabled = true;
|
||||
handContainer.hidden = false;
|
||||
gameButtonsContainer.hidden = false;
|
||||
turnNumberLabel.setTurnNumber(null);
|
||||
|
||||
const stage = currentReplay.games[gameNumber].stage;
|
||||
board.resize(stage.copyGrid());
|
||||
const startSpaces = stage.getStartSpaces(currentGame.players.length);
|
||||
for (let i = 0; i < currentGame.players.length; i++) {
|
||||
board.grid[startSpaces[i].x][startSpaces[i].y] = Space.SpecialInactive1 | i;
|
||||
playerBars[i].points = 1;
|
||||
playerBars[i].pointsDelta = null;
|
||||
playerBars[i].pointsTo = null;
|
||||
playerBars[i].specialPoints = 0;
|
||||
}
|
||||
board.refresh();
|
||||
|
||||
replayUpdateHand();
|
||||
}
|
||||
|
||||
flipButton.addEventListener('click', () => {
|
||||
if (currentGame == null) return;
|
||||
if (replayAnimationAbortController) {
|
||||
|
|
@ -326,6 +409,7 @@ flipButton.addEventListener('click', () => {
|
|||
if (currentReplay.watchingPlayer >= currentGame.players.length)
|
||||
currentReplay.watchingPlayer = 0;
|
||||
gamePage.dataset.myPlayerIndex = currentReplay.watchingPlayer.toString();
|
||||
gamePage.dataset.uiBaseColourIsSpecialColour = currentGame.players[currentReplay.watchingPlayer].uiBaseColourIsSpecialColour?.toString();
|
||||
board.flip = currentReplay.watchingPlayer % 2 != 0;
|
||||
clearShowDeck();
|
||||
replayUpdateHand();
|
||||
|
|
@ -426,6 +510,7 @@ function loadPlayers(players: Player[]) {
|
|||
const player = players[i];
|
||||
currentGame!.players[i] = players[i];
|
||||
playerBars[i].name = player.name;
|
||||
playerBars[i].wins = players[i].gamesWon;
|
||||
updateStats(i, scores);
|
||||
if (player.colour.r || player.colour.g || player.colour.b) {
|
||||
document.body.style.setProperty(`--primary-colour-${i + 1}`, `rgb(${player.colour.r}, ${player.colour.g}, ${player.colour.b})`);
|
||||
|
|
@ -448,8 +533,16 @@ function updateStats(playerIndex: number, scores: number[]) {
|
|||
playerBars[playerIndex].statPassesElement.innerText = currentGame.players[playerIndex].passes.toString();
|
||||
}
|
||||
|
||||
/** Shows the waiting indication for the specified player. */
|
||||
function showWaiting(playerIndex: number) {
|
||||
const el = document.createElement('div');
|
||||
el.className = 'cardBack waiting';
|
||||
playContainers[playerIndex].appendChild(el);
|
||||
}
|
||||
|
||||
/** Shows the ready indication for the specified player. */
|
||||
function showReady(playerIndex: number) {
|
||||
clearChildren(playContainers[playerIndex]);
|
||||
const el = document.createElement('div');
|
||||
el.className = 'cardBack';
|
||||
el.innerText = 'Ready';
|
||||
|
|
@ -583,6 +676,7 @@ function showResult() {
|
|||
el.classList.remove('lose');
|
||||
el.classList.remove('draw');
|
||||
el.innerText = 'Victory';
|
||||
playerBars[i].wins++;
|
||||
} else {
|
||||
el.classList.remove('win');
|
||||
el.classList.remove('lose');
|
||||
|
|
@ -598,15 +692,27 @@ function showResult() {
|
|||
}
|
||||
|
||||
handContainer.hidden = true;
|
||||
replayNextButton.enabled = false;
|
||||
if (!currentReplay) {
|
||||
playRow.hidden = true;
|
||||
spectatorRow.hidden = false;
|
||||
replayPreviousButton.hidden = true;
|
||||
replayNextButton.hidden = true;
|
||||
shareReplayLinkButton.hidden = false;
|
||||
replayPreviousGameButton.buttonElement.hidden = true;
|
||||
replayPreviousButton.buttonElement.hidden = true;
|
||||
replayNextButton.buttonElement.hidden = true;
|
||||
replayNextGameButton.buttonElement.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';
|
||||
if (currentGame.state == GameState.SetEnded) {
|
||||
leaveButton.hidden = false;
|
||||
nextGameButton.buttonElement.hidden = currentGame.goalWinCount != null;
|
||||
shareReplayLinkButton.hidden = false;
|
||||
canShareReplay = navigator.canShare && navigator.canShare({ url: window.location.href, title: 'Tableturf Battle Replay' });
|
||||
shareReplayLinkButton.innerText = canShareReplay ? 'Share replay link' : 'Copy replay link';
|
||||
} else {
|
||||
leaveButton.hidden = currentGame.me != null;
|
||||
nextGameButton.buttonElement.hidden = currentGame.me == null;
|
||||
}
|
||||
nextGameButton.enabled = currentGame.me != null && !currentGame.players[currentGame.me.playerIndex].isReady;
|
||||
nextGameButton.buttonElement.innerHTML = nextGameButton.enabled ? 'Next game' : '<div class="loadingSpinner"></div> Waiting for other player';
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -717,7 +823,7 @@ function replayUpdateHand() {
|
|||
|
||||
handButtons.clear();
|
||||
|
||||
const playerData = currentReplay.replayPlayerData[currentReplay.watchingPlayer];
|
||||
const playerData = currentReplay.games[currentReplay.gameNumber].playerData[currentReplay.watchingPlayer];
|
||||
populateShowDeck(playerData.deck);
|
||||
for (const b of showDeckButtons)
|
||||
b.buttonElement.parentElement!.className = '';
|
||||
|
|
@ -1018,7 +1124,28 @@ document.addEventListener('keydown', e => {
|
|||
}
|
||||
});
|
||||
|
||||
shareReplayLinkButton.addEventListener('click', _ => {
|
||||
replayPreviousGameButton.buttonElement.addEventListener('click', () => {
|
||||
if (!currentGame || !currentReplay) return;
|
||||
if (currentReplay.gameNumber > 0)
|
||||
replaySwitchGame(currentReplay.gameNumber - 1);
|
||||
});
|
||||
|
||||
replayNextGameButton.buttonElement.addEventListener('click', () => {
|
||||
if (!currentGame || !currentReplay) return;
|
||||
if (currentReplay.gameNumber < currentReplay.games.length - 1)
|
||||
replaySwitchGame(currentReplay.gameNumber + 1);
|
||||
});
|
||||
nextGameButton.buttonElement.addEventListener('click', () => {
|
||||
if (!currentGame) return;
|
||||
nextGameButton.enabled = false;
|
||||
nextGameButton.buttonElement.innerHTML = '<div class="loadingSpinner"></div> Waiting for other player';
|
||||
let req = new XMLHttpRequest();
|
||||
req.open('POST', `${config.apiBaseUrl}/games/${currentGame.id}/nextGame`);
|
||||
req.addEventListener('error', () => communicationError());
|
||||
req.send(new URLSearchParams({ clientToken }).toString());
|
||||
});
|
||||
|
||||
shareReplayLinkButton.addEventListener('click', () => {
|
||||
shareReplayLinkButton.disabled = true;
|
||||
|
||||
let req = new XMLHttpRequest();
|
||||
|
|
@ -1048,7 +1175,7 @@ function leaveButton_click(e: MouseEvent) {
|
|||
newGameButton.focus();
|
||||
}
|
||||
|
||||
document.getElementById('leaveButton')!.addEventListener('click', leaveButton_click);
|
||||
leaveButton.addEventListener('click', leaveButton_click);
|
||||
|
||||
const colBoxes = [
|
||||
[
|
||||
|
|
|
|||
|
|
@ -15,6 +15,7 @@ const gameSetupDialog = document.getElementById('gameSetupDialog') as HTMLDialog
|
|||
const gameSetupForm = document.getElementById('gameSetupForm') as HTMLFormElement;
|
||||
const maxPlayersBox = document.getElementById('maxPlayersBox') as HTMLSelectElement;
|
||||
const turnTimeLimitBox = document.getElementById('turnTimeLimitBox') as HTMLInputElement;
|
||||
const goalWinCountBox = document.getElementById('goalWinCountBox') as HTMLSelectElement;
|
||||
|
||||
let shownMaxPlayersWarning = false;
|
||||
|
||||
|
|
@ -107,6 +108,8 @@ function createRoom(useOptionsForm: boolean) {
|
|||
data.append('maxPlayers', maxPlayersBox.value);
|
||||
if (turnTimeLimitBox.value)
|
||||
data.append('turnTimeLimit', turnTimeLimitBox.value);
|
||||
if (goalWinCountBox.value)
|
||||
data.append('goalWinCount', goalWinCountBox.value);
|
||||
}
|
||||
request.send(data.toString());
|
||||
setLoadingMessage('Creating a room...');
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@ interface Player {
|
|||
uiBaseColourIsSpecialColour?: boolean;
|
||||
totalSpecialPoints: number;
|
||||
passes: number;
|
||||
gamesWon: number;
|
||||
}
|
||||
|
||||
interface Colour {
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ class PlayerBar {
|
|||
readonly element: HTMLElement;
|
||||
|
||||
private nameElement: HTMLElement;
|
||||
private winsElement: HTMLElement;
|
||||
private specialPointsContainer: HTMLElement;
|
||||
private pointsElement: HTMLElement;
|
||||
private pointsContainer: HTMLElement;
|
||||
|
|
@ -15,12 +16,15 @@ class PlayerBar {
|
|||
statSpecialPointsElement: HTMLElement;
|
||||
statPassesElement: HTMLElement;
|
||||
|
||||
private _wins: number = 0;
|
||||
|
||||
constructor(element: HTMLDivElement) {
|
||||
this.element = element;
|
||||
this.playerIndex = parseInt(element.dataset.index!);
|
||||
if (isNaN(this.playerIndex))
|
||||
throw new Error('Missing player index');
|
||||
this.nameElement = element.getElementsByClassName('name')[0] as HTMLElement;
|
||||
this.winsElement = element.getElementsByClassName('wins')[0] as HTMLElement;
|
||||
this.specialPointsContainer = element.getElementsByClassName('specialPoints')[0] as HTMLElement;
|
||||
|
||||
let pointsContainer: HTMLElement | null = null;
|
||||
|
|
@ -47,6 +51,23 @@ class PlayerBar {
|
|||
get name() { return this.nameElement.innerText; }
|
||||
set name(value: string) { this.nameElement.innerText = value; }
|
||||
|
||||
get wins() { return this._wins; }
|
||||
set wins(value: number) {
|
||||
this._wins = value;
|
||||
clearChildren(this.winsElement);
|
||||
if (value) {
|
||||
if (value < 5) {
|
||||
for (let i = 0; i < value; i++) {
|
||||
this.winsElement.appendChild(document.createElement('div'));
|
||||
}
|
||||
}
|
||||
const el = document.createElement('div');
|
||||
el.classList.add('winCount');
|
||||
el.innerText = value.toString();
|
||||
this.winsElement.appendChild(el);
|
||||
}
|
||||
}
|
||||
|
||||
get points() { return parseInt(this.pointsElement.innerText); }
|
||||
set points(value: number) { this.pointsElement.innerText = value.toString(); }
|
||||
|
||||
|
|
|
|||
|
|
@ -5,9 +5,3 @@ interface PlayerData {
|
|||
cardsUsed: number[];
|
||||
move: Move | null;
|
||||
}
|
||||
|
||||
interface ReplayPlayerData {
|
||||
deck: Card[],
|
||||
initialDrawOrder: number[],
|
||||
drawOrder: number[]
|
||||
}
|
||||
|
|
|
|||
180
TableturfBattleClient/src/ReplayLoader.ts
Normal file
180
TableturfBattleClient/src/ReplayLoader.ts
Normal file
|
|
@ -0,0 +1,180 @@
|
|||
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);
|
||||
const version = dataView.getUint8(0);
|
||||
const players = [ ];
|
||||
let goalWinCount = null;
|
||||
switch (version) {
|
||||
case 1: {
|
||||
const n = dataView.getUint8(1);
|
||||
const stage = stageDatabase.stages[n & 0x1F];
|
||||
const numPlayers = n >> 5;
|
||||
|
||||
let pos = 2;
|
||||
const playerData = [ ];
|
||||
currentReplay = { gameNumber: 0, games: [ ], turns: [ ], placements: [ ], watchingPlayer: 0 };
|
||||
for (let i = 0; i < numPlayers; i++) {
|
||||
const len = dataView.getUint8(pos + 34);
|
||||
const 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) },
|
||||
uiBaseColourIsSpecialColour: false,
|
||||
totalSpecialPoints: 0,
|
||||
passes: 0,
|
||||
gamesWon: 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);
|
||||
}
|
||||
playerData.push({ deck, initialDrawOrder, drawOrder, won: false });
|
||||
pos += 35 + len;
|
||||
}
|
||||
|
||||
const turns = [ ];
|
||||
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;
|
||||
}
|
||||
turns.push(turn);
|
||||
}
|
||||
currentReplay.games.push({ stage, playerData, turns });
|
||||
break;
|
||||
}
|
||||
case 2: {
|
||||
const n = dataView.getUint8(1);
|
||||
const numPlayers = n & 0x0F;
|
||||
goalWinCount = n >> 4;
|
||||
if (goalWinCount == 0) goalWinCount = null;
|
||||
|
||||
let pos = 2;
|
||||
currentReplay = { gameNumber: 0, games: [ ], turns: [ ], placements: [ ], watchingPlayer: 0 };
|
||||
for (let i = 0; i < numPlayers; i++) {
|
||||
const n2 = dataView.getUint8(pos + 9);
|
||||
const len = n2 & 0x7F;
|
||||
const player = {
|
||||
name: new TextDecoder().decode(new DataView(bytes.buffer, pos + 10, 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) },
|
||||
uiBaseColourIsSpecialColour: (n2 & 0x80) != 0,
|
||||
totalSpecialPoints: 0,
|
||||
passes: 0,
|
||||
gamesWon: 0
|
||||
};
|
||||
players.push(player);
|
||||
pos += 10 + len;
|
||||
}
|
||||
|
||||
while (pos < dataView.byteLength) {
|
||||
const stage = stageDatabase.stages[dataView.getUint8(pos + 0)];
|
||||
const playerData = [ ];
|
||||
pos++;
|
||||
for (let i = 0; i < numPlayers; i++) {
|
||||
const deck = [ ];
|
||||
const initialDrawOrder = [ ];
|
||||
const drawOrder = [ ];
|
||||
let won = false;
|
||||
for (let j = 0; j < 15; j++) {
|
||||
deck.push(cardDatabase.get(dataView.getUint8(pos + j)));
|
||||
}
|
||||
for (let j = 0; j < 2; j++) {
|
||||
initialDrawOrder.push(dataView.getUint8(pos + 15 + j) & 0xF);
|
||||
initialDrawOrder.push(dataView.getUint8(pos + 15 + j) >> 4 & 0xF);
|
||||
}
|
||||
for (let j = 0; j < 8; j++) {
|
||||
drawOrder.push(dataView.getUint8(pos + 17 + j) & 0xF);
|
||||
if (j == 7)
|
||||
won = (dataView.getUint8(pos + 17 + j) & 0x80) != 0;
|
||||
else
|
||||
drawOrder.push(dataView.getUint8(pos + 17 + j) >> 4 & 0xF);
|
||||
}
|
||||
playerData.push({ deck, initialDrawOrder, drawOrder, won });
|
||||
pos += 25;
|
||||
}
|
||||
const turns = replayLoadTurns(dataView, numPlayers, pos);
|
||||
pos += 48 * numPlayers;
|
||||
currentReplay.games.push({ stage, playerData, turns });
|
||||
}
|
||||
break;
|
||||
}
|
||||
default:
|
||||
throw new RangeError('Unknown replay data version');
|
||||
}
|
||||
|
||||
currentGame = {
|
||||
id: 'replay',
|
||||
state: GameState.Redraw,
|
||||
me: null,
|
||||
players: players,
|
||||
maxPlayers: players.length,
|
||||
turnNumber: 0,
|
||||
turnTimeLimit: null,
|
||||
turnTimeLeft: null,
|
||||
goalWinCount: goalWinCount,
|
||||
webSocket: null
|
||||
};
|
||||
|
||||
loadPlayers(players);
|
||||
setUrl(`replay/${encodeToUrlSafeBase64(bytes)}`)
|
||||
initReplay();
|
||||
}
|
||||
|
||||
function replayLoadTurns(dataView: DataView, numPlayers: number, pos: number) {
|
||||
const turns = [ ];
|
||||
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;
|
||||
}
|
||||
turns.push(turn);
|
||||
}
|
||||
return turns;
|
||||
}
|
||||
|
|
@ -117,12 +117,14 @@ function onGameStateChange(game: any, playerData: PlayerData | null) {
|
|||
gamePage.classList.remove('gameEnded');
|
||||
switch (game.state) {
|
||||
case GameState.WaitingForPlayers:
|
||||
case GameState.ChoosingStage:
|
||||
initLobbyPage(window.location.toString());
|
||||
showPage('lobby');
|
||||
clearConfirmLeavingGame();
|
||||
lobbySelectedStageSection.hidden = true;
|
||||
lobbyStageSection.hidden = !playerData || game.players[playerData.playerIndex]?.isReady;
|
||||
break;
|
||||
case GameState.Preparing:
|
||||
case GameState.ChoosingDeck:
|
||||
showPage('lobby');
|
||||
if (currentGame.me) setConfirmLeavingGame();
|
||||
if (selectedStageIndicator)
|
||||
|
|
@ -138,7 +140,8 @@ function onGameStateChange(game: any, playerData: PlayerData | null) {
|
|||
break;
|
||||
case GameState.Redraw:
|
||||
case GameState.Ongoing:
|
||||
case GameState.Ended:
|
||||
case GameState.GameEnded:
|
||||
case GameState.SetEnded:
|
||||
board.autoHighlight = false;
|
||||
redrawModal.hidden = true;
|
||||
if (playerData) {
|
||||
|
|
@ -148,7 +151,7 @@ function onGameStateChange(game: any, playerData: PlayerData | null) {
|
|||
initSpectator();
|
||||
|
||||
currentGame.turnNumber = game.turnNumber;
|
||||
gameButtonsContainer.hidden = currentGame.me == null || game.state == GameState.Ended;
|
||||
gameButtonsContainer.hidden = currentGame.me == null || game.state == GameState.GameEnded || game.state == GameState.SetEnded;
|
||||
|
||||
switch (game.state) {
|
||||
case GameState.Redraw:
|
||||
|
|
@ -160,6 +163,8 @@ function onGameStateChange(game: any, playerData: PlayerData | null) {
|
|||
timeLabel.paused = false;
|
||||
break;
|
||||
case GameState.Ongoing:
|
||||
for (let i = 0; i < currentGame.players.length; i++)
|
||||
showWaiting(i);
|
||||
if (currentGame.me) setConfirmLeavingGame();
|
||||
turnNumberLabel.setTurnNumber(game.turnNumber);
|
||||
board.autoHighlight = true;
|
||||
|
|
@ -168,7 +173,8 @@ function onGameStateChange(game: any, playerData: PlayerData | null) {
|
|||
timeLabel.paused = false;
|
||||
resetPlayControls();
|
||||
break;
|
||||
case GameState.Ended:
|
||||
case GameState.GameEnded:
|
||||
case GameState.SetEnded:
|
||||
clearConfirmLeavingGame();
|
||||
gamePage.classList.add('gameEnded');
|
||||
turnNumberLabel.setTurnNumber(null);
|
||||
|
|
@ -239,6 +245,7 @@ function setupWebSocket(gameID: string) {
|
|||
turnNumber: payload.data.turnNumber,
|
||||
turnTimeLimit: payload.data.turnTimeLimit,
|
||||
turnTimeLeft: payload.data.turnTimeLeft,
|
||||
goalWinCount: payload.data.goalWinCount,
|
||||
webSocket: webSocket
|
||||
};
|
||||
|
||||
|
|
@ -265,8 +272,7 @@ function setupWebSocket(gameID: string) {
|
|||
onGameStateChange(payload.data, payload.playerData);
|
||||
|
||||
for (let i = 0; i < currentGame.players.length; i++) {
|
||||
if (currentGame.players[i].isReady)
|
||||
showReady(i);
|
||||
if (currentGame.players[i].isReady) showReady(i);
|
||||
}
|
||||
|
||||
if (currentGame.me) {
|
||||
|
|
@ -317,9 +323,7 @@ function setupWebSocket(gameID: string) {
|
|||
lobbyDeckSection.hidden = true;
|
||||
}
|
||||
|
||||
if (playContainers[payload.data.playerIndex].getElementsByTagName('div').length == 0) {
|
||||
showReady(payload.data.playerIndex);
|
||||
}
|
||||
showReady(payload.data.playerIndex);
|
||||
break;
|
||||
case 'stateChange':
|
||||
clearReady();
|
||||
|
|
@ -345,6 +349,8 @@ function setupWebSocket(gameID: string) {
|
|||
player.specialPoints = payload.data.game.players[i].specialPoints;
|
||||
player.totalSpecialPoints = payload.data.game.players[i].totalSpecialPoints;
|
||||
player.passes = payload.data.game.players[i].passes;
|
||||
player.gamesWon = payload.data.game.players[i].gamesWon;
|
||||
player.isReady = payload.data.game.players[i].isReady;
|
||||
|
||||
const move = payload.data.moves[i];
|
||||
const button = new CardButton(move.card);
|
||||
|
|
@ -370,7 +376,7 @@ function setupWebSocket(gameID: string) {
|
|||
turnNumberLabel.setTurnNumber(payload.data.game.turnNumber);
|
||||
clearPlayContainers();
|
||||
if (payload.event == 'gameEnd') {
|
||||
currentGame.state = GameState.Ended;
|
||||
currentGame.state = payload.data.game.state;
|
||||
clearConfirmLeavingGame();
|
||||
gameButtonsContainer.hidden = true;
|
||||
passButton.enabled = false;
|
||||
|
|
@ -386,6 +392,8 @@ function setupWebSocket(gameID: string) {
|
|||
timeLabel.paused = false;
|
||||
if (payload.data.game.turnTimeLeft)
|
||||
timeLabel.show();
|
||||
for (let i = 0; i < currentGame.players.length; i++)
|
||||
showWaiting(i);
|
||||
}
|
||||
})();
|
||||
break;
|
||||
|
|
@ -399,101 +407,6 @@ 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;
|
||||
|
|
|
|||
|
|
@ -597,6 +597,11 @@ dialog::backdrop {
|
|||
padding-right: 10px;
|
||||
}
|
||||
|
||||
#gamePage:not(.boardFlipped):not([data-players="2"]) .playerBar:is([data-index="1"], [data-index="2"]) .wins,
|
||||
#gamePage.boardFlipped:not([data-players="2"]) .playerBar:is([data-index="0"], [data-index="3"]) .wins {
|
||||
float: left;
|
||||
}
|
||||
|
||||
#gamePage.deckTest #scoreSection { display: none; }
|
||||
#scoreSection:not([hidden]) {
|
||||
grid-column: score-column;
|
||||
|
|
@ -624,7 +629,7 @@ dialog::backdrop {
|
|||
justify-content: center;
|
||||
}
|
||||
|
||||
#handContainer {
|
||||
#handContainer:not([hidden]) {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
grid-template-rows: 1fr 1fr;
|
||||
|
|
@ -638,26 +643,44 @@ dialog::backdrop {
|
|||
grid-template-columns: 1fr 1fr;
|
||||
}
|
||||
|
||||
:is(#playRow, #testControlsHeader) > button {
|
||||
grid-row: button-row;
|
||||
.gameButton {
|
||||
border: none;
|
||||
margin: 0.5em;
|
||||
padding: 0 0.5em;
|
||||
margin: 0.25rem;
|
||||
padding: 0.25rem 0.5rem;
|
||||
font: inherit;
|
||||
text-align: center;
|
||||
background: var(--player-ui-base-colour);
|
||||
text-shadow: 0 0 4px black;
|
||||
border-radius: 0.5em;
|
||||
border-radius: 0.5rem;
|
||||
line-height: 1.5em;
|
||||
}
|
||||
.gameButton:hover { background: var(--player-ui-highlight-colour); }
|
||||
.gameButton:focus-within { outline: 2px solid var(--player-ui-highlight2-colour); }
|
||||
.gameButton:is(:active, .checked) { background: var(--player-ui-highlight2-colour); }
|
||||
.gameButton.disabled:not(.checked) { background: grey; }
|
||||
|
||||
:is(#playRow, #testControlsHeader, #spectatorRow) > button.replayButton:not([hidden]) {
|
||||
font-size: 60%;
|
||||
flex-basis: 7em;
|
||||
display: flex;
|
||||
flex-flow: column;
|
||||
align-items: center;
|
||||
gap: 0.5em;
|
||||
}
|
||||
:is(#playRow, #testControlsHeader, #testControlsFooter) > button:hover { background: var(--player-ui-highlight-colour); }
|
||||
:is(#playRow, #testControlsHeader, #testControlsFooter) > button:focus-within { outline: 2px solid var(--player-ui-highlight2-colour); }
|
||||
:is(#playRow, #testControlsHeader, #testControlsFooter) > button:is(:active, .checked) { background: var(--player-ui-highlight2-colour); }
|
||||
:is(#playRow, #testControlsHeader, #testControlsFooter) > button.disabled:not(.checked) { background: grey; }
|
||||
|
||||
#spectatorRow:not([hidden]) {
|
||||
display: grid;
|
||||
grid-template-columns: auto 1fr 1fr auto;
|
||||
gap: 0.5em;
|
||||
display: flex;
|
||||
gap: 0.25em;
|
||||
}
|
||||
#spectatorRow > * {
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
#nextGameButton:not([hidden]) {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
gap: 0.25em;
|
||||
}
|
||||
|
||||
#debugColour input {
|
||||
|
|
@ -703,21 +726,8 @@ dialog::backdrop {
|
|||
#showDeckList > li.used {
|
||||
opacity: 0.35;
|
||||
}
|
||||
#showDeckCloseButton, #testControlsFooter button {
|
||||
border: none;
|
||||
font-family: inherit;
|
||||
color: inherit;
|
||||
margin: 0.5em;
|
||||
padding: 0 0.5em;
|
||||
text-align: center;
|
||||
background: var(--player-ui-base-colour);
|
||||
text-shadow: 0 0 4px black;
|
||||
border-radius: 0.5em;
|
||||
font-size: inherit;
|
||||
}
|
||||
#showDeckCloseButton {
|
||||
width: 40%;
|
||||
grid-row: button-row;
|
||||
}
|
||||
|
||||
#testControls:not([hidden]) {
|
||||
|
|
@ -981,6 +991,24 @@ dialog::backdrop {
|
|||
to { left: 50%; top: 50%; right: 50%; bottom: 50%; background: var(--special-accent-colour); visibility: hidden; }
|
||||
}
|
||||
|
||||
.wins {
|
||||
float: right;
|
||||
display: flex;
|
||||
margin-top: 1.25em;
|
||||
}
|
||||
.wins > div {
|
||||
display: inline-block;
|
||||
background: url('assets/external/IconMedal_00.webp') center/cover;
|
||||
width: 2em;
|
||||
height: 2em;
|
||||
}
|
||||
.winCount {
|
||||
font-size: larger;
|
||||
text-align: center;
|
||||
text-shadow: 0.1em 0.1em 0 black;
|
||||
}
|
||||
.winCount:not(:first-child) { display: none; }
|
||||
|
||||
.result {
|
||||
font-family: 'Splatoon 1';
|
||||
color: var(--colour);
|
||||
|
|
@ -1172,7 +1200,7 @@ dialog::backdrop {
|
|||
top: 0;
|
||||
right: -50vw;
|
||||
bottom: 0;
|
||||
padding-right: 50vw; /* Centre the dialog over the board but extend the backdrop all the way to the right of the page. */
|
||||
padding-right: 50vw; /* Centre the dialog over the board but extend the backdrop all the way to the right of the page. */
|
||||
background: #000000C0;
|
||||
z-index: 1;
|
||||
}
|
||||
|
|
@ -1441,18 +1469,21 @@ dialog::backdrop {
|
|||
left: -3em;
|
||||
top: 3em;
|
||||
}
|
||||
.playerBar[data-index="0"] {
|
||||
grid-row: player-row-0;
|
||||
}
|
||||
.playerBar[data-index="1"] {
|
||||
grid-row: player-row-1;
|
||||
}
|
||||
.playerBar[data-index="2"] {
|
||||
grid-row: player-row-0;
|
||||
}
|
||||
.playerBar[data-index="3"] {
|
||||
grid-row: player-row-1;
|
||||
}
|
||||
.playerBar[data-index="0"] { grid-row: player-row-0; }
|
||||
.playerBar[data-index="1"] { grid-row: player-row-1; }
|
||||
.playerBar[data-index="2"] { grid-row: player-row-0; }
|
||||
.playerBar[data-index="3"] { grid-row: player-row-1; }
|
||||
|
||||
#gamePage.boardFlipped .playerBar[data-index="0"] { grid-row: player-row-1; }
|
||||
#gamePage.boardFlipped .playerBar[data-index="1"] { grid-row: player-row-0; }
|
||||
#gamePage.boardFlipped .playerBar[data-index="2"] { grid-row: player-row-1; }
|
||||
#gamePage.boardFlipped .playerBar[data-index="3"] { grid-row: player-row-0; }
|
||||
|
||||
#gamePage:not(.boardFlipped):not([data-players="2"]) .playerBar:is([data-index="1"], [data-index="2"]) .wins,
|
||||
#gamePage.boardFlipped:not([data-players="2"]) .playerBar:is([data-index="0"], [data-index="3"]) .wins {
|
||||
float: initial;
|
||||
justify-content: end;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 40rem) {
|
||||
|
|
@ -1501,6 +1532,8 @@ dialog::backdrop {
|
|||
.playerBar[data-index="1"] { grid-column: 1 / span 4; grid-row: 1; }
|
||||
.playerBar[data-index="2"] { grid-column: 1 / span 4; grid-row: 3; }
|
||||
.playerBar[data-index="3"] { grid-column: 1 / span 4; grid-row: 1; }
|
||||
|
||||
.wins { margin-right: 8em; }
|
||||
|
||||
#gamePage.boardFlipped .playerBar[data-index="3"] { grid-column: 1 / span 4; grid-row: 3; }
|
||||
#gamePage.boardFlipped .playerBar[data-index="2"] { grid-column: 1 / span 4; grid-row: 1; }
|
||||
|
|
|
|||
|
|
@ -40,5 +40,5 @@ public class PlayerData {
|
|||
this.Move = move;
|
||||
this.CardsUsed = cardsUsed;
|
||||
}
|
||||
public PlayerData(int playerIndex, Player player) : this(playerIndex, player.Hand, player.Deck, player.Move, player.CardsUsed) { }
|
||||
public PlayerData(int playerIndex, Player player) : this(playerIndex, player.Hand, player.CurrentGameData.Deck, player.Move, player.CardsUsed) { }
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Net;
|
||||
using System.Numerics;
|
||||
using System.Text;
|
||||
|
||||
using Newtonsoft.Json;
|
||||
|
|
@ -24,6 +25,9 @@ public class Game {
|
|||
[JsonProperty("startSpaces")]
|
||||
public Point[]? StartSpaces;
|
||||
|
||||
[JsonProperty("goalWinCount")]
|
||||
public int? GoalWinCount { get; set; }
|
||||
|
||||
[JsonProperty("turnTimeLimit")]
|
||||
public int? TurnTimeLimit { get; set; }
|
||||
[JsonProperty("turnTimeLeft")]
|
||||
|
|
@ -31,6 +35,8 @@ public class Game {
|
|||
[JsonIgnore]
|
||||
internal DateTime abandonedSince = DateTime.UtcNow;
|
||||
|
||||
internal List<string> setStages = new();
|
||||
|
||||
public Game(int maxPlayers) => this.MaxPlayers = maxPlayers;
|
||||
|
||||
public bool TryAddPlayer(Player player, out int playerIndex, out Error error) {
|
||||
|
|
@ -73,9 +79,9 @@ public class Game {
|
|||
|
||||
public bool CanPlay(int playerIndex, Card card, int x, int y, int rotation, bool isSpecialAttack) {
|
||||
if (card is null) throw new ArgumentNullException(nameof(card));
|
||||
if (this.Board == null) return false;
|
||||
if (this.Board is null || this.Players[playerIndex].CurrentGameData is not SingleGameData gameData) return false;
|
||||
|
||||
if (isSpecialAttack && (this.Players[playerIndex].SpecialPoints < card.SpecialCost))
|
||||
if (isSpecialAttack && (gameData.SpecialPoints < card.SpecialCost))
|
||||
return false;
|
||||
|
||||
var isAnchored = false;
|
||||
|
|
@ -124,7 +130,7 @@ public class Game {
|
|||
}
|
||||
|
||||
internal void Tick() {
|
||||
if (this.State == GameState.WaitingForPlayers && this.Players.Count >= 2 && this.Players.All(p => p.selectedStageIndex != null)) {
|
||||
if (this.State is GameState.WaitingForPlayers or GameState.ChoosingStage && this.Players.Count >= 2 && this.Players.All(p => p.selectedStageIndex != null)) {
|
||||
// Choose colours.
|
||||
for (int i = 0; i < this.Players.Count; i++) {
|
||||
this.Players[i].Colour = i switch {
|
||||
|
|
@ -154,6 +160,7 @@ public class Game {
|
|||
if (stageIndex < 0) stageIndex = random.Next(StageDatabase.Stages.Count);
|
||||
var stage = StageDatabase.Stages[stageIndex];
|
||||
this.StageName = stage.Name;
|
||||
this.setStages.Add(stage.Name);
|
||||
this.Board = (Space[,]) stage.grid.Clone();
|
||||
|
||||
// Place starting positions.
|
||||
|
|
@ -162,9 +169,9 @@ public class Game {
|
|||
for (int i = 0; i < this.Players.Count; i++)
|
||||
this.Board[list[i].X, list[i].Y] = Space.SpecialInactive1 | (Space) i;
|
||||
|
||||
this.State = GameState.Preparing;
|
||||
this.State = GameState.ChoosingDeck;
|
||||
this.SendEvent("stateChange", this, true);
|
||||
} else if (this.State == GameState.Preparing && this.Players.All(p => p.Deck != null)) {
|
||||
} else if (this.State == GameState.ChoosingDeck && this.Players.All(p => p.CurrentGameData.Deck != null)) {
|
||||
// Draw cards.
|
||||
var random = new Random();
|
||||
foreach (var player in this.Players)
|
||||
|
|
@ -196,7 +203,7 @@ public class Game {
|
|||
|
||||
foreach (var player in this.Players) {
|
||||
var move = player.Move!;
|
||||
player.turns.Add(new() { CardNumber = move.Card.Number, X = move.X, Y = move.Y, Rotation = move.Rotation, IsPass = move.IsPass, IsTimeout = move.IsTimeout, IsSpecialAttack = move.IsSpecialAttack });
|
||||
player.CurrentGameData.turns.Add(new() { CardNumber = move.Card.Number, X = move.X, Y = move.Y, Rotation = move.Rotation, IsPass = move.IsPass, IsTimeout = move.IsTimeout, IsSpecialAttack = move.IsSpecialAttack });
|
||||
}
|
||||
|
||||
// Place the ink.
|
||||
|
|
@ -210,12 +217,12 @@ public class Game {
|
|||
player.CardsUsed.Add(move.Card.Number);
|
||||
|
||||
if (move.IsPass) {
|
||||
player.Passes++;
|
||||
player.SpecialPoints++;
|
||||
player.CurrentGameData.Passes++;
|
||||
player.CurrentGameData.SpecialPoints++;
|
||||
} else {
|
||||
if (move.IsSpecialAttack) {
|
||||
anySpecialAttacks = true;
|
||||
player.SpecialPoints -= move.Card.SpecialCost;
|
||||
player.CurrentGameData.SpecialPoints -= move.Card.SpecialCost;
|
||||
}
|
||||
if (placementData == null || move.Card.Size != placementData.Value.cardSize) {
|
||||
if (placementData != null)
|
||||
|
|
@ -280,19 +287,42 @@ public class Game {
|
|||
if (!anyEmptySpace) {
|
||||
var player = this.Players[(int) this.Board[x, y] & 3]!;
|
||||
this.Board[x, y] |= Space.SpecialActive1;
|
||||
player.SpecialPoints++;
|
||||
player.TotalSpecialPoints++;
|
||||
player.CurrentGameData.SpecialPoints++;
|
||||
player.CurrentGameData.TotalSpecialPoints++;
|
||||
specialSpacesActivated.Add(new(x, y));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (this.TurnNumber == 12) {
|
||||
this.State = GameState.Ended;
|
||||
if (this.TurnNumber >= 12) {
|
||||
this.TurnTimeLeft = null;
|
||||
this.SendEvent("gameEnd", new { game = this, moves, placements, specialSpacesActivated }, true);
|
||||
this.State = this.GoalWinCount is not null ? GameState.GameEnded : GameState.SetEnded;
|
||||
|
||||
// Determine the winner and check whether a player has won the set yet.
|
||||
var scores = new int[this.Players.Count]; int? winner = null; int maxScore = 0;
|
||||
for (var x = 0; x < this.Board.GetLength(0); x++) {
|
||||
for (var y = 0; y < this.Board.GetLength(1); y++) {
|
||||
if (((int) this.Board[x, y] & 0xC) != 0)
|
||||
scores[(int) this.Board[x, y] & 0x3]++;
|
||||
}
|
||||
}
|
||||
for (var i = 0; i < scores.Length; i++) {
|
||||
if (scores[i] > maxScore) {
|
||||
winner = i;
|
||||
maxScore = scores[i];
|
||||
} else if (scores[i] == maxScore)
|
||||
winner = null;
|
||||
}
|
||||
if (winner is not null) {
|
||||
this.Players[winner.Value].CurrentGameData.won = true;
|
||||
this.Players[winner.Value].GamesWon++;
|
||||
if (this.Players[winner.Value].GamesWon >= this.GoalWinCount)
|
||||
this.State = GameState.SetEnded;
|
||||
}
|
||||
|
||||
foreach (var player in this.Players) player.ClearMoves();
|
||||
this.SendEvent("gameEnd", new { game = this, moves, placements, specialSpacesActivated }, true);
|
||||
} else {
|
||||
this.TurnNumber++;
|
||||
this.TurnTimeLeft = this.TurnTimeLimit + (anySpecialAttacks ? 6 : 4) + coveringMoves + (specialSpacesActivated.Count > 0 ? 1 : 0); // Extra seconds for the animations.
|
||||
|
|
@ -300,8 +330,8 @@ public class Game {
|
|||
// Draw cards.
|
||||
foreach (var player in this.Players) {
|
||||
var index = player.GetHandIndex(player.Move!.Card.Number);
|
||||
var draw = player.drawOrder![this.TurnNumber + 2];
|
||||
player.Hand![index] = player.Deck![draw];
|
||||
var draw = player.CurrentGameData.drawOrder![this.TurnNumber + 2];
|
||||
player.Hand![index] = player.CurrentGameData.Deck![draw];
|
||||
player.ClearMoves();
|
||||
}
|
||||
this.SendEvent("turn", new { game = this, moves, placements, specialSpacesActivated }, true);
|
||||
|
|
@ -328,6 +358,17 @@ public class Game {
|
|||
}
|
||||
}
|
||||
}
|
||||
} else if (this.State is GameState.GameEnded or GameState.SetEnded && this.Players.All(p => p.Move != null)) {
|
||||
foreach (var player in this.Players) {
|
||||
player.selectedStageIndex = null;
|
||||
player.Hand = null;
|
||||
player.CardsUsed.Clear();
|
||||
player.Games.Add(new());
|
||||
player.ClearMoves();
|
||||
}
|
||||
this.State = GameState.ChoosingStage;
|
||||
this.TurnTimeLeft = this.TurnTimeLimit;
|
||||
this.SendEvent("stateChange", this, true);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -356,16 +397,14 @@ public class Game {
|
|||
}
|
||||
|
||||
public void WriteReplayData(Stream stream) {
|
||||
const int VERSION = 1;
|
||||
const int VERSION = 2;
|
||||
|
||||
if (this.State < GameState.Ended)
|
||||
throw new InvalidOperationException("Can't save a replay until the game has ended.");
|
||||
if (this.State < GameState.SetEnded)
|
||||
throw new InvalidOperationException("Can't save a replay until the set has ended.");
|
||||
|
||||
using var writer = new BinaryWriter(stream, Encoding.UTF8, true);
|
||||
writer.Write((byte) VERSION);
|
||||
|
||||
var stageNumber = Enumerable.Range(0, StageDatabase.Stages.Count).First(i => this.StageName == StageDatabase.Stages[i].Name);
|
||||
writer.Write((byte) (stageNumber | (this.Players.Count << 5)));
|
||||
writer.Write((byte) (this.Players.Count | (this.GoalWinCount ?? 0) << 4));
|
||||
foreach (var player in this.Players) {
|
||||
writer.Write((byte) player.Colour.R);
|
||||
writer.Write((byte) player.Colour.G);
|
||||
|
|
@ -376,21 +415,32 @@ public class Game {
|
|||
writer.Write((byte) player.SpecialAccentColour.R);
|
||||
writer.Write((byte) player.SpecialAccentColour.G);
|
||||
writer.Write((byte) player.SpecialAccentColour.B);
|
||||
foreach (var card in player.Deck!)
|
||||
writer.Write((byte) card.Number);
|
||||
for (int i = 0; i < 4; i += 2)
|
||||
writer.Write((byte) (player.initialDrawOrder![i] | player.initialDrawOrder[i + 1] << 4));
|
||||
for (int i = 0; i < 15; i += 2)
|
||||
writer.Write((byte) (player.drawOrder![i] | (i < 14 ? player.drawOrder[i + 1] << 4 : player.UIBaseColourIsSpecialColour ? 0x80 : 0)));
|
||||
writer.Write(player.Name);
|
||||
|
||||
var nameBytes = Encoding.UTF8.GetBytes(player.Name);
|
||||
writer.Write((byte) (nameBytes.Length | (player.UIBaseColourIsSpecialColour ? 0x80 : 0)));
|
||||
writer.Write(nameBytes);
|
||||
}
|
||||
for (int i = 0; i < 12; i++) {
|
||||
for (int i = 0; i < this.Players[0].Games.Count; i++) {
|
||||
var stageNumber = Enumerable.Range(0, StageDatabase.Stages.Count).First(j => this.setStages[i] == StageDatabase.Stages[j].Name);
|
||||
writer.Write((byte) stageNumber);
|
||||
|
||||
foreach (var player in this.Players) {
|
||||
var move = player.turns[i];
|
||||
writer.Write((byte) move.CardNumber);
|
||||
writer.Write((byte) ((move.Rotation & 3) | (move.IsPass ? 0x80 : 0) | (move.IsSpecialAttack ? 0x40 : 0) | (move.IsTimeout ? 0x20 : 0)));
|
||||
writer.Write((sbyte) move.X);
|
||||
writer.Write((sbyte) move.Y);
|
||||
var gameData = player.Games[i];
|
||||
foreach (var card in gameData.Deck!)
|
||||
writer.Write((byte) card.Number);
|
||||
for (int j = 0; j < 4; j += 2)
|
||||
writer.Write((byte) (gameData.initialDrawOrder![j] | gameData.initialDrawOrder[j + 1] << 4));
|
||||
for (int j = 0; j < 15; j += 2)
|
||||
writer.Write((byte) (gameData.drawOrder![j] | (j < 14 ? gameData.drawOrder[j + 1] << 4 : gameData.won ? 0x80 : 0)));
|
||||
}
|
||||
for (int j = 0; j < 12; j++) {
|
||||
foreach (var player in this.Players) {
|
||||
var move = player.Games[i].turns[j];
|
||||
writer.Write((byte) move.CardNumber);
|
||||
writer.Write((byte) ((move.Rotation & 3) | (move.IsPass ? 0x80 : 0) | (move.IsSpecialAttack ? 0x40 : 0) | (move.IsTimeout ? 0x20 : 0)));
|
||||
writer.Write((sbyte) move.X);
|
||||
writer.Write((sbyte) move.Y);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,8 +2,10 @@
|
|||
|
||||
public enum GameState {
|
||||
WaitingForPlayers,
|
||||
Preparing,
|
||||
ChoosingStage,
|
||||
ChoosingDeck,
|
||||
Redraw,
|
||||
Ongoing,
|
||||
Ended
|
||||
}
|
||||
GameEnded,
|
||||
SetEnded
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,4 @@
|
|||
using System.Diagnostics.CodeAnalysis;
|
||||
|
||||
using Newtonsoft.Json;
|
||||
using Newtonsoft.Json;
|
||||
|
||||
namespace TableturfBattleServer;
|
||||
public class Player {
|
||||
|
|
@ -16,29 +14,10 @@ public class Player {
|
|||
public Colour SpecialAccentColour { get; set; }
|
||||
[JsonProperty("uiBaseColourIsSpecialColour")]
|
||||
public bool UIBaseColourIsSpecialColour { get; set; }
|
||||
[JsonProperty("specialPoints")]
|
||||
public int SpecialPoints { get; set; }
|
||||
|
||||
[JsonProperty("totalSpecialPoints")]
|
||||
public int TotalSpecialPoints { get; set; }
|
||||
[JsonProperty("passes")]
|
||||
public int Passes { get; set; }
|
||||
|
||||
[JsonProperty("isReady")]
|
||||
public bool IsReady => this.game.State switch {
|
||||
GameState.WaitingForPlayers => this.selectedStageIndex != null,
|
||||
GameState.Preparing => this.Deck != null,
|
||||
_ => this.Move != null
|
||||
};
|
||||
|
||||
[JsonIgnore]
|
||||
internal List<ReplayTurn> turns = new(12);
|
||||
|
||||
[JsonIgnore]
|
||||
private readonly Game game;
|
||||
[JsonIgnore]
|
||||
internal Card[]? Deck;
|
||||
[JsonIgnore]
|
||||
internal readonly List<int> CardsUsed = new(12);
|
||||
[JsonIgnore]
|
||||
internal Card[]? Hand;
|
||||
|
|
@ -46,10 +25,31 @@ public class Player {
|
|||
internal Move? Move;
|
||||
[JsonIgnore]
|
||||
internal Move? ProvisionalMove;
|
||||
|
||||
[JsonProperty("gamesWon")]
|
||||
public int GamesWon { get; set; }
|
||||
|
||||
[JsonIgnore]
|
||||
internal int[]? initialDrawOrder;
|
||||
internal List<SingleGameData> Games { get; } = new() { new() };
|
||||
|
||||
[JsonIgnore]
|
||||
internal int[]? drawOrder;
|
||||
public SingleGameData CurrentGameData => this.Games[^1];
|
||||
|
||||
[JsonProperty("specialPoints")]
|
||||
public int SpecialPoints => this.CurrentGameData.SpecialPoints;
|
||||
|
||||
[JsonProperty("totalSpecialPoints")]
|
||||
public int TotalSpecialPoints => this.CurrentGameData.TotalSpecialPoints;
|
||||
[JsonProperty("passes")]
|
||||
public int Passes => this.CurrentGameData.Passes;
|
||||
|
||||
[JsonProperty("isReady")]
|
||||
public bool IsReady => this.game.State switch {
|
||||
GameState.WaitingForPlayers or GameState.ChoosingStage => this.selectedStageIndex != null,
|
||||
GameState.ChoosingDeck => this.CurrentGameData.Deck != null,
|
||||
_ => this.Move != null
|
||||
};
|
||||
|
||||
[JsonIgnore]
|
||||
internal int? selectedStageIndex;
|
||||
|
||||
|
|
@ -64,19 +64,18 @@ public class Player {
|
|||
this.ProvisionalMove = null;
|
||||
}
|
||||
|
||||
[MemberNotNull(nameof(drawOrder))]
|
||||
internal void Shuffle(Random random) {
|
||||
this.drawOrder = new int[15];
|
||||
this.initialDrawOrder ??= this.drawOrder;
|
||||
for (int i = 0; i < 15; i++) this.drawOrder[i] = i;
|
||||
this.CurrentGameData.drawOrder = new int[15];
|
||||
this.CurrentGameData.initialDrawOrder ??= this.CurrentGameData.drawOrder;
|
||||
for (int i = 0; i < 15; i++) this.CurrentGameData.drawOrder[i] = i;
|
||||
for (int i = 14; i > 0; i--) {
|
||||
var j = random.Next(i);
|
||||
(this.drawOrder[i], this.drawOrder[j]) = (this.drawOrder[j], this.drawOrder[i]);
|
||||
(this.CurrentGameData.drawOrder[i], this.CurrentGameData.drawOrder[j]) = (this.CurrentGameData.drawOrder[j], this.CurrentGameData.drawOrder[i]);
|
||||
}
|
||||
if (this.Deck != null) {
|
||||
if (this.CurrentGameData.Deck != null) {
|
||||
this.Hand = new Card[4];
|
||||
for (int i = 0; i < 4; i++) {
|
||||
this.Hand[i] = this.Deck[this.drawOrder[i]];
|
||||
this.Hand[i] = this.CurrentGameData.Deck[this.CurrentGameData.drawOrder[i]];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -6,13 +6,9 @@ using System.Text;
|
|||
using System.Text.RegularExpressions;
|
||||
using System.Timers;
|
||||
using System.Web;
|
||||
|
||||
using Newtonsoft.Json;
|
||||
|
||||
using TableturfBattleServer.DTO;
|
||||
|
||||
using WebSocketSharp.Server;
|
||||
|
||||
using HttpListenerRequest = WebSocketSharp.Net.HttpListenerRequest;
|
||||
using HttpListenerResponse = WebSocketSharp.Net.HttpListenerResponse;
|
||||
using Timer = System.Timers.Timer;
|
||||
|
|
@ -128,6 +124,10 @@ internal class Program {
|
|||
SetErrorResponse(e.Response, new(HttpStatusCode.UnprocessableEntity, "InvalidName", "Missing name."));
|
||||
return;
|
||||
}
|
||||
if (name.Length > 32) {
|
||||
SetErrorResponse(e.Response, new(HttpStatusCode.UnprocessableEntity, "InvalidName", "Name is too long."));
|
||||
return;
|
||||
}
|
||||
var maxPlayers = 2;
|
||||
if (d.TryGetValue("maxPlayers", out var maxPlayersString)) {
|
||||
if (!int.TryParse(maxPlayersString, out maxPlayers) || maxPlayers < 2 || maxPlayers > 4) {
|
||||
|
|
@ -143,6 +143,14 @@ internal class Program {
|
|||
}
|
||||
turnTimeLimit = turnTimeLimit2;
|
||||
}
|
||||
int? goalWinCount = null;
|
||||
if (d.TryGetValue("goalWinCount", out var goalWinCountString) && goalWinCountString != "") {
|
||||
if (!int.TryParse(goalWinCountString, out var goalWinCount2) || goalWinCount2 < 1) {
|
||||
SetErrorResponse(e.Response, new(HttpStatusCode.UnprocessableEntity, "InvalidGoalWinCount", "Invalid goal win count."));
|
||||
return;
|
||||
}
|
||||
goalWinCount = goalWinCount2;
|
||||
}
|
||||
if (d.TryGetValue("clientToken", out var tokenString) && tokenString != "") {
|
||||
if (!Guid.TryParse(tokenString, out clientToken)) {
|
||||
SetErrorResponse(e.Response, new(HttpStatusCode.UnprocessableEntity, "InvalidClientToken", "Invalid client token."));
|
||||
|
|
@ -150,7 +158,7 @@ internal class Program {
|
|||
}
|
||||
} else
|
||||
clientToken = Guid.NewGuid();
|
||||
var game = new Game(maxPlayers) { TurnTimeLimit = turnTimeLimit };
|
||||
var game = new Game(maxPlayers) { GoalWinCount = goalWinCount, TurnTimeLimit = turnTimeLimit };
|
||||
game.Players.Add(new(game, name, clientToken));
|
||||
games.Add(game.ID, game);
|
||||
timer.Start();
|
||||
|
|
@ -221,6 +229,10 @@ internal class Program {
|
|||
SetErrorResponse(e.Response, new(HttpStatusCode.UnprocessableEntity, "InvalidName", "Missing name."));
|
||||
return;
|
||||
}
|
||||
if (name.Length > 32) {
|
||||
SetErrorResponse(e.Response, new(HttpStatusCode.UnprocessableEntity, "InvalidName", "Name is too long."));
|
||||
return;
|
||||
}
|
||||
if (d.TryGetValue("clientToken", out var tokenString) && tokenString != "") {
|
||||
if (!Guid.TryParse(tokenString, out clientToken)) {
|
||||
SetErrorResponse(e.Response, new(HttpStatusCode.BadRequest, "InvalidClientToken", "Invalid client token."));
|
||||
|
|
@ -317,7 +329,7 @@ internal class Program {
|
|||
SetErrorResponse(e.Response, new(HttpStatusCode.UnprocessableEntity, "NotInGame", "You're not in the game."));
|
||||
return;
|
||||
}
|
||||
if (player.Deck != null) {
|
||||
if (player.CurrentGameData.Deck != null) {
|
||||
SetErrorResponse(e.Response, new(HttpStatusCode.Conflict, "DeckAlreadyChosen", "You've already chosen a deck."));
|
||||
return;
|
||||
}
|
||||
|
|
@ -357,7 +369,7 @@ internal class Program {
|
|||
}
|
||||
}
|
||||
|
||||
player.Deck = cards.Select(CardDatabase.GetCard).ToArray();
|
||||
player.CurrentGameData.Deck = cards.Select(CardDatabase.GetCard).ToArray();
|
||||
e.Response.StatusCode = (int) HttpStatusCode.NoContent;
|
||||
game.SendPlayerReadyEvent(playerIndex, false);
|
||||
timer.Start();
|
||||
|
|
@ -473,13 +485,48 @@ internal class Program {
|
|||
}
|
||||
break;
|
||||
}
|
||||
case "nextGame": {
|
||||
if (e.Request.HttpMethod != "POST") {
|
||||
e.Response.AddHeader("Allow", "POST");
|
||||
SetErrorResponse(e.Response, new(HttpStatusCode.MethodNotAllowed, "MethodNotAllowed", "Invalid request method for this endpoint."));
|
||||
} else if (e.Request.ContentLength64 >= 65536) {
|
||||
e.Response.StatusCode = (int) HttpStatusCode.RequestEntityTooLarge;
|
||||
} else {
|
||||
try {
|
||||
if (game.State is not (GameState.GameEnded or GameState.SetEnded)) {
|
||||
SetErrorResponse(e.Response, new(HttpStatusCode.Conflict, "GameNotSetUp", "You can't do that in this game state."));
|
||||
return;
|
||||
}
|
||||
|
||||
var d = DecodeFormData(e.Request.InputStream);
|
||||
if (!d.TryGetValue("clientToken", out var tokenString) || !Guid.TryParse(tokenString, out var clientToken)) {
|
||||
SetErrorResponse(e.Response, new(HttpStatusCode.BadRequest, "InvalidClientToken", "Invalid client token."));
|
||||
return;
|
||||
}
|
||||
if (!game.GetPlayer(clientToken, out var playerIndex, out var player)) {
|
||||
SetErrorResponse(e.Response, new(HttpStatusCode.Forbidden, "NotInGame", "You're not in the game."));
|
||||
return;
|
||||
}
|
||||
|
||||
if (player.Move == null) {
|
||||
player.Move = new(player.Hand![0], false, 0, 0, 0, false, false); // Dummy move to indicate that the player is ready.
|
||||
game.SendPlayerReadyEvent(playerIndex, false);
|
||||
}
|
||||
e.Response.StatusCode = (int) HttpStatusCode.NoContent;
|
||||
timer.Start();
|
||||
} catch (ArgumentException) {
|
||||
e.Response.StatusCode = (int) HttpStatusCode.BadRequest;
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
case "replay": {
|
||||
if (e.Request.HttpMethod != "GET") {
|
||||
e.Response.AddHeader("Allow", "GET");
|
||||
SetErrorResponse(e.Response, new(HttpStatusCode.MethodNotAllowed, "MethodNotAllowed", "Invalid request method for this endpoint."));
|
||||
} else {
|
||||
if (game.State != GameState.Ended) {
|
||||
SetErrorResponse(e.Response, new(HttpStatusCode.Conflict, "GameInProgress", "You can't see the replay until the game has ended."));
|
||||
if (game.State != GameState.SetEnded) {
|
||||
SetErrorResponse(e.Response, new(HttpStatusCode.Conflict, "GameInProgress", "You can't see the replay until the set has ended."));
|
||||
return;
|
||||
}
|
||||
var ms = new MemoryStream();
|
||||
|
|
|
|||
25
TableturfBattleServer/SingleGameData.cs
Normal file
25
TableturfBattleServer/SingleGameData.cs
Normal file
|
|
@ -0,0 +1,25 @@
|
|||
using Newtonsoft.Json;
|
||||
|
||||
namespace TableturfBattleServer;
|
||||
public class SingleGameData {
|
||||
[JsonProperty("specialPoints")]
|
||||
public int SpecialPoints { get; set; }
|
||||
|
||||
[JsonProperty("totalSpecialPoints")]
|
||||
public int TotalSpecialPoints { get; set; }
|
||||
[JsonProperty("passes")]
|
||||
public int Passes { get; set; }
|
||||
|
||||
[JsonIgnore]
|
||||
internal bool won = false;
|
||||
|
||||
[JsonIgnore]
|
||||
internal List<ReplayTurn> turns = new(12);
|
||||
|
||||
[JsonIgnore]
|
||||
internal Card[]? Deck;
|
||||
[JsonIgnore]
|
||||
internal int[]? initialDrawOrder;
|
||||
[JsonIgnore]
|
||||
internal int[]? drawOrder;
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user