Compare commits

..

15 Commits
4.4.0 ... 4.7.3

Author SHA1 Message Date
TeamCity
49a1bd40ff ci: bump version to v4.7.3 2025-12-04 01:32:05 +00:00
f847424b9c fix: BAC-25 Race Condition: Websocket Promises (#99)
Reviewed-on: #99
Reviewed-by: lq64 <lq@blackhole.local>
Co-authored-by: Janis <janis.e.20@gmx.de>
Co-committed-by: Janis <janis.e.20@gmx.de>
2025-12-04 02:29:19 +01:00
TeamCity
d50a576e31 ci: bump version to v4.7.2 2025-12-03 11:17:48 +00:00
TeamCity
eba2ad6232 ci: bump version to v4.7.1 2025-12-03 09:22:04 +00:00
14961cce01 chore: BAC-17 Add knockoutwhistfrontend submodule (#98)
Reviewed-on: #98
Co-authored-by: Janis <janis.e.20@gmx.de>
Co-committed-by: Janis <janis.e.20@gmx.de>
2025-12-03 10:19:15 +01:00
TeamCity
dcb5d7373f ci: bump version to v4.7.0 2025-12-03 08:20:51 +00:00
d57e6efa98 feat(ui): FRO-7 Endscreen (#97)
Added a nice look to the endscreen. Implemented a ranking method inside GameLobby to get an order.

Co-authored-by: LQ63 <lkhermann@web.de>
Reviewed-on: #97
Reviewed-by: Janis <janis-e@gmx.de>
Co-authored-by: lq64 <lq@blackhole.local>
Co-committed-by: lq64 <lq@blackhole.local>
2025-12-03 09:18:11 +01:00
TeamCity
4156e1c9ce ci: bump version to v4.6.2 2025-12-01 20:21:02 +00:00
358556612e fix: FRO-6 Websocket Close Handle (#96)
Reviewed-on: #96
Co-authored-by: Janis <janis.e.20@gmx.de>
Co-committed-by: Janis <janis.e.20@gmx.de>
2025-12-01 21:17:58 +01:00
TeamCity
7f82d2eeae ci: bump version to v4.6.1 2025-12-01 19:44:13 +00:00
a55f0b4b61 fix(api): BAC-23 Remove old polling code (#95)
Reviewed-on: #95
Co-authored-by: Janis <janis.e.20@gmx.de>
Co-committed-by: Janis <janis.e.20@gmx.de>
2025-12-01 20:41:27 +01:00
TeamCity
f115c03ecb ci: bump version to v4.6.0 2025-12-01 19:07:26 +00:00
fd2467a9ea feat(api): BAC-11 Websocket - Return to Lobby (#94)
Reviewed-on: #94
Co-authored-by: Janis <janis.e.20@gmx.de>
Co-committed-by: Janis <janis.e.20@gmx.de>
2025-12-01 20:04:17 +01:00
TeamCity
9d3f3940a9 ci: bump version to v4.5.0 2025-12-01 18:53:23 +00:00
0541bb58d1 feat(api): BAC-10 Websockets - Kick Users (#93)
Reviewed-on: #93
Co-authored-by: Janis <janis.e.20@gmx.de>
Co-committed-by: Janis <janis.e.20@gmx.de>
2025-12-01 19:50:19 +01:00
16 changed files with 247 additions and 113 deletions

5
.gitmodules vendored
View File

@@ -2,3 +2,8 @@
path = knockoutwhist path = knockoutwhist
branch = main branch = main
url = https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist.git url = https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist.git
[submodule "knockoutwhistfrontend"]
path = knockoutwhistfrontend
branch = main
url = https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Frontend.git

View File

@@ -172,3 +172,35 @@
* **api:** Fixed websocket routing ([#88](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/issues/88)) ([46c96d4](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/commit/46c96d4ceb935ac91fc515a1fdaef195e5ebc0a7)) * **api:** Fixed websocket routing ([#88](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/issues/88)) ([46c96d4](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/commit/46c96d4ceb935ac91fc515a1fdaef195e5ebc0a7))
* **api:** fixes - reimplemented animations ([#90](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/issues/90)) ([cfcd967](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/commit/cfcd967ce08ecf07f3f06826c337f684eb3b0c5f)) * **api:** fixes - reimplemented animations ([#90](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/issues/90)) ([cfcd967](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/commit/cfcd967ce08ecf07f3f06826c337f684eb3b0c5f))
## (2025-12-01)
### Features
* **api:** BAC-10 Websockets - Kick Users ([#93](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/issues/93)) ([0541bb5](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/commit/0541bb58d19efd98d134b3d0412f39b4b1001783))
## (2025-12-01)
### Features
* **api:** BAC-11 Websocket - Return to Lobby ([#94](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/issues/94)) ([fd2467a](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/commit/fd2467a9ea22dca64d5152a5a3e6db86d9a6f345))
## (2025-12-01)
### Bug Fixes
* **api:** BAC-23 Remove old polling code ([#95](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/issues/95)) ([a55f0b4](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/commit/a55f0b4b6164a47e3524422650ed99d10f9c8b0d))
## (2025-12-01)
### Bug Fixes
* FRO-6 Websocket Close Handle ([#96](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/issues/96)) ([3585566](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/commit/358556612ec74601c8b31125e4e65f750abf8c4c))
## (2025-12-03)
### Features
* **ui:** FRO-7 Endscreen ([#97](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/issues/97)) ([d57e6ef](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/commit/d57e6efa985ca07c32f9f54595fe7393dbdf4d8a))
## (2025-12-03)
## (2025-12-03)
## (2025-12-04)
### Bug Fixes
* BAC-25 Race Condition: Websocket Promises ([#99](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/issues/99)) ([f847424](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/commit/f847424b9cea423ace5661d1efb6e4f01483c655))

1
knockoutwhistfrontend Submodule

Submodule knockoutwhistfrontend added at 575d1b5e8e

View File

@@ -20,6 +20,7 @@ import play.api.libs.json.{JsObject, Json}
import java.util.{Timer, TimerTask, UUID} import java.util.{Timer, TimerTask, UUID}
import scala.collection.mutable import scala.collection.mutable
import scala.collection.mutable.ListBuffer import scala.collection.mutable.ListBuffer
import scala.util.Try
class GameLobby private( class GameLobby private(
val logic: GameLogic, val logic: GameLogic,
@@ -277,6 +278,37 @@ class GameLobby private(
def getUsers: Set[User] = { def getUsers: Set[User] = {
users.values.map(d => d.user).toSet users.values.map(d => d.user).toSet
} }
def getFinalRanking: List[(String, (Int, Int))] = {
Try {
val match1 = getMatch
if (!match1.isOver) {
List.empty
} else {
val winnerName = logic.getWinner.get.name
val allPlayerNames = match1.totalplayers.map(_.name)
val roundlist = match1.roundlist
val playerMetrics: Map[String, (Int, Int)] = allPlayerNames.map { name =>
val roundsWon = roundlist.count { round =>
round.winner.exists(_.name == name)
}
val totalTricksWon = roundlist.flatMap(_.tricklist).count { trick =>
trick.winner.exists(_.name == name)
}
name -> (roundsWon, totalTricksWon)
}.toMap
val winnerMetrics = playerMetrics(winnerName)
val remainingPlayersMetrics = playerMetrics.view.filterKeys(_ != winnerName).toList
val sortedRemainingPlayers = remainingPlayersMetrics.sortBy { case (_, (rounds, tricks)) =>
(-rounds, -tricks)
}
(winnerName, winnerMetrics) :: sortedRemainingPlayers
}
}.getOrElse(List())
}
private def transmitToAll(event: JsObject): Unit = { private def transmitToAll(event: JsObject): Unit = {
users.values.foreach(session => { users.values.foreach(session => {

View File

@@ -26,6 +26,7 @@ class UserSession(val user: User, val host: Boolean, val gameLobby: GameLobby) e
else canInteract = Some(InteractionType.Card) else canInteract = Some(InteractionType.Card)
case _ => case _ =>
} }
websocketActor.foreach(_.solveRequests())
websocketActor.foreach(_.transmitEventToClient(event)) websocketActor.foreach(_.transmitEventToClient(event))
} }
@@ -38,38 +39,41 @@ class UserSession(val user: User, val host: Boolean, val gameLobby: GameLobby) e
} }
def handleWebResponse(eventType: String, data: JsObject): Unit = { def handleWebResponse(eventType: String, data: JsObject): Unit = {
lock.lock() eventType match {
val result = Try { case "ping" =>
eventType match { // No action needed for Ping
case "ping" => ()
// No action needed for Ping case "StartGame" =>
() gameLobby.startGame(user)
case "StartGame" => case "PlayCard" =>
gameLobby.startGame(user) val maybeCardIndex: Option[String] = (data \ "cardindex").asOpt[String]
case "PlayCard" => maybeCardIndex match {
val maybeCardIndex: Option[String] = (data \ "cardindex").asOpt[String] case Some(index) =>
maybeCardIndex match { val session = gameLobby.getUserSession(user.id)
case Some(index) => gameLobby.playCard(session, index.toInt)
val session = gameLobby.getUserSession(user.id) case None =>
gameLobby.playCard(session, index.toInt) println("Card Index not found or is not a number.")
case None => }
println("Card Index not found or is not a number.") case "PickTrumpsuit" =>
} val maybeSuitIndex: Option[Int] = (data \ "suitIndex").asOpt[Int]
case "PickTrumpsuit" => maybeSuitIndex match {
val maybeSuitIndex: Option[Int] = (data \ "suitIndex").asOpt[Int] case Some(index) =>
maybeSuitIndex match { val session = gameLobby.getUserSession(user.id)
case Some(index) => gameLobby.selectTrump(session, index)
val session = gameLobby.getUserSession(user.id) case None =>
gameLobby.selectTrump(session, index) println("Card Index not found or is not a number.")
case None => }
println("Card Index not found or is not a number.") case "KickPlayer" =>
} val maybePlayerId: Option[String] = (data \ "playerId").asOpt[String]
} maybePlayerId match {
} case Some(id) =>
lock.unlock() val playerUUID = UUID.fromString(id)
if (result.isFailure) { gameLobby.leaveGame(playerUUID, true)
val throwable = result.failed.get case None =>
throw throwable println("Player ID not found or is not a valid UUID.")
}
case "ReturnToLobby" =>
gameLobby.returnToLobby(this)
} }
} }

View File

@@ -5,6 +5,7 @@ import org.apache.pekko.actor.{Actor, ActorRef}
import play.api.libs.json.{JsObject, JsValue, Json} import play.api.libs.json.{JsObject, JsValue, Json}
import util.WebsocketEventMapper import util.WebsocketEventMapper
import scala.collection.mutable
import scala.util.{Failure, Success, Try} import scala.util.{Failure, Success, Try}
class UserWebsocketActor( class UserWebsocketActor(
@@ -12,6 +13,8 @@ class UserWebsocketActor(
session: UserSession session: UserSession
) extends Actor { ) extends Actor {
private val requests: mutable.Map[String, String] = mutable.Map()
{ {
session.lock.lock() session.lock.lock()
if (session.websocketActor.isDefined) { if (session.websocketActor.isDefined) {
@@ -48,12 +51,14 @@ class UserWebsocketActor(
} }
private def handle(json: JsValue): Unit = { private def handle(json: JsValue): Unit = {
session.lock.lock()
val idOpt = (json \ "id").asOpt[String] val idOpt = (json \ "id").asOpt[String]
if (idOpt.isEmpty) { if (idOpt.isEmpty) {
transmitJsonToClient(Json.obj( transmitJsonToClient(Json.obj(
"status" -> "error", "status" -> "error",
"error" -> "Missing 'id' field" "error" -> "Missing 'id' field"
)) ))
session.lock.unlock()
return return
} }
val id = idOpt.get val id = idOpt.get
@@ -65,17 +70,25 @@ class UserWebsocketActor(
"status" -> "error", "status" -> "error",
"error" -> "Missing 'event' field" "error" -> "Missing 'event' field"
)) ))
session.lock.unlock()
return return
} }
val statusOpt = (json \ "status").asOpt[String] val statusOpt = (json \ "status").asOpt[String]
if (statusOpt.isDefined) { if (statusOpt.isDefined) {
session.lock.unlock()
return return
} }
val event = eventOpt.get val event = eventOpt.get
val data = (json \ "data").asOpt[JsObject].getOrElse(Json.obj()) val data = (json \ "data").asOpt[JsObject].getOrElse(Json.obj())
requests += (id -> event)
val result = Try { val result = Try {
session.handleWebResponse(event, data) session.handleWebResponse(event, data)
} }
if (!requests.contains(id)) {
session.lock.unlock()
return
}
requests -= id
if (result.isSuccess) { if (result.isSuccess) {
transmitJsonToClient(Json.obj( transmitJsonToClient(Json.obj(
"id" -> id, "id" -> id,
@@ -90,6 +103,7 @@ class UserWebsocketActor(
"error" -> result.failed.get.getMessage "error" -> result.failed.get.getMessage
)) ))
} }
session.lock.unlock()
} }
def transmitJsonToClient(jsonObj: JsValue): Unit = { def transmitJsonToClient(jsonObj: JsValue): Unit = {
@@ -100,4 +114,20 @@ class UserWebsocketActor(
transmitJsonToClient(WebsocketEventMapper.toJson(event, session)) transmitJsonToClient(WebsocketEventMapper.toJson(event, session))
} }
def solveRequests(): Unit = {
if (!session.lock.isHeldByCurrentThread)
return;
if (requests.isEmpty)
return;
val pendingRequests = requests.toMap
requests.clear()
pendingRequests.foreach { case (id, event) =>
transmitJsonToClient(Json.obj(
"id" -> id,
"event" -> event,
"status" -> "success"
))
}
}
} }

View File

@@ -1,38 +1,108 @@
@(user: Option[model.users.User], gamelobby: logic.game.GameLobby) @(user: Option[model.users.User], gamelobby: logic.game.GameLobby)
<main class="lobby-background vh-100" id="lobbybackground"> <div class="lobby-background vh-100 d-flex align-items-start justify-content-center pt-5">
<div class="container d-flex flex-column" style="height: calc(100vh - 1rem);"> <div class="container py-5">
<div class="row"> <div class="row justify-content-center">
<div class="col"> <div class="col-lg-8 col-xl-6">
<div class="p-3 text-center fs-4">Winner: @gamelobby.getLogic.getWinner</div> <div class="card shadow-lg mb-5 text-center bg-white border-0 rounded-4">
</div> <div class="card-body p-4 p-md-5">
</div> <h1 class="card-title display-5 fw-bold text-success mb-3">Match Over!</h1>
<div class="row justify-content-center align-items-center flex-grow-1"> <p class="fs-4 text-muted">Congratulations to the winner:</p>
@if((gamelobby.getUserSession(user.get.id).host)) { <h2 class="display-3 fw-bolder mb-4 text-primary">
<div class="col-12 text-center mb-5"> @gamelobby.getLogic.getWinner.get.name
<div class="btn btn-success" onclick="backToLobby('@gamelobby.id')">Return to lobby</div> </h2>
</div> </div>
} else {
<div class="col-12 text-center mt-3">
<div class="spinner-border mt-1" role="status">
<span class="visually-hidden">Loading...</span>
</div> </div>
<div class="card shadow mb-5 border-0 rounded-4 overflow-hidden">
<div class="card-header bg-dark text-white text-center fs-5 fw-semibold">
Final Standings
</div>
<div class="d-flex justify-content-between align-items-center p-2 text-uppercase fw-bold border-bottom bg-light">
<div class="d-flex align-items-center">
Player
</div>
<div class="d-flex flex-row gap-3">
<span class="fs-6 text-dark text-center" style="width: 5rem;">Rounds won</span>
<span class="fs-6 text-dark text-center" style="width: 5rem;">Tricks won</span>
</div>
</div>
<div>
@gamelobby.getFinalRanking.zipWithIndex.map { case ((playerName, (wonRounds, tricksWon)), index) =>
@defining(index + 1) { rank =>
<div class="d-flex justify-content-between align-items-center p-3 border-bottom @if(rank == 1){bg-success-subtle fw-bold}">
<div class="d-flex align-items-center">
<span class="badge @if(rank == 1){bg-warning text-dark fs-6} else {bg-secondary} rounded-pill me-3">#@rank</span>
@playerName
</div>
<div class="d-flex flex-row gap-3">
<span class="fs-6 text-muted text-center" style="width: 5rem;">@wonRounds</span>
<span class="fs-6 text-muted text-center" style="width: 5rem;">@tricksWon</span>
</div>
</div>
}
}
</div>
@if(gamelobby.getFinalRanking.isEmpty) {
<div class="p-3 text-center text-muted">No final scores available.</div>
}
</div>
@if(user.isDefined && gamelobby.getUserSession(user.get.id).host) {
<div class="col-12 text-center mt-4">
<div class="btn btn-success btn-lg shadow" onclick="handleReturnToLobby()">
Return to Lobby
</div>
</div>
} else {
<div class="col-12 text-center mt-4">
<div class="text-primary">
<div class="spinner-grow text-primary mt-1" role="status">
<span class="visually-hidden">Loading...</span>
</div>
<p class="mt-3 fs-6 fw-semibold">
Waiting for the Host to continue...
</p>
</div>
</div>
}
</div> </div>
}
</div> </div>
</div> </div>
</main> </div>
<script> <script>
function waitForFunction(name, checkInterval = 100) { function fireConfetti() {
return new Promise(resolve => { let duration = 3 * 1000;
const timer = setInterval(() => { let animationEnd = Date.now() + duration;
if (typeof window[name] === "function") { let defaults = { startVelocity: 30, spread: 360, ticks: 60, zIndex: 0 };
clearInterval(timer);
resolve(window[name]); function randomInRange(min, max) {
} return Math.random() * (max - min) + min;
}, checkInterval);
});
} }
waitForFunction("pollForUpdates").then(fn => fn('@gamelobby.id'));
connectWebSocket() let interval = setInterval(function() {
let timeLeft = animationEnd - Date.now();
if (timeLeft <= 0) {
return clearInterval(interval);
}
let particleCount = 50 * (timeLeft / duration);
// Left burst
confetti(Object.assign({}, defaults, {
particleCount,
origin: { x: randomInRange(0.1, 0.3), y: Math.random() - 0.2 }
}));
// Right burst
confetti(Object.assign({}, defaults, {
particleCount,
origin: { x: randomInRange(0.7, 0.9), y: Math.random() - 0.2 }
}));
}, 250);
}
connectWebSocket();
fireConfetti();
</script> </script>

View File

@@ -130,17 +130,6 @@
</main> </main>
</div> </div>
<script> <script>
function waitForFunction(name, checkInterval = 100) {
return new Promise(resolve => {
const timer = setInterval(() => {
if (typeof window[name] === "function") {
clearInterval(timer);
resolve(window[name]);
}
}, checkInterval);
});
}
waitForFunction("pollForUpdates").then(fn => fn('@gamelobby.id'));
connectWebSocket() connectWebSocket()
canPlayCard = @gamelobby.logic.getCurrentPlayer.contains(player); canPlayCard = @gamelobby.logic.getCurrentPlayer.contains(player);
</script> </script>

View File

@@ -64,16 +64,5 @@
</div> </div>
</div> </div>
<script> <script>
function waitForFunction(name, checkInterval = 100) {
return new Promise(resolve => {
const timer = setInterval(() => {
if (typeof window[name] === "function") {
clearInterval(timer);
resolve(window[name]);
}
}, checkInterval);
});
}
waitForFunction("pollForUpdates").then(fn => fn('@gamelobby.id'));
connectWebSocket() connectWebSocket()
</script> </script>

View File

@@ -110,16 +110,5 @@
</div> </div>
</div> </div>
<script> <script>
function waitForFunction(name, checkInterval = 100) {
return new Promise(resolve => {
const timer = setInterval(() => {
if (typeof window[name] === "function") {
clearInterval(timer);
resolve(window[name]);
}
}, checkInterval);
});
}
waitForFunction("pollForUpdates").then(fn => fn('@gamelobby.id'));
connectWebSocket() connectWebSocket()
</script> </script>

View File

@@ -60,7 +60,7 @@
<a href="#" class="btn btn-danger disabled" aria-disabled="true" tabindex="-1">Remove</a> <a href="#" class="btn btn-danger disabled" aria-disabled="true" tabindex="-1">Remove</a>
} else { } else {
<h5 class="card-title">@playersession.name</h5> <h5 class="card-title">@playersession.name</h5>
<div class="btn btn-danger" onclick="removePlayer('@gamelobby.id', '@playersession.id')"> <div class="btn btn-danger" onclick="handleKickPlayer('@playersession.id')">
Remove</div> Remove</div>
} }
</div> </div>
@@ -98,18 +98,5 @@
</div> </div>
</main> </main>
<script> <script>
/*
function waitForFunction(name, checkInterval = 100) {
return new Promise(resolve => {
const timer = setInterval(() => {
if (typeof window[name] === "function") {
clearInterval(timer);
resolve(window[name]);
}
}, checkInterval);
});
}
waitForFunction("pollForUpdates").then(fn => fn('@gamelobby.id'));
*/
connectWebSocket() connectWebSocket()
</script> </script>

View File

@@ -17,7 +17,7 @@
<link rel="shortcut icon" type="image/png" href="@routes.Assets.versioned("images/favicon.png")"> <link rel="shortcut icon" type="image/png" href="@routes.Assets.versioned("images/favicon.png")">
<link href="https://cdn.jsdelivr.net/npm/bootstrap@@5.3.8/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-sRIl4kxILFvY47J16cr9ZwB07vP4J8+LH7qKQnuqkuIAvNWLzeN8tE5YBujZqJLB" crossorigin="anonymous"> <link href="https://cdn.jsdelivr.net/npm/bootstrap@@5.3.8/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-sRIl4kxILFvY47J16cr9ZwB07vP4J8+LH7qKQnuqkuIAvNWLzeN8tE5YBujZqJLB" crossorigin="anonymous">
</head> </head>
<script src="https://cdn.jsdelivr.net/npm/canvas-confetti@@1.6.0/dist/confetti.browser.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@@5.3.8/dist/js/bootstrap.bundle.min.js" integrity="sha384-FKyoEForCGlyvwx9Hj09JcYn3nv7wiPVlz7YYwJrWVcXK/BmnVDxM+D2scQbITxI" crossorigin="anonymous"></script> <script src="https://cdn.jsdelivr.net/npm/bootstrap@@5.3.8/dist/js/bootstrap.bundle.min.js" integrity="sha384-FKyoEForCGlyvwx9Hj09JcYn3nv7wiPVlz7YYwJrWVcXK/BmnVDxM+D2scQbITxI" crossorigin="anonymous"></script>
<script src="https://code.jquery.com/jquery-3.6.0.min.js"></script> <script src="https://code.jquery.com/jquery-3.6.0.min.js"></script>
<script src="@routes.JavaScriptRoutingController.javascriptRoutes()" type="text/javascript"></script> <script src="@routes.JavaScriptRoutingController.javascriptRoutes()" type="text/javascript"></script>

View File

@@ -227,7 +227,7 @@ function receiveLobbyUpdateEvent(eventData) {
const inner = user.self ? `<h5 class="card-title">${user.name} (You)</h5> const inner = user.self ? `<h5 class="card-title">${user.name} (You)</h5>
<a href="#" class="btn btn-danger disabled" aria-disabled="true" tabindex="-1">Remove</a>` <a href="#" class="btn btn-danger disabled" aria-disabled="true" tabindex="-1">Remove</a>`
: ` <h5 class="card-title">${user.name}</h5> : ` <h5 class="card-title">${user.name}</h5>
<div class="btn btn-danger" onclick="removePlayer('${user.id}')">Remove</div>` <div class="btn btn-danger" onclick="handleKickPlayer('${user.id}')">Remove</div>`
newHtml += `<div class="col-auto my-auto m-3"> newHtml += `<div class="col-auto my-auto m-3">
<div class="card" style="width: 18rem;"> <div class="card" style="width: 18rem;">

View File

@@ -73,5 +73,10 @@ function handleTrumpSelection(object) {
} }
function handleKickPlayer(playerId) { function handleKickPlayer(playerId) {
// TODO needs implementation sendEvent("KickPlayer", {
playerId: playerId
})
}
function handleReturnToLobby() {
sendEvent("ReturnToLobby")
} }

View File

@@ -87,6 +87,7 @@ function setupSocketHandlers(socket) {
} else { } else {
console.warn('Connection died unexpectedly.'); console.warn('Connection died unexpectedly.');
} }
location.href = "/mainmenu";
}; };
} }

View File

@@ -1,3 +1,3 @@
MAJOR=4 MAJOR=4
MINOR=4 MINOR=7
PATCH=0 PATCH=3