Show disconnected players; allow leaving games before start

This commit is contained in:
Andrio Celos
2023-11-21 10:42:38 +11:00
parent 752e987fd3
commit 46d5aed44c
14 changed files with 221 additions and 40 deletions

View File

@@ -0,0 +1,4 @@
<!-- This file is part of Bootstrap, available under the MIT License. -->
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="white" class="bi bi-wifi-off" viewBox="0 0 16 16">
<path d="M10.706 3.294A12.545 12.545 0 0 0 8 3C5.259 3 2.723 3.882.663 5.379a.485.485 0 0 0-.048.736.518.518 0 0 0 .668.05A11.448 11.448 0 0 1 8 4c.63 0 1.249.05 1.852.148l.854-.854zM8 6c-1.905 0-3.68.56-5.166 1.526a.48.48 0 0 0-.063.745.525.525 0 0 0 .652.065 8.448 8.448 0 0 1 3.51-1.27L8 6zm2.596 1.404.785-.785c.63.24 1.227.545 1.785.907a.482.482 0 0 1 .063.745.525.525 0 0 1-.652.065 8.462 8.462 0 0 0-1.98-.932zM8 10l.933-.933a6.455 6.455 0 0 1 2.013.637c.285.145.326.524.1.75l-.015.015a.532.532 0 0 1-.611.09A5.478 5.478 0 0 0 8 10zm4.905-4.905.747-.747c.59.3 1.153.645 1.685 1.03a.485.485 0 0 1 .047.737.518.518 0 0 1-.668.05 11.493 11.493 0 0 0-1.811-1.07zM9.02 11.78c.238.14.236.464.04.66l-.707.706a.5.5 0 0 1-.707 0l-.707-.707c-.195-.195-.197-.518.04-.66A1.99 1.99 0 0 1 8 11.5c.374 0 .723.102 1.021.28zm4.355-9.905a.53.53 0 0 1 .75.75l-10.75 10.75a.53.53 0 0 1-.75-.75l10.75-10.75z"/>
</svg>

After

Width:  |  Height:  |  Size: 1.1 KiB

View File

@@ -45,8 +45,5 @@ let currentReplay: {
watchingPlayer: number
} | null = null;
const playerList = document.getElementById('playerList')!;
const playerListItems: HTMLElement[] = [ ];
const canPlayCard = [ false, false, false, false ];
const canPlayCardAsSpecialAttack = [ false, false, false, false ];

View File

