mirror of
https://github.com/AndrioCelos/TableturfBattleApp.git
synced 2026-08-24 11:54:29 -05:00
Show disconnected players; allow leaving games before start
This commit is contained in:
4
TableturfBattleClient/assets/wifi-off.svg
Normal file
4
TableturfBattleClient/assets/wifi-off.svg
Normal 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 |
@@ -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 ];
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -2,6 +2,7 @@ interface Player {
|
||||
name: string;
|
||||
specialPoints: number;
|
||||
isReady: boolean;
|
||||
isOnline: boolean;
|
||||
colour: Colour;
|
||||
specialColour: Colour;
|
||||
specialAccentColour: Colour;
|
||||
|
||||
@@ -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');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -2,7 +2,7 @@ class WinCounter {
|
||||
readonly parent: HTMLElement;
|
||||
private _wins: number = 0;
|
||||
|
||||
constructor(element: HTMLDivElement) {
|
||||
constructor(element: HTMLElement) {
|
||||
this.parent = element;
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Globalization;
|
||||
using System.Net;
|
||||
using System.Reflection;
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user