diff --git a/knockoutwhistweb/app/assets/stylesheets/dark-mode.less b/knockoutwhistweb/app/assets/stylesheets/dark-mode.less
index 6249ca9..17b7f1a 100644
--- a/knockoutwhistweb/app/assets/stylesheets/dark-mode.less
+++ b/knockoutwhistweb/app/assets/stylesheets/dark-mode.less
@@ -1,6 +1,13 @@
@media (prefers-color-scheme: dark) {
:root {
--background-image: url('/assets/images/background.png');
- --color: white;
+ --color: #f8f9fa; /* Light text on dark bg */
+
+ /* Bootstrap variable overrides for dark mode */
+ --bs-body-color: var(--color);
+ --bs-link-color: #66b2ff;
+ --bs-link-hover-color: #99ccff;
+ --bs-border-color: rgba(255, 255, 255, 0.2);
+ --bs-heading-color: var(--color);
}
}
diff --git a/knockoutwhistweb/app/assets/stylesheets/main.less b/knockoutwhistweb/app/assets/stylesheets/main.less
index 8bc3b69..b723d4c 100644
--- a/knockoutwhistweb/app/assets/stylesheets/main.less
+++ b/knockoutwhistweb/app/assets/stylesheets/main.less
@@ -2,6 +2,19 @@
@import "dark-mode.less";
@import "login.less";
+/* Provide default (light) variables so the site works even if light-mode.less fails */
+:root {
+ --background-image: url('/assets/images/img.png');
+ --color: #212529; /* Bootstrap body text default */
+
+ /* Bootstrap variable overrides for light mode */
+ --bs-body-color: var(--color) !important;
+ --bs-link-color: #0d6efd !important;
+ --bs-link-hover-color: #0a58ca !important;
+ --bs-border-color: rgba(0, 0, 0, 0.125) !important;
+ --bs-heading-color: var(--color) !important;
+}
+
@background-image: var(--background-image);
@color: var(--color);
@keyframes slideIn {
@@ -10,8 +23,33 @@
}
.game-field-background {
background-image: @background-image;
- background-size: 100vw 100vh;
+ background-size: cover;
+ background-position: center center;
background-repeat: no-repeat;
+ background-attachment: fixed;
+}
+
+.navbar-header{
+ text-align:center;
+}
+
+.navbar-toggle {
+ float: none;
+ margin-right:0;
+}
+
+/* Ensure body text color follows theme variable and works with Bootstrap */
+body {
+ color: @color;
+}
+
+.footer {
+ width: 100%;
+ text-align: center;
+ font-size: 12px;
+ color: @color;
+ padding: 0.5rem 0;
+ flex-grow: 1; /* fill remaining vertical space as visual footer background */
}
.game-field {
@@ -19,6 +57,7 @@
inset: 0;
overflow: auto;
}
+
#sessions {
display: flex;
flex-direction: column;
@@ -70,8 +109,22 @@
&:nth-child(7) { animation-delay: 3.5s; }
}
}
+#card-slide {
+ div {
+ animation: slideIn 0.5s ease-out forwards;
+ animation-fill-mode: backwards;
+ &:nth-child(1) { animation-delay: 0.5s; }
+ &:nth-child(2) { animation-delay: 1s; }
+ &:nth-child(3) { animation-delay: 1.5s; }
+ &:nth-child(4) { animation-delay: 2s; }
+ &:nth-child(5) { animation-delay: 2.5s; }
+ &:nth-child(6) { animation-delay: 3s; }
+ &:nth-child(7) { animation-delay: 3.5s; }
+ }
+}
+
#cardsplayed {
display: flex;
flex-direction: row;
diff --git a/knockoutwhistweb/app/controllers/IngameController.scala b/knockoutwhistweb/app/controllers/IngameController.scala
index 59749c3..373701d 100644
--- a/knockoutwhistweb/app/controllers/IngameController.scala
+++ b/knockoutwhistweb/app/controllers/IngameController.scala
@@ -8,6 +8,7 @@ import model.sessions.{PlayerSession, UserSession}
import play.api.*
import play.api.mvc.*
+import java.util.UUID
import javax.inject.*
import scala.util.Try
@@ -28,11 +29,11 @@ class IngameController @Inject()(
game match {
case Some(g) =>
g.logic.getCurrentState match {
- case Lobby => Ok("Lobby: " + gameId)
+ case Lobby => Ok(views.html.lobby.lobby(Some(request.user), g))
case InGame =>
Ok(views.html.ingame.ingame(
g.getPlayerByUser(request.user),
- g.logic
+ g
))
case SelectTrump =>
Ok(views.html.ingame.selecttrump(
@@ -63,7 +64,7 @@ class IngameController @Inject()(
}
}
if (result.isSuccess) {
- NoContent
+ Redirect(routes.IngameController.game(gameId))
} else {
val throwable = result.failed.get
throwable match {
@@ -78,6 +79,16 @@ class IngameController @Inject()(
}
}
}
+ def kickPlayer(gameId: String, playerToKick: UUID): Action[AnyContent] = authAction { implicit request: AuthenticatedRequest[AnyContent] =>
+ val game = podManager.getGame(gameId)
+ 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)
+ 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)
val result = Try {
diff --git a/knockoutwhistweb/app/controllers/MainMenuController.scala b/knockoutwhistweb/app/controllers/MainMenuController.scala
index 7032b52..55c4a1b 100644
--- a/knockoutwhistweb/app/controllers/MainMenuController.scala
+++ b/knockoutwhistweb/app/controllers/MainMenuController.scala
@@ -21,7 +21,7 @@ class MainMenuController @Inject()(
// Pass the request-handling function directly to authAction (no nested Action)
def mainMenu(): Action[AnyContent] = authAction { implicit request: AuthenticatedRequest[AnyContent] =>
- Ok(views.html.mainmenu.navbar(Some(request.user)))
+ Ok(views.html.mainmenu.creategame(Some(request.user)))
}
def index(): Action[AnyContent] = authAction { implicit request: AuthenticatedRequest[AnyContent] =>
@@ -29,12 +29,20 @@ class MainMenuController @Inject()(
}
def createGame(): Action[AnyContent] = authAction { implicit request: AuthenticatedRequest[AnyContent] =>
- val gameLobby = podManager.createGame(
- host = request.user,
- name = s"${request.user.name}'s Game",
- maxPlayers = 4
- )
- Redirect(routes.IngameController.game(gameLobby.id))
+ 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 gameLobby = podManager.createGame(
+ host = request.user,
+ name = gamename,
+ maxPlayers = playeramount.toInt
+ )
+ Redirect(routes.IngameController.game(gameLobby.id))
+ } else {
+ BadRequest("Invalid form submission")
+ }
+
}
def joinGame(): Action[AnyContent] = authAction { implicit request: AuthenticatedRequest[AnyContent] =>
@@ -53,9 +61,7 @@ class MainMenuController @Inject()(
}
}
- def rules(): Action[AnyContent] = {
- Action { implicit request =>
- Ok(views.html.mainmenu.rules())
- }
+ def rules(): Action[AnyContent] = authAction { implicit request: AuthenticatedRequest[AnyContent] =>
+ Ok(views.html.mainmenu.rules(Some(request.user)))
}
}
\ No newline at end of file
diff --git a/knockoutwhistweb/app/logic/game/GameLobby.scala b/knockoutwhistweb/app/logic/game/GameLobby.scala
index 6743bdb..486bf4f 100644
--- a/knockoutwhistweb/app/logic/game/GameLobby.scala
+++ b/knockoutwhistweb/app/logic/game/GameLobby.scala
@@ -88,12 +88,12 @@ class GameLobby private(
* Remove the user from the game lobby.
* @param user the user who wants to leave the game.
*/
- def leaveGame(user: User): Unit = {
- val sessionOpt = users.get(user.id)
+ def leaveGame(userId: UUID): Unit = {
+ val sessionOpt = users.get(userId)
if (sessionOpt.isEmpty) {
throw new NotInThisGameException("You are not in this game!")
}
- users.remove(user.id)
+ users.remove(userId)
}
/**
@@ -176,6 +176,14 @@ class GameLobby private(
getPlayerBySession(getUserSession(user.id))
}
+ def getPlayers: mutable.Map[UUID, UserSession] = {
+ users.clone()
+ }
+
+ def getLogic: GameLogic = {
+ logic
+ }
+
private def getPlayerBySession(userSession: UserSession): AbstractPlayer = {
val playerOption = getMatch.totalplayers.find(_.id == userSession.id)
if (playerOption.isEmpty) {
diff --git a/knockoutwhistweb/app/logic/user/impl/StubUserManager.scala b/knockoutwhistweb/app/logic/user/impl/StubUserManager.scala
index 44f71ee..398a908 100644
--- a/knockoutwhistweb/app/logic/user/impl/StubUserManager.scala
+++ b/knockoutwhistweb/app/logic/user/impl/StubUserManager.scala
@@ -22,6 +22,12 @@ class StubUserManager @Inject()(val config: Config) extends UserManager {
id = java.util.UUID.fromString("223e4567-e89b-12d3-a456-426614174000"),
name = "Leon",
passwordHash = UserHash.hashPW("password123")
+ ),
+ "Jakob" -> User(
+ internalId = 2L,
+ id = java.util.UUID.fromString("323e4567-e89b-12d3-a456-426614174000"),
+ name = "Jakob",
+ passwordHash = UserHash.hashPW("password123")
)
)
diff --git a/knockoutwhistweb/app/views/ingame/ingame.scala.html b/knockoutwhistweb/app/views/ingame/ingame.scala.html
index f587dbc..4dc9f97 100644
--- a/knockoutwhistweb/app/views/ingame/ingame.scala.html
+++ b/knockoutwhistweb/app/views/ingame/ingame.scala.html
@@ -1,50 +1,71 @@
-@(player: de.knockoutwhist.player.AbstractPlayer, logic: de.knockoutwhist.control.GameLogic)
+@import de.knockoutwhist.control.controllerBaseImpl.sublogic.util.TrickUtil
+
+@(player: de.knockoutwhist.player.AbstractPlayer, gamelobby: logic.game.GameLobby)
@main("Ingame") {
-
-
Knockout Whist
-
-
Next Player:
-
@logic.getPlayerQueue.get.duplicate().nextPlayer()
-
-
-
-
Trumpsuit:
-
@logic.getCurrentRound.get.trumpSuit
+
+
+
+
+
+
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
+ }
+ }
+
+
+
+
+
+
+
+
+
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"/>
+ }
+
+
-
-
First Card
- @if(logic.getCurrentTrick.get.firstCard.isDefined) {
- @util.WebUIUtils.cardtoImage(logic.getCurrentTrick.get.firstCard.get)
- } else {
- @views.html.render.card.apply("images/cards/1B.png")("Blank Card")
+
+
+
+ @for((cardplayed, player) <- gamelobby.getLogic.getCurrentTrick.get.cards) {
+
+
+
+ @util.WebUIUtils.cardtoImage(cardplayed) width="100%"/>
+
+
+ @player
+
+
+
+ }
+
+
+
+
+ @for(i <- player.currentHand().get.cards.indices) {
+
+
+
}
-
-
@logic.getCurrentPlayer.get has to play a card!
- @if(logic.getCurrentTrick.get.cards.nonEmpty) {
-
Cards played
- } else {
-
Cards played
- }
-
-
- @for((cardplayed, player) <- logic.getCurrentTrick.get.cards) {
-
-
@player
- @util.WebUIUtils.cardtoImage(cardplayed)
-
- }
-
-
-
Your cards
-
- @for(card <- player.currentHand().get.cards) {
- @util.WebUIUtils.cardtoImage(card)
- }
-
-
-
-
-}
\ No newline at end of file
+}
diff --git a/knockoutwhistweb/app/views/lobby/lobby.scala.html b/knockoutwhistweb/app/views/lobby/lobby.scala.html
new file mode 100644
index 0000000..090b1a7
--- /dev/null
+++ b/knockoutwhistweb/app/views/lobby/lobby.scala.html
@@ -0,0 +1,84 @@
+@(user: Option[model.users.User], gamelobby: logic.game.GameLobby)
+
+@main("Lobby") {
+
+
+
+
+
+ Lobby-Name: @gamelobby.name
+
+
+
+
+
+
+
+
Playeramount: @gamelobby.getPlayers.size / @gamelobby.maxPlayers
+
+
+
+
+ @if((gamelobby.getUserSession(user.get.id).host)) {
+ @for(playersession <- gamelobby.getPlayers.values) {
+
+
+

+
+ @if(playersession.id == user.get.id) {
+
@playersession.name (You)
+
Your text could be here!
+
Remove
+ } else {
+
@playersession.name
+
Your text could be here!
+
+ }
+
+
+
+ }
+
+ } else {
+ @for(playersession <- gamelobby.getPlayers.values) {
+
+
+

+
+ @if(playersession.id == user.get.id) {
+
@playersession.name (You)
+
Your text could be here!
+ } else {
+
@playersession.name
+
Your text could be here!
+ }
+
+
+
+ }
+
+
+
Waiting for the host to start the game...
+
+
+
+ }
+
+
+}
\ 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 219707e..60aa0e4 100644
--- a/knockoutwhistweb/app/views/login/login.scala.html
+++ b/knockoutwhistweb/app/views/login/login.scala.html
@@ -1,20 +1,21 @@
@()
@main("Login") {
+
+
+
+
@* Here's where we render the page title `String`. *@
@title
-
-
- @* And here's where we render the `Html` object containing
- * the page content. *@
- @content
+
+
+ @* 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
new file mode 100644
index 0000000..1e5905c
--- /dev/null
+++ b/knockoutwhistweb/app/views/mainmenu/creategame.scala.html
@@ -0,0 +1,32 @@
+@(user: Option[model.users.User])
+
+@main("Create Game") {
+@navbar(user)
+
+}
\ 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 a5f9cc4..3008b6d 100644
--- a/knockoutwhistweb/app/views/mainmenu/navbar.scala.html
+++ b/knockoutwhistweb/app/views/mainmenu/navbar.scala.html
@@ -1,17 +1,16 @@
@(user: Option[model.users.User])
-@main("Knockout Whist - Main Menu") {