@@ -498,6 +498,7 @@ function loadPlayers(players: Player[]) {
const player = players[i];
currentGame!.game.players[i] = players[i];
playerBars[i].name = player.name;
playerBars[i].setOnline(player.isOnline);
playerBars[i].winCounter.wins = players[i].gamesWon;
updateStats(i, scores);
}
@@ -509,7 +510,7 @@ function loadPlayers(players: Player[]) {
}
function updateColours() {
if (currentGame == null) return;
if (currentGame == null || currentGame.game.players.length == 0) return;
for (let i = 0; i < currentGame.game.players.length; i++) {
if (currentGame.game.players[i].colour.r > 0 || currentGame.game.players[i].colour.g > 0 || currentGame.game.players[i].colour.b > 0) {
setColour(i, 0, currentGame.game.players[i].colour);

View File

@@ -1,4 +1,9 @@
const playerList = document.getElementById('playerList')!;
const playerListSlots: HTMLElement[] = [ ];
const playerListNames: HTMLElement[] = [ ];
const lobbyWinCounters: WinCounter[] = [ ];
const playerListItemsToRemove: HTMLElement[] = [ ];
let playerListItemToRemove: HTMLElement | null = null;
const stageButtons = new CheckButtonGroup<number>(document.getElementById('stageList')!);
const shareLinkButton = document.getElementById('shareLinkButton') as HTMLButtonElement;
@@ -153,18 +158,20 @@ qrCodeDialog.addEventListener('click', e => {
});
function lobbyResetSlots() {
if (!currentGame) throw new Error('No current game');
for (const li of playerListItems)
playerList.removeChild(li);
playerListItems.splice(0);
if (!currentGame) throw new TypeError('No current game');
playerListSlots.splice(0);
playerListNames.splice(0);
lobbyWinCounters.splice(0);
clearChildren(playerList);
for (let i = 0; i < currentGame.game.maxPlayers; i++) {
var el = document.createElement('li');
el.className = 'empty';
el.innerText = 'Waiting...';
playerListItems.push(el);
const el = document.createElement('li');
const placeholder = document.createElement('div');
placeholder.className = 'placeholder';
placeholder.innerText = 'Waiting...';
playerList.appendChild(el);
el.appendChild(placeholder);
playerListSlots.push(el);
}
lobbyLockSettings(currentGame.me?.playerIndex != 0);
@@ -175,33 +182,92 @@ function lobbyLockSettings(lock: boolean) {
}
function clearReady() {
if (!currentGame) throw new Error('No current game');
if (!currentGame) throw new TypeError('No current game');
lobbyStageSubmitButton.disabled = false;
stageSelectionFormLoadingSection.hidden = true;
for (var i = 0; i < currentGame.game.players.length; i++) {
currentGame.game.players[i].isReady = false;
playerListItems[i].className = 'filled';
playerListNames[i].classList.remove('ready');
}
}
function lobbyAddPlayer(playerIndex: number) {
if (!currentGame) throw new Error('No current game');
const listItem = playerListItems[playerIndex];
function lobbyAddPlayer() {
if (!currentGame) throw new TypeError('No current game');
if (playerListItemToRemove) {
playerListItemToRemove.removeEventListener('animationend', playerListItem_animationEnd);
playerListItem_animationEnd();
}
const playerIndex = playerListNames.length;
const slot = playerListSlots[playerIndex];
const player = currentGame.game.players[playerIndex];
listItem.innerText = player.name;
listItem.className = player.isReady ? 'filled ready' : 'filled';
const el = document.createElement('div');
el.className = 'wins';
el.title = 'Battles won';
listItem.appendChild(el);
const winCounter = new WinCounter(el);
winCounter.wins = currentGame.game.players[playerIndex].gamesWon;
el.classList.add('filled');
if (player.isReady) el.classList.add('ready');
if (!player.isOnline) el.classList.add('disconnected');
el.innerText = player.name;
slot.appendChild(el);
playerListNames.push(el);
const el2 = document.createElement('img');
el2.src = 'assets/wifi-off.svg';
el2.className = 'disconnectedIcon';
el2.title = 'Disconnected';
el.appendChild(el2);
const el3 = document.createElement('div');
el3.className = 'wins';
el3.title = 'Battles won';
el.appendChild(el3);
const winCounter = new WinCounter(el3);
winCounter.wins = player.gamesWon;
lobbyWinCounters.push(winCounter);
}
function lobbyRemovePlayer(playerIndex: number) {
if (!currentGame) throw new TypeError('No current game');
// Animate the leaving player and all entries below them to mimic the original game.
for (let i = playerIndex; i < playerListNames.length; i++)
(<HTMLElement>playerListSlots[i].lastElementChild).classList.add('removed');
const el = <HTMLElement>playerListSlots[playerIndex].lastElementChild;
el.classList.add('removed');
playerListItemsToRemove.push(el);
if (playerListItemToRemove)
playerListItemToRemove.removeEventListener('animationend', playerListItem_animationEnd);
playerListItemToRemove = el;
el.addEventListener('animationend', playerListItem_animationEnd);
playerListNames.splice(playerIndex, 1);
}
function playerListItem_animationEnd() {
for (const el of playerListItemsToRemove)
el.parentElement!.removeChild(el);
playerListItemsToRemove.splice(0);
playerListItemToRemove = null;
for (let i = 0; i < playerListNames.length; i++) {
playerListNames[i].classList.remove('removed');
if (playerListNames[i].parentElement != playerListSlots[i]) {
playerListNames[i].parentElement!.removeChild(playerListNames[i]);
playerListSlots[i].appendChild(playerListNames[i]);
}
}
}
function lobbySetReady(playerIndex: number) {
playerListItems[playerIndex].className = 'filled ready';
playerListNames[playerIndex].classList.add('ready');
}
function lobbySetOnline(playerIndex: number, isOnline: boolean) {
if (isOnline) playerListNames[playerIndex].classList.remove('disconnected');
else playerListNames[playerIndex].classList.add('disconnected');
}
function initDeckSelection() {

View File

@@ -2,6 +2,7 @@ interface Player {
name: string;
specialPoints: number;
isReady: boolean;
isOnline: boolean;
colour: Colour;
specialColour: Colour;
specialAccentColour: Colour;

View File

@@ -48,7 +48,15 @@ class PlayerBar {
}
get name() { return this.nameElement.innerText; }
set name(value: string) { this.nameElement.innerText = value; }
set name(value: string) {
this.nameElement.innerText = value;
const el2 = document.createElement('img');
el2.src = 'assets/wifi-off.svg';
el2.className = 'disconnectedIcon';
el2.title = 'Disconnected';
this.nameElement.appendChild(el2);
}
get points() { return parseInt(this.pointsElement.innerText); }
set points(value: number) { this.pointsElement.innerText = value.toString(); }
@@ -116,4 +124,9 @@ class PlayerBar {
this.element.hidden = !value;
this.pointsContainer.hidden = !value;
}
setOnline(value: boolean) {
if (value) this.element.classList.remove('disconnected');
else this.element.classList.add('disconnected');
}
}

View File

@@ -55,6 +55,7 @@ class ReplayLoader {
name: this.readString(),
specialPoints: 0,
isReady: false,
isOnline: true,
colour,
specialColour,
specialAccentColour,
@@ -226,6 +227,7 @@ class ReplayLoader {
name: this.readString(len),
specialPoints: 0,
isReady: false,
isOnline: true,
colour,
specialColour,
specialAccentColour,

View File

@@ -2,7 +2,7 @@ class WinCounter {
readonly parent: HTMLElement;
private _wins: number = 0;
constructor(element: HTMLDivElement) {
constructor(element: HTMLElement) {
this.parent = element;
}

View File

@@ -128,7 +128,7 @@ function onGameStateChange(game: any, playerData: PlayerData | null) {
gamePage.dataset.myPlayerIndex = playerData ? playerData.playerIndex.toString() : '';
gamePage.dataset.uiBaseColourIsSpecialColour = (userConfig.colourLock
? (playerData?.playerIndex ?? 0) != 1
: game.players[playerData?.playerIndex ?? 0].uiBaseColourIsSpecialColour ?? true).toString();
: game.players[playerData?.playerIndex ?? 0]?.uiBaseColourIsSpecialColour ?? true).toString();
if (game.state != GameState.WaitingForPlayers)
lobbyLockSettings(true);
@@ -274,7 +274,7 @@ function setupWebSocket(gameID: string) {
lobbyResetSlots();
for (let i = 0; i < currentGame.game.players.length; i++)
lobbyAddPlayer(i);
lobbyAddPlayer();
onGameSettingsChange();
for (let i = 0; i < playerBars.length; i++) {
@@ -318,7 +318,7 @@ function setupWebSocket(gameID: string) {
}
} else {
if (currentGame == null) {
communicationError();
if (payload.event != 'playerOnline') communicationError();
return;
}
switch (payload.event) {
@@ -329,7 +329,14 @@ function setupWebSocket(gameID: string) {
case 'join':
if (payload.data.playerIndex == currentGame.game.players.length) {
currentGame.game.players.push(payload.data.player);
lobbyAddPlayer(payload.data.playerIndex);
lobbyAddPlayer();
} else
communicationError();
break;
case 'leave':
if (payload.data.playerIndex < currentGame.game.players.length) {
currentGame.game.players.splice(payload.data.playerIndex, 1);
lobbyRemovePlayer(payload.data.playerIndex);
}
else
communicationError();
@@ -344,6 +351,11 @@ function setupWebSocket(gameID: string) {
showReady(payload.data.playerIndex);
break;
case 'playerOnline':
currentGame.game.players[payload.data.playerIndex].isOnline = payload.data.isOnline;
lobbySetOnline(payload.data.playerIndex, payload.data.isOnline);
playerBars[payload.data.playerIndex].setOnline(payload.data.isOnline);
break;
case 'stateChange':
clearReady();
onGameStateChange(payload.data, payload.playerData);

View File

@@ -153,18 +153,35 @@ footer {
}
#playerList li {
width: calc(100% - 3em);
width: calc(100% - 2em);
margin: 0.5em 1em;
background: #111;
border-radius: 0.5em;
position: relative;
}
#playerList li > div {
padding: 0.5em;
border-radius: 0.5em;
background: #111;
text-shadow: 1px 1px black;
box-sizing: border-box;
}
#playerList li .placeholder {
user-select: none;
}
#playerList .filled {
position: absolute;
left: 0;
top: 0;
width: 100%;
bottom: 0;
background: var(--theme-colour);
position: relative;
animation: 0.33s linear playerListFlyIn;
animation: 0.33s linear forwards playerListFlyIn;
}
#playerList .removed {
animation: 0.33s linear forwards playerListFlyOut;
}
#playerList .ready::after {
@@ -176,11 +193,31 @@ footer {
font-size: x-large;
}
#playerList .disconnected {
color: darkgrey;
}
@keyframes playerListFlyIn {
from { left: -100%; }
from { left: -120%; }
to { left: 0; }
}
@keyframes playerListFlyOut {
from { left: 0%; }
to { left: -120%; }
}
.disconnectedIcon {
display: none;
height: 1.5rem;
margin-left: 0.5em;
vertical-align: middle;
}
.disconnected .disconnectedIcon {
display: inline;
}
.wins {
display: flex;
}
@@ -1167,6 +1204,10 @@ rect.special, g.specialCost rect {
margin: 0.5em 0;
}
.disconnected .name {
color: darkgrey;
}
.specialPoints div {
display: inline-block;
width: 1.25em;

View File

@@ -2,6 +2,7 @@
using System.Net;
using System.Text;
using Newtonsoft.Json;
using WebSocketSharp.Server;
namespace TableturfBattleServer;
public class Game {
@@ -460,7 +461,7 @@ public class Game {
}
} else if (this.TurnTimeLeft != null) {
--this.TurnTimeLeft;
if (this.TurnTimeLeft <= -3) { // Add a small grace period to account for network lag.
if (this.TurnTimeLeft <= -3 || (this.TurnTimeLeft <= 0 && this.Players.All(p => p.IsReady || !p.IsOnline))) { // Add a small grace period to account for network lag for online players.
for (var i = 0; i < this.Players.Count; i++) {
var player = this.Players[i];
if (player.Move == null) {
@@ -594,6 +595,28 @@ public class Game {
internal void SendPlayerReadyEvent(int playerIndex, bool isTimeout) => this.SendEvent("playerReady", new { playerIndex, isTimeout }, false);
internal void AddConnection(int playerIndex, TableturfWebSocketBehaviour connection) {
var player = this.Players[playerIndex];
player.AddConnection(connection);
if (!player.IsOnline) {
player.DisconnectedAt = null;
this.SendEvent("playerOnline", new { playerIndex, player.IsOnline }, false);
}
}
internal void RemoveConnection(Player player, TableturfWebSocketBehaviour connection) {
var playerIndex = this.Players.IndexOf(player);
player.RemoveConnection(connection);
if (player.IsOnline && player.Connections.Count == 0) {
if (this.State == GameState.WaitingForPlayers) {
this.Players.RemoveAt(playerIndex);
this.SendEvent("leave", new { playerIndex }, false);
} else {
player.DisconnectedAt = DateTime.UtcNow;
this.SendEvent("playerOnline", new { playerIndex, player.IsOnline }, false);
}
}
}
internal void SendEvent<T>(string eventType, T data, bool includePlayerData) {
foreach (var session in Program.httpServer!.WebSocketServices.Hosts.First().Sessions.Sessions) {
if (session is TableturfWebSocketBehaviour behaviour && behaviour.GameID == this.ID) {

View File

@@ -10,6 +10,12 @@ public class Player {
public Colour SpecialAccentColour { get; set; }
public bool UIBaseColourIsSpecialColour { get; set; }
[JsonIgnore]
internal List<TableturfWebSocketBehaviour> Connections { get; } = new();
[JsonIgnore]
public DateTime? DisconnectedAt { get; set; }
public bool IsOnline => this.DisconnectedAt == null;
public StageSelectionPrompt? StageSelectionPrompt { get; set; }
[JsonIgnore]
@@ -85,4 +91,16 @@ public class Player {
}
return -1;
}
internal void AddConnection(TableturfWebSocketBehaviour connection) {
lock (this.Connections) {
this.Connections.Add(connection);
}
}
internal void RemoveConnection(TableturfWebSocketBehaviour connection) {
lock (this.Connections) {
this.Connections.Remove(connection);
}
}
}

View File

@@ -1,4 +1,4 @@
using System.Diagnostics.CodeAnalysis;
using System.Diagnostics.CodeAnalysis;
using System.Globalization;
using System.Net;
using System.Reflection;

View File

@@ -20,20 +20,23 @@ internal class TableturfWebSocketBehaviour : WebSocketBehavior {
// Send an initial state payload.
if (Program.TryGetGame(this.GameID, out var game)) {
this.Game = game;
DTO.PlayerData? playerData = null;
for (int i = 0; i < game.Players.Count; i++) {
var player = game.Players[i];
if (player.Token == this.ClientToken) {
this.Player = player;
playerData = new(i, player);
game.AddConnection(i, this);
break;
}
}
this.Game = game;
this.Send(JsonUtils.Serialise(new DTO.WebSocketPayloadWithPlayerData<Game?>("sync", game, playerData)));
} else
this.Send(JsonUtils.Serialise(new DTO.WebSocketPayloadWithPlayerData<Game?>("sync", null, null)));
}
protected override void OnClose(WebSocketSharp.CloseEventArgs e) => this.Game?.RemoveConnection(this.Player, this);
internal void SendInternal(string data) => this.Send(data);
}