From 6b760ccb075850f24213deabf7fd7b621f708c24 Mon Sep 17 00:00:00 2001 From: Janis Date: Wed, 19 Nov 2025 22:37:48 +0100 Subject: [PATCH] feat(game)!: Add winner display and return to lobby functionality --- knockoutwhist | 2 +- .../app/assets/stylesheets/main.less | 10 + .../app/controllers/IngameController.scala | 162 ++++++++------ .../JavaScriptRoutingController.scala | 10 +- .../app/controllers/MainMenuController.scala | 35 +++- .../app/controllers/PollingController.scala | 14 +- .../app/controllers/UserController.scala | 26 ++- .../app/logic/game/GameLobby.scala | 13 +- .../app/logic/game/PollingEvents.scala | 1 + .../app/views/ingame/finishedMatch.scala.html | 37 ++++ .../app/views/ingame/ingame.scala.html | 33 +-- .../app/views/ingame/selecttrump.scala.html | 51 +++-- .../app/views/ingame/tie.scala.html | 22 +- .../app/views/lobby/lobby.scala.html | 102 ++++----- .../app/views/login/login.scala.html | 75 ++++--- .../app/views/mainmenu/creategame.scala.html | 4 +- .../app/views/mainmenu/navbar.scala.html | 4 +- knockoutwhistweb/conf/routes | 3 +- knockoutwhistweb/public/javascripts/main.js | 197 +++++++++++++++++- 19 files changed, 562 insertions(+), 239 deletions(-) create mode 100644 knockoutwhistweb/app/views/ingame/finishedMatch.scala.html diff --git a/knockoutwhist b/knockoutwhist index a5dcf3e..372f20c 160000 --- a/knockoutwhist +++ b/knockoutwhist @@ -1 +1 @@ -Subproject commit a5dcf3ee904ab548479e23ca7b146df14a835b80 +Subproject commit 372f20ca6c3308dee21d9fc946689a8fd77cb465 diff --git a/knockoutwhistweb/app/assets/stylesheets/main.less b/knockoutwhistweb/app/assets/stylesheets/main.less index b726d46..b9713a8 100644 --- a/knockoutwhistweb/app/assets/stylesheets/main.less +++ b/knockoutwhistweb/app/assets/stylesheets/main.less @@ -49,6 +49,16 @@ box-shadow: 3px 3px 3px @highlightcolor; } +.inactive::after { + content: ""; + position: absolute; + inset: 0; /* cover the whole container */ + background: rgba(0, 0, 0, 0.50); + z-index: 10; + border-radius: 6px; + pointer-events: none; /* user can't click through overlay */ +} + .bottom-div { position: fixed; bottom: 0; diff --git a/knockoutwhistweb/app/controllers/IngameController.scala b/knockoutwhistweb/app/controllers/IngameController.scala index 83cc988..7a7afad 100644 --- a/knockoutwhistweb/app/controllers/IngameController.scala +++ b/knockoutwhistweb/app/controllers/IngameController.scala @@ -1,14 +1,16 @@ package controllers import auth.{AuthAction, AuthenticatedRequest} -import de.knockoutwhist.control.GameState.{InGame, Lobby, SelectTrump, TieBreak} +import de.knockoutwhist.control.GameState.{FinishedMatch, InGame, Lobby, SelectTrump, TieBreak} import exceptions.* import logic.PodManager import logic.game.GameLobby import model.sessions.UserSession +import model.users.User import play.api.* import play.api.libs.json.{JsValue, Json} import play.api.mvc.* +import play.twirl.api.Html import java.util.UUID import javax.inject.* @@ -22,29 +24,47 @@ class IngameController @Inject() ( val authAction: AuthAction, implicit val ec: ExecutionContext ) extends AbstractController(cc) { + + def returnInnerHTML(gameLobby: GameLobby, user: User): Html = { + gameLobby.logic.getCurrentState match { + case Lobby => views.html.lobby.lobby(Some(user), gameLobby) + case InGame => + views.html.ingame.ingame( + gameLobby.getPlayerByUser(user), + gameLobby + ) + case SelectTrump => + views.html.ingame.selecttrump( + gameLobby.getPlayerByUser(user), + gameLobby + ) + case TieBreak => + views.html.ingame.tie( + gameLobby.getPlayerByUser(user), + gameLobby + ) + case FinishedMatch => + views.html.ingame.finishedMatch( + Some(user), + gameLobby + ) + case _ => + throw new IllegalStateException(s"Invalid game state for in-game view. GameId: ${gameLobby.id}" + s" State: ${gameLobby.logic.getCurrentState}") + } + } + def game(gameId: String): Action[AnyContent] = authAction { implicit request: AuthenticatedRequest[AnyContent] => val game = podManager.getGame(gameId) game match { case Some(g) => - g.logic.getCurrentState match { - case Lobby => Ok(views.html.lobby.lobby(Some(request.user), g)) - case InGame => - Ok(views.html.ingame.ingame( - g.getPlayerByUser(request.user), - g - )) - case SelectTrump => - Ok(views.html.ingame.selecttrump( - g.getPlayerByUser(request.user), - g - )) - case TieBreak => - Ok(views.html.ingame.tie( - g.getPlayerByUser(request.user), - g - )) - case _ => - InternalServerError(s"Invalid game state for in-game view. GameId: $gameId" + s" State: ${g.logic.getCurrentState}") + val results = Try { + returnInnerHTML(g, request.user) + + } + if (results.isSuccess) { + Ok(views.html.main("In-Game - Knockout Whist")(results.get)) + } else { + InternalServerError(results.failed.get.getMessage) } case None => NotFound("Game not found") @@ -126,32 +146,7 @@ class IngameController @Inject() ( )) } } - def joinGame(gameId: String): Action[AnyContent] = authAction { implicit request: AuthenticatedRequest[AnyContent] => - val game = podManager.getGame(gameId) - val result = Try { - game match { - case Some(g) => - g.addUser(request.user) - case None => - NotFound("Game not found") - } - } - if (result.isSuccess) { - Redirect(routes.IngameController.game(gameId)) - } else { - val throwable = result.failed.get - throwable match { - case _: GameFullException => - BadRequest(throwable.getMessage) - case _: IllegalArgumentException => - BadRequest(throwable.getMessage) - case _: IllegalStateException => - BadRequest(throwable.getMessage) - case _ => - InternalServerError(throwable.getMessage) - } - } - } + def playCard(gameId: String): Action[AnyContent] = authAction { implicit request: AuthenticatedRequest[AnyContent] => { val game = podManager.getGame(gameId) game match { @@ -255,15 +250,30 @@ class IngameController @Inject() ( 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 + )) } } } @@ -276,7 +286,10 @@ class IngameController @Inject() ( val game = podManager.getGame(gameId) game match { case Some(g) => - val trumpOpt = request.body.asFormUrlEncoded.flatMap(_.get("trump").flatMap(_.headOption)) + val jsonBody = request.body.asJson + val trumpOpt: Option[String] = jsonBody.flatMap { jsValue => + (jsValue \ "trump").asOpt[String] + } trumpOpt match { case Some(trump) => var optSession: Option[UserSession] = None @@ -293,13 +306,25 @@ class IngameController @Inject() ( val throwable = result.failed.get throwable match { case _: IllegalArgumentException => - 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 _: 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 => @@ -313,7 +338,10 @@ class IngameController @Inject() ( val game = podManager.getGame(gameId) game match { case Some(g) => - val tieOpt = request.body.asFormUrlEncoded.flatMap(_.get("tie").flatMap(_.headOption)) + val jsonBody = request.body.asJson + val tieOpt: Option[String] = jsonBody.flatMap { jsValue => + (jsValue \ "tie").asOpt[String] + } tieOpt match { case Some(tie) => var optSession: Option[UserSession] = None @@ -330,13 +358,25 @@ class IngameController @Inject() ( val throwable = result.failed.get throwable match { case _: IllegalArgumentException => - 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 _: 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 => diff --git a/knockoutwhistweb/app/controllers/JavaScriptRoutingController.scala b/knockoutwhistweb/app/controllers/JavaScriptRoutingController.scala index 92beeab..b2869d6 100644 --- a/knockoutwhistweb/app/controllers/JavaScriptRoutingController.scala +++ b/knockoutwhistweb/app/controllers/JavaScriptRoutingController.scala @@ -12,16 +12,22 @@ class JavaScriptRoutingController @Inject()( val authAction: AuthAction, val podManager: PodManager ) extends BaseController { - def javascriptRoutes(): Action[AnyContent] = authAction { implicit request: AuthenticatedRequest[AnyContent] => + def javascriptRoutes(): Action[AnyContent] = + Action { implicit request => Ok( JavaScriptReverseRouter("jsRoutes")( routes.javascript.MainMenuController.createGame, routes.javascript.IngameController.startGame, + routes.javascript.MainMenuController.joinGame, routes.javascript.IngameController.kickPlayer, routes.javascript.IngameController.leaveGame, routes.javascript.IngameController.playCard, routes.javascript.IngameController.playDogCard, - routes.javascript.PollingController.polling + routes.javascript.IngameController.playTrump, + routes.javascript.IngameController.playTie, + routes.javascript.IngameController.returnToLobby, + routes.javascript.PollingController.polling, + routes.javascript.UserController.login_Post ) ).as("text/javascript") } diff --git a/knockoutwhistweb/app/controllers/MainMenuController.scala b/knockoutwhistweb/app/controllers/MainMenuController.scala index b4c0e4b..bf41033 100644 --- a/knockoutwhistweb/app/controllers/MainMenuController.scala +++ b/knockoutwhistweb/app/controllers/MainMenuController.scala @@ -17,12 +17,13 @@ import javax.inject.* class MainMenuController @Inject()( val controllerComponents: ControllerComponents, val authAction: AuthAction, - val podManager: PodManager + val podManager: PodManager, + val ingameController: IngameController ) extends BaseController { // Pass the request-handling function directly to authAction (no nested Action) def mainMenu(): Action[AnyContent] = authAction { implicit request: AuthenticatedRequest[AnyContent] => - Ok(views.html.mainmenu.creategame(Some(request.user))) + Ok(views.html.main("KnockOutWhist")(views.html.mainmenu.creategame(Some(request.user)))) } def index(): Action[AnyContent] = authAction { implicit request: AuthenticatedRequest[AnyContent] => @@ -45,7 +46,8 @@ class MainMenuController @Inject()( ) Ok(Json.obj( "status" -> "success", - "redirectUrl" -> routes.IngameController.game(gameLobby.id).url + "redirectUrl" -> routes.IngameController.game(gameLobby.id).url, + "content" -> ingameController.returnInnerHTML(gameLobby, request.user).toString )) } else { BadRequest(Json.obj( @@ -57,18 +59,31 @@ class MainMenuController @Inject()( } def joinGame(): Action[AnyContent] = authAction { implicit request: AuthenticatedRequest[AnyContent] => - val postData = request.body.asFormUrlEncoded - if (postData.isDefined) { - val gameId = postData.get.get("gameId").flatMap(_.headOption).getOrElse("") - val game = podManager.getGame(gameId) + val jsonBody = request.body.asJson + val gameId: Option[String] = jsonBody.flatMap { jsValue => + (jsValue \ "gameId").asOpt[String] + } + if (gameId.isDefined) { + val game = podManager.getGame(gameId.get) game match { case Some(g) => - Redirect(routes.IngameController.joinGame(gameId)) + g.addUser(request.user) + Ok(Json.obj( + "status" -> "success", + "redirectUrl" -> routes.IngameController.game(g.id).url, + "content" -> ingameController.returnInnerHTML(g, request.user).toString + )) case None => - NotFound("Game not found") + NotFound(Json.obj( + "status" -> "failure", + "errorMessage" -> "No Game found" + )) } } else { - BadRequest("Invalid form submission") + BadRequest(Json.obj( + "status" -> "failure", + "errorMessage" -> "Invalid form submission" + )) } } diff --git a/knockoutwhistweb/app/controllers/PollingController.scala b/knockoutwhistweb/app/controllers/PollingController.scala index bce96b2..360f2e7 100644 --- a/knockoutwhistweb/app/controllers/PollingController.scala +++ b/knockoutwhistweb/app/controllers/PollingController.scala @@ -6,7 +6,7 @@ import de.knockoutwhist.cards.Hand import de.knockoutwhist.player.AbstractPlayer import logic.PodManager import logic.game.{GameLobby, PollingEvents} -import logic.game.PollingEvents.{CardPlayed, LobbyCreation, LobbyUpdate, NewRound, ReloadEvent} +import logic.game.PollingEvents.{CardPlayed, LobbyCreation, LobbyUpdate, NewRound, NewTrick, ReloadEvent} import model.sessions.UserSession import model.users.User import play.api.libs.json.{JsArray, JsValue, Json} @@ -26,6 +26,7 @@ class PollingController @Inject() ( val cc: ControllerComponents, val podManager: PodManager, val authAction: AuthAction, + val ingameController: IngameController, implicit val ec: ExecutionContext ) extends AbstractController(cc) { @@ -64,7 +65,8 @@ class PollingController @Inject() ( "trickCards" -> trickCardsJson, "scoreTable" -> scoreTableJson, "firstCardId" -> firstCardId, - "nextPlayer" -> nextPlayer + "nextPlayer" -> nextPlayer, + "yourTurn" -> (game.logic.getCurrentPlayer.get == player) ) } @@ -88,6 +90,11 @@ class PollingController @Inject() ( val hand = player.currentHand() val jsonResponse = buildCardPlayResponse(game, hand, player, true) Ok(jsonResponse) + case NewTrick => + val player = game.getPlayerByUser(userSession.user) + val hand = player.currentHand() + val jsonResponse = buildCardPlayResponse(game, hand, player, false) + Ok(jsonResponse) case CardPlayed => val player = game.getPlayerByUser(userSession.user) val hand = player.currentHand() @@ -98,7 +105,8 @@ class PollingController @Inject() ( case ReloadEvent => val jsonResponse = Json.obj( "status" -> "reloadEvent", - "redirectUrl" -> routes.IngameController.game(game.id).url + "redirectUrl" -> routes.IngameController.game(game.id).url, + "content" -> ingameController.returnInnerHTML(game, userSession.user).toString ) Ok(jsonResponse) } diff --git a/knockoutwhistweb/app/controllers/UserController.scala b/knockoutwhistweb/app/controllers/UserController.scala index d3e5ebf..7cd46de 100644 --- a/knockoutwhistweb/app/controllers/UserController.scala +++ b/knockoutwhistweb/app/controllers/UserController.scala @@ -3,6 +3,7 @@ package controllers import auth.{AuthAction, AuthenticatedRequest} import logic.user.{SessionManager, UserManager} import play.api.* +import play.api.libs.json.Json import play.api.mvc.* import javax.inject.* @@ -28,28 +29,35 @@ class UserController @Inject()( if (possibleUser.isDefined) { Redirect(routes.MainMenuController.mainMenu()) } else { - Ok(views.html.login.login()) + Ok(views.html.main("Login")(views.html.login.login())) } } else { - Ok(views.html.login.login()) + Ok(views.html.main("Login")(views.html.login.login())) } } } def login_Post(): Action[AnyContent] = { Action { implicit request => - val postData = request.body.asFormUrlEncoded - if (postData.isDefined) { + val jsonBody = request.body.asJson + val username: Option[String] = jsonBody.flatMap { jsValue => + (jsValue \ "username").asOpt[String] + } + val password: Option[String] = jsonBody.flatMap { jsValue => + (jsValue \ "password").asOpt[String] + } + if (username.isDefined && password.isDefined) { // Extract username and password from form data - val username = postData.get.get("username").flatMap(_.headOption).getOrElse("") - val password = postData.get.get("password").flatMap(_.headOption).getOrElse("") - val possibleUser = userManager.authenticate(username, password) + val possibleUser = userManager.authenticate(username.get, password.get) if (possibleUser.isDefined) { - Redirect(routes.MainMenuController.mainMenu()).withCookies( + Ok(Json.obj( + "status" -> "success", + "redirectUrl" -> routes.MainMenuController.mainMenu().url, + "content" -> views.html.mainmenu.creategame(possibleUser).toString + )).withCookies( Cookie("sessionId", sessionManager.createSession(possibleUser.get)) ) } else { - println("Failed login attempt for user: " + username) Unauthorized("Invalid username or password") } } else { diff --git a/knockoutwhistweb/app/logic/game/GameLobby.scala b/knockoutwhistweb/app/logic/game/GameLobby.scala index 418f685..c894af5 100644 --- a/knockoutwhistweb/app/logic/game/GameLobby.scala +++ b/knockoutwhistweb/app/logic/game/GameLobby.scala @@ -4,14 +4,15 @@ import de.knockoutwhist.cards.{Hand, Suit} import de.knockoutwhist.control.GameLogic import de.knockoutwhist.control.GameState.{InGame, Lobby, MainMenu} import de.knockoutwhist.control.controllerBaseImpl.sublogic.util.{MatchUtil, PlayerUtil} -import de.knockoutwhist.events.global.{CardPlayedEvent, GameStateChangeEvent, SessionClosed} +import de.knockoutwhist.events.global.tie.TieTurnEvent +import de.knockoutwhist.events.global.{CardPlayedEvent, GameStateChangeEvent, NewTrickEvent, SessionClosed} import de.knockoutwhist.events.player.{PlayerEvent, ReceivedHandEvent} import de.knockoutwhist.player.Playertype.HUMAN import de.knockoutwhist.player.{AbstractPlayer, PlayerFactory} import de.knockoutwhist.rounds.{Match, Round, Trick} import de.knockoutwhist.utils.events.{EventListener, SimpleEvent} import exceptions.* -import logic.game.PollingEvents.{CardPlayed, LobbyCreation, LobbyUpdate, NewRound, ReloadEvent} +import logic.game.PollingEvents.{CardPlayed, LobbyCreation, LobbyUpdate, NewRound, NewTrick, ReloadEvent} import model.sessions.{InteractionType, UserSession} import model.users.User @@ -82,8 +83,13 @@ class GameLobby private( users.get(event.playerId).foreach(session => session.updatePlayer(event)) case event: CardPlayedEvent => addToQueue(CardPlayed) + case event: TieTurnEvent => + addToQueue(ReloadEvent) + users.get(event.player.id).foreach(session => session.updatePlayer(event)) case event: PlayerEvent => users.get(event.playerId).foreach(session => session.updatePlayer(event)) + case event: NewTrickEvent => + addToQueue(NewTrick) case event: GameStateChangeEvent => if (event.oldState == MainMenu && event.newState == Lobby) { return @@ -197,6 +203,7 @@ class GameLobby private( throw new CantPlayCardException("You can't skip this round!") } logic.playerInputLogic.receivedDog(None) + return } val hand = getHand(player) val card = hand.cards(cardIndex) @@ -229,7 +236,7 @@ class GameLobby private( } def returnToLobby(userSession: UserSession): Unit = { - if (users.contains(userSession.id)) { + if (!users.contains(userSession.id)) { throw new NotInThisGameException("You are not in this game!") } val session = users(userSession.id) diff --git a/knockoutwhistweb/app/logic/game/PollingEvents.scala b/knockoutwhistweb/app/logic/game/PollingEvents.scala index 3fa5525..cc1117e 100644 --- a/knockoutwhistweb/app/logic/game/PollingEvents.scala +++ b/knockoutwhistweb/app/logic/game/PollingEvents.scala @@ -3,6 +3,7 @@ package logic.game enum PollingEvents { case CardPlayed case NewRound + case NewTrick case ReloadEvent case LobbyUpdate case LobbyCreation diff --git a/knockoutwhistweb/app/views/ingame/finishedMatch.scala.html b/knockoutwhistweb/app/views/ingame/finishedMatch.scala.html new file mode 100644 index 0000000..155eac6 --- /dev/null +++ b/knockoutwhistweb/app/views/ingame/finishedMatch.scala.html @@ -0,0 +1,37 @@ +@(user: Option[model.users.User], gamelobby: logic.game.GameLobby) + +
+
+
+
+
Winner: @gamelobby.getLogic.getWinner
+
+
+
+ @if((gamelobby.getUserSession(user.get.id).host)) { +
+
Return to lobby
+
+ } else { +
+
+ Loading... +
+
+ } +
+
+
+ \ No newline at end of file diff --git a/knockoutwhistweb/app/views/ingame/ingame.scala.html b/knockoutwhistweb/app/views/ingame/ingame.scala.html index c1c4557..b357fc4 100644 --- a/knockoutwhistweb/app/views/ingame/ingame.scala.html +++ b/knockoutwhistweb/app/views/ingame/ingame.scala.html @@ -1,8 +1,8 @@ @import de.knockoutwhist.control.controllerBaseImpl.sublogic.util.TrickUtil +@import de.knockoutwhist.utils.Implicits.* @(player: de.knockoutwhist.player.AbstractPlayer, gamelobby: logic.game.GameLobby) -@main("Ingame") {
@@ -72,16 +72,18 @@
-
+
@for(i <- player.currentHand().get.cards.indices) {
-
- @util.WebUIUtils.cardtoImage(player.currentHand().get.cards(i)) width="120px" style="border-radius: 6px"/> -
- @if(player.isInDogLife) { -
- +
+ @util.WebUIUtils.cardtoImage(player.currentHand().get.cards(i)) width="120px" style="border-radius: 6px"/>
+ @if(player.isInDogLife) { +
+ +
}
} @@ -91,8 +93,15 @@
-} diff --git a/knockoutwhistweb/app/views/ingame/selecttrump.scala.html b/knockoutwhistweb/app/views/ingame/selecttrump.scala.html index f9969c0..d6041fa 100644 --- a/knockoutwhistweb/app/views/ingame/selecttrump.scala.html +++ b/knockoutwhistweb/app/views/ingame/selecttrump.scala.html @@ -1,6 +1,5 @@ @(player: de.knockoutwhist.player.AbstractPlayer, gamelobby: logic.game.GameLobby) -@main("Selecting Trumpsuit...") {
@@ -18,36 +17,24 @@
-
- - -
+
+ @util.WebUIUtils.cardtoImage(de.knockoutwhist.cards.Card(de.knockoutwhist.cards.CardValue.Ace, de.knockoutwhist.cards.Suit.Spades)) width="120px" style="border-radius: 6px"/> +
-
- - -
+
+ @util.WebUIUtils.cardtoImage(de.knockoutwhist.cards.Card(de.knockoutwhist.cards.CardValue.Ace, de.knockoutwhist.cards.Suit.Hearts)) width="120px" style="border-radius: 6px"/> +
-
- - -
+
+ @util.WebUIUtils.cardtoImage(de.knockoutwhist.cards.Card(de.knockoutwhist.cards.CardValue.Ace, de.knockoutwhist.cards.Suit.Diamonds)) width="120px" style="border-radius: 6px"/> +
-
- - -
+
+ @util.WebUIUtils.cardtoImage(de.knockoutwhist.cards.Card(de.knockoutwhist.cards.CardValue.Ace, de.knockoutwhist.cards.Suit.Clubs)) width="120px" style="border-radius: 6px"/> +
@@ -69,4 +56,16 @@
-} \ No newline at end of file + \ 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 134f234..1d8c801 100644 --- a/knockoutwhistweb/app/views/ingame/tie.scala.html +++ b/knockoutwhistweb/app/views/ingame/tie.scala.html @@ -1,6 +1,4 @@ @(player: de.knockoutwhist.player.AbstractPlayer, gamelobby: logic.game.GameLobby) - -@main("Tie") {
@@ -28,7 +26,7 @@ Pick a number between 1 and @{maxNum + 1}. The resulting card will be your card for the cut.
-
+
@@ -36,9 +34,9 @@
- +
-
+
Currently Picked Cards
@@ -104,4 +102,16 @@
-} \ No newline at end of file + \ No newline at end of file diff --git a/knockoutwhistweb/app/views/lobby/lobby.scala.html b/knockoutwhistweb/app/views/lobby/lobby.scala.html index b7c53ba..b4d304d 100644 --- a/knockoutwhistweb/app/views/lobby/lobby.scala.html +++ b/knockoutwhistweb/app/views/lobby/lobby.scala.html @@ -1,6 +1,5 @@ @(user: Option[model.users.User], gamelobby: logic.game.GameLobby) -@main("Lobby") {
@@ -9,7 +8,7 @@
Lobby-Name: @gamelobby.name
-
Exit
+
Exit
@@ -19,57 +18,64 @@
- @if((gamelobby.getUserSession(user.get.id).host)) { -
- @for(playersession <- gamelobby.getPlayers.values) { -
-
- Profile -
- @if(playersession.id == user.get.id) { -
@playersession.name (You)
- Remove - } else { -
@playersession.name
-
Remove
- } -
-
+ @if((gamelobby.getUserSession(user.get.id).host)) { +
+ @for(playersession <- gamelobby.getPlayers.values) { +
+
+ Profile +
+ @if(playersession.id == user.get.id) { +
@playersession.name (You)
+ Remove + } else { +
@playersession.name
+
Remove
+ }
- } -
-
-
Start Game
-
- } 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...
} +
+
+
Start Game
+
+ } 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... +
+
+ }
-} \ No newline at end of file + 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')); + \ No newline at end of file diff --git a/knockoutwhistweb/app/views/login/login.scala.html b/knockoutwhistweb/app/views/login/login.scala.html index f81630e..cb36915 100644 --- a/knockoutwhistweb/app/views/login/login.scala.html +++ b/knockoutwhistweb/app/views/login/login.scala.html @@ -1,47 +1,42 @@ @() +
+ - -} \ No newline at end of file + \ No newline at end of file diff --git a/knockoutwhistweb/app/views/mainmenu/navbar.scala.html b/knockoutwhistweb/app/views/mainmenu/navbar.scala.html index 7f46751..c19cd00 100644 --- a/knockoutwhistweb/app/views/mainmenu/navbar.scala.html +++ b/knockoutwhistweb/app/views/mainmenu/navbar.scala.html @@ -23,8 +23,8 @@ Rules -
diff --git a/knockoutwhistweb/conf/routes b/knockoutwhistweb/conf/routes index 3ff29a9..0f24f52 100644 --- a/knockoutwhistweb/conf/routes +++ b/knockoutwhistweb/conf/routes @@ -24,8 +24,7 @@ GET /logout controllers.UserController.logout() # In-game routes 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/start controllers.IngameController.startGame(id: String) POST /game/:id/kickPlayer/:playerToKick controllers.IngameController.kickPlayer(id: String, playerToKick: String) POST /game/:id/trump controllers.IngameController.playTrump(id: String) diff --git a/knockoutwhistweb/public/javascripts/main.js b/knockoutwhistweb/public/javascripts/main.js index d5fa1ba..3308f4d 100644 --- a/knockoutwhistweb/public/javascripts/main.js +++ b/knockoutwhistweb/public/javascripts/main.js @@ -88,7 +88,8 @@ function pollForUpdates(gameId) { const $handElement = $('#card-slide'); const $lobbyElement = $('#lobbybackground'); const $mainmenuElement = $('#main-menu-screen') - if (!$handElement.length && !$lobbyElement.length && !$mainmenuElement.length) { + const $mainbody = $('#main-body') + if (!$handElement.length && !$lobbyElement.length && !$mainmenuElement.length && !$mainbody.length) { setTimeout(() => pollForUpdates(gameId), 1000); return; } @@ -141,6 +142,13 @@ function pollForUpdates(gameId) { } $handElement.html(newHandHTML); + + if (data.yourTurn) { + $handElement.removeClass('inactive'); + } else { + $handElement.addClass('inactive'); + } + $('#current-player-name').text(data.currentPlayerName) if (data.nextPlayer) { $('#next-player-name').text(data.nextPlayer); @@ -207,7 +215,7 @@ function pollForUpdates(gameId) { } } else if (data.status === "reloadEvent") { console.log("[DEBUG] Reload event received. Redirecting..."); - window.location.href = data.redirectUrl; + exchangeBody(data.content, "Knockout Whist - Ingame", data.redirectUrl); } else if (data.status === "lobbyUpdate") { console.log("[DEBUG] Entering 'lobbyUpdate' logic."); @@ -280,6 +288,26 @@ function createGameJS() { sendGameCreationRequest(jsonObj); } +function backToLobby(gameId) { + const route = jsRoutes.controllers.IngameController.returnToLobby(gameId); + + $.ajax({ + url: route.url, + type: route.type, + contentType: 'application/json', + dataType: 'json', + error: ((jqXHR) => { + const errorData = JSON.parse(jqXHR.responseText); + if (errorData && errorData.errorMessage) { + alert(`${errorData.errorMessage}`); + } else { + alert(`An unexpected error occurred. Please try again. Status: ${jqXHR.status}`); + } + }) + }) + +} + function sendGameCreationRequest(dataObject) { const route = jsRoutes.controllers.MainMenuController.createGame(); @@ -291,7 +319,7 @@ function sendGameCreationRequest(dataObject) { dataType: 'json', success: (data => { if (data.status === 'success') { - window.location.href = data.redirectUrl; + exchangeBody(data.content, "Knockout Whist - Lobby", data.redirectUrl); } }), error: ((jqXHR) => { @@ -304,9 +332,19 @@ function sendGameCreationRequest(dataObject) { }) }) } + +function exchangeBody(content, title = "Knockout Whist", url = null) { + if (url) { + window.history.pushState({}, title, url); + } + $("#main-body").html(content); + document.title = title; +} + function startGame(gameId) { sendGameStartRequest(gameId) } + function sendGameStartRequest(gameId) { const route = jsRoutes.controllers.IngameController.startGame(gameId); @@ -357,21 +395,28 @@ function sendRemovePlayerRequest(gameId, playersessionId) { }) } -function leaveGame(gameId) { - sendLeavePlayerRequest(gameId) -} +function login() { + const username = $('#username').val(); + const password = $('#password').val(); -function sendLeavePlayerRequest(gameId) { + const jsonObj = { + username: username, + password: password + }; - const route = jsRoutes.controllers.IngameController.leaveGame(gameId); + const route = jsRoutes.controllers.UserController.login_Post(); $.ajax({ url: route.url, type: route.type, + contentType: 'application/json', dataType: 'json', + data: JSON.stringify(jsonObj), success: (data => { if (data.status === 'success') { - window.location.href = data.redirectUrl; + exchangeBody(data.content, "Knockout Whist - Main Menu", data.redirectUrl); + return } + alert('Login failed. Please check your credentials and try again.'); }), error: ((jqXHR) => { const errorData = JSON.parse(jqXHR.responseText); @@ -381,6 +426,118 @@ function sendLeavePlayerRequest(gameId) { alert(`An unexpected error occurred. Please try again. Status: ${jqXHR.status}`); } }) + }); +} + +function joinGame() { + const gameId = $('#gameId').val(); + + const jsonObj = { + gameId: gameId + }; + + const route = jsRoutes.controllers.MainMenuController.joinGame(); + $.ajax({ + url: route.url, + type: route.type, + contentType: 'application/json', + dataType: 'json', + data: JSON.stringify(jsonObj), + success: (data => { + if (data.status === 'success') { + exchangeBody(data.content, "Knockout Whist - Lobby", data.redirectUrl); + return + } + alert('Could not join the game. Please check the Game ID and try again.'); + }), + error: ((jqXHR) => { + const errorData = JSON.parse(jqXHR.responseText); + if (errorData && errorData.errorMessage) { + alert(`${errorData.errorMessage}`); + } else { + alert(`An unexpected error occurred. Please try again. Status: ${jqXHR.status}`); + } + }) + }); + return false +} + +function selectTie(gameId) { + const route = jsRoutes.controllers.IngameController.playTie(gameId); + const jsonObj = { + tie: $('#tieNumber').val() + }; + + $.ajax({ + url: route.url, + type: route.type, + contentType: 'application/json', + dataType: 'json', + data: JSON.stringify(jsonObj), + error: (jqXHR => { + let error; + try { + error = JSON.parse(jqXHR.responseText); + } catch (e) { + console.error("Failed to parse error response:", e); + } + if (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); + $.ajax({ + url: route.url, + type: route.type, + dataType: 'json', + error: ((jqXHR) => { + const errorData = JSON.parse(jqXHR.responseText); + if (errorData && errorData.errorMessage) { + alert(`${errorData.errorMessage}`); + } else { + alert(`An unexpected error occurred. Please try again. Status: ${jqXHR.status}`); + } + }) + }) +} + +function handleTrumpSelection(cardobject, gameId) { + const trumpId = cardobject.dataset.trump; + const jsonObj = { + trump: trumpId + } + + const route = jsRoutes.controllers.IngameController.playTrump(gameId); + + $.ajax({ + url: route.url, + type: route.type, + contentType: 'application/json', + dataType: 'json', + data: JSON.stringify(jsonObj), + error: (jqXHR => { + let error; + try { + error = JSON.parse(jqXHR.responseText); + } catch (e) { + console.error("Failed to parse error response:", e); + } + if (error?.errorMessage) { + alert(`${error.errorMessage}`); + } else { + alert('An unexpected error occurred. Please try again.'); + } + }) }) } @@ -393,6 +550,22 @@ function handlePlayCard(cardobject, gameId, dog = false) { } function handleSkipDogLife(cardobject, gameId) { + + const wiggleKeyframes = [ + { transform: 'translateX(0)' }, + { transform: 'translateX(-5px)' }, + { transform: 'translateX(5px)' }, + { transform: 'translateX(-5px)' }, + { transform: 'translateX(0)' } + ]; + + const wiggleTiming = { + duration: 400, + iterations: 1, + easing: 'ease-in-out', + fill: 'forwards' + }; + const route = jsRoutes.controllers.IngameController.playDogCard(gameId); $.ajax({ @@ -410,7 +583,9 @@ function handleSkipDogLife(cardobject, gameId) { } catch (e) { console.error("Failed to parse error response:", e); } - if (error?.errorMessage) { + if (error?.errorMessage.includes("You can't skip this round!")) { + cardobject.parentElement.animate(wiggleKeyframes, wiggleTiming); + } else if (error?.errorMessage) { alert(`${error.errorMessage}`); } else { alert('An unexpected error occurred. Please try again.'); @@ -434,7 +609,7 @@ function sendPlayCardRequest(jsonObj, gameId, cardobject, dog) { easing: 'ease-in-out', fill: 'forwards' }; - const route = dog ? jsRoutes.controllers.IngameController.playCard(gameId) : jsRoutes.controllers.IngameController.playDogCard(gameId); + const route = dog === "true" ? jsRoutes.controllers.IngameController.playDogCard(gameId) : jsRoutes.controllers.IngameController.playCard(gameId); $.ajax({ url: route.url,