From c220e54bb8d87f4f0f37a089bcd993e8df806123 Mon Sep 17 00:00:00 2001 From: lq64 Date: Wed, 12 Nov 2025 11:44:21 +0100 Subject: [PATCH 1/2] feat(ui): added js routing, updated ingame ui, added tricktable (#50) This merge request has full JS routing for calling specific endpoints. Game is fully playable but doesn't have polling yet. This version already has the UI changes adressed in MR #43 so first merge MR #43 and then this one or only merge this one because it already has the UI changes :) Co-authored-by: LQ63 Reviewed-on: https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/pulls/50 Reviewed-by: Janis --- .../app/assets/stylesheets/dark-mode.less | 2 +- .../app/assets/stylesheets/light-mode.less | 1 + .../app/assets/stylesheets/main.less | 35 +++- .../app/controllers/IngameController.scala | 106 ++++++++-- .../JavaScriptRoutingController.scala | 26 +++ .../app/controllers/MainMenuController.scala | 23 ++- .../app/views/ingame/ingame.scala.html | 120 +++++++----- .../app/views/lobby/lobby.scala.html | 115 +++++------ knockoutwhistweb/app/views/main.scala.html | 9 +- .../app/views/mainmenu/creategame.scala.html | 6 +- knockoutwhistweb/conf/routes | 5 +- knockoutwhistweb/public/javascripts/main.js | 185 +++++++++++++++++- 12 files changed, 473 insertions(+), 160 deletions(-) create mode 100644 knockoutwhistweb/app/controllers/JavaScriptRoutingController.scala diff --git a/knockoutwhistweb/app/assets/stylesheets/dark-mode.less b/knockoutwhistweb/app/assets/stylesheets/dark-mode.less index 02d2efa..c34aab1 100644 --- a/knockoutwhistweb/app/assets/stylesheets/dark-mode.less +++ b/knockoutwhistweb/app/assets/stylesheets/dark-mode.less @@ -3,7 +3,7 @@ --background-image: url('/assets/images/background.png') !important; --color: #f8f9fa !important; /* Light text on dark bg */ --highlightscolor: rgba(131, 131, 131, 0.75) !important; - + --background-color: #192734; /* Bootstrap variable overrides for dark mode */ --bs-body-color: var(--color); --bs-link-color: #66b2ff; diff --git a/knockoutwhistweb/app/assets/stylesheets/light-mode.less b/knockoutwhistweb/app/assets/stylesheets/light-mode.less index 5975dcc..3054bc2 100644 --- a/knockoutwhistweb/app/assets/stylesheets/light-mode.less +++ b/knockoutwhistweb/app/assets/stylesheets/light-mode.less @@ -2,4 +2,5 @@ --background-image: url('/assets/images/img.png'); --color: black; --highlightscolor: rgba(0, 0, 0, 0.75); + --background-color: rgba(228, 232, 237, 1); } diff --git a/knockoutwhistweb/app/assets/stylesheets/main.less b/knockoutwhistweb/app/assets/stylesheets/main.less index d41bf29..145789b 100644 --- a/knockoutwhistweb/app/assets/stylesheets/main.less +++ b/knockoutwhistweb/app/assets/stylesheets/main.less @@ -14,7 +14,7 @@ --bs-border-color: rgba(0, 0, 0, 0.125) !important; --bs-heading-color: var(--color) !important; } - +@background-color: var(--background-color); @highlightcolor: var(--highlightscolor); @background-image: var(--background-image); @color: var(--color); @@ -24,10 +24,14 @@ } .game-field-background { background-image: @background-image; - background-size: cover; - background-position: center center; - background-repeat: no-repeat; - background-attachment: fixed; + max-width: 1400px; + margin: 0 auto; + min-height: 100vh; +} +.lobby-background { + background-color: @background-color; + width: 100%; + height: 100vh; } .navbar-header{ @@ -45,8 +49,11 @@ .bottom-div { position: fixed; bottom: 0; - left: 0; + left: 50%; + transform: translateX(-50%); + max-width: 1400px; width: 100%; + margin: 0; text-align: center; padding: 10px; } @@ -205,4 +212,20 @@ body { color: @color; font-size: 1.5em; font-family: Arial, serif; +} +.score-table { + background-color: rgba(255, 255, 255, 0.1); + border-radius: 8px; + padding: 10px; + margin-bottom: 20px; + backdrop-filter: blur(8px); + box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1); +} +.score-header { + font-weight: bold; + color: #000000; + border-bottom: 1px solid rgba(255, 255, 255, 0.3); +} +.score-row { + color: #000000; } \ No newline at end of file diff --git a/knockoutwhistweb/app/controllers/IngameController.scala b/knockoutwhistweb/app/controllers/IngameController.scala index 373701d..d808f05 100644 --- a/knockoutwhistweb/app/controllers/IngameController.scala +++ b/knockoutwhistweb/app/controllers/IngameController.scala @@ -6,6 +6,7 @@ import exceptions.{CantPlayCardException, GameFullException, NotEnoughPlayersExc import logic.PodManager import model.sessions.{PlayerSession, UserSession} import play.api.* +import play.api.libs.json.Json import play.api.mvc.* import java.util.UUID @@ -64,30 +65,70 @@ class IngameController @Inject()( } } if (result.isSuccess) { - Redirect(routes.IngameController.game(gameId)) + Ok(Json.obj( + "status" -> "success", + "redirectUrl" -> routes.IngameController.game(gameId).url + )) } else { val throwable = result.failed.get throwable match { case _: NotInThisGameException => - BadRequest(throwable.getMessage) + BadRequest(Json.obj( + "status" -> "failure", + "errorMessage" -> throwable.getMessage + )) case _: NotHostException => - Forbidden(throwable.getMessage) + Forbidden(Json.obj( + "status" -> "failure", + "errorMessage" -> throwable.getMessage + )) case _: NotEnoughPlayersException => - BadRequest(throwable.getMessage) + BadRequest(Json.obj( + "status" -> "failure", + "errorMessage" -> throwable.getMessage + )) case _ => - InternalServerError(throwable.getMessage) + InternalServerError(Json.obj( + "status" -> "failure", + "errorMessage" -> throwable.getMessage + )) } } } - def kickPlayer(gameId: String, playerToKick: UUID): Action[AnyContent] = authAction { implicit request: AuthenticatedRequest[AnyContent] => + def kickPlayer(gameId: String, playerToKick: String): Action[AnyContent] = authAction { implicit request: AuthenticatedRequest[AnyContent] => val game = podManager.getGame(gameId) - game.get.leaveGame(playerToKick) - Redirect(routes.IngameController.game(gameId)) + val playerToKickUUID = UUID.fromString(playerToKick) + val result = Try { + game.get.leaveGame(playerToKickUUID) + } + if(result.isSuccess) { + Ok(Json.obj( + "status" -> "success", + "redirectUrl" -> routes.IngameController.game(gameId).url + )) + } else { + InternalServerError(Json.obj( + "status" -> "failure", + "errorMessage" -> "Something went wrong." + )) + } } def leaveGame(gameId: String): Action[AnyContent] = authAction { implicit request: AuthenticatedRequest[AnyContent] => val game = podManager.getGame(gameId) - game.get.leaveGame(request.user.id) - Redirect(routes.MainMenuController.mainMenu()) + val result = Try { + game.get.leaveGame(request.user.id) + } + if (result.isSuccess) { + Ok(Json.obj( + "status" -> "success", + "redirectUrl" -> routes.MainMenuController.mainMenu().url + )) + } else { + InternalServerError(Json.obj( + "status" -> "failure", + "errorMessage" -> "Something went wrong." + )) + } } def joinGame(gameId: String): Action[AnyContent] = authAction { implicit request: AuthenticatedRequest[AnyContent] => val game = podManager.getGame(gameId) @@ -119,7 +160,10 @@ class IngameController @Inject()( val game = podManager.getGame(gameId) game match { case Some(g) => - val cardIdOpt = request.body.asFormUrlEncoded.flatMap(_.get("cardId").flatMap(_.headOption)) + val jsonBody = request.body.asJson + val cardIdOpt: Option[String] = jsonBody.flatMap { jsValue => + (jsValue \ "cardID").asOpt[String] + } cardIdOpt match { case Some(cardId) => var optSession: Option[UserSession] = None @@ -131,27 +175,51 @@ class IngameController @Inject()( } optSession.foreach(_.lock.unlock()) if (result.isSuccess) { - NoContent + Ok(Json.obj( + "status" -> "success", + "redirectUrl" -> routes.IngameController.game(gameId).url + )) } else { val throwable = result.failed.get throwable match { case _: CantPlayCardException => - BadRequest(throwable.getMessage) + BadRequest(Json.obj( + "status" -> "failure", + "errorMessage" -> throwable.getMessage + )) case _: NotInThisGameException => - BadRequest(throwable.getMessage) + BadRequest(Json.obj( + "status" -> "failure", + "errorMessage" -> throwable.getMessage + )) case _: IllegalArgumentException => - BadRequest(throwable.getMessage) + BadRequest(Json.obj( + "status" -> "failure", + "errorMessage" -> throwable.getMessage + )) case _: IllegalStateException => - BadRequest(throwable.getMessage) + BadRequest(Json.obj( + "status" -> "failure", + "errorMessage" -> throwable.getMessage + )) case _ => - InternalServerError(throwable.getMessage) + InternalServerError(Json.obj( + "status" -> "failure", + "errorMessage" -> throwable.getMessage + )) } } case None => - BadRequest("cardId parameter is missing") + BadRequest(Json.obj( + "status" -> "failure", + "errorMessage" -> "cardId Parameter is missing" + )) } case None => - NotFound("Game not found") + NotFound(Json.obj( + "status" -> "failure", + "errorMessage" -> "Game not found" + )) } } } diff --git a/knockoutwhistweb/app/controllers/JavaScriptRoutingController.scala b/knockoutwhistweb/app/controllers/JavaScriptRoutingController.scala new file mode 100644 index 0000000..2cd5f06 --- /dev/null +++ b/knockoutwhistweb/app/controllers/JavaScriptRoutingController.scala @@ -0,0 +1,26 @@ +package controllers + +import auth.{AuthAction, AuthenticatedRequest} +import logic.PodManager +import play.api.mvc.{Action, AnyContent, BaseController, ControllerComponents} +import play.api.routing.JavaScriptReverseRouter + +import javax.inject.Inject + +class JavaScriptRoutingController @Inject()( + val controllerComponents: ControllerComponents, + val authAction: AuthAction, + val podManager: PodManager + ) extends BaseController { + def javascriptRoutes(): Action[AnyContent] = authAction { implicit request: AuthenticatedRequest[AnyContent] => + Ok( + JavaScriptReverseRouter("jsRoutes")( + routes.javascript.MainMenuController.createGame, + routes.javascript.IngameController.startGame, + routes.javascript.IngameController.kickPlayer, + routes.javascript.IngameController.leaveGame, + routes.javascript.IngameController.playCard + ) + ).as("text/javascript") + } +} diff --git a/knockoutwhistweb/app/controllers/MainMenuController.scala b/knockoutwhistweb/app/controllers/MainMenuController.scala index 55c4a1b..b4c0e4b 100644 --- a/knockoutwhistweb/app/controllers/MainMenuController.scala +++ b/knockoutwhistweb/app/controllers/MainMenuController.scala @@ -3,6 +3,7 @@ package controllers import auth.{AuthAction, AuthenticatedRequest} import logic.PodManager import play.api.* +import play.api.libs.json.Json import play.api.mvc.* import javax.inject.* @@ -29,18 +30,28 @@ class MainMenuController @Inject()( } def createGame(): Action[AnyContent] = authAction { implicit request: AuthenticatedRequest[AnyContent] => - val postData = request.body.asFormUrlEncoded - if (postData.isDefined) { - val gamename = postData.get.get("lobbyname").flatMap(_.headOption).getOrElse(s"${request.user.name}'s Game") - val playeramount = postData.get.get("playeramount").flatMap(_.headOption).getOrElse("") + val jsonBody = request.body.asJson + if (jsonBody.isDefined) { + val gamename: String = (jsonBody.get \ "lobbyname").asOpt[String] + .getOrElse(s"${request.user.name}'s Game") + + val playeramount: String = (jsonBody.get \ "playeramount").asOpt[String] + .getOrElse(throw new IllegalArgumentException("Player amount is required.")) + val gameLobby = podManager.createGame( host = request.user, name = gamename, maxPlayers = playeramount.toInt ) - Redirect(routes.IngameController.game(gameLobby.id)) + Ok(Json.obj( + "status" -> "success", + "redirectUrl" -> routes.IngameController.game(gameLobby.id).url + )) } else { - BadRequest("Invalid form submission") + BadRequest(Json.obj( + "status" -> "failure", + "errorMessage" -> "Invalid form submission" + )) } } diff --git a/knockoutwhistweb/app/views/ingame/ingame.scala.html b/knockoutwhistweb/app/views/ingame/ingame.scala.html index db56c27..271fc28 100644 --- a/knockoutwhistweb/app/views/ingame/ingame.scala.html +++ b/knockoutwhistweb/app/views/ingame/ingame.scala.html @@ -3,68 +3,86 @@ @(player: de.knockoutwhist.player.AbstractPlayer, gamelobby: logic.game.GameLobby) @main("Ingame") { -
+
+
+
-
-
-

Current Player

-

@gamelobby.getLogic.getCurrentPlayer.get.name

- @if(!TrickUtil.isOver(gamelobby.getLogic.getCurrentMatch.get, gamelobby.getLogic.getPlayerQueue.get)) { +
+
+

Current Player

+

@gamelobby.getLogic.getCurrentPlayer.get.name

+ @if(!TrickUtil.isOver(gamelobby.getLogic.getCurrentMatch.get, gamelobby.getLogic.getPlayerQueue.get)) {

Next Player

@for(nextplayer <- gamelobby.getLogic.getPlayerQueue.get.duplicate()) { -

@nextplayer

+

@nextplayer

} - } -
+ } +
-
- @for((cardplayed, player) <- gamelobby.getLogic.getCurrentTrick.get.cards) { -
-
-
- @util.WebUIUtils.cardtoImage(cardplayed) width="100%"/> -
-
- @player +
+ +
+

Tricks Won

+ +
+
PLAYER
+
TRICKS
+
+ + @for(player <- gamelobby.getLogic.getPlayerQueue.get.toList.sortBy { p => + -(gamelobby.getLogic.getCurrentRound.get.tricklist.filter { trick => trick.winner.contains(p) }.size) + }) { +
+
@player.name
+
+ @(gamelobby.getLogic.getCurrentRound.get.tricklist.filter { trick => trick.winner.contains(player) }.size)
+ } +
- } +
+ @for((cardplayed, player) <- gamelobby.getLogic.getCurrentTrick.get.cards) { +
+
+
+ @util.WebUIUtils.cardtoImage(cardplayed) width="100%"/> +
+
+ @player +
+
+
+ } +
+
+
+

Trumpsuit

+

@gamelobby.getLogic.getCurrentRound.get.trumpSuit

+ +
First Card
+
+ @if(gamelobby.getLogic.getCurrentTrick.get.firstCard.isDefined) { + @util.WebUIUtils.cardtoImage(gamelobby.getLogic.getCurrentTrick.get.firstCard.get) width="80px"/> + } else { + @views.html.render.card.apply("images/cards/1B.png")("Blank Card") width="80px"/> + } +
+
-
-

Trumpsuit

-

@gamelobby.getLogic.getCurrentRound.get.trumpSuit

- -
First Card
-
- @if(gamelobby.getLogic.getCurrentTrick.get.firstCard.isDefined) { - @util.WebUIUtils.cardtoImage(gamelobby.getLogic.getCurrentTrick.get.firstCard.get) width="80px"/> - } else { - @views.html.render.card.apply("images/cards/1B.png")("Blank Card") width="80px"/> - } +
+
+ @for(i <- player.currentHand().get.cards.indices) { +
+
+ @util.WebUIUtils.cardtoImage(player.currentHand().get.cards(i)) width="120px" style="border-radius: 6px"/> +
+
+ }
- - -
- -
- -
-
- @for(i <- player.currentHand().get.cards.indices) { -
-
- - -
-
- } -
-
-
+
+
} diff --git a/knockoutwhistweb/app/views/lobby/lobby.scala.html b/knockoutwhistweb/app/views/lobby/lobby.scala.html index 46eabb6..75ba8d4 100644 --- a/knockoutwhistweb/app/views/lobby/lobby.scala.html +++ b/knockoutwhistweb/app/views/lobby/lobby.scala.html @@ -1,82 +1,69 @@ @(user: Option[model.users.User], gamelobby: logic.game.GameLobby) @main("Lobby") { -
-
-
-
-
- Lobby-Name: @gamelobby.name +
+
+
+
+
+
+ Lobby-Name: @gamelobby.name +
+
Exit
-
- -
-
-
-
-
Playeramount: @gamelobby.getPlayers.size / @gamelobby.maxPlayers
+
+
+
Playeramount: @gamelobby.getPlayers.size / @gamelobby.maxPlayers
+
-
-
- - @if((gamelobby.getUserSession(user.get.id).host)) { - @for(playersession <- gamelobby.getPlayers.values) { -
-
+
+ @if((gamelobby.getUserSession(user.get.id).host)) { + @for(playersession <- gamelobby.getPlayers.values) { +
+
+ Profile +
+ @if(playersession.id == user.get.id) { +
@playersession.name (You)
+ @*

Your text could be here!

*@ + Remove + } else { +
@playersession.name
+ @*

Your text could be here!

*@ +
Remove
+ } +
+
+
+ } +
+
Start Game
+
+ } else { + @for(playersession <- gamelobby.getPlayers.values) { +
Profile
@if(playersession.id == user.get.id) { -
@playersession.name (You)
-@*

Your text could be here!

*@ - Remove +
@playersession.name (You)
} else { -
@playersession.name
-@*

Your text could be here!

*@ -
- -
+
@playersession.name
}
-
-
- } -
- -
- } else { - @for(playersession <- gamelobby.getPlayers.values) { -
-
- Profile -
- @if(playersession.id == user.get.id) { -
@playersession.name (You)
- } else { -
@playersession.name
- } -
-
-
- } -
-
-

Waiting for the host to start the game...

-
-
-
-
-
-
- Loading...
+ } + +
+

Waiting for the host to start the game...

+
+ Loading... +
-
- } + } +
-
+
} \ No newline at end of file diff --git a/knockoutwhistweb/app/views/main.scala.html b/knockoutwhistweb/app/views/main.scala.html index daccc3a..e1cc5d1 100644 --- a/knockoutwhistweb/app/views/main.scala.html +++ b/knockoutwhistweb/app/views/main.scala.html @@ -18,16 +18,11 @@ - -
+ @* And here's where we render the `Html` object containing * the page content. *@ @content -
- -
-
- + diff --git a/knockoutwhistweb/app/views/mainmenu/creategame.scala.html b/knockoutwhistweb/app/views/mainmenu/creategame.scala.html index 1e5905c..fd680fd 100644 --- a/knockoutwhistweb/app/views/mainmenu/creategame.scala.html +++ b/knockoutwhistweb/app/views/mainmenu/creategame.scala.html @@ -2,7 +2,7 @@ @main("Create Game") { @navbar(user) -
+
@@ -25,8 +25,8 @@
- +
Create Game
- + } \ No newline at end of file diff --git a/knockoutwhistweb/conf/routes b/knockoutwhistweb/conf/routes index bba5dda..9a1a4a6 100644 --- a/knockoutwhistweb/conf/routes +++ b/knockoutwhistweb/conf/routes @@ -3,7 +3,8 @@ # https://www.playframework.com/documentation/latest/ScalaRouting # ~~~~ - +# For the javascript routing +GET /assets/js/routes controllers.JavaScriptRoutingController.javascriptRoutes() # Primary routes GET / controllers.MainMenuController.index() GET /assets/*file controllers.Assets.versioned(path="/public", file: Asset) @@ -25,6 +26,6 @@ GET /logout controllers.UserController.logout() GET /game/:id controllers.IngameController.game(id: String) GET /game/:id/join controllers.IngameController.joinGame(id: String) GET /game/:id/start controllers.IngameController.startGame(id: String) -POST /game/:id/kickPlayer controllers.IngameController.kickPlayer(id: String, playerId: java.util.UUID) +POST /game/:id/kickPlayer controllers.IngameController.kickPlayer(id: String, playerId: String) GET /game/:id/leaveGame controllers.IngameController.leaveGame(id: String) POST /game/:id/playCard controllers.IngameController.playCard(id: String) \ No newline at end of file diff --git a/knockoutwhistweb/public/javascripts/main.js b/knockoutwhistweb/public/javascripts/main.js index 36ec495..aa8ec29 100644 --- a/knockoutwhistweb/public/javascripts/main.js +++ b/knockoutwhistweb/public/javascripts/main.js @@ -77,4 +77,187 @@ }) }) }) -})() \ No newline at end of file +})() + +function createGameJS() { + let lobbyName = document.getElementById("lobbyname").value; + if (lobbyName === "") { + lobbyName = "DefaultLobby" + } + const playerAmount = document.getElementById("playeramount").value; + const jsonObj = { + lobbyname: lobbyName, + playeramount: playerAmount + } + sendGameCreationRequest(jsonObj); +} + +function sendGameCreationRequest(dataObject) { + const route = jsRoutes.controllers.MainMenuController.createGame(); + + fetch(route.url, { + method: route.type, + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify(dataObject) + }) + .then(response => { + return response.json().then(data => { + if (!response.ok) { + return Promise.reject(data); + } + return data; + }); + }) + .then(data => { + if (data.status === 'success') { + window.location.href = data.redirectUrl; + } + }) + .catch(error => { + if (error && error.errorMessage) { + alert(`${error.errorMessage}`); + } else { + alert('An unexpected error occurred. Please try again.'); + } + }); +} +function startGame(gameId) { + sendGameStartRequest(gameId) +} +function sendGameStartRequest(gameId) { + const route = jsRoutes.controllers.IngameController.startGame(gameId); + + fetch(route.url, { + method: route.type, + }) + .then(response => { + return response.json().then(data => { + if (!response.ok) { + return Promise.reject(data); + } + return data; + }); + }) + .then(data => { + // SUCCESS BLOCK: data is the { status: 'success', ... } object + if (data.status === 'success') { + window.location.href = data.redirectUrl; + } + }) + .catch(error => { + if (error && error.errorMessage) { + alert(`${error.errorMessage}`); + } else { + alert('An unexpected error occurred. Please try again.'); + } + }); +} +function removePlayer(gameid, playersessionId) { + sendRemovePlayerRequest(gameid, playersessionId) +} + +function sendRemovePlayerRequest(gameId, playersessionId) { + const route = jsRoutes.controllers.IngameController.kickPlayer(gameId, playersessionId); + + fetch(route.url, { + method: route.type, + headers: { + 'Content-Type': 'application/json', + } + }) + .then(response => { + return response.json().then(data => { + if (!response.ok) { + return Promise.reject(data); + } + return data; + }); + }) + .then(data => { + // SUCCESS BLOCK: data is the { status: 'success', ... } object + if (data.status === 'success') { + window.location.href = data.redirectUrl; + } + }) + .catch(error => { + if (error && error.errorMessage) { + alert(`${error.errorMessage}`); + } else { + alert('An unexpected error occurred. Please try again.'); + } + }); +} +function leaveGame(gameId) { + sendLeavePlayerRequest(gameId) +} +function sendLeavePlayerRequest(gameId) { + + const route = jsRoutes.controllers.IngameController.leaveGame(gameId); + fetch(route.url, { + method: route.type, + }) + .then(response => { + return response.json().then(data => { + if (!response.ok) { + return Promise.reject(data); + } + return data; + }); + }) + .then(data => { + // SUCCESS BLOCK: data is the { status: 'success', ... } object + if (data.status === 'success') { + window.location.href = data.redirectUrl; + } + }) + .catch(error => { + if (error && error.errorMessage) { + alert(`${error.errorMessage}`); + } else { + alert('An unexpected error occurred. Please try again.'); + } + }); +} + +function handlePlayCard(cardobject, gameId) { + const cardId = cardobject.dataset.cardId; + const jsonObj = { + cardID: cardId + } + sendPlayCardRequest(jsonObj, gameId) +} + +function sendPlayCardRequest(jsonObj, gameId) { + const route = jsRoutes.controllers.IngameController.playCard(gameId); + + fetch(route.url, { + method: route.type, + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify(jsonObj) + }) + .then(response => { + return response.json().then(data => { + if (!response.ok) { + return Promise.reject(data); + } + return data; + }); + }) + .then(data => { + if (data.status === 'success') { + window.location.href = data.redirectUrl; + } + }) + .catch(error => { + if (error && error.errorMessage) { + alert(`${error.errorMessage}`); + } else { + alert('An unexpected error occurred. Please try again.'); + } + }); +} + From 5d245d0011a5fb03193514303b45702cd8329224 Mon Sep 17 00:00:00 2001 From: Janis Date: Thu, 13 Nov 2025 08:20:30 +0100 Subject: [PATCH 2/2] feat(ui): implement tie & trump menu, fixed some critical bugs (#52) Co-authored-by: LQ63 Reviewed-on: https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/pulls/52 Co-authored-by: Janis Co-committed-by: Janis --- knockoutwhist | 2 +- .../app/assets/stylesheets/main.less | 42 ++- .../app/controllers/IngameController.scala | 8 +- .../app/logic/game/GameLobby.scala | 3 + .../app/views/ingame/selecttrump.scala.html | 85 ++++-- .../app/views/ingame/tie.scala.html | 122 +++++++-- knockoutwhistweb/app/views/main.scala.html | 1 + .../app/views/mainmenu/creategame.scala.html | 2 +- .../app/views/mainmenu/navbar.scala.html | 2 +- .../app/views/mainmenu/rules.scala.html | 244 +++++++++++++----- knockoutwhistweb/conf/routes | 5 + 11 files changed, 398 insertions(+), 118 deletions(-) diff --git a/knockoutwhist b/knockoutwhist index b9a7b0a..5aa1cef 160000 --- a/knockoutwhist +++ b/knockoutwhist @@ -1 +1 @@ -Subproject commit b9a7b0a2af7cef7225bf1a0388ebf58171a173f2 +Subproject commit 5aa1cef35689d2df8a89e2d8864fc5fcf9c30e33 diff --git a/knockoutwhistweb/app/assets/stylesheets/main.less b/knockoutwhistweb/app/assets/stylesheets/main.less index 145789b..447f7c9 100644 --- a/knockoutwhistweb/app/assets/stylesheets/main.less +++ b/knockoutwhistweb/app/assets/stylesheets/main.less @@ -33,6 +33,10 @@ width: 100%; height: 100vh; } +.lobby-background { + background-color: @background-color; + +} .navbar-header{ text-align:center; @@ -78,6 +82,10 @@ body { overflow: auto; } +.navbar-drop-shadow { + box-shadow: 0 1px 15px 0 #000000 +} + #sessions { display: flex; flex-direction: column; @@ -228,4 +236,36 @@ body { } .score-row { color: #000000; -} \ No newline at end of file +} + +/* In-game centered stage and blurred sides overlay */ +.ingame-stage { + min-height: 100vh; + display: flex; + align-items: center; + justify-content: center; + padding: 2rem 1rem; +} + +/* Wrapper that adds a backdrop blur to the background outside the centered card */ +.blur-sides { + position: relative; +} + +/* Create an overlay that blurs everything behind it, except the central content area */ +.blur-sides::before { + content: ""; + position: fixed; + inset: 0; + pointer-events: none; + /* fallback: subtle vignette if backdrop-filter unsupported */ + background: radial-gradient(ellipse at center, rgba(0,0,0,0) 30%, rgba(0,0,0,0.35) 100%); +} + +@supports ((-webkit-backdrop-filter: blur(8px)) or (backdrop-filter: blur(8px))) { + .blur-sides::before { + background: rgba(0,0,0,0.08); + -webkit-backdrop-filter: blur(10px) saturate(110%); + backdrop-filter: blur(10px) saturate(110%); + } +} diff --git a/knockoutwhistweb/app/controllers/IngameController.scala b/knockoutwhistweb/app/controllers/IngameController.scala index d808f05..6f301b8 100644 --- a/knockoutwhistweb/app/controllers/IngameController.scala +++ b/knockoutwhistweb/app/controllers/IngameController.scala @@ -39,12 +39,14 @@ class IngameController @Inject()( case SelectTrump => Ok(views.html.ingame.selecttrump( g.getPlayerByUser(request.user), - g.logic + g.logic, + gameId )) case TieBreak => Ok(views.html.ingame.tie( g.getPlayerByUser(request.user), - g.logic + g.logic, + gameId )) case _ => InternalServerError(s"Invalid game state for in-game view. GameId: $gameId" + s" State: ${g.logic.getCurrentState}") @@ -318,7 +320,7 @@ class IngameController @Inject()( val session = g.getUserSession(request.user.id) optSession = Some(session) session.lock.lock() - g.selectTie(g.getUserSession(request.user.id), tie.toInt) + g.selectTie(g.getUserSession(request.user.id), tie.toInt - 1) } optSession.foreach(_.lock.unlock()) if (result.isSuccess) { diff --git a/knockoutwhistweb/app/logic/game/GameLobby.scala b/knockoutwhistweb/app/logic/game/GameLobby.scala index 486bf4f..0e8ac9f 100644 --- a/knockoutwhistweb/app/logic/game/GameLobby.scala +++ b/knockoutwhistweb/app/logic/game/GameLobby.scala @@ -157,6 +157,9 @@ class GameLobby private( */ def selectTie(userSession: UserSession, tieNumber: Int): Unit = { val player = getPlayerInteractable(userSession, InteractionType.TieChoice) + val highestNumber = logic.playerTieLogic.highestAllowedNumber() + if (tieNumber < 0 || tieNumber > highestNumber) + throw new IllegalArgumentException(s"Selected number $tieNumber is out of allowed range (0 to $highestNumber)") userSession.resetCanInteract() logic.playerTieLogic.receivedTieBreakerCard(tieNumber) } diff --git a/knockoutwhistweb/app/views/ingame/selecttrump.scala.html b/knockoutwhistweb/app/views/ingame/selecttrump.scala.html index 8aef1e3..82c58c2 100644 --- a/knockoutwhistweb/app/views/ingame/selecttrump.scala.html +++ b/knockoutwhistweb/app/views/ingame/selecttrump.scala.html @@ -1,27 +1,72 @@ -@(player: de.knockoutwhist.player.AbstractPlayer, logic: de.knockoutwhist.control.GameLogic) +@(player: de.knockoutwhist.player.AbstractPlayer, logic: de.knockoutwhist.control.GameLogic, gameId: String) @main("Selecting Trumpsuit...") {
- @if(player.equals(logic.getCurrentMatch.get.roundlist.last.winner.get)) { -

Knockout Whist

-

You (@player.toString) have won the last round! Select a trumpsuit for the next round!

-

Available trumpsuits are displayed below:

-
- @util.WebUIUtils.cardtoImage(de.knockoutwhist.cards.Card(de.knockoutwhist.cards.CardValue.Ace, de.knockoutwhist.cards.Suit.Spades)) - @util.WebUIUtils.cardtoImage(de.knockoutwhist.cards.Card(de.knockoutwhist.cards.CardValue.Ace, de.knockoutwhist.cards.Suit.Clubs)) - @util.WebUIUtils.cardtoImage(de.knockoutwhist.cards.Card(de.knockoutwhist.cards.CardValue.Ace, de.knockoutwhist.cards.Suit.Hearts)) - @util.WebUIUtils.cardtoImage(de.knockoutwhist.cards.Card(de.knockoutwhist.cards.CardValue.Ace, de.knockoutwhist.cards.Suit.Diamonds)) -
-

Your cards

+
+
+
+
+
+
+

Select Trump Suit

+
+
+ @if(player.equals(logic.getCurrentMatch.get.roundlist.last.winner.get)) { + -
- @for(card <- player.currentHand().get.cards) { - @util.WebUIUtils.cardtoImage(card) - } +
+
+
+ + +
+
+
+
+ + +
+
+
+
+ + +
+
+
+
+ + +
+
+
+
+ @for(i <- player.currentHand().get.cards.indices) { +
+ @util.WebUIUtils.cardtoImage(player.currentHand().get.cards(i)) width="120px" style="border-radius: 6px"/> +
+ } +
+ } else { + + } +
+
+
+
- } else { -

Knockout Whist

-

@player.toString is choosing a trumpsuit. Starting new round when @player.toString picked a trumpsuit...

- } +
} \ No newline at end of file diff --git a/knockoutwhistweb/app/views/ingame/tie.scala.html b/knockoutwhistweb/app/views/ingame/tie.scala.html index 505c68d..7aad171 100644 --- a/knockoutwhistweb/app/views/ingame/tie.scala.html +++ b/knockoutwhistweb/app/views/ingame/tie.scala.html @@ -1,27 +1,107 @@ -@(player: de.knockoutwhist.player.AbstractPlayer, logic: de.knockoutwhist.control.GameLogic) +@(player: de.knockoutwhist.player.AbstractPlayer, logic: de.knockoutwhist.control.GameLogic, gameId: String) @main("Tie") {
-

Knockout Whist

-

The last Round was tied between - @for(players <- logic.playerTieLogic.getTiedPlayers) { - @players - } -

- @if(player.equals(logic.playerTieLogic.currentTiePlayer())) { -

Pick a card between 1 and @logic.playerTieLogic.highestAllowedNumber()! The resulting card will be your card for the cut.

- } else { -

@logic.playerTieLogic.currentTiePlayer() is currently picking his number for the cut.

-

Currently picked Cards:

-
- @for((player, card) <- logic.playerTieLogic.getSelectedCard) { -
-

@player

- @util.WebUIUtils.cardtoImage(card) -
- } -
- } +
+
+
+
+
+
+

Tie Break

+
+
+
+

+ The last round was tied between: + + @for(players <- logic.playerTieLogic.getTiedPlayers) { + @players + } + +

+
+ @if(player.equals(logic.playerTieLogic.currentTiePlayer())) { + @defining(logic.playerTieLogic.highestAllowedNumber()) { maxNum => + + +
+
+ +
+
+ +
+
+ +
+
+
Currently Picked Cards
+ +
+ @if(logic.playerTieLogic.getSelectedCard.nonEmpty) { + @for((player, card) <- logic.playerTieLogic.getSelectedCard) { +
+
+
+

@player

+
+ @util.WebUIUtils.cardtoImage(card) +
+
+
+
+ } + } else { +
+ +
+ } +
+ } + } else { + + +
Currently Picked Cards
+ +
+ @if(logic.playerTieLogic.getSelectedCard.nonEmpty) { + @for((player, card) <- logic.playerTieLogic.getSelectedCard) { +
+
+
+

@player

+
+ @util.WebUIUtils.cardtoImage(card) +
+
+
+
+ } + } else { +
+ +
+ } +
+ } + +
+
+
+
+
+
} \ No newline at end of file diff --git a/knockoutwhistweb/app/views/main.scala.html b/knockoutwhistweb/app/views/main.scala.html index e1cc5d1..50f3d41 100644 --- a/knockoutwhistweb/app/views/main.scala.html +++ b/knockoutwhistweb/app/views/main.scala.html @@ -22,6 +22,7 @@ @* And here's where we render the `Html` object containing * the page content. *@ @content + diff --git a/knockoutwhistweb/app/views/mainmenu/creategame.scala.html b/knockoutwhistweb/app/views/mainmenu/creategame.scala.html index fd680fd..4daeb42 100644 --- a/knockoutwhistweb/app/views/mainmenu/creategame.scala.html +++ b/knockoutwhistweb/app/views/mainmenu/creategame.scala.html @@ -3,7 +3,7 @@ @main("Create Game") { @navbar(user)
-
+
diff --git a/knockoutwhistweb/app/views/mainmenu/navbar.scala.html b/knockoutwhistweb/app/views/mainmenu/navbar.scala.html index 5d21070..57d8d41 100644 --- a/knockoutwhistweb/app/views/mainmenu/navbar.scala.html +++ b/knockoutwhistweb/app/views/mainmenu/navbar.scala.html @@ -1,6 +1,6 @@ @(user: Option[model.users.User])
+} diff --git a/knockoutwhistweb/conf/routes b/knockoutwhistweb/conf/routes index 9a1a4a6..90a4542 100644 --- a/knockoutwhistweb/conf/routes +++ b/knockoutwhistweb/conf/routes @@ -27,5 +27,10 @@ GET /game/:id controllers.IngameController.game(id: String GET /game/:id/join controllers.IngameController.joinGame(id: String) GET /game/:id/start controllers.IngameController.startGame(id: String) POST /game/:id/kickPlayer controllers.IngameController.kickPlayer(id: String, playerId: String) + +POST /game/:id/trump controllers.IngameController.playTrump(id: String) +POST /game/:id/tie controllers.IngameController.playTie(id: String) + +POST /game/:id/kickPlayer controllers.IngameController.kickPlayer(id: String, playerId: java.util.UUID) GET /game/:id/leaveGame controllers.IngameController.leaveGame(id: String) POST /game/:id/playCard controllers.IngameController.playCard(id: String) \ No newline at end of file