Compare commits
8 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
dcb5d7373f | ||
| d57e6efa98 | |||
|
|
4156e1c9ce | ||
| 358556612e | |||
|
|
7f82d2eeae | ||
| a55f0b4b61 | |||
|
|
f115c03ecb | ||
| fd2467a9ea |
20
CHANGELOG.md
20
CHANGELOG.md
@@ -177,3 +177,23 @@
|
||||
### 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))
|
||||
|
||||
@@ -20,6 +20,7 @@ import play.api.libs.json.{JsObject, Json}
|
||||
import java.util.{Timer, TimerTask, UUID}
|
||||
import scala.collection.mutable
|
||||
import scala.collection.mutable.ListBuffer
|
||||
import scala.util.Try
|
||||
|
||||
class GameLobby private(
|
||||
val logic: GameLogic,
|
||||
@@ -277,6 +278,37 @@ class GameLobby private(
|
||||
def getUsers: Set[User] = {
|
||||
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 = {
|
||||
users.values.foreach(session => {
|
||||
|
||||
@@ -73,6 +73,8 @@ class UserSession(val user: User, val host: Boolean, val gameLobby: GameLobby) e
|
||||
case None =>
|
||||
println("Player ID not found or is not a valid UUID.")
|
||||
}
|
||||
case "ReturnToLobby" =>
|
||||
gameLobby.returnToLobby(this)
|
||||
}
|
||||
}
|
||||
lock.unlock()
|
||||
|
||||
@@ -1,38 +1,108 @@
|
||||
@(user: Option[model.users.User], gamelobby: logic.game.GameLobby)
|
||||
|
||||
<main class="lobby-background vh-100" id="lobbybackground">
|
||||
<div class="container d-flex flex-column" style="height: calc(100vh - 1rem);">
|
||||
<div class="row">
|
||||
<div class="col">
|
||||
<div class="p-3 text-center fs-4">Winner: @gamelobby.getLogic.getWinner</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row justify-content-center align-items-center flex-grow-1">
|
||||
@if((gamelobby.getUserSession(user.get.id).host)) {
|
||||
<div class="col-12 text-center mb-5">
|
||||
<div class="btn btn-success" onclick="backToLobby('@gamelobby.id')">Return to lobby</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 class="lobby-background vh-100 d-flex align-items-start justify-content-center pt-5">
|
||||
<div class="container py-5">
|
||||
<div class="row justify-content-center">
|
||||
<div class="col-lg-8 col-xl-6">
|
||||
<div class="card shadow-lg mb-5 text-center bg-white border-0 rounded-4">
|
||||
<div class="card-body p-4 p-md-5">
|
||||
<h1 class="card-title display-5 fw-bold text-success mb-3">Match Over!</h1>
|
||||
<p class="fs-4 text-muted">Congratulations to the winner:</p>
|
||||
<h2 class="display-3 fw-bolder mb-4 text-primary">
|
||||
@gamelobby.getLogic.getWinner.get.name
|
||||
</h2>
|
||||
</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>
|
||||
</main>
|
||||
</div>
|
||||
<script>
|
||||
function waitForFunction(name, checkInterval = 100) {
|
||||
return new Promise(resolve => {
|
||||
const timer = setInterval(() => {
|
||||
if (typeof window[name] === "function") {
|
||||
clearInterval(timer);
|
||||
resolve(window[name]);
|
||||
}
|
||||
}, checkInterval);
|
||||
});
|
||||
function fireConfetti() {
|
||||
let duration = 3 * 1000;
|
||||
let animationEnd = Date.now() + duration;
|
||||
let defaults = { startVelocity: 30, spread: 360, ticks: 60, zIndex: 0 };
|
||||
|
||||
function randomInRange(min, max) {
|
||||
return Math.random() * (max - min) + min;
|
||||
}
|
||||
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>
|
||||
@@ -130,17 +130,6 @@
|
||||
</main>
|
||||
</div>
|
||||
<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()
|
||||
canPlayCard = @gamelobby.logic.getCurrentPlayer.contains(player);
|
||||
</script>
|
||||
|
||||
@@ -64,16 +64,5 @@
|
||||
</div>
|
||||
</div>
|
||||
<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()
|
||||
</script>
|
||||
@@ -110,16 +110,5 @@
|
||||
</div>
|
||||
</div>
|
||||
<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()
|
||||
</script>
|
||||
@@ -98,18 +98,5 @@
|
||||
</div>
|
||||
</main>
|
||||
<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()
|
||||
</script>
|
||||
@@ -17,7 +17,7 @@
|
||||
<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">
|
||||
</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://code.jquery.com/jquery-3.6.0.min.js"></script>
|
||||
<script src="@routes.JavaScriptRoutingController.javascriptRoutes()" type="text/javascript"></script>
|
||||
|
||||
@@ -77,3 +77,6 @@ function handleKickPlayer(playerId) {
|
||||
playerId: playerId
|
||||
})
|
||||
}
|
||||
function handleReturnToLobby() {
|
||||
sendEvent("ReturnToLobby")
|
||||
}
|
||||
@@ -87,6 +87,7 @@ function setupSocketHandlers(socket) {
|
||||
} else {
|
||||
console.warn('Connection died unexpectedly.');
|
||||
}
|
||||
location.href = "/mainmenu";
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
MAJOR=4
|
||||
MINOR=5
|
||||
MINOR=7
|
||||
PATCH=0
|
||||
|
||||
Reference in New Issue
Block a user