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 343b498..5179c42 100644 --- a/knockoutwhistweb/app/assets/stylesheets/main.less +++ b/knockoutwhistweb/app/assets/stylesheets/main.less @@ -79,6 +79,10 @@ body { overflow: auto; } +.navbar-drop-shadow { + box-shadow: 0 1px 15px 0 #000000 +} + #sessions { display: flex; flex-direction: column; @@ -224,4 +228,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 fcfa2f6..6a5ece1 100644 --- a/knockoutwhistweb/app/controllers/IngameController.scala +++ b/knockoutwhistweb/app/controllers/IngameController.scala @@ -21,101 +21,12 @@ import scala.concurrent.Future import scala.util.Try import scala.concurrent.ExecutionContext @Singleton -class IngameController @Inject() ( - val cc: ControllerComponents, - val podManager: PodManager, - val authAction: AuthAction, - implicit val ec: ExecutionContext - ) extends AbstractController(cc) { +class IngameController @Inject()( + val controllerComponents: ControllerComponents, + val authAction: AuthAction, + val podManager: PodManager + ) extends BaseController { - // --- Helper function (defined outside match/if for scope) --- - def buildSuccessResponse(game: GameLobby, hand: Option[Hand]): JsValue = { - // NOTE: Replace the unsafe .get calls here if game state is not guaranteed - val currentRound = game.logic.getCurrentRound.get - val currentTrick = game.logic.getCurrentTrick.get - - // JSON Building Logic: - val trickCardsJson = Json.toJson( - currentTrick.cards.map { case (card, player) => - Json.obj("cardId" -> WebUIUtils.cardtoString(card), "player" -> player.name) - } - ) - val scoreTableJson = Json.toJson( - game.getLogic.getPlayerQueue.get.toList.map { player => - Json.obj( - "name" -> player.name, - "tricks" -> currentRound.tricklist.count(_.winner.contains(player)) - ) - } - ) - - val stringHand = hand.map { h => - val cardStrings = h.cards.map(WebUIUtils.cardtoString(_)) - Json.toJson(cardStrings).as[JsArray] - }.getOrElse(Json.arr()) - - val firstCardId = currentTrick.firstCard.map(WebUIUtils.cardtoString).getOrElse("BLANK") - val nextPlayer = game.getLogic.getPlayerQueue.get.duplicate().nextPlayer().name - Json.obj( - "status" -> "cardPlayed", - "handData" -> stringHand, - "currentPlayerName" -> game.logic.getCurrentPlayer.get.name, - "trumpSuit" -> currentRound.trumpSuit.toString, - "trickCards" -> trickCardsJson, - "scoreTable" -> scoreTableJson, - "firstCardId" -> firstCardId, - "nextPlayer" -> nextPlayer - ) - } - - def handleEvent(event: PollingEvents, game: GameLobby, user: User): Result = { - event match { - case CardPlayed => - val player = game.getPlayerByUser(user) - val hand = player.currentHand() - val jsonResponse = buildSuccessResponse(game, hand) - Ok(jsonResponse) - case GameStarted => - val jsonResponse = Json.obj( - "status" -> "gameStart", - "redirectUrl" -> routes.IngameController.game(game.id).url - ) - Ok(jsonResponse) - } - } - // --- Main Polling Action --- - def polling(gameId: String): Action[AnyContent] = authAction.async { implicit request: AuthenticatedRequest[AnyContent] => - - val playerId = request.user.id - - // 1. Safely look up the game - podManager.getGame(gameId) match { - case Some(game) => - - // 2. Short-Poll Check (Check for missed events) - if (game.getPollingState.nonEmpty) { - val event = game.getPollingState.dequeue() - - Future.successful(handleEvent(event, game, request.user)) - } else { - - val eventPromise = game.registerWaiter(playerId) - - eventPromise.future.map { event => - game.removeWaiter(playerId) - handleEvent(event, game, request.user) - }.recover { - case _: Throwable => - game.removeWaiter(playerId) - NoContent - } - } - - case None => - // Game not found - Future.successful(NotFound("Game not found.")) - } - } def game(gameId: String): Action[AnyContent] = authAction { implicit request: AuthenticatedRequest[AnyContent] => val game = podManager.getGame(gameId) game match { @@ -156,70 +67,30 @@ class IngameController @Inject() ( } } if (result.isSuccess) { - Ok(Json.obj( - "status" -> "success", - "redirectUrl" -> routes.IngameController.game(gameId).url - )) + Redirect(routes.IngameController.game(gameId)) } else { val throwable = result.failed.get throwable match { case _: NotInThisGameException => - BadRequest(Json.obj( - "status" -> "failure", - "errorMessage" -> throwable.getMessage - )) + BadRequest(throwable.getMessage) case _: NotHostException => - Forbidden(Json.obj( - "status" -> "failure", - "errorMessage" -> throwable.getMessage - )) + Forbidden(throwable.getMessage) case _: NotEnoughPlayersException => - BadRequest(Json.obj( - "status" -> "failure", - "errorMessage" -> throwable.getMessage - )) + BadRequest(throwable.getMessage) case _ => - InternalServerError(Json.obj( - "status" -> "failure", - "errorMessage" -> throwable.getMessage - )) + InternalServerError(throwable.getMessage) } } } - def kickPlayer(gameId: String, playerToKick: String): Action[AnyContent] = authAction { implicit request: AuthenticatedRequest[AnyContent] => + def kickPlayer(gameId: String, playerToKick: UUID): Action[AnyContent] = authAction { implicit request: AuthenticatedRequest[AnyContent] => val game = podManager.getGame(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." - )) - } + game.get.leaveGame(playerToKick) + Redirect(routes.IngameController.game(gameId)) } def leaveGame(gameId: String): Action[AnyContent] = authAction { implicit request: AuthenticatedRequest[AnyContent] => val game = podManager.getGame(gameId) - 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." - )) - } + game.get.leaveGame(request.user.id) + Redirect(routes.MainMenuController.mainMenu()) } def joinGame(gameId: String): Action[AnyContent] = authAction { implicit request: AuthenticatedRequest[AnyContent] => val game = podManager.getGame(gameId) @@ -251,10 +122,7 @@ class IngameController @Inject() ( val game = podManager.getGame(gameId) game match { case Some(g) => - val jsonBody = request.body.asJson - val cardIdOpt: Option[String] = jsonBody.flatMap { jsValue => - (jsValue \ "cardID").asOpt[String] - } + val cardIdOpt = request.body.asFormUrlEncoded.flatMap(_.get("cardId").flatMap(_.headOption)) cardIdOpt match { case Some(cardId) => var optSession: Option[UserSession] = None @@ -266,51 +134,27 @@ class IngameController @Inject() ( } optSession.foreach(_.lock.unlock()) if (result.isSuccess) { - Ok(Json.obj( - "status" -> "success", - "redirectUrl" -> routes.IngameController.game(gameId).url - )) + NoContent } else { val throwable = result.failed.get throwable match { case _: CantPlayCardException => - BadRequest(Json.obj( - "status" -> "failure", - "errorMessage" -> throwable.getMessage - )) + BadRequest(throwable.getMessage) case _: NotInThisGameException => - BadRequest(Json.obj( - "status" -> "failure", - "errorMessage" -> throwable.getMessage - )) + BadRequest(throwable.getMessage) case _: IllegalArgumentException => - BadRequest(Json.obj( - "status" -> "failure", - "errorMessage" -> throwable.getMessage - )) + BadRequest(throwable.getMessage) case _: IllegalStateException => - BadRequest(Json.obj( - "status" -> "failure", - "errorMessage" -> throwable.getMessage - )) + BadRequest(throwable.getMessage) case _ => - InternalServerError(Json.obj( - "status" -> "failure", - "errorMessage" -> throwable.getMessage - )) + InternalServerError(throwable.getMessage) } } case None => - BadRequest(Json.obj( - "status" -> "failure", - "errorMessage" -> "cardId Parameter is missing" - )) + BadRequest("cardId parameter is missing") } case None => - NotFound(Json.obj( - "status" -> "failure", - "errorMessage" -> "Game not found" - )) + NotFound("Game not found") } } } @@ -409,7 +253,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 a897368..cd28c35 100644 --- a/knockoutwhistweb/app/logic/game/GameLobby.scala +++ b/knockoutwhistweb/app/logic/game/GameLobby.scala @@ -188,6 +188,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...") {
You (@player.toString) have won the last round! Select a trumpsuit for the next round!
-Available trumpsuits are displayed below:
-Your cards
+@player.toString is choosing a trumpsuit. Starting new round when @player.toString picked a trumpsuit...
- } +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:
-@player
- @util.WebUIUtils.cardtoImage(card) -+ The last round was tied between: + + @for(players <- logic.playerTieLogic.getTiedPlayers) { + @players + } + +
+@player
+@player
+