feat(ui): add Lobby and Main Menu Body #38
@@ -1,6 +1,13 @@
|
|||||||
@media (prefers-color-scheme: dark) {
|
@media (prefers-color-scheme: dark) {
|
||||||
:root {
|
:root {
|
||||||
--background-image: url('/assets/images/background.png');
|
--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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,6 +2,19 @@
|
|||||||
@import "dark-mode.less";
|
@import "dark-mode.less";
|
||||||
@import "login.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);
|
@background-image: var(--background-image);
|
||||||
@color: var(--color);
|
@color: var(--color);
|
||||||
@keyframes slideIn {
|
@keyframes slideIn {
|
||||||
@@ -10,8 +23,33 @@
|
|||||||
}
|
}
|
||||||
.game-field-background {
|
.game-field-background {
|
||||||
background-image: @background-image;
|
background-image: @background-image;
|
||||||
background-size: 100vw 100vh;
|
background-size: cover;
|
||||||
|
background-position: center center;
|
||||||
background-repeat: no-repeat;
|
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 {
|
.game-field {
|
||||||
@@ -19,6 +57,7 @@
|
|||||||
inset: 0;
|
inset: 0;
|
||||||
overflow: auto;
|
overflow: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
#sessions {
|
#sessions {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
@@ -70,8 +109,22 @@
|
|||||||
&:nth-child(7) { animation-delay: 3.5s; }
|
&: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 {
|
#cardsplayed {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import model.sessions.{PlayerSession, UserSession}
|
|||||||
import play.api.*
|
import play.api.*
|
||||||
import play.api.mvc.*
|
import play.api.mvc.*
|
||||||
|
|
||||||
|
import java.util.UUID
|
||||||
import javax.inject.*
|
import javax.inject.*
|
||||||
import scala.util.Try
|
import scala.util.Try
|
||||||
|
|
||||||
@@ -28,11 +29,11 @@ class IngameController @Inject()(
|
|||||||
game match {
|
game match {
|
||||||
case Some(g) =>
|
case Some(g) =>
|
||||||
g.logic.getCurrentState match {
|
g.logic.getCurrentState match {
|
||||||
case Lobby => Ok("Lobby: " + gameId)
|
case Lobby => Ok(views.html.lobby.lobby(Some(request.user), g))
|
||||||
case InGame =>
|
case InGame =>
|
||||||
Ok(views.html.ingame.ingame(
|
Ok(views.html.ingame.ingame(
|
||||||
g.getPlayerByUser(request.user),
|
g.getPlayerByUser(request.user),
|
||||||
g.logic
|
g
|
||||||
))
|
))
|
||||||
case SelectTrump =>
|
case SelectTrump =>
|
||||||
Ok(views.html.ingame.selecttrump(
|
Ok(views.html.ingame.selecttrump(
|
||||||
@@ -63,7 +64,7 @@ class IngameController @Inject()(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (result.isSuccess) {
|
if (result.isSuccess) {
|
||||||
NoContent
|
Redirect(routes.IngameController.game(gameId))
|
||||||
} else {
|
} else {
|
||||||
val throwable = result.failed.get
|
val throwable = result.failed.get
|
||||||
throwable match {
|
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] =>
|
def joinGame(gameId: String): Action[AnyContent] = authAction { implicit request: AuthenticatedRequest[AnyContent] =>
|
||||||
val game = podManager.getGame(gameId)
|
val game = podManager.getGame(gameId)
|
||||||
val result = Try {
|
val result = Try {
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ class MainMenuController @Inject()(
|
|||||||
|
|
||||||
// Pass the request-handling function directly to authAction (no nested Action)
|
// Pass the request-handling function directly to authAction (no nested Action)
|
||||||
def mainMenu(): Action[AnyContent] = authAction { implicit request: AuthenticatedRequest[AnyContent] =>
|
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] =>
|
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] =>
|
def createGame(): Action[AnyContent] = authAction { implicit request: AuthenticatedRequest[AnyContent] =>
|
||||||
val gameLobby = podManager.createGame(
|
val postData = request.body.asFormUrlEncoded
|
||||||
host = request.user,
|
if (postData.isDefined) {
|
||||||
name = s"${request.user.name}'s Game",
|
val gamename = postData.get.get("lobbyname").flatMap(_.headOption).getOrElse(s"${request.user.name}'s Game")
|
||||||
maxPlayers = 4
|
val playeramount = postData.get.get("playeramount").flatMap(_.headOption).getOrElse("")
|
||||||
)
|
val gameLobby = podManager.createGame(
|
||||||
Redirect(routes.IngameController.game(gameLobby.id))
|
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] =>
|
def joinGame(): Action[AnyContent] = authAction { implicit request: AuthenticatedRequest[AnyContent] =>
|
||||||
@@ -53,9 +61,7 @@ class MainMenuController @Inject()(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
def rules(): Action[AnyContent] = {
|
def rules(): Action[AnyContent] = authAction { implicit request: AuthenticatedRequest[AnyContent] =>
|
||||||
Action { implicit request =>
|
Ok(views.html.mainmenu.rules(Some(request.user)))
|
||||||
Ok(views.html.mainmenu.rules())
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -88,12 +88,12 @@ class GameLobby private(
|
|||||||
* Remove the user from the game lobby.
|
* Remove the user from the game lobby.
|
||||||
* @param user the user who wants to leave the game.
|
* @param user the user who wants to leave the game.
|
||||||
*/
|
*/
|
||||||
def leaveGame(user: User): Unit = {
|
def leaveGame(userId: UUID): Unit = {
|
||||||
val sessionOpt = users.get(user.id)
|
val sessionOpt = users.get(userId)
|
||||||
if (sessionOpt.isEmpty) {
|
if (sessionOpt.isEmpty) {
|
||||||
throw new NotInThisGameException("You are not in this game!")
|
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))
|
getPlayerBySession(getUserSession(user.id))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
def getPlayers: mutable.Map[UUID, UserSession] = {
|
||||||
|
users.clone()
|
||||||
|
}
|
||||||
|
|
||||||
|
def getLogic: GameLogic = {
|
||||||
|
logic
|
||||||
|
}
|
||||||
|
|
||||||
private def getPlayerBySession(userSession: UserSession): AbstractPlayer = {
|
private def getPlayerBySession(userSession: UserSession): AbstractPlayer = {
|
||||||
val playerOption = getMatch.totalplayers.find(_.id == userSession.id)
|
val playerOption = getMatch.totalplayers.find(_.id == userSession.id)
|
||||||
if (playerOption.isEmpty) {
|
if (playerOption.isEmpty) {
|
||||||
|
|||||||
@@ -22,6 +22,12 @@ class StubUserManager @Inject()(val config: Config) extends UserManager {
|
|||||||
id = java.util.UUID.fromString("223e4567-e89b-12d3-a456-426614174000"),
|
id = java.util.UUID.fromString("223e4567-e89b-12d3-a456-426614174000"),
|
||||||
name = "Leon",
|
name = "Leon",
|
||||||
passwordHash = UserHash.hashPW("password123")
|
passwordHash = UserHash.hashPW("password123")
|
||||||
|
),
|
||||||
|
"Jakob" -> User(
|
||||||
|
internalId = 2L,
|
||||||
|
id = java.util.UUID.fromString("323e4567-e89b-12d3-a456-426614174000"),
|
||||||
|
name = "Jakob",
|
||||||
|
passwordHash = UserHash.hashPW("password123")
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -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") {
|
@main("Ingame") {
|
||||||
<div id="ingame" class="game-field game-field-background">
|
<div class="container py-5">
|
||||||
<h1>Knockout Whist</h1>
|
|
||||||
<div id="nextPlayers">
|
<!-- Header Row -->
|
||||||
<p>Next Player:</p>
|
<div class="row mb-4 text-center">
|
||||||
<p>@logic.getPlayerQueue.get.duplicate().nextPlayer()</p>
|
<div class="col-md-4 text-start">
|
||||||
</div>
|
<h4 class="fw-semibold mb-1">Current Player</h4>
|
||||||
<div id="firstCard">
|
<p class="fs-5 text-primary">@gamelobby.getLogic.getCurrentPlayer.get.name</p>
|
||||||
<div id="trumpsuit">
|
@if(!TrickUtil.isOver(gamelobby.getLogic.getCurrentMatch.get, gamelobby.getLogic.getPlayerQueue.get)) {
|
||||||
<p>Trumpsuit: </p>
|
<h4 class="fw-semibold mb-1">Next Player</h4>
|
||||||
<p>@logic.getCurrentRound.get.trumpSuit</p>
|
@for(nextplayer <- gamelobby.getLogic.getPlayerQueue.get.duplicate()) {
|
||||||
|
<p class="fs-5 text-primary">@nextplayer</p>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Middle column without "Cards Played" -->
|
||||||
|
<div class="col-md-4 text-center">
|
||||||
|
<!-- You can leave this empty or add something else here if needed -->
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col-md-4 text-end">
|
||||||
|
<h4 class="fw-semibold mb-1">Trumpsuit</h4>
|
||||||
|
<p class="fs-5 text-primary">@gamelobby.getLogic.getCurrentRound.get.trumpSuit</p>
|
||||||
|
|
||||||
|
<h5 class="fw-semibold mt-4 mb-1">First Card</h5>
|
||||||
|
<div class="d-inline-block border rounded shadow-sm p-1 bg-light">
|
||||||
|
@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"/>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div id="firstCardObject">
|
|
||||||
<p>First Card</p>
|
<!-- Cards Played -->
|
||||||
@if(logic.getCurrentTrick.get.firstCard.isDefined) {
|
<div class="row justify-content-center g-3 mb-5">
|
||||||
@util.WebUIUtils.cardtoImage(logic.getCurrentTrick.get.firstCard.get)
|
@for((cardplayed, player) <- gamelobby.getLogic.getCurrentTrick.get.cards) {
|
||||||
} else {
|
<div class="col-auto">
|
||||||
@views.html.render.card.apply("images/cards/1B.png")("Blank Card")
|
<div class="card text-center shadow-sm border-0" style="width: 7rem;">
|
||||||
|
<div class="p-2">
|
||||||
|
@util.WebUIUtils.cardtoImage(cardplayed) width="100%"/>
|
||||||
|
</div>
|
||||||
|
<div class="card-body p-2 bg-light">
|
||||||
|
<small class="fw-semibold text-secondary">@player</small>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Player Hand at the BOTTOM -->
|
||||||
|
<div class="row justify-content-center g-2 mt-4" id="card-slide">
|
||||||
|
@for(i <- player.currentHand().get.cards.indices) {
|
||||||
|
<div class="col-auto">
|
||||||
|
<form action="@(routes.IngameController.playCard(gamelobby.id))" method="post" class="m-0 p-0">
|
||||||
|
<input type="hidden" name="cardId" value="@i" />
|
||||||
|
<button type="submit" class="btn btn-outline-light p-0 border-0 shadow-none">
|
||||||
|
@util.WebUIUtils.cardtoImage(player.currentHand().get.cards(i)) width="120px"/>
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<p>@logic.getCurrentPlayer.get has to play a card!</p>
|
|
||||||
@if(logic.getCurrentTrick.get.cards.nonEmpty) {
|
|
||||||
<p>Cards played</p>
|
|
||||||
} else {
|
|
||||||
<p id="invisible">Cards played</p>
|
|
||||||
}
|
|
||||||
|
|
||||||
<div id="cardsplayed">
|
|
||||||
@for((cardplayed, player) <- logic.getCurrentTrick.get.cards) {
|
|
||||||
<div id="playedcardplayer">
|
|
||||||
<p>@player</p>
|
|
||||||
@util.WebUIUtils.cardtoImage(cardplayed)
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<p>Your cards</p>
|
|
||||||
<div id="playercards">
|
|
||||||
@for(card <- player.currentHand().get.cards) {
|
|
||||||
@util.WebUIUtils.cardtoImage(card)
|
|
||||||
}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
|
|
||||||
</div>
|
|
||||||
}
|
}
|
||||||
84
knockoutwhistweb/app/views/lobby/lobby.scala.html
Normal file
84
knockoutwhistweb/app/views/lobby/lobby.scala.html
Normal file
@@ -0,0 +1,84 @@
|
|||||||
|
@(user: Option[model.users.User], gamelobby: logic.game.GameLobby)
|
||||||
|
|
||||||
|
@main("Lobby") {
|
||||||
|
<div class="container">
|
||||||
|
<div class="row">
|
||||||
|
<div class="col">
|
||||||
|
<div class="p-3 fs-1 d-flex align-items-center">
|
||||||
|
<div class="text-center" style="flex-grow: 1;">
|
||||||
|
Lobby-Name: @gamelobby.name
|
||||||
|
</div>
|
||||||
|
<form action="@(routes.IngameController.leaveGame(gamelobby.id))">
|
||||||
|
<button type="submit" class="btn btn-danger ms-auto">Exit</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="row">
|
||||||
|
<div class="col">
|
||||||
|
<div class="p-3 text-center fs-4">Playeramount: @gamelobby.getPlayers.size / @gamelobby.maxPlayers</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="row justify-content-center">
|
||||||
|
|
||||||
|
@if((gamelobby.getUserSession(user.get.id).host)) {
|
||||||
|
@for(playersession <- gamelobby.getPlayers.values) {
|
||||||
|
<div class="col-auto">
|
||||||
|
<div class="card" style="width: 18rem;">
|
||||||
|
<img src="@routes.Assets.versioned("images/profile.png")" alt="Profile" class="card-img-top w-50 mx-auto mt-3" />
|
||||||
|
<div class="card-body">
|
||||||
|
@if(playersession.id == user.get.id) {
|
||||||
|
<h5 class="card-title">@playersession.name (You)</h5>
|
||||||
|
<p class="card-text">Your text could be here!</p>
|
||||||
|
<a href="#" class="btn btn-danger disabled" aria-disabled="true" tabindex="-1">Remove</a>
|
||||||
|
} else {
|
||||||
|
<h5 class="card-title">@playersession.name</h5>
|
||||||
|
<p class="card-text">Your text could be here!</p>
|
||||||
|
<form action="@(routes.IngameController.kickPlayer(gamelobby.id, playersession.id))" method="post">
|
||||||
|
<button type="submit" class="btn btn-danger">Remove</button>
|
||||||
|
</form>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
<div class="row">
|
||||||
|
<div class="col text-center mt-3">
|
||||||
|
<a href="@(routes.IngameController.startGame(gamelobby.id))" class="btn btn-success">Start Game</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
} else {
|
||||||
|
@for(playersession <- gamelobby.getPlayers.values) {
|
||||||
|
<div class="col-auto">
|
||||||
|
<div class="card" style="width: 18rem;">
|
||||||
|
<img src="@routes.Assets.versioned("images/profile.png")" alt="Profile" class="card-img-top w-50 mx-auto mt-3" />
|
||||||
|
<div class="card-body">
|
||||||
|
@if(playersession.id == user.get.id) {
|
||||||
|
<h5 class="card-title">@playersession.name (You)</h5>
|
||||||
|
<p class="card-text">Your text could be here!</p>
|
||||||
|
} else {
|
||||||
|
<h5 class="card-title">@playersession.name</h5>
|
||||||
|
<p class="card-text">Your text could be here!</p>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
<div class="row">
|
||||||
|
<div class="col mt-3">
|
||||||
|
<p class="text-center fs-4">Waiting for the host to start the game...</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="row">
|
||||||
|
<div class="col mt-1">
|
||||||
|
<div class="text-center">
|
||||||
|
<div class="spinner-border" role="status">
|
||||||
|
<span class="visually-hidden">Loading...</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
@@ -1,20 +1,21 @@
|
|||||||
@()
|
@()
|
||||||
|
|
||||||
@main("Login") {
|
@main("Login") {
|
||||||
|
|
||||||
<div class="login-box">
|
<div class="login-box">
|
||||||
<div class="card login-card p-4">
|
<div class="card login-card p-4">
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
<h3 class="text-center mb-4">Login</h3>
|
<h3 class="text-center mb-4 text-body">Login</h3>
|
||||||
|
|
||||||
<form action="@routes.UserController.login_Post()" method="post">
|
<form action="@routes.UserController.login_Post()" method="post">
|
||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
<label for="username" class="form-label">Username</label>
|
<label for="username" class="form-label text-body">Username</label>
|
||||||
<input type="text" class="form-control" id="username" name="username" placeholder="Enter Username" required>
|
<input type="text" class="form-control text-body" id="username" name="username" placeholder="Enter Username" required>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
<label for="password" class="form-label">Password</label>
|
<label for="password" class="form-label text-body">Password</label>
|
||||||
<input type="password" class="form-control" id="password" name="password" placeholder="Enter password" required>
|
<input type="password" class="form-control text-body" id="password" name="password" placeholder="Enter password" required>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="d-flex justify-content-between align-items-center mb-3">
|
<div class="d-flex justify-content-between align-items-center mb-3">
|
||||||
@@ -34,6 +35,11 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<script src="@routes.Assets.versioned("/javascripts/particles.js")"></script>
|
<script src="@routes.Assets.versioned("/javascripts/particles.js")"></script>
|
||||||
|
<script>
|
||||||
|
particlesJS.load('particles-js', 'assets/conf/particlesjs-config.json', function() {
|
||||||
|
console.log('callback - particles.js config loaded');
|
||||||
|
});
|
||||||
|
</script>
|
||||||
<div id="particles-js" style="background-color: rgb(182, 25, 36);
|
<div id="particles-js" style="background-color: rgb(182, 25, 36);
|
||||||
background-size: cover;
|
background-size: cover;
|
||||||
background-repeat: no-repeat;
|
background-repeat: no-repeat;
|
||||||
|
|||||||
@@ -3,24 +3,30 @@
|
|||||||
* handles the rendering of the page header and body tags. It takes
|
* handles the rendering of the page header and body tags. It takes
|
||||||
* two arguments, a `String` for the title of the page and an `Html`
|
* two arguments, a `String` for the title of the page and an `Html`
|
||||||
* object to insert into the body of the page.
|
* object to insert into the body of the page.
|
||||||
*@
|
*@
|
||||||
@(title: String)(content: Html)
|
@(title: String)(content: Html)
|
||||||
|
|
||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html lang="en">
|
<html lang="en">
|
||||||
<head>
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
@* Here's where we render the page title `String`. *@
|
@* Here's where we render the page title `String`. *@
|
||||||
<title>@title</title>
|
<title>@title</title>
|
||||||
<link rel="stylesheet" media="screen" href="@routes.Assets.versioned("stylesheets/main.css")">
|
<link rel="stylesheet" media="screen" href="@routes.Assets.versioned("stylesheets/main.css")">
|
||||||
<link rel="shortcut icon" type="image/png" href="@routes.Assets.versioned("images/favicon.png")">
|
<link rel="shortcut icon" type="image/png" href="@routes.Assets.versioned("images/favicon.png")">
|
||||||
<link href="https://cdn.jsdelivr.net/npm/bootstrap@@5.3.8/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-sRIl4kxILFvY47J16cr9ZwB07vP4J8+LH7qKQnuqkuIAvNWLzeN8tE5YBujZqJLB" crossorigin="anonymous">
|
<link href="https://cdn.jsdelivr.net/npm/bootstrap@@5.3.8/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-sRIl4kxILFvY47J16cr9ZwB07vP4J8+LH7qKQnuqkuIAvNWLzeN8tE5YBujZqJLB" crossorigin="anonymous">
|
||||||
|
|
||||||
</head>
|
</head>
|
||||||
|
|
||||||
<body>
|
<body class="d-flex flex-column min-vh-100 game-field-background">
|
||||||
@* And here's where we render the `Html` object containing
|
<main>
|
||||||
* the page content. *@
|
@* And here's where we render the `Html` object containing
|
||||||
@content
|
* the page content. *@
|
||||||
|
@content
|
||||||
|
</main>
|
||||||
|
|
||||||
|
<footer class="footer">
|
||||||
|
</footer>
|
||||||
|
|
||||||
<script src="@routes.Assets.versioned("javascripts/main.js")" type="text/javascript"></script>
|
<script src="@routes.Assets.versioned("javascripts/main.js")" type="text/javascript"></script>
|
||||||
<script src="https://cdn.jsdelivr.net/npm/bootstrap@@5.3.8/dist/js/bootstrap.bundle.min.js" integrity="sha384-FKyoEForCGlyvwx9Hj09JcYn3nv7wiPVlz7YYwJrWVcXK/BmnVDxM+D2scQbITxI" crossorigin="anonymous"></script>
|
<script src="https://cdn.jsdelivr.net/npm/bootstrap@@5.3.8/dist/js/bootstrap.bundle.min.js" integrity="sha384-FKyoEForCGlyvwx9Hj09JcYn3nv7wiPVlz7YYwJrWVcXK/BmnVDxM+D2scQbITxI" crossorigin="anonymous"></script>
|
||||||
|
|||||||
32
knockoutwhistweb/app/views/mainmenu/creategame.scala.html
Normal file
32
knockoutwhistweb/app/views/mainmenu/creategame.scala.html
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
@(user: Option[model.users.User])
|
||||||
|
|
||||||
|
@main("Create Game") {
|
||||||
|
@navbar(user)
|
||||||
|
<form action="@routes.MainMenuController.createGame()" method="post" class="game-field-background">
|
||||||
|
<div class="w-50 mx-auto">
|
||||||
|
<div class="mt-3">
|
||||||
|
<label for="lobbyname" class="form-label">Lobby-Name</label>
|
||||||
|
<input type="text" class="form-control" id="lobbyname" name="lobbyname" placeholder="Lobby 1" required>
|
||||||
|
</div>
|
||||||
|
<div class="form-check form-switch mt-3">
|
||||||
|
<input class="form-check-input" type="checkbox" id="visibilityswitch" disabled>
|
||||||
|
<label class="form-check-label" for="visibilityswitch">public/private</label>
|
||||||
|
</div>
|
||||||
|
<div class="mt-3">
|
||||||
|
<label for="playeramount" class="form-label">Playeramount:</label>
|
||||||
|
<input type="range" class="form-range text-body" min="2" max="7" value="2" id="playeramount" name="playeramount">
|
||||||
|
<div class="d-flex justify-content-between">
|
||||||
|
<span>2</span>
|
||||||
|
<span>3</span>
|
||||||
|
<span>4</span>
|
||||||
|
<span>5</span>
|
||||||
|
<span>6</span>
|
||||||
|
<span>7</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="mt-3 text-center">
|
||||||
|
<button type="submit" class="btn btn-success">Create Game</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
}
|
||||||
@@ -1,17 +1,16 @@
|
|||||||
@(user: Option[model.users.User])
|
@(user: Option[model.users.User])
|
||||||
@main("Knockout Whist - Main Menu") {
|
|
||||||
<nav class="navbar navbar-expand-lg bg-body-tertiary">
|
<nav class="navbar navbar-expand-lg bg-body-tertiary">
|
||||||
<div class="container-fluid">
|
<div class="container d-flex justify-content-center">
|
||||||
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navBar" aria-controls="navBar" aria-expanded="false" aria-label="Toggle navigation">
|
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navBar" aria-controls="navBar" aria-expanded="false" aria-label="Toggle navigation">
|
||||||
<span class="navbar-toggler-icon"></span>
|
<span class="navbar-toggler-icon"></span>
|
||||||
</button>
|
</button>
|
||||||
<div class="collapse navbar-collapse" id="navBar">
|
<div class="collapse navbar-collapse justify-content-center" id="navBar">
|
||||||
<a class="navbar-brand" href="@routes.MainMenuController.mainMenu()">KnockOutWhist</a>
|
<a class="navbar-brand ms-auto" href="@routes.MainMenuController.mainMenu()">KnockOutWhist</a>
|
||||||
<div class="navbar-nav me-auto mb-2 mb-lg-0">
|
<div class="navbar-nav me-auto mb-2 mb-lg-0">
|
||||||
<ul class="navbar-nav mb-2 mb-lg-0">
|
<ul class="navbar-nav mb-2 mb-lg-0">
|
||||||
@if(user.isDefined) {
|
@if(user.isDefined) {
|
||||||
<li class="nav-item">
|
<li class="nav-item">
|
||||||
<a class="nav-link active" aria-current="page" href="#">Create Game</a>
|
<a class="nav-link active" aria-current="page" href="@routes.MainMenuController.mainMenu()">Create Game</a>
|
||||||
</li>
|
</li>
|
||||||
<li class="nav-item">
|
<li class="nav-item">
|
||||||
<a class="nav-link disabled" aria-disabled="true">Lobbies</a>
|
<a class="nav-link disabled" aria-disabled="true">Lobbies</a>
|
||||||
@@ -53,4 +52,3 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</nav>
|
</nav>
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,63 +1,76 @@
|
|||||||
@()
|
@(user: Option[model.users.User])
|
||||||
|
|
||||||
@main("Rules") {
|
@main("Rules") {
|
||||||
<div id="rules" class="game-field game-field-background">
|
@navbar(user)
|
||||||
<table>
|
<div id="rules">
|
||||||
<caption>Rules Overview and Equipment</caption>
|
<div class="container my-4">
|
||||||
<thead>
|
<div class="card shadow-sm rounded-3">
|
||||||
<tr>
|
<div class="card-header text-white text-center">
|
||||||
<th>Section</th>
|
<h4 class="mb-0 text-body">Game Rules Overview</h4>
|
||||||
<th>Details</th>
|
</div>
|
||||||
</tr>
|
<div class="card-body p-0">
|
||||||
</thead>
|
<div class="table-responsive">
|
||||||
<tbody>
|
<table class="table table-striped table-hover mb-0 align-middle">
|
||||||
<tr>
|
<thead class="table-dark">
|
||||||
<td>Players</td>
|
<tr>
|
||||||
<td>Two to seven players. The aim is to be the last player left in the game.</td>
|
<th scope="col">Section</th>
|
||||||
</tr>
|
<th scope="col">Details</th>
|
||||||
<tr>
|
</tr>
|
||||||
<td>Aim</td>
|
</thead>
|
||||||
<td>To be the last player left in at the end of the game, with the object in each hand being to win a majority of tricks.</td>
|
<tbody>
|
||||||
</tr>
|
<tr>
|
||||||
<tr>
|
<td>Players</td>
|
||||||
<td>Equipment</td>
|
<td>Two to seven players. The aim is to be the last player left in the game.</td>
|
||||||
<td>A standard 52-card pack is used.</td>
|
</tr>
|
||||||
</tr>
|
<tr>
|
||||||
<tr>
|
<td>Aim</td>
|
||||||
<td>Card Ranks</td>
|
<td>To be the last player left at the end of the game, with the object in each hand being to win a majority of tricks.</td>
|
||||||
<td>In each suit, cards rank from highest to lowest: A K Q J 10 9 8 7 6 5 4 3 2.</td>
|
</tr>
|
||||||
</tr>
|
<tr>
|
||||||
<tr>
|
<td>Equipment</td>
|
||||||
<td>Deal (First Hand)</td>
|
<td>A standard 52-card pack is used.</td>
|
||||||
<td>The dealer deals seven cards to each player. One card is turned up to determine the trump suit for the round.</td>
|
</tr>
|
||||||
</tr>
|
<tr>
|
||||||
<tr>
|
<td>Card Ranks</td>
|
||||||
<td>Deal (Subsequent Hands)</td>
|
<td>In each suit, cards rank from highest to lowest: A K Q J 10 9 8 7 6 5 4 3 2.</td>
|
||||||
<td>The deal rotates clockwise. The player who took the most tricks in the previous hand selects the trump suit. If there's a tie for the highest number of tricks, players cut cards to decide who calls trumps. One fewer card is dealt in each successive hand until the final hand consists of one card each.</td>
|
</tr>
|
||||||
</tr>
|
<tr>
|
||||||
<tr>
|
<td>Deal (First Hand)</td>
|
||||||
<td>Play</td>
|
<td>The dealer deals seven cards to each player. One card is turned up to determine the trump suit for the round.</td>
|
||||||
<td>The player to the dealer's left (eldest hand) leads the first trick. Any card can be led. Other players must follow suit if possible. A player with no cards of the suit led may play any card.</td>
|
</tr>
|
||||||
</tr>
|
<tr>
|
||||||
<tr>
|
<td>Deal (Subsequent Hands)</td>
|
||||||
<td>Winning a Trick</td>
|
<td>The deal rotates clockwise. The player who took the most tricks in the previous hand selects the trump suit. If there's a tie, players cut cards to decide who calls trumps. One fewer card is dealt in each successive hand until the final hand consists of one card each.</td>
|
||||||
<td>The highest card of the suit led wins, unless a trump is played, in which case the highest trump wins. The winner of the trick leads the next.</td>
|
</tr>
|
||||||
</tr>
|
<tr>
|
||||||
<tr>
|
<td>Play</td>
|
||||||
<td>Leading Trumps</td>
|
<td>The player to the dealer's left (eldest hand) leads the first trick. Any card can be led. Other players must follow suit if possible. A player with no cards of the suit led may play any card.</td>
|
||||||
<td>Some rules disallow leading trumps before the suit has been 'broken' (a trump has been played to the lead of another suit). Leading trumps is always permissible if a player holds only trumps.</td>
|
</tr>
|
||||||
</tr>
|
<tr>
|
||||||
<tr>
|
<td>Winning a Trick</td>
|
||||||
<td>Knockout</td>
|
<td>The highest card of the suit led wins, unless a trump is played, in which case the highest trump wins. The winner of the trick leads the next.</td>
|
||||||
<td>At the end of each hand, any player who took no tricks is knocked out and takes no further part in the game.</td>
|
</tr>
|
||||||
</tr>
|
<tr>
|
||||||
<tr>
|
<td>Leading Trumps</td>
|
||||||
<td>Winning the Game</td>
|
<td>Some rules disallow leading trumps before the suit has been 'broken' (a trump has been played to the lead of another suit). Leading trumps is always permissible if a player holds only trumps.</td>
|
||||||
<td>The game is won when a player takes all the tricks in a round, as all other players are knocked out, leaving only one player remaining.</td>
|
</tr>
|
||||||
</tr>
|
<tr>
|
||||||
</tbody>
|
<td>Knockout</td>
|
||||||
<td>Dog Life</td>
|
<td>At the end of each hand, any player who took no tricks is knocked out and takes no further part in the game.</td>
|
||||||
<td>The first player who takes no tricks is awarded a "dog's life". In the next hand, that player is dealt one card and can decide which trick to play it to. Each time a trick is played the "dog" may either play the card or knock on the table and wait to play it later. If the dog wins a trick, the player to the left leads to the next and the dog re-enters the game properly in the next hand. If the dog fails, they are knocked out.</td>
|
</tr>
|
||||||
</table>
|
<tr>
|
||||||
|
<td>Winning the Game</td>
|
||||||
|
<td>The game is won when a player takes all the tricks in a round, as all other players are knocked out, leaving only one player remaining.</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>Dog Life</td>
|
||||||
|
<td>The first player who takes no tricks is awarded a "dog's life". In the next hand, that player is dealt one card and can decide which trick to play it to. Each time a trick is played the "dog" may either play the card or knock on the table and wait to play it later. If the dog wins a trick, the player to the left leads the next and the dog re-enters the game properly in the next hand. If the dog fails, they are knocked out.</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
@@ -1,2 +1,2 @@
|
|||||||
@(src: String)(alt: String)
|
@(src: String)(alt: String)
|
||||||
<img src="@routes.Assets.versioned(src)" alt="@alt"/>
|
<img src="@routes.Assets.versioned(src)" alt="@alt"
|
||||||
@@ -24,6 +24,7 @@ GET /logout controllers.UserController.logout()
|
|||||||
# In-game routes
|
# In-game routes
|
||||||
GET /game/:id controllers.IngameController.game(id: String)
|
GET /game/:id controllers.IngameController.game(id: String)
|
||||||
GET /game/:id/join controllers.IngameController.joinGame(id: String)
|
GET /game/:id/join controllers.IngameController.joinGame(id: String)
|
||||||
POST /game/:id/start controllers.IngameController.startGame(id: String)
|
GET /game/:id/start controllers.IngameController.startGame(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)
|
POST /game/:id/playCard controllers.IngameController.playCard(id: String)
|
||||||
@@ -1,3 +1,80 @@
|
|||||||
particlesJS.load('particles-js', 'assets/conf/particlesjs-config.json', function() {
|
/*!
|
||||||
console.log('callback - particles.js config loaded');
|
* Color mode toggler for Bootstrap's docs (https://getbootstrap.com/)
|
||||||
});
|
* Copyright 2011-2025 The Bootstrap Authors
|
||||||
|
* Licensed under the Creative Commons Attribution 3.0 Unported License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
(() => {
|
||||||
|
'use strict'
|
||||||
|
|
||||||
|
const getStoredTheme = () => localStorage.getItem('theme')
|
||||||
|
const setStoredTheme = theme => localStorage.setItem('theme', theme)
|
||||||
|
|
||||||
|
const getPreferredTheme = () => {
|
||||||
|
const storedTheme = getStoredTheme()
|
||||||
|
if (storedTheme) {
|
||||||
|
return storedTheme
|
||||||
|
}
|
||||||
|
|
||||||
|
return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light'
|
||||||
|
}
|
||||||
|
|
||||||
|
const setTheme = theme => {
|
||||||
|
if (theme === 'auto') {
|
||||||
|
document.documentElement.setAttribute('data-bs-theme', (window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light'))
|
||||||
|
} else {
|
||||||
|
document.documentElement.setAttribute('data-bs-theme', theme)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
setTheme(getPreferredTheme())
|
||||||
|
|
||||||
|
const showActiveTheme = (theme, focus = false) => {
|
||||||
|
const themeSwitcher = document.querySelector('#bd-theme')
|
||||||
|
|
||||||
|
if (!themeSwitcher) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const themeSwitcherText = document.querySelector('#bd-theme-text')
|
||||||
|
const activeThemeIcon = document.querySelector('.theme-icon-active use')
|
||||||
|
const btnToActive = document.querySelector(`[data-bs-theme-value="${theme}"]`)
|
||||||
|
const svgOfActiveBtn = btnToActive.querySelector('svg use').getAttribute('href')
|
||||||
|
|
||||||
|
document.querySelectorAll('[data-bs-theme-value]').forEach(element => {
|
||||||
|
element.classList.remove('active')
|
||||||
|
element.setAttribute('aria-pressed', 'false')
|
||||||
|
})
|
||||||
|
|
||||||
|
btnToActive.classList.add('active')
|
||||||
|
btnToActive.setAttribute('aria-pressed', 'true')
|
||||||
|
activeThemeIcon.setAttribute('href', svgOfActiveBtn)
|
||||||
|
const themeSwitcherLabel = `${themeSwitcherText.textContent} (${btnToActive.dataset.bsThemeValue})`
|
||||||
|
themeSwitcher.setAttribute('aria-label', themeSwitcherLabel)
|
||||||
|
|
||||||
|
if (focus) {
|
||||||
|
themeSwitcher.focus()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', () => {
|
||||||
|
const storedTheme = getStoredTheme()
|
||||||
|
if (storedTheme !== 'light' && storedTheme !== 'dark') {
|
||||||
|
setTheme(getPreferredTheme())
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
window.addEventListener('DOMContentLoaded', () => {
|
||||||
|
showActiveTheme(getPreferredTheme())
|
||||||
|
|
||||||
|
document.querySelectorAll('[data-bs-theme-value]')
|
||||||
|
.forEach(toggle => {
|
||||||
|
toggle.addEventListener('click', () => {
|
||||||
|
const theme = toggle.getAttribute('data-bs-theme-value')
|
||||||
|
setStoredTheme(theme)
|
||||||
|
setTheme(theme)
|
||||||
|
showActiveTheme(theme, true)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})()
|
||||||
Reference in New Issue
Block a user