feat: Add database configuration and update routing for game creation
@@ -1,444 +0,0 @@
|
|||||||
package controllers
|
|
||||||
|
|
||||||
import auth.{AuthAction, AuthenticatedRequest}
|
|
||||||
import de.knockoutwhist.control.GameState
|
|
||||||
import de.knockoutwhist.control.GameState.*
|
|
||||||
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 util.GameUtil
|
|
||||||
|
|
||||||
import java.util.UUID
|
|
||||||
import javax.inject.*
|
|
||||||
import scala.concurrent.ExecutionContext
|
|
||||||
import scala.util.Try
|
|
||||||
|
|
||||||
@Singleton
|
|
||||||
class IngameController @Inject()(
|
|
||||||
val cc: ControllerComponents,
|
|
||||||
val authAction: AuthAction,
|
|
||||||
implicit val ec: ExecutionContext
|
|
||||||
) extends AbstractController(cc) {
|
|
||||||
|
|
||||||
def game(gameId: String): Action[AnyContent] = authAction { implicit request: AuthenticatedRequest[AnyContent] =>
|
|
||||||
val game = PodManager.getGame(gameId)
|
|
||||||
game match {
|
|
||||||
case Some(g) =>
|
|
||||||
val results = Try {
|
|
||||||
IngameController.returnInnerHTML(g, g.logic.getCurrentState, request.user)
|
|
||||||
}
|
|
||||||
if (results.isSuccess) {
|
|
||||||
Ok(views.html.main("Knockout Whist - " + GameUtil.stateToTitle(g.logic.getCurrentState))(results.get))
|
|
||||||
} else {
|
|
||||||
InternalServerError(results.failed.get.getMessage)
|
|
||||||
}
|
|
||||||
case None =>
|
|
||||||
Redirect(routes.MainMenuController.mainMenu())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
def startGame(gameId: String): Action[AnyContent] = authAction { implicit request: AuthenticatedRequest[AnyContent] =>
|
|
||||||
val game = PodManager.getGame(gameId)
|
|
||||||
val result = Try {
|
|
||||||
game match {
|
|
||||||
case Some(g) =>
|
|
||||||
g.startGame(request.user)
|
|
||||||
case None =>
|
|
||||||
NotFound("Game not found")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (result.isSuccess) {
|
|
||||||
Ok(Json.obj(
|
|
||||||
"status" -> "success",
|
|
||||||
"redirectUrl" -> routes.IngameController.game(gameId).url,
|
|
||||||
"content" -> IngameController.returnInnerHTML(game.get, game.get.logic.getCurrentState, request.user).toString()
|
|
||||||
))
|
|
||||||
} else {
|
|
||||||
val throwable = result.failed.get
|
|
||||||
throwable match {
|
|
||||||
case _: NotInThisGameException =>
|
|
||||||
BadRequest(Json.obj(
|
|
||||||
"status" -> "failure",
|
|
||||||
"errorMessage" -> throwable.getMessage
|
|
||||||
))
|
|
||||||
case _: NotHostException =>
|
|
||||||
Forbidden(Json.obj(
|
|
||||||
"status" -> "failure",
|
|
||||||
"errorMessage" -> throwable.getMessage
|
|
||||||
))
|
|
||||||
case _: NotEnoughPlayersException =>
|
|
||||||
BadRequest(Json.obj(
|
|
||||||
"status" -> "failure",
|
|
||||||
"errorMessage" -> throwable.getMessage
|
|
||||||
))
|
|
||||||
case _ =>
|
|
||||||
InternalServerError(Json.obj(
|
|
||||||
"status" -> "failure",
|
|
||||||
"errorMessage" -> throwable.getMessage
|
|
||||||
))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
def kickPlayer(gameId: String, playerToKick: String): Action[AnyContent] = authAction { implicit request: AuthenticatedRequest[AnyContent] =>
|
|
||||||
val game = PodManager.getGame(gameId)
|
|
||||||
val playerToKickUUID = UUID.fromString(playerToKick)
|
|
||||||
val result = Try {
|
|
||||||
game.get.leaveGame(playerToKickUUID, true)
|
|
||||||
}
|
|
||||||
if (result.isSuccess) {
|
|
||||||
Ok(Json.obj(
|
|
||||||
"status" -> "success",
|
|
||||||
"redirectUrl" -> routes.IngameController.game(gameId).url
|
|
||||||
))
|
|
||||||
} else {
|
|
||||||
InternalServerError(Json.obj(
|
|
||||||
"status" -> "failure",
|
|
||||||
"errorMessage" -> "Something went wrong."
|
|
||||||
))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
def leaveGame(gameId: String): Action[AnyContent] = authAction { implicit request: AuthenticatedRequest[AnyContent] =>
|
|
||||||
val game = PodManager.getGame(gameId)
|
|
||||||
val result = Try {
|
|
||||||
game.get.leaveGame(request.user.id, false)
|
|
||||||
}
|
|
||||||
if (result.isSuccess) {
|
|
||||||
Ok(Json.obj(
|
|
||||||
"status" -> "success",
|
|
||||||
"redirectUrl" -> routes.MainMenuController.mainMenu().url,
|
|
||||||
"content" -> views.html.mainmenu.creategame(Some(request.user)).toString
|
|
||||||
))
|
|
||||||
} else {
|
|
||||||
InternalServerError(Json.obj(
|
|
||||||
"status" -> "failure",
|
|
||||||
"errorMessage" -> "Something went wrong."
|
|
||||||
))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
def playCard(gameId: String): Action[AnyContent] = authAction { implicit request: AuthenticatedRequest[AnyContent] => {
|
|
||||||
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]
|
|
||||||
}
|
|
||||||
cardIdOpt match {
|
|
||||||
case Some(cardId) =>
|
|
||||||
var optSession: Option[UserSession] = None
|
|
||||||
val result = Try {
|
|
||||||
val session = g.getUserSession(request.user.id)
|
|
||||||
optSession = Some(session)
|
|
||||||
session.lock.lock()
|
|
||||||
g.playCard(session, cardId.toInt)
|
|
||||||
}
|
|
||||||
optSession.foreach(_.lock.unlock())
|
|
||||||
if (result.isSuccess) {
|
|
||||||
Ok(Json.obj(
|
|
||||||
"status" -> "success"
|
|
||||||
))
|
|
||||||
} else {
|
|
||||||
val throwable = result.failed.get
|
|
||||||
throwable match {
|
|
||||||
case _: CantPlayCardException =>
|
|
||||||
BadRequest(Json.obj(
|
|
||||||
"status" -> "failure",
|
|
||||||
"errorMessage" -> throwable.getMessage
|
|
||||||
))
|
|
||||||
case _: NotInThisGameException =>
|
|
||||||
BadRequest(Json.obj(
|
|
||||||
"status" -> "failure",
|
|
||||||
"errorMessage" -> throwable.getMessage
|
|
||||||
))
|
|
||||||
case _: IllegalArgumentException =>
|
|
||||||
BadRequest(Json.obj(
|
|
||||||
"status" -> "failure",
|
|
||||||
"errorMessage" -> throwable.getMessage
|
|
||||||
))
|
|
||||||
case _: IllegalStateException =>
|
|
||||||
BadRequest(Json.obj(
|
|
||||||
"status" -> "failure",
|
|
||||||
"errorMessage" -> throwable.getMessage
|
|
||||||
))
|
|
||||||
case _: NotInteractableException =>
|
|
||||||
BadRequest(Json.obj(
|
|
||||||
"status" -> "failure",
|
|
||||||
"errorMessage" -> throwable.getMessage
|
|
||||||
))
|
|
||||||
case _ =>
|
|
||||||
InternalServerError(Json.obj(
|
|
||||||
"status" -> "failure",
|
|
||||||
"errorMessage" -> throwable.getMessage
|
|
||||||
))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
case None =>
|
|
||||||
BadRequest(Json.obj(
|
|
||||||
"status" -> "failure",
|
|
||||||
"errorMessage" -> "cardId Parameter is missing"
|
|
||||||
))
|
|
||||||
}
|
|
||||||
case None =>
|
|
||||||
NotFound(Json.obj(
|
|
||||||
"status" -> "failure",
|
|
||||||
"errorMessage" -> "Game not found"
|
|
||||||
))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
def playDogCard(gameId: String): Action[AnyContent] = authAction { implicit request: AuthenticatedRequest[AnyContent] => {
|
|
||||||
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]
|
|
||||||
}
|
|
||||||
var optSession: Option[UserSession] = None
|
|
||||||
val result = Try {
|
|
||||||
cardIdOpt match {
|
|
||||||
case Some(cardId) if cardId == "skip" =>
|
|
||||||
val session = g.getUserSession(request.user.id)
|
|
||||||
optSession = Some(session)
|
|
||||||
session.lock.lock()
|
|
||||||
g.playDogCard(session, -1)
|
|
||||||
case Some(cardId) =>
|
|
||||||
val session = g.getUserSession(request.user.id)
|
|
||||||
optSession = Some(session)
|
|
||||||
session.lock.lock()
|
|
||||||
g.playDogCard(session, cardId.toInt)
|
|
||||||
case None =>
|
|
||||||
throw new IllegalArgumentException("cardId parameter is missing")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
optSession.foreach(_.lock.unlock())
|
|
||||||
if (result.isSuccess) {
|
|
||||||
NoContent
|
|
||||||
} else {
|
|
||||||
val throwable = result.failed.get
|
|
||||||
throwable match {
|
|
||||||
case _: CantPlayCardException =>
|
|
||||||
BadRequest(Json.obj(
|
|
||||||
"status" -> "failure",
|
|
||||||
"errorMessage" -> throwable.getMessage
|
|
||||||
))
|
|
||||||
case _: NotInThisGameException =>
|
|
||||||
BadRequest(Json.obj(
|
|
||||||
"status" -> "failure",
|
|
||||||
"errorMessage" -> throwable.getMessage
|
|
||||||
))
|
|
||||||
case _: IllegalArgumentException =>
|
|
||||||
BadRequest(Json.obj(
|
|
||||||
"status" -> "failure",
|
|
||||||
"errorMessage" -> throwable.getMessage
|
|
||||||
))
|
|
||||||
case _: IllegalStateException =>
|
|
||||||
BadRequest(Json.obj(
|
|
||||||
"status" -> "failure",
|
|
||||||
"errorMessage" -> throwable.getMessage
|
|
||||||
))
|
|
||||||
case _ =>
|
|
||||||
InternalServerError(Json.obj(
|
|
||||||
"status" -> "failure",
|
|
||||||
"errorMessage" -> throwable.getMessage
|
|
||||||
))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
case None =>
|
|
||||||
NotFound("Game not found")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
def playTrump(gameId: String): Action[AnyContent] = authAction { implicit request: AuthenticatedRequest[AnyContent] =>
|
|
||||||
val game = PodManager.getGame(gameId)
|
|
||||||
game match {
|
|
||||||
case Some(g) =>
|
|
||||||
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
|
|
||||||
val result = Try {
|
|
||||||
val session = g.getUserSession(request.user.id)
|
|
||||||
optSession = Some(session)
|
|
||||||
session.lock.lock()
|
|
||||||
g.selectTrump(session, trump.toInt)
|
|
||||||
}
|
|
||||||
optSession.foreach(_.lock.unlock())
|
|
||||||
if (result.isSuccess) {
|
|
||||||
NoContent
|
|
||||||
} else {
|
|
||||||
val throwable = result.failed.get
|
|
||||||
throwable match {
|
|
||||||
case _: IllegalArgumentException =>
|
|
||||||
BadRequest(Json.obj(
|
|
||||||
"status" -> "failure",
|
|
||||||
"errorMessage" -> throwable.getMessage
|
|
||||||
))
|
|
||||||
case _: NotInThisGameException =>
|
|
||||||
BadRequest(Json.obj(
|
|
||||||
"status" -> "failure",
|
|
||||||
"errorMessage" -> throwable.getMessage
|
|
||||||
))
|
|
||||||
case _: IllegalStateException =>
|
|
||||||
BadRequest(Json.obj(
|
|
||||||
"status" -> "failure",
|
|
||||||
"errorMessage" -> throwable.getMessage
|
|
||||||
))
|
|
||||||
case _ =>
|
|
||||||
InternalServerError(Json.obj(
|
|
||||||
"status" -> "failure",
|
|
||||||
"errorMessage" -> throwable.getMessage
|
|
||||||
))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
case None =>
|
|
||||||
BadRequest("trump parameter is missing")
|
|
||||||
}
|
|
||||||
case None =>
|
|
||||||
NotFound("Game not found")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
def playTie(gameId: String): Action[AnyContent] = authAction { implicit request: AuthenticatedRequest[AnyContent] =>
|
|
||||||
val game = PodManager.getGame(gameId)
|
|
||||||
game match {
|
|
||||||
case Some(g) =>
|
|
||||||
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
|
|
||||||
val result = Try {
|
|
||||||
val session = g.getUserSession(request.user.id)
|
|
||||||
optSession = Some(session)
|
|
||||||
session.lock.lock()
|
|
||||||
g.selectTie(g.getUserSession(request.user.id), tie.toInt)
|
|
||||||
}
|
|
||||||
optSession.foreach(_.lock.unlock())
|
|
||||||
if (result.isSuccess) {
|
|
||||||
NoContent
|
|
||||||
} else {
|
|
||||||
val throwable = result.failed.get
|
|
||||||
throwable match {
|
|
||||||
case _: IllegalArgumentException =>
|
|
||||||
BadRequest(Json.obj(
|
|
||||||
"status" -> "failure",
|
|
||||||
"errorMessage" -> throwable.getMessage
|
|
||||||
))
|
|
||||||
case _: NotInThisGameException =>
|
|
||||||
BadRequest(Json.obj(
|
|
||||||
"status" -> "failure",
|
|
||||||
"errorMessage" -> throwable.getMessage
|
|
||||||
))
|
|
||||||
case _: IllegalStateException =>
|
|
||||||
BadRequest(Json.obj(
|
|
||||||
"status" -> "failure",
|
|
||||||
"errorMessage" -> throwable.getMessage
|
|
||||||
))
|
|
||||||
case _ =>
|
|
||||||
InternalServerError(Json.obj(
|
|
||||||
"status" -> "failure",
|
|
||||||
"errorMessage" -> throwable.getMessage
|
|
||||||
))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
case None =>
|
|
||||||
BadRequest("tie parameter is missing")
|
|
||||||
}
|
|
||||||
case None =>
|
|
||||||
NotFound("Game not found")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
def returnToLobby(gameId: String): Action[AnyContent] = authAction { implicit request: AuthenticatedRequest[AnyContent] =>
|
|
||||||
val game = PodManager.getGame(gameId)
|
|
||||||
game match {
|
|
||||||
case Some(g) =>
|
|
||||||
val result = Try {
|
|
||||||
val session = g.getUserSession(request.user.id)
|
|
||||||
g.returnToLobby(session)
|
|
||||||
}
|
|
||||||
if (result.isSuccess) {
|
|
||||||
Ok(Json.obj(
|
|
||||||
"status" -> "success",
|
|
||||||
"redirectUrl" -> routes.IngameController.game(gameId).url
|
|
||||||
))
|
|
||||||
} else {
|
|
||||||
val throwable = result.failed.get
|
|
||||||
throwable match {
|
|
||||||
case _: NotInThisGameException =>
|
|
||||||
BadRequest(Json.obj(
|
|
||||||
"status" -> "failure",
|
|
||||||
"errorMessage" -> throwable.getMessage
|
|
||||||
))
|
|
||||||
case _: IllegalStateException =>
|
|
||||||
BadRequest(Json.obj(
|
|
||||||
"status" -> "failure",
|
|
||||||
"errorMessage" -> throwable.getMessage
|
|
||||||
))
|
|
||||||
case _ =>
|
|
||||||
InternalServerError(Json.obj(
|
|
||||||
"status" -> "failure",
|
|
||||||
"errorMessage" -> throwable.getMessage
|
|
||||||
))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
case None =>
|
|
||||||
NotFound(Json.obj(
|
|
||||||
"status" -> "failure",
|
|
||||||
"errorMessage" -> "Game not found"
|
|
||||||
))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
object IngameController {
|
|
||||||
|
|
||||||
def returnInnerHTML(gameLobby: GameLobby, gameState: GameState, user: User): Html = {
|
|
||||||
gameState 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}")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -1,24 +0,0 @@
|
|||||||
package controllers
|
|
||||||
|
|
||||||
import auth.AuthAction
|
|
||||||
import play.api.mvc.{Action, AnyContent, BaseController, ControllerComponents}
|
|
||||||
import play.api.routing.JavaScriptReverseRouter
|
|
||||||
|
|
||||||
import javax.inject.Inject
|
|
||||||
|
|
||||||
class JavaScriptRoutingController @Inject()(
|
|
||||||
val controllerComponents: ControllerComponents,
|
|
||||||
val authAction: AuthAction,
|
|
||||||
) extends BaseController {
|
|
||||||
def javascriptRoutes(): Action[AnyContent] =
|
|
||||||
Action { implicit request =>
|
|
||||||
Ok(
|
|
||||||
JavaScriptReverseRouter("jsRoutes")(
|
|
||||||
routes.javascript.MainMenuController.createGame,
|
|
||||||
routes.javascript.MainMenuController.joinGame,
|
|
||||||
routes.javascript.MainMenuController.navSPA,
|
|
||||||
routes.javascript.UserController.login_Post
|
|
||||||
)
|
|
||||||
).as("text/javascript")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -19,15 +19,6 @@ class MainMenuController @Inject()(
|
|||||||
val authAction: AuthAction
|
val authAction: AuthAction
|
||||||
) extends BaseController {
|
) 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.main("Knockout Whist - Create Game")(views.html.mainmenu.creategame(Some(request.user))))
|
|
||||||
}
|
|
||||||
|
|
||||||
def index(): Action[AnyContent] = authAction { implicit request: AuthenticatedRequest[AnyContent] =>
|
|
||||||
Redirect(routes.MainMenuController.mainMenu())
|
|
||||||
}
|
|
||||||
|
|
||||||
def createGame(): Action[AnyContent] = authAction { implicit request: AuthenticatedRequest[AnyContent] =>
|
def createGame(): Action[AnyContent] = authAction { implicit request: AuthenticatedRequest[AnyContent] =>
|
||||||
val jsonBody = request.body.asJson
|
val jsonBody = request.body.asJson
|
||||||
if (jsonBody.isDefined) {
|
if (jsonBody.isDefined) {
|
||||||
@@ -61,9 +52,7 @@ class MainMenuController @Inject()(
|
|||||||
case Some(g) =>
|
case Some(g) =>
|
||||||
g.addUser(request.user)
|
g.addUser(request.user)
|
||||||
Ok(Json.obj(
|
Ok(Json.obj(
|
||||||
"status" -> "success",
|
"status" -> "success"
|
||||||
"redirectUrl" -> routes.IngameController.game(g.id).url,
|
|
||||||
"content" -> IngameController.returnInnerHTML(g, g.logic.getCurrentState, request.user).toString
|
|
||||||
))
|
))
|
||||||
case None =>
|
case None =>
|
||||||
NotFound(Json.obj(
|
NotFound(Json.obj(
|
||||||
@@ -72,31 +61,4 @@ class MainMenuController @Inject()(
|
|||||||
))
|
))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
def rules(): Action[AnyContent] = authAction { implicit request: AuthenticatedRequest[AnyContent] =>
|
|
||||||
Ok(views.html.main("Knockout Whist - Rules")(views.html.mainmenu.rules(Some(request.user))))
|
|
||||||
}
|
|
||||||
|
|
||||||
def navSPA(location: String): Action[AnyContent] = authAction { implicit request: AuthenticatedRequest[AnyContent] =>
|
|
||||||
location match {
|
|
||||||
case "0" => // Main Menu
|
|
||||||
Ok(Json.obj(
|
|
||||||
"status" -> "success",
|
|
||||||
"redirectUrl" -> routes.MainMenuController.mainMenu().url,
|
|
||||||
"content" -> views.html.mainmenu.creategame(Some(request.user)).toString
|
|
||||||
))
|
|
||||||
case "1" => // Rules
|
|
||||||
Ok(Json.obj(
|
|
||||||
"status" -> "success",
|
|
||||||
"redirectUrl" -> routes.MainMenuController.rules().url,
|
|
||||||
"content" -> views.html.mainmenu.rules(Some(request.user)).toString
|
|
||||||
))
|
|
||||||
case _ =>
|
|
||||||
BadRequest(Json.obj(
|
|
||||||
"status" -> "failure",
|
|
||||||
"errorMessage" -> "Invalid form submission"
|
|
||||||
))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -4,6 +4,7 @@ import auth.AuthAction
|
|||||||
import com.typesafe.config.Config
|
import com.typesafe.config.Config
|
||||||
import logic.user.{SessionManager, UserManager}
|
import logic.user.{SessionManager, UserManager}
|
||||||
import model.users.User
|
import model.users.User
|
||||||
|
import play.api.Configuration
|
||||||
import play.api.libs.json.Json
|
import play.api.libs.json.Json
|
||||||
import play.api.mvc.*
|
import play.api.mvc.*
|
||||||
import play.api.mvc.Cookie.SameSite.Lax
|
import play.api.mvc.Cookie.SameSite.Lax
|
||||||
@@ -18,7 +19,7 @@ class OpenIDController @Inject()(
|
|||||||
val openIDService: OpenIDConnectService,
|
val openIDService: OpenIDConnectService,
|
||||||
val sessionManager: SessionManager,
|
val sessionManager: SessionManager,
|
||||||
val userManager: UserManager,
|
val userManager: UserManager,
|
||||||
val config: Config
|
val config: Configuration
|
||||||
)(implicit ec: ExecutionContext) extends BaseController {
|
)(implicit ec: ExecutionContext) extends BaseController {
|
||||||
|
|
||||||
def loginWithProvider(provider: String) = Action.async { implicit request =>
|
def loginWithProvider(provider: String) = Action.async { implicit request =>
|
||||||
@@ -62,7 +63,7 @@ class OpenIDController @Inject()(
|
|||||||
openIDService.getUserInfo(provider, tokenResponse.accessToken).map {
|
openIDService.getUserInfo(provider, tokenResponse.accessToken).map {
|
||||||
case Some(userInfo) =>
|
case Some(userInfo) =>
|
||||||
// Store user info in session for username selection
|
// Store user info in session for username selection
|
||||||
Redirect("http://localhost:5173/select-username")
|
Redirect(config.get[String]("app.url") + "/select-username")
|
||||||
.withSession(
|
.withSession(
|
||||||
"oauth_user_info" -> Json.toJson(userInfo).toString(),
|
"oauth_user_info" -> Json.toJson(userInfo).toString(),
|
||||||
"oauth_provider" -> provider,
|
"oauth_provider" -> provider,
|
||||||
|
|||||||
@@ -8,9 +8,6 @@ import play.twirl.api.Html
|
|||||||
import scalafx.scene.image.Image
|
import scalafx.scene.image.Image
|
||||||
|
|
||||||
object WebUIUtils {
|
object WebUIUtils {
|
||||||
def cardtoImage(card: Card): Html = {
|
|
||||||
views.html.render.card.apply(cardToPath(card))(card.toString)
|
|
||||||
}
|
|
||||||
|
|
||||||
def cardToPath(card: Card): String = {
|
def cardToPath(card: Card): String = {
|
||||||
f"images/cards/${cardtoString(card)}.png"
|
f"images/cards/${cardtoString(card)}.png"
|
||||||
|
|||||||
@@ -36,16 +36,12 @@ object WebsocketEventMapper {
|
|||||||
|
|
||||||
// Register all custom mappers here
|
// Register all custom mappers here
|
||||||
registerCustomMapper(ReceivedHandEventMapper)
|
registerCustomMapper(ReceivedHandEventMapper)
|
||||||
//registerCustomMapper(GameStateEventMapper)
|
|
||||||
registerCustomMapper(CardPlayedEventMapper)
|
registerCustomMapper(CardPlayedEventMapper)
|
||||||
registerCustomMapper(NewRoundEventMapper)
|
registerCustomMapper(NewRoundEventMapper)
|
||||||
registerCustomMapper(NewTrickEventMapper)
|
registerCustomMapper(NewTrickEventMapper)
|
||||||
registerCustomMapper(TrickEndEventMapper)
|
registerCustomMapper(TrickEndEventMapper)
|
||||||
registerCustomMapper(RequestCardEventMapper)
|
registerCustomMapper(RequestCardEventMapper)
|
||||||
registerCustomMapper(LobbyUpdateEventMapper)
|
registerCustomMapper(LobbyUpdateEventMapper)
|
||||||
registerCustomMapper(LeftEventMapper)
|
|
||||||
registerCustomMapper(KickEventMapper)
|
|
||||||
registerCustomMapper(SessionClosedMapper)
|
|
||||||
registerCustomMapper(TurnEventMapper)
|
registerCustomMapper(TurnEventMapper)
|
||||||
|
|
||||||
def toJson(obj: SimpleEvent, session: UserSession): JsValue = {
|
def toJson(obj: SimpleEvent, session: UserSession): JsValue = {
|
||||||
@@ -54,7 +50,6 @@ object WebsocketEventMapper {
|
|||||||
}else {
|
}else {
|
||||||
None
|
None
|
||||||
}
|
}
|
||||||
//println(s"This is getting sent to client: EVENT: ${obj.id}, STATE: ${session.gameLobby.getLogic.getCurrentState.toString}, STATEDATA: ${stateToJson(session)}, DATA: ${data}")
|
|
||||||
Json.obj(
|
Json.obj(
|
||||||
"id" -> ("request-" + java.util.UUID.randomUUID().toString),
|
"id" -> ("request-" + java.util.UUID.randomUUID().toString),
|
||||||
"event" -> obj.id,
|
"event" -> obj.id,
|
||||||
|
|||||||
@@ -1,19 +0,0 @@
|
|||||||
package util.mapper
|
|
||||||
|
|
||||||
import controllers.IngameController
|
|
||||||
import de.knockoutwhist.events.global.GameStateChangeEvent
|
|
||||||
import model.sessions.UserSession
|
|
||||||
import play.api.libs.json.{JsObject, Json}
|
|
||||||
import util.GameUtil
|
|
||||||
|
|
||||||
object GameStateEventMapper extends SimpleEventMapper[GameStateChangeEvent] {
|
|
||||||
|
|
||||||
override def id: String = "GameStateChangeEvent"
|
|
||||||
|
|
||||||
override def toJson(event: GameStateChangeEvent, session: UserSession): JsObject = {
|
|
||||||
Json.obj(
|
|
||||||
"title" -> ("Knockout Whist - " + GameUtil.stateToTitle(event.newState)),
|
|
||||||
"content" -> IngameController.returnInnerHTML(session.gameLobby, event.newState, session.user).toString
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,19 +0,0 @@
|
|||||||
package util.mapper
|
|
||||||
|
|
||||||
import controllers.routes
|
|
||||||
import events.KickEvent
|
|
||||||
import model.sessions.UserSession
|
|
||||||
import play.api.libs.json.{JsObject, Json}
|
|
||||||
|
|
||||||
object KickEventMapper extends SimpleEventMapper[KickEvent] {
|
|
||||||
|
|
||||||
override def id: String = "KickEvent"
|
|
||||||
|
|
||||||
override def toJson(event: KickEvent, session: UserSession): JsObject = {
|
|
||||||
Json.obj(
|
|
||||||
"url" -> routes.MainMenuController.mainMenu().url,
|
|
||||||
"content" -> views.html.mainmenu.creategame(Some(session.user)).toString,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -1,19 +0,0 @@
|
|||||||
package util.mapper
|
|
||||||
|
|
||||||
import controllers.routes
|
|
||||||
import events.{KickEvent, LeftEvent}
|
|
||||||
import model.sessions.UserSession
|
|
||||||
import play.api.libs.json.{JsObject, Json}
|
|
||||||
|
|
||||||
object LeftEventMapper extends SimpleEventMapper[LeftEvent] {
|
|
||||||
|
|
||||||
override def id: String = "LeftEvent"
|
|
||||||
|
|
||||||
override def toJson(event: LeftEvent, session: UserSession): JsObject = {
|
|
||||||
Json.obj(
|
|
||||||
"url" -> routes.MainMenuController.mainMenu().url,
|
|
||||||
"content" -> views.html.mainmenu.creategame(Some(session.user)).toString
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -1,19 +0,0 @@
|
|||||||
package util.mapper
|
|
||||||
|
|
||||||
import controllers.routes
|
|
||||||
import de.knockoutwhist.events.global.SessionClosed
|
|
||||||
import model.sessions.UserSession
|
|
||||||
import play.api.libs.json.{JsObject, Json}
|
|
||||||
|
|
||||||
object SessionClosedMapper extends SimpleEventMapper[SessionClosed] {
|
|
||||||
|
|
||||||
override def id: String = "SessionClosed"
|
|
||||||
|
|
||||||
override def toJson(event: SessionClosed, session: UserSession): JsObject = {
|
|
||||||
Json.obj(
|
|
||||||
"url" -> routes.MainMenuController.mainMenu().url,
|
|
||||||
"content" -> views.html.mainmenu.creategame(Some(session.user)).toString,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -1,108 +0,0 @@
|
|||||||
@(user: Option[model.users.User], gamelobby: logic.game.GameLobby)
|
|
||||||
|
|
||||||
<div class="lobby-background vh-100 d-flex align-items-start justify-content-center pt-5">
|
|
||||||
<div class="container py-5">
|
|
||||||
<div class="row justify-content-center">
|
|
||||||
<div class="col-lg-8 col-xl-6">
|
|
||||||
<div class="card shadow-lg mb-5 text-center bg-white border-0 rounded-4">
|
|
||||||
<div class="card-body p-4 p-md-5">
|
|
||||||
<h1 class="card-title display-5 fw-bold text-success mb-3">Match Over!</h1>
|
|
||||||
<p class="fs-4 text-muted">Congratulations to the winner:</p>
|
|
||||||
<h2 class="display-3 fw-bolder mb-4 text-primary">
|
|
||||||
@gamelobby.getLogic.getWinner.get.name
|
|
||||||
</h2>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="card shadow mb-5 border-0 rounded-4 overflow-hidden">
|
|
||||||
<div class="card-header bg-dark text-white text-center fs-5 fw-semibold">
|
|
||||||
Final Standings
|
|
||||||
</div>
|
|
||||||
<div class="d-flex justify-content-between align-items-center p-2 text-uppercase fw-bold border-bottom bg-light">
|
|
||||||
<div class="d-flex align-items-center">
|
|
||||||
Player
|
|
||||||
</div>
|
|
||||||
<div class="d-flex flex-row gap-3">
|
|
||||||
<span class="fs-6 text-dark text-center" style="width: 5rem;">Rounds won</span>
|
|
||||||
<span class="fs-6 text-dark text-center" style="width: 5rem;">Tricks won</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
@gamelobby.getFinalRanking.zipWithIndex.map { case ((playerName, (wonRounds, tricksWon)), index) =>
|
|
||||||
@defining(index + 1) { rank =>
|
|
||||||
<div class="d-flex justify-content-between align-items-center p-3 border-bottom @if(rank == 1){bg-success-subtle fw-bold}">
|
|
||||||
<div class="d-flex align-items-center">
|
|
||||||
<span class="badge @if(rank == 1){bg-warning text-dark fs-6} else {bg-secondary} rounded-pill me-3">#@rank</span>
|
|
||||||
@playerName
|
|
||||||
</div>
|
|
||||||
<div class="d-flex flex-row gap-3">
|
|
||||||
<span class="fs-6 text-muted text-center" style="width: 5rem;">@wonRounds</span>
|
|
||||||
<span class="fs-6 text-muted text-center" style="width: 5rem;">@tricksWon</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</div>
|
|
||||||
@if(gamelobby.getFinalRanking.isEmpty) {
|
|
||||||
<div class="p-3 text-center text-muted">No final scores available.</div>
|
|
||||||
}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
|
|
||||||
@if(user.isDefined && gamelobby.getUserSession(user.get.id).host) {
|
|
||||||
<div class="col-12 text-center mt-4">
|
|
||||||
<div class="btn btn-success btn-lg shadow" onclick="handleReturnToLobby()">
|
|
||||||
Return to Lobby
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
} else {
|
|
||||||
<div class="col-12 text-center mt-4">
|
|
||||||
<div class="text-primary">
|
|
||||||
<div class="spinner-grow text-primary mt-1" role="status">
|
|
||||||
<span class="visually-hidden">Loading...</span>
|
|
||||||
</div>
|
|
||||||
<p class="mt-3 fs-6 fw-semibold">
|
|
||||||
Waiting for the Host to continue...
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<script>
|
|
||||||
function fireConfetti() {
|
|
||||||
let duration = 3 * 1000;
|
|
||||||
let animationEnd = Date.now() + duration;
|
|
||||||
let defaults = { startVelocity: 30, spread: 360, ticks: 60, zIndex: 0 };
|
|
||||||
|
|
||||||
function randomInRange(min, max) {
|
|
||||||
return Math.random() * (max - min) + min;
|
|
||||||
}
|
|
||||||
|
|
||||||
let interval = setInterval(function() {
|
|
||||||
let timeLeft = animationEnd - Date.now();
|
|
||||||
|
|
||||||
if (timeLeft <= 0) {
|
|
||||||
return clearInterval(interval);
|
|
||||||
}
|
|
||||||
|
|
||||||
let particleCount = 50 * (timeLeft / duration);
|
|
||||||
|
|
||||||
// Left burst
|
|
||||||
confetti(Object.assign({}, defaults, {
|
|
||||||
particleCount,
|
|
||||||
origin: { x: randomInRange(0.1, 0.3), y: Math.random() - 0.2 }
|
|
||||||
}));
|
|
||||||
|
|
||||||
// Right burst
|
|
||||||
confetti(Object.assign({}, defaults, {
|
|
||||||
particleCount,
|
|
||||||
origin: { x: randomInRange(0.7, 0.9), y: Math.random() - 0.2 }
|
|
||||||
}));
|
|
||||||
}, 250);
|
|
||||||
}
|
|
||||||
connectWebSocket();
|
|
||||||
fireConfetti();
|
|
||||||
</script>
|
|
||||||
@@ -1,42 +0,0 @@
|
|||||||
@import de.knockoutwhist.control.controllerBaseImpl.sublogic.util.TrickUtil
|
|
||||||
@import de.knockoutwhist.utils.Implicits.*
|
|
||||||
|
|
||||||
@(player: de.knockoutwhist.player.AbstractPlayer, gamelobby: logic.game.GameLobby)
|
|
||||||
|
|
||||||
<div class="lobby-background vh-100">
|
|
||||||
<main class="game-field-background vh-100 ingame-side-shadow">
|
|
||||||
<div class="py-5 container-xxl">
|
|
||||||
|
|
||||||
<div class="row ms-4 me-4">
|
|
||||||
<div class="col-4 mt-5 text-start" id="turn-component"></div>
|
|
||||||
|
|
||||||
<div class="col-4 text-center">
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
<div id="score-table"></div>
|
|
||||||
|
|
||||||
|
|
||||||
<div class="d-flex justify-content-center g-3 mb-5" id="trick-cards-container"></div>
|
|
||||||
|
|
||||||
</div>
|
|
||||||
|
|
||||||
|
|
||||||
<div class="col-4 mt-5 text-end" id="game-info-component"></div>
|
|
||||||
|
|
||||||
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="row justify-content-center g-2 mt-4 bottom-div" style="backdrop-filter: blur(4px);
|
|
||||||
margin-left: 0;
|
|
||||||
margin-right: 0;">
|
|
||||||
<div id="player-hand-container"></div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</main>
|
|
||||||
</div>
|
|
||||||
<script>
|
|
||||||
connectWebSocket()
|
|
||||||
canPlayCard = @gamelobby.logic.getCurrentPlayer.contains(player);
|
|
||||||
globalThis.initGameVueComponents()
|
|
||||||
</script>
|
|
||||||
@@ -1,68 +0,0 @@
|
|||||||
@(player: de.knockoutwhist.player.AbstractPlayer, gamelobby: logic.game.GameLobby)
|
|
||||||
|
|
||||||
<div id="selecttrumpsuit" class="game-field game-field-background">
|
|
||||||
<div class="ingame-stage blur-sides">
|
|
||||||
<div class="container py-4">
|
|
||||||
<div class="row justify-content-center">
|
|
||||||
<div class="col-12">
|
|
||||||
<div class="card shadow-sm">
|
|
||||||
<div class="card-header text-center">
|
|
||||||
<h3 class="mb-0">Select Trump Suit</h3>
|
|
||||||
</div>
|
|
||||||
<div class="card-body">
|
|
||||||
@if(gamelobby.logic.getCurrentMatch.isDefined) {
|
|
||||||
@if(player.equals(gamelobby.logic.getCurrentMatch.get.roundlist.last.winner.get)) {
|
|
||||||
<div class="alert alert-info" role="alert" aria-live="polite">
|
|
||||||
You (@player.toString) won the last round. Choose the trump suit for the next round.
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="row justify-content-center col-auto mb-5">
|
|
||||||
<div class="col-auto handcard">
|
|
||||||
<div class="btn btn-outline-light p-0 border-0 shadow-none" data-trump="0" style="border-radius: 6px" onclick="handleTrumpSelection(this)">
|
|
||||||
@util.WebUIUtils.cardtoImage(de.knockoutwhist.cards.Card(de.knockoutwhist.cards.CardValue.Ace, de.knockoutwhist.cards.Suit.Spades))
|
|
||||||
width="120px" style="border-radius: 6px"/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="col-auto handcard">
|
|
||||||
<div class="btn btn-outline-light p-0 border-0 shadow-none" data-trump="1" style="border-radius: 6px" onclick="handleTrumpSelection(this)">
|
|
||||||
@util.WebUIUtils.cardtoImage(de.knockoutwhist.cards.Card(de.knockoutwhist.cards.CardValue.Ace, de.knockoutwhist.cards.Suit.Hearts))
|
|
||||||
width="120px" style="border-radius: 6px"/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="col-auto handcard">
|
|
||||||
<div class="btn btn-outline-light p-0 border-0 shadow-none" data-trump="2" style="border-radius: 6px" onclick="handleTrumpSelection(this)">
|
|
||||||
@util.WebUIUtils.cardtoImage(de.knockoutwhist.cards.Card(de.knockoutwhist.cards.CardValue.Ace, de.knockoutwhist.cards.Suit.Diamonds))
|
|
||||||
width="120px" style="border-radius: 6px"/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="col-auto handcard">
|
|
||||||
<div class="btn btn-outline-light p-0 border-0 shadow-none" data-trump="3" style="border-radius: 6px" onclick="handleTrumpSelection(this)">
|
|
||||||
@util.WebUIUtils.cardtoImage(de.knockoutwhist.cards.Card(de.knockoutwhist.cards.CardValue.Ace, de.knockoutwhist.cards.Suit.Clubs))
|
|
||||||
width="120px" style="border-radius: 6px"/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="row justify-content-center ingame-cards-slide" id="card-slide">
|
|
||||||
@for(i <- player.currentHand().get.cards.indices) {
|
|
||||||
<div class="col-auto" style="border-radius: 6px">
|
|
||||||
@util.WebUIUtils.cardtoImage(player.currentHand().get.cards(i)) width="120px" style="border-radius: 6px"/>
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
</div>
|
|
||||||
} else {
|
|
||||||
<div class="alert alert-warning" role="alert" aria-live="polite">
|
|
||||||
@gamelobby.logic.getCurrentMatch.get.roundlist.last.winner.get.name
|
|
||||||
is choosing a trumpsuit. The new round will start once a suit is picked.
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<script>
|
|
||||||
connectWebSocket()
|
|
||||||
</script>
|
|
||||||
@@ -1,114 +0,0 @@
|
|||||||
@(player: de.knockoutwhist.player.AbstractPlayer, gamelobby: logic.game.GameLobby)
|
|
||||||
<div id="tie" class="game-field game-field-background">
|
|
||||||
<div class="ingame-stage blur-sides">
|
|
||||||
<div class="container py-4">
|
|
||||||
<div class="row justify-content-center">
|
|
||||||
<div class="col-12 col-md-10 col-lg-8">
|
|
||||||
<div class="card shadow-sm">
|
|
||||||
<div class="card-header text-center">
|
|
||||||
<h3 class="mb-0">Tie Break</h3>
|
|
||||||
</div>
|
|
||||||
<div class="card-body">
|
|
||||||
<div class="mb-3">
|
|
||||||
<p class="card-text">
|
|
||||||
The last round was tied between:
|
|
||||||
<span class="ms-1">
|
|
||||||
@for(players <- gamelobby.logic.playerTieLogic.getTiedPlayers) {
|
|
||||||
<span class="badge text-bg-secondary me-1">@players</span>
|
|
||||||
}
|
|
||||||
</span>
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
@if(gamelobby.logic.playerTieLogic.currentTiePlayer().contains(player)) {
|
|
||||||
@defining(gamelobby.logic.playerTieLogic.highestAllowedNumber()) { maxNum =>
|
|
||||||
<div class="alert alert-info" role="alert" aria-live="polite">
|
|
||||||
Pick a number between 1 and @{
|
|
||||||
maxNum + 1
|
|
||||||
}.
|
|
||||||
The resulting card will be your card for the cut.
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="row g-2 align-items-center">
|
|
||||||
<div class="col-auto">
|
|
||||||
<label for="tieNumber" class="col-form-label">Your number</label>
|
|
||||||
</div>
|
|
||||||
<div class="col-auto">
|
|
||||||
<input type="number" id="tieNumber" class="form-control" name="tie" min="1" max="@{
|
|
||||||
maxNum + 1
|
|
||||||
}" placeholder="1" required>
|
|
||||||
</div>
|
|
||||||
<div class="col-auto">
|
|
||||||
<button onclick="selectTie('@gamelobby.id')" class="btn btn-primary">
|
|
||||||
Confirm</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<h6 class="mt-4 mb-3">Currently Picked Cards</h6>
|
|
||||||
|
|
||||||
<div id="cardsplayed" class="row g-3 justify-content-center">
|
|
||||||
@if(gamelobby.logic.playerTieLogic.getSelectedCard.nonEmpty) {
|
|
||||||
@for((player, card) <- gamelobby.logic.playerTieLogic.getSelectedCard) {
|
|
||||||
<div class="col-6">
|
|
||||||
<div class="card shadow-sm border-0 h-100 text-center">
|
|
||||||
<div class="card-body d-flex flex-column justify-content-between">
|
|
||||||
<p class="card-text fw-semibold mb-2 text-primary">@player</p>
|
|
||||||
<div class="card-img-top">
|
|
||||||
@util.WebUIUtils.cardtoImage(card)
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
<div class="col-12">
|
|
||||||
<div class="alert alert-info text-center" role="alert">
|
|
||||||
<i class="bi bi-info-circle me-2"></i>
|
|
||||||
No cards have been selected yet.
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
<div class="alert alert-warning" role="alert" aria-live="polite">
|
|
||||||
<strong>@gamelobby.logic.playerTieLogic.currentTiePlayer().get</strong>
|
|
||||||
is currently picking a number for the cut.
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<h6 class="mt-4 mb-3">Currently Picked Cards</h6>
|
|
||||||
|
|
||||||
<div id="cardsplayed" class="row g-3 justify-content-center">
|
|
||||||
@if(gamelobby.logic.playerTieLogic.getSelectedCard.nonEmpty) {
|
|
||||||
@for((player, card) <- gamelobby.logic.playerTieLogic.getSelectedCard) {
|
|
||||||
<div class="col-6 col-sm-4 col-md-3 col-lg-2">
|
|
||||||
<div class="card shadow-sm border-0 h-100 text-center">
|
|
||||||
<div class="card-body d-flex flex-column justify-content-between">
|
|
||||||
<p class="card-text fw-semibold mb-2 text-primary">@player</p>
|
|
||||||
<div class="card-img-top">
|
|
||||||
@util.WebUIUtils.cardtoImage(card)
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
<div class="col-12">
|
|
||||||
<div class="alert alert-info text-center" role="alert">
|
|
||||||
<i class="bi bi-info-circle me-2"></i>
|
|
||||||
No cards have been selected yet.
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<script>
|
|
||||||
connectWebSocket()
|
|
||||||
</script>
|
|
||||||
@@ -1,38 +0,0 @@
|
|||||||
@(user: Option[model.users.User], gamelobby: logic.game.GameLobby)
|
|
||||||
@import play.api.libs.json._
|
|
||||||
<div id="lobby-app-mount"></div>
|
|
||||||
|
|
||||||
<script>
|
|
||||||
// Initialisierung des momentanen Lobby Standes, welcher direkt gerendert werden muss.
|
|
||||||
|
|
||||||
const initialLobbyName = '@gamelobby.name';
|
|
||||||
const initialLobbyId = '@gamelobby.id';
|
|
||||||
|
|
||||||
const initialIsHost = @{user.map(u => gamelobby.getUserSession(u.id)).exists(_.host)};
|
|
||||||
const initialMaxPlayers = @{gamelobby.maxPlayers};
|
|
||||||
|
|
||||||
const initialPlayers = JSON.parse('@Html({
|
|
||||||
val currentUserId = user.map(_.id).getOrElse(java.util.UUID.randomUUID())
|
|
||||||
|
|
||||||
val playerListForVue = gamelobby.getUsers.toSeq.map { u =>
|
|
||||||
val isSelf = u.id == currentUserId
|
|
||||||
val playerDogStatus = false
|
|
||||||
|
|
||||||
Json.obj(
|
|
||||||
"id" -> u.id.toString,
|
|
||||||
"name" -> u.name,
|
|
||||||
"self" -> isSelf,
|
|
||||||
"dog" -> playerDogStatus
|
|
||||||
)
|
|
||||||
}
|
|
||||||
Json.stringify(Json.toJson(playerListForVue))
|
|
||||||
})');
|
|
||||||
connectWebSocket();
|
|
||||||
globalThis.initLobbyVueComponents(
|
|
||||||
initialLobbyName,
|
|
||||||
initialLobbyId,
|
|
||||||
initialIsHost,
|
|
||||||
initialMaxPlayers,
|
|
||||||
initialPlayers
|
|
||||||
);
|
|
||||||
</script>
|
|
||||||
@@ -1,43 +0,0 @@
|
|||||||
@()
|
|
||||||
<div class="login-box">
|
|
||||||
<div class="card login-card p-4">
|
|
||||||
<div class="card-body">
|
|
||||||
<h3 class="text-center mb-4 text-body">Login</h3>
|
|
||||||
<form onsubmit="login(); return false;">
|
|
||||||
<div class="mb-3">
|
|
||||||
<label for="username" class="form-label text-body">Username</label>
|
|
||||||
<input type="text" class="form-control text-body" id="username" name="username" placeholder="Enter Username" required>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="mb-3">
|
|
||||||
<label for="password" class="form-label text-body">Password</label>
|
|
||||||
<input type="password" class="form-control text-body" id="password" name="password" placeholder="Enter password" required>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="d-flex justify-content-between align-items-center mb-3">
|
|
||||||
<a href="#" class="text-decoration-none">Forgot password?</a>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="d-grid" >
|
|
||||||
<button type="submit" class="btn btn-primary">Login</button>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
|
|
||||||
<p class="text-center mt-3">
|
|
||||||
Don’t have an account?
|
|
||||||
<a href="#" class="text-decoration-none">Sign up</a>
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<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');
|
|
||||||
});
|
|
||||||
disconnectWebSocket();
|
|
||||||
</script>
|
|
||||||
<div id="particles-js" style="background-color: rgb(11, 8, 8);
|
|
||||||
background-size: cover;
|
|
||||||
background-repeat: no-repeat;
|
|
||||||
background-position: 50% 50%;"></div>
|
|
||||||
@@ -1,36 +0,0 @@
|
|||||||
@*
|
|
||||||
* This template is called from the `index` template. This template
|
|
||||||
* 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`
|
|
||||||
* object to insert into the body of the page.
|
|
||||||
*@
|
|
||||||
@(title: String)(content: Html)
|
|
||||||
|
|
||||||
<!DOCTYPE html>
|
|
||||||
<html lang="en">
|
|
||||||
<head>
|
|
||||||
<meta charset="utf-8">
|
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
||||||
@* Here's where we render the page title `String`. *@
|
|
||||||
<title>@title</title>
|
|
||||||
<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 href="https://cdn.jsdelivr.net/npm/bootstrap@@5.3.8/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-sRIl4kxILFvY47J16cr9ZwB07vP4J8+LH7qKQnuqkuIAvNWLzeN8tE5YBujZqJLB" crossorigin="anonymous">
|
|
||||||
</head>
|
|
||||||
<script src="https://cdn.jsdelivr.net/npm/canvas-confetti@@1.6.0/dist/confetti.browser.min.js"></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://code.jquery.com/jquery-3.6.0.min.js"></script>
|
|
||||||
<script src="https://unpkg.com/vue/dist/vue.global.prod.js"></script>
|
|
||||||
<script src="@routes.JavaScriptRoutingController.javascriptRoutes()" type="text/javascript"></script>
|
|
||||||
<script src="@routes.Assets.versioned("javascripts/websocket.js")" type="text/javascript"></script>
|
|
||||||
<script src="@routes.Assets.versioned("javascripts/events.js")" type="text/javascript"></script>
|
|
||||||
<script src="@routes.Assets.versioned("javascripts/interact.js")" type="text/javascript"></script>
|
|
||||||
<script src="@routes.Assets.versioned("javascripts/main.js")" type="text/javascript"></script>
|
|
||||||
|
|
||||||
<body class="d-flex flex-column min-vh-100" id="main-body">
|
|
||||||
<div id="alerts-container"></div>
|
|
||||||
@* And here's where we render the `Html` object containing
|
|
||||||
* the page content. *@
|
|
||||||
@content
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
@@ -1,33 +0,0 @@
|
|||||||
@(user: Option[model.users.User])
|
|
||||||
|
|
||||||
@navbar(user)
|
|
||||||
<main class="lobby-background flex-grow-1">
|
|
||||||
<div class="w-25 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">
|
|
||||||
<div class="btn btn-success" onclick="createGameJS()">Create Game</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</main>
|
|
||||||
<script>
|
|
||||||
disconnectWebSocket();
|
|
||||||
</script>
|
|
||||||
@@ -1,55 +0,0 @@
|
|||||||
@(user: Option[model.users.User])
|
|
||||||
<nav class="navbar navbar-expand-lg bg-body-tertiary navbar-drop-shadow">
|
|
||||||
<div class="container d-flex justify-content-start">
|
|
||||||
<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>
|
|
||||||
</button>
|
|
||||||
<div class="collapse navbar-collapse justify-content-center" id="navBar">
|
|
||||||
<a class="navbar-brand ms-auto" href="@routes.MainMenuController.mainMenu()">
|
|
||||||
<img src="@routes.Assets.versioned("images/logo.png")" alt="Logo" width="30" height="20" class="d-inline-block align-text-top">
|
|
||||||
KnockOutWhist
|
|
||||||
</a>
|
|
||||||
<div class="navbar-nav me-auto mb-2 mb-lg-0">
|
|
||||||
<ul class="navbar-nav mb-2 mb-lg-0">
|
|
||||||
@if(user.isDefined) {
|
|
||||||
<li class="nav-item">
|
|
||||||
<a class="nav-link active" aria-current="page" onclick="navSpa('0', 'Knockout Whist - Create Game'); return false;" href="@routes.MainMenuController.mainMenu()">
|
|
||||||
Create Game</a>
|
|
||||||
</li>
|
|
||||||
<li class="nav-item">
|
|
||||||
<a class="nav-link disabled" aria-disabled="true">Lobbies</a>
|
|
||||||
</li>
|
|
||||||
}
|
|
||||||
<li class="nav-item">
|
|
||||||
<a class="nav-link active" onclick="navSpa('1', 'Knockout Whist - Rules'); return false;" href="@routes.MainMenuController.rules()">
|
|
||||||
Rules</a>
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
<form class="navbar-nav me-auto mb-2 mb-lg-0" onsubmit="joinGame(); return false;">
|
|
||||||
<input class="form-control me-2" type="text" placeholder="Enter GameCode" id="gameId" aria-label="Join Game"/>
|
|
||||||
<button class="btn btn-outline-success" type="submit">Join</button>
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
@* Right side: profile dropdown if logged in, otherwise Login / Sign Up buttons *@
|
|
||||||
@if(user.isDefined) {
|
|
||||||
<ul class="navbar-nav ms-auto mb-2 mb-lg-0">
|
|
||||||
<li class="nav-item dropdown">
|
|
||||||
<a class="nav-link dropdown-toggle d-flex align-items-center" href="#" id="profileDropdown" role="button" data-bs-toggle="dropdown" aria-expanded="false">
|
|
||||||
<img src="@routes.Assets.versioned("images/profile.png")" alt="Profile" class="rounded-circle" width="30" height="30" />
|
|
||||||
<span class="ms-2">@user.get.name</span>
|
|
||||||
</a>
|
|
||||||
<ul class="dropdown-menu dropdown-menu-end" aria-labelledby="profileDropdown">
|
|
||||||
<li><a class="dropdown-item disabled" href="#" tabindex="-1" aria-disabled="true">
|
|
||||||
Stats</a></li>
|
|
||||||
<li><a class="dropdown-item disabled" href="#" tabindex="-1" aria-disabled="true">
|
|
||||||
Settings</a></li>
|
|
||||||
<li><hr class="dropdown-divider"></li>
|
|
||||||
</ul>
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
}
|
|
||||||
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</nav>
|
|
||||||
@@ -1,180 +0,0 @@
|
|||||||
@(user: Option[model.users.User])
|
|
||||||
@navbar(user)
|
|
||||||
|
|
||||||
<main class="lobby-background flex-grow-1">
|
|
||||||
<div class="container my-4" style="max-width: 980px;">
|
|
||||||
<div class="card rules-card shadow-sm rounded-3 overflow-hidden">
|
|
||||||
<div class="card-header text-center py-3 border-0">
|
|
||||||
<h3 class="mb-0 rules-title">Game Rules Overview</h3>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="card-body p-0">
|
|
||||||
<style>
|
|
||||||
|
|
||||||
</style>
|
|
||||||
|
|
||||||
<div class="accordion rules-accordion" id="rulesAccordion">
|
|
||||||
<div class="accordion-item">
|
|
||||||
<h2 class="accordion-header" id="headingPlayers">
|
|
||||||
<button class="accordion-button" type="button" data-bs-toggle="collapse" data-bs-target="#collapsePlayers" aria-expanded="true" aria-controls="collapsePlayers">
|
|
||||||
Players
|
|
||||||
</button>
|
|
||||||
</h2>
|
|
||||||
<div id="collapsePlayers" class="accordion-collapse collapse show" aria-labelledby="headingPlayers" data-bs-parent="#rulesAccordion">
|
|
||||||
<div class="accordion-body">
|
|
||||||
Two to seven players. The aim is to be the last player left in the game.
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="accordion-item">
|
|
||||||
<h2 class="accordion-header" id="headingAim">
|
|
||||||
<button class="accordion-button collapsed" type="button" data-bs-toggle="collapse" data-bs-target="#collapseAim" aria-expanded="false" aria-controls="collapseAim">
|
|
||||||
Aim
|
|
||||||
</button>
|
|
||||||
</h2>
|
|
||||||
<div id="collapseAim" class="accordion-collapse collapse" aria-labelledby="headingAim" data-bs-parent="#rulesAccordion">
|
|
||||||
<div class="accordion-body">
|
|
||||||
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.
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="accordion-item">
|
|
||||||
<h2 class="accordion-header" id="headingEquipment">
|
|
||||||
<button class="accordion-button collapsed" type="button" data-bs-toggle="collapse" data-bs-target="#collapseEquipment" aria-expanded="false" aria-controls="collapseEquipment">
|
|
||||||
Equipment
|
|
||||||
</button>
|
|
||||||
</h2>
|
|
||||||
<div id="collapseEquipment" class="accordion-collapse collapse" aria-labelledby="headingEquipment" data-bs-parent="#rulesAccordion">
|
|
||||||
<div class="accordion-body">
|
|
||||||
A standard 52-card pack is used.
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="accordion-item">
|
|
||||||
<h2 class="accordion-header" id="headingRanks">
|
|
||||||
<button class="accordion-button collapsed" type="button" data-bs-toggle="collapse" data-bs-target="#collapseRanks" aria-expanded="false" aria-controls="collapseRanks">
|
|
||||||
Card Ranks
|
|
||||||
</button>
|
|
||||||
</h2>
|
|
||||||
<div id="collapseRanks" class="accordion-collapse collapse" aria-labelledby="headingRanks" data-bs-parent="#rulesAccordion">
|
|
||||||
<div class="accordion-body">
|
|
||||||
In each suit, cards rank from highest to lowest: A K Q J 10 9 8 7 6 5 4 3 2.
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="accordion-item">
|
|
||||||
<h2 class="accordion-header" id="headingDealFirst">
|
|
||||||
<button class="accordion-button collapsed" type="button" data-bs-toggle="collapse" data-bs-target="#collapseDealFirst" aria-expanded="false" aria-controls="collapseDealFirst">
|
|
||||||
Deal (First Hand)
|
|
||||||
</button>
|
|
||||||
</h2>
|
|
||||||
<div id="collapseDealFirst" class="accordion-collapse collapse" aria-labelledby="headingDealFirst" data-bs-parent="#rulesAccordion">
|
|
||||||
<div class="accordion-body">
|
|
||||||
The dealer deals seven cards to each player. One card is turned up to determine the trump suit for the round.
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="accordion-item">
|
|
||||||
<h2 class="accordion-header" id="headingDealSubsequent">
|
|
||||||
<button class="accordion-button collapsed" type="button" data-bs-toggle="collapse" data-bs-target="#collapseDealSubsequent" aria-expanded="false" aria-controls="collapseDealSubsequent">
|
|
||||||
Deal (Subsequent Hands)
|
|
||||||
</button>
|
|
||||||
</h2>
|
|
||||||
<div id="collapseDealSubsequent" class="accordion-collapse collapse" aria-labelledby="headingDealSubsequent" data-bs-parent="#rulesAccordion">
|
|
||||||
<div class="accordion-body">
|
|
||||||
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.
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="accordion-item">
|
|
||||||
<h2 class="accordion-header" id="headingPlay">
|
|
||||||
<button class="accordion-button collapsed" type="button" data-bs-toggle="collapse" data-bs-target="#collapsePlay" aria-expanded="false" aria-controls="collapsePlay">
|
|
||||||
Play
|
|
||||||
</button>
|
|
||||||
</h2>
|
|
||||||
<div id="collapsePlay" class="accordion-collapse collapse" aria-labelledby="headingPlay" data-bs-parent="#rulesAccordion">
|
|
||||||
<div class="accordion-body">
|
|
||||||
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.
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="accordion-item">
|
|
||||||
<h2 class="accordion-header" id="headingWinningTrick">
|
|
||||||
<button class="accordion-button collapsed" type="button" data-bs-toggle="collapse" data-bs-target="#collapseWinningTrick" aria-expanded="false" aria-controls="collapseWinningTrick">
|
|
||||||
Winning a Trick
|
|
||||||
</button>
|
|
||||||
</h2>
|
|
||||||
<div id="collapseWinningTrick" class="accordion-collapse collapse" aria-labelledby="headingWinningTrick" data-bs-parent="#rulesAccordion">
|
|
||||||
<div class="accordion-body">
|
|
||||||
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.
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="accordion-item">
|
|
||||||
<h2 class="accordion-header" id="headingLeadingTrumps">
|
|
||||||
<button class="accordion-button collapsed" type="button" data-bs-toggle="collapse" data-bs-target="#collapseLeadingTrumps" aria-expanded="false" aria-controls="collapseLeadingTrumps">
|
|
||||||
Leading Trumps
|
|
||||||
</button>
|
|
||||||
</h2>
|
|
||||||
<div id="collapseLeadingTrumps" class="accordion-collapse collapse" aria-labelledby="headingLeadingTrumps" data-bs-parent="#rulesAccordion">
|
|
||||||
<div class="accordion-body">
|
|
||||||
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.
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="accordion-item">
|
|
||||||
<h2 class="accordion-header" id="headingKnockout">
|
|
||||||
<button class="accordion-button collapsed" type="button" data-bs-toggle="collapse" data-bs-target="#collapseKnockout" aria-expanded="false" aria-controls="collapseKnockout">
|
|
||||||
Knockout
|
|
||||||
</button>
|
|
||||||
</h2>
|
|
||||||
<div id="collapseKnockout" class="accordion-collapse collapse" aria-labelledby="headingKnockout" data-bs-parent="#rulesAccordion">
|
|
||||||
<div class="accordion-body">
|
|
||||||
At the end of each hand, any player who took no tricks is knocked out and takes no further part in the game.
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="accordion-item">
|
|
||||||
<h2 class="accordion-header" id="headingWinningGame">
|
|
||||||
<button class="accordion-button collapsed" type="button" data-bs-toggle="collapse" data-bs-target="#collapseWinningGame" aria-expanded="false" aria-controls="collapseWinningGame">
|
|
||||||
Winning the Game
|
|
||||||
</button>
|
|
||||||
</h2>
|
|
||||||
<div id="collapseWinningGame" class="accordion-collapse collapse" aria-labelledby="headingWinningGame" data-bs-parent="#rulesAccordion">
|
|
||||||
<div class="accordion-body">
|
|
||||||
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.
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="accordion-item">
|
|
||||||
<h2 class="accordion-header" id="headingDogLife">
|
|
||||||
<button class="accordion-button collapsed" type="button" data-bs-toggle="collapse" data-bs-target="#collapseDogLife" aria-expanded="false" aria-controls="collapseDogLife">
|
|
||||||
Dog Life
|
|
||||||
</button>
|
|
||||||
</h2>
|
|
||||||
<div id="collapseDogLife" class="accordion-collapse collapse" aria-labelledby="headingDogLife" data-bs-parent="#rulesAccordion">
|
|
||||||
<div class="accordion-body">
|
|
||||||
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.
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</main>
|
|
||||||
<script>
|
|
||||||
disconnectWebSocket();
|
|
||||||
</script>
|
|
||||||
@@ -1,2 +0,0 @@
|
|||||||
@(src: String)(alt: String)
|
|
||||||
<img src="@routes.Assets.versioned(src)" alt="@alt"
|
|
||||||
@@ -1,3 +0,0 @@
|
|||||||
@(text: String)
|
|
||||||
<p>@text</p>
|
|
||||||
|
|
||||||
28
knockoutwhistweb/conf/db.conf
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
|
||||||
|
# Database configuration - PostgreSQL with environment variables
|
||||||
|
db.default.driver=org.postgresql.Driver
|
||||||
|
db.default.url=${?DATABASE_URL}
|
||||||
|
db.default.username=${?DB_USER}
|
||||||
|
db.default.password=${?DB_PASSWORD}
|
||||||
|
db.default.password=""
|
||||||
|
|
||||||
|
# JPA/Hibernate configuration
|
||||||
|
jpa.default=defaultPersistenceUnit
|
||||||
|
|
||||||
|
# Hibernate specific settings
|
||||||
|
hibernate.dialect=org.hibernate.dialect.PostgreSQLDialect
|
||||||
|
hibernate.hbm2ddl.auto=update
|
||||||
|
hibernate.show_sql=false
|
||||||
|
hibernate.format_sql=true
|
||||||
|
hibernate.use_sql_comments=true
|
||||||
|
|
||||||
|
# Connection pool settings
|
||||||
|
db.default.hikaricp.maximumPoolSize=20
|
||||||
|
db.default.hikaricp.minimumIdle=5
|
||||||
|
db.default.hikaricp.connectionTimeout=30000
|
||||||
|
db.default.hikaricp.idleTimeout=600000
|
||||||
|
db.default.hikaricp.maxLifetime=1800000
|
||||||
|
|
||||||
|
# PostgreSQL specific settings
|
||||||
|
db.default.hikaricp.connectionTestQuery="SELECT 1"
|
||||||
|
db.default.hikaricp.poolName="KnockOutWhistPool"
|
||||||
@@ -1,4 +1,5 @@
|
|||||||
include "application.conf"
|
include "application.conf"
|
||||||
|
include "db.conf"
|
||||||
|
|
||||||
play.http.secret.key="zg8^v0R*:7-m.>^8T2B1q)sE3MV_9=M{K9zx8,<3}"
|
play.http.secret.key="zg8^v0R*:7-m.>^8T2B1q)sE3MV_9=M{K9zx8,<3}"
|
||||||
|
|
||||||
@@ -13,43 +14,16 @@ play.filters.cors {
|
|||||||
allowedHttpHeaders = ["Accept", "Content-Type", "Origin", "X-Requested-With"]
|
allowedHttpHeaders = ["Accept", "Content-Type", "Origin", "X-Requested-With"]
|
||||||
}
|
}
|
||||||
|
|
||||||
# Database configuration - PostgreSQL with environment variables
|
|
||||||
db.default.driver=org.postgresql.Driver
|
|
||||||
db.default.url=${?DATABASE_URL}
|
|
||||||
db.default.url="jdbc:postgresql://localhost:5432/knockoutwhist"
|
|
||||||
db.default.username=${?DB_USER}
|
|
||||||
db.default.username="postgres"
|
|
||||||
db.default.password=${?DB_PASSWORD}
|
|
||||||
db.default.password=""
|
|
||||||
|
|
||||||
# JPA/Hibernate configuration
|
|
||||||
jpa.default=defaultPersistenceUnit
|
|
||||||
|
|
||||||
# Hibernate specific settings
|
|
||||||
hibernate.dialect=org.hibernate.dialect.PostgreSQLDialect
|
|
||||||
hibernate.hbm2ddl.auto=update
|
|
||||||
hibernate.show_sql=false
|
|
||||||
hibernate.format_sql=true
|
|
||||||
hibernate.use_sql_comments=true
|
|
||||||
|
|
||||||
# Connection pool settings
|
|
||||||
db.default.hikaricp.maximumPoolSize=20
|
|
||||||
db.default.hikaricp.minimumIdle=5
|
|
||||||
db.default.hikaricp.connectionTimeout=30000
|
|
||||||
db.default.hikaricp.idleTimeout=600000
|
|
||||||
db.default.hikaricp.maxLifetime=1800000
|
|
||||||
|
|
||||||
# PostgreSQL specific settings
|
|
||||||
db.default.hikaricp.connectionTestQuery="SELECT 1"
|
|
||||||
db.default.hikaricp.poolName="KnockOutWhistPool"
|
|
||||||
|
|
||||||
# OpenID Connect Configuration
|
# OpenID Connect Configuration
|
||||||
openid {
|
openid {
|
||||||
|
|
||||||
|
selectUserRoute="https://knockout.janis-eccarius.de/select-user"
|
||||||
|
|
||||||
discord {
|
discord {
|
||||||
clientId = ${?DISCORD_CLIENT_ID}
|
clientId = ${?DISCORD_CLIENT_ID}
|
||||||
clientSecret = ${?DISCORD_CLIENT_SECRET}
|
clientSecret = ${?DISCORD_CLIENT_SECRET}
|
||||||
redirectUri = ${?DISCORD_REDIRECT_URI}
|
redirectUri = ${?DISCORD_REDIRECT_URI}
|
||||||
redirectUri = "http://localhost:9000/auth/discord/callback"
|
redirectUri = "https://knockout.janis-eccarius.de/auth/discord/callback"
|
||||||
}
|
}
|
||||||
|
|
||||||
keycloak {
|
keycloak {
|
||||||
@@ -57,6 +31,6 @@ openid {
|
|||||||
clientSecret = "your-keycloak-client-secret"
|
clientSecret = "your-keycloak-client-secret"
|
||||||
redirectUri = "https://knockout.janis-eccarius.de/api/auth/keycloak/callback"
|
redirectUri = "https://knockout.janis-eccarius.de/api/auth/keycloak/callback"
|
||||||
authUrl = ${?KEYCLOAK_AUTH_URL}
|
authUrl = ${?KEYCLOAK_AUTH_URL}
|
||||||
authUrl = "http://localhost:8080/realms/master"
|
authUrl = "https://identity.janis-eccarius.de/realms/master"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,19 +1,4 @@
|
|||||||
# Routes
|
# Create game rounds
|
||||||
# This file defines all application routes (Higher priority routes first)
|
|
||||||
# https://www.playframework.com/documentation/latest/ScalaRouting
|
|
||||||
# ~~~~
|
|
||||||
|
|
||||||
# For the javascript routing
|
|
||||||
GET /assets/js/routes controllers.JavaScriptRoutingController.javascriptRoutes()
|
|
||||||
# Primary routes
|
|
||||||
GET / controllers.MainMenuController.index()
|
|
||||||
GET /assets/*file controllers.Assets.versioned(path="/public", file: Asset)
|
|
||||||
|
|
||||||
# Main menu routes
|
|
||||||
GET /mainmenu controllers.MainMenuController.mainMenu()
|
|
||||||
GET /rules controllers.MainMenuController.rules()
|
|
||||||
GET /navSPA/:pType controllers.MainMenuController.navSPA(pType)
|
|
||||||
|
|
||||||
POST /createGame controllers.MainMenuController.createGame()
|
POST /createGame controllers.MainMenuController.createGame()
|
||||||
POST /joinGame/:gameId controllers.MainMenuController.joinGame(gameId: String)
|
POST /joinGame/:gameId controllers.MainMenuController.joinGame(gameId: String)
|
||||||
|
|
||||||
@@ -29,9 +14,6 @@ GET /auth/:provider/callback controllers.OpenIDController.callback(provi
|
|||||||
GET /select-username controllers.OpenIDController.selectUsername()
|
GET /select-username controllers.OpenIDController.selectUsername()
|
||||||
POST /submit-username controllers.OpenIDController.submitUsername()
|
POST /submit-username controllers.OpenIDController.submitUsername()
|
||||||
|
|
||||||
# In-game routes
|
|
||||||
GET /game/:id controllers.IngameController.game(id: String)
|
|
||||||
|
|
||||||
# Websocket
|
# Websocket
|
||||||
GET /websocket controllers.WebsocketController.socket()
|
GET /websocket controllers.WebsocketController.socket()
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
include "application.conf"
|
include "application.conf"
|
||||||
|
include "db.conf"
|
||||||
|
|
||||||
play.http.context="/api"
|
play.http.context="/api"
|
||||||
|
|
||||||
|
|||||||
@@ -1,110 +0,0 @@
|
|||||||
{
|
|
||||||
"particles": {
|
|
||||||
"number": {
|
|
||||||
"value": 80,
|
|
||||||
"density": {
|
|
||||||
"enable": true,
|
|
||||||
"value_area": 800
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"color": {
|
|
||||||
"value": "#ffffff"
|
|
||||||
},
|
|
||||||
"shape": {
|
|
||||||
"type": "circle",
|
|
||||||
"stroke": {
|
|
||||||
"width": 0,
|
|
||||||
"color": "#000000"
|
|
||||||
},
|
|
||||||
"polygon": {
|
|
||||||
"nb_sides": 5
|
|
||||||
},
|
|
||||||
"image": {
|
|
||||||
"src": "img/github.svg",
|
|
||||||
"width": 100,
|
|
||||||
"height": 100
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"opacity": {
|
|
||||||
"value": 0.5,
|
|
||||||
"random": false,
|
|
||||||
"anim": {
|
|
||||||
"enable": false,
|
|
||||||
"speed": 1,
|
|
||||||
"opacity_min": 0.1,
|
|
||||||
"sync": false
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"size": {
|
|
||||||
"value": 3,
|
|
||||||
"random": true,
|
|
||||||
"anim": {
|
|
||||||
"enable": false,
|
|
||||||
"speed": 40,
|
|
||||||
"size_min": 0.1,
|
|
||||||
"sync": false
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"line_linked": {
|
|
||||||
"enable": true,
|
|
||||||
"distance": 150,
|
|
||||||
"color": "#ffffff",
|
|
||||||
"opacity": 0.4,
|
|
||||||
"width": 1
|
|
||||||
},
|
|
||||||
"move": {
|
|
||||||
"enable": true,
|
|
||||||
"speed": 1,
|
|
||||||
"direction": "none",
|
|
||||||
"random": false,
|
|
||||||
"straight": false,
|
|
||||||
"out_mode": "out",
|
|
||||||
"bounce": false,
|
|
||||||
"attract": {
|
|
||||||
"enable": false,
|
|
||||||
"rotateX": 600,
|
|
||||||
"rotateY": 1200
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"interactivity": {
|
|
||||||
"detect_on": "canvas",
|
|
||||||
"events": {
|
|
||||||
"onhover": {
|
|
||||||
"enable": false,
|
|
||||||
"mode": "repulse"
|
|
||||||
},
|
|
||||||
"onclick": {
|
|
||||||
"enable": false,
|
|
||||||
"mode": "push"
|
|
||||||
},
|
|
||||||
"resize": true
|
|
||||||
},
|
|
||||||
"modes": {
|
|
||||||
"grab": {
|
|
||||||
"distance": 400,
|
|
||||||
"line_linked": {
|
|
||||||
"opacity": 1
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"bubble": {
|
|
||||||
"distance": 400,
|
|
||||||
"size": 40,
|
|
||||||
"duration": 2,
|
|
||||||
"opacity": 8,
|
|
||||||
"speed": 3
|
|
||||||
},
|
|
||||||
"repulse": {
|
|
||||||
"distance": 200,
|
|
||||||
"duration": 0.4
|
|
||||||
},
|
|
||||||
"push": {
|
|
||||||
"particles_nb": 4
|
|
||||||
},
|
|
||||||
"remove": {
|
|
||||||
"particles_nb": 2
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"retina_detect": true
|
|
||||||
}
|
|
||||||
|
Before Width: | Height: | Size: 2.8 MiB |
|
Before Width: | Height: | Size: 4.3 KiB |
|
Before Width: | Height: | Size: 16 KiB |
|
Before Width: | Height: | Size: 4.0 KiB |
|
Before Width: | Height: | Size: 8.4 KiB |
|
Before Width: | Height: | Size: 6.2 KiB |
|
Before Width: | Height: | Size: 6.5 KiB |
|
Before Width: | Height: | Size: 16 KiB |
|
Before Width: | Height: | Size: 7.7 KiB |
|
Before Width: | Height: | Size: 10 KiB |
|
Before Width: | Height: | Size: 7.3 KiB |
|
Before Width: | Height: | Size: 7.6 KiB |
|
Before Width: | Height: | Size: 9.1 KiB |
|
Before Width: | Height: | Size: 9.1 KiB |
|
Before Width: | Height: | Size: 6.6 KiB |
|
Before Width: | Height: | Size: 7.0 KiB |
|
Before Width: | Height: | Size: 8.6 KiB |
|
Before Width: | Height: | Size: 11 KiB |
|
Before Width: | Height: | Size: 7.9 KiB |
|
Before Width: | Height: | Size: 8.4 KiB |
|
Before Width: | Height: | Size: 10 KiB |
|
Before Width: | Height: | Size: 12 KiB |
|
Before Width: | Height: | Size: 8.6 KiB |
|
Before Width: | Height: | Size: 9.2 KiB |
|
Before Width: | Height: | Size: 12 KiB |
|
Before Width: | Height: | Size: 13 KiB |
|
Before Width: | Height: | Size: 9.0 KiB |
|
Before Width: | Height: | Size: 9.6 KiB |
|
Before Width: | Height: | Size: 12 KiB |
|
Before Width: | Height: | Size: 15 KiB |
|
Before Width: | Height: | Size: 11 KiB |
|
Before Width: | Height: | Size: 11 KiB |
|
Before Width: | Height: | Size: 14 KiB |
|
Before Width: | Height: | Size: 16 KiB |
|
Before Width: | Height: | Size: 11 KiB |
|
Before Width: | Height: | Size: 12 KiB |
|
Before Width: | Height: | Size: 15 KiB |
|
Before Width: | Height: | Size: 6.6 KiB |
|
Before Width: | Height: | Size: 3.5 KiB |
|
Before Width: | Height: | Size: 5.2 KiB |
|
Before Width: | Height: | Size: 2.8 KiB |
|
Before Width: | Height: | Size: 5.3 KiB |
|
Before Width: | Height: | Size: 3.4 KiB |
|
Before Width: | Height: | Size: 12 KiB |
|
Before Width: | Height: | Size: 11 KiB |
|
Before Width: | Height: | Size: 68 KiB |
|
Before Width: | Height: | Size: 72 KiB |
|
Before Width: | Height: | Size: 66 KiB |
|
Before Width: | Height: | Size: 67 KiB |
|
Before Width: | Height: | Size: 60 KiB |
|
Before Width: | Height: | Size: 61 KiB |
|
Before Width: | Height: | Size: 68 KiB |
|
Before Width: | Height: | Size: 68 KiB |
|
Before Width: | Height: | Size: 64 KiB |
|
Before Width: | Height: | Size: 73 KiB |
|
Before Width: | Height: | Size: 73 KiB |
|
Before Width: | Height: | Size: 72 KiB |
|
Before Width: | Height: | Size: 16 KiB |
|
Before Width: | Height: | Size: 11 KiB |
|
Before Width: | Height: | Size: 11 KiB |
|
Before Width: | Height: | Size: 14 KiB |
|
Before Width: | Height: | Size: 687 B |
|
Before Width: | Height: | Size: 3.3 MiB |
|
Before Width: | Height: | Size: 19 KiB |
|
Before Width: | Height: | Size: 36 KiB |
@@ -1,653 +0,0 @@
|
|||||||
var canPlayCard = false;
|
|
||||||
const PlayerHandComponent = {
|
|
||||||
data() {
|
|
||||||
return {
|
|
||||||
hand: [],
|
|
||||||
isDogPhase: false,
|
|
||||||
isAwaitingResponse: false,
|
|
||||||
};
|
|
||||||
},
|
|
||||||
computed: {
|
|
||||||
isHandInactive() {
|
|
||||||
//TODO: Needs implementation
|
|
||||||
}
|
|
||||||
},
|
|
||||||
template: `
|
|
||||||
<div class="row justify-content-center g-2 mt-4 bottom-div"
|
|
||||||
style="backdrop-filter: blur(4px); margin-left: 0; margin-right: 0;">
|
|
||||||
|
|
||||||
<div id="card-slide" class="row justify-content-center ingame-cards-slide" :class="{'inactive': isHandInactive }">
|
|
||||||
|
|
||||||
<div v-for="card in hand" :key="card.idx" class="col-auto handcard" style="border-radius: 6px">
|
|
||||||
<div class="btn btn-outline-light p-0 border-0 shadow-none"
|
|
||||||
:data-card-id="card.idx"
|
|
||||||
style="border-radius: 6px"
|
|
||||||
@click="handlePlayCard(card.idx)">
|
|
||||||
|
|
||||||
<img :src="getCardImagePath(card.card)" width="120px" style="border-radius: 6px" :alt="card.card"/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div v-if="isDogPhase" class="mt-2">
|
|
||||||
<button class="btn btn-danger" @click="handleSkipDogLife()">Skip Turn</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
`,
|
|
||||||
methods: {
|
|
||||||
updateHand(eventData) {
|
|
||||||
this.hand = eventData.hand.map(card => ({
|
|
||||||
idx: parseInt(card.idx, 10),
|
|
||||||
card: card.card
|
|
||||||
}));
|
|
||||||
this.isDogPhase = false;
|
|
||||||
|
|
||||||
console.log("Vue Data Updated. Hand size:", this.hand.length);
|
|
||||||
|
|
||||||
if (this.hand.length > 0) {
|
|
||||||
console.log("First card path check:", this.getCardImagePath(this.hand[0].card));
|
|
||||||
}
|
|
||||||
},
|
|
||||||
handlePlayCard(cardidx) {
|
|
||||||
if(this.isAwaitingResponse) return
|
|
||||||
if(!canPlayCard) return
|
|
||||||
canPlayCard = false;
|
|
||||||
this.isAwaitingResponse = true
|
|
||||||
|
|
||||||
|
|
||||||
console.debug(`Playing card ${cardidx} from hand`)
|
|
||||||
|
|
||||||
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 targetButton = this.$el.querySelector(`[data-card-id="${cardidx}"]`);
|
|
||||||
const cardElement = targetButton ? targetButton.closest('.handcard') : null;
|
|
||||||
|
|
||||||
const payload = {
|
|
||||||
cardindex: cardidx.toString(),
|
|
||||||
isDog: false
|
|
||||||
}
|
|
||||||
sendEventAndWait("PlayCard", payload).then(
|
|
||||||
() => {
|
|
||||||
this.hand = this.hand.filter(card => card.idx !== cardidx);
|
|
||||||
|
|
||||||
this.hand.forEach((card, index) => {
|
|
||||||
card.idx = index;
|
|
||||||
})
|
|
||||||
this.isAwaitingResponse = false;
|
|
||||||
}
|
|
||||||
).catch(
|
|
||||||
(err) => {
|
|
||||||
if (cardElement) {
|
|
||||||
cardElement.animate(wiggleKeyframes, wiggleTiming);
|
|
||||||
} else {
|
|
||||||
console.warn(`Could not find DOM element for card index ${cardidx} to wiggle.`);
|
|
||||||
}
|
|
||||||
this.isAwaitingResponse = false;
|
|
||||||
canPlayCard = true;
|
|
||||||
|
|
||||||
}
|
|
||||||
)
|
|
||||||
},
|
|
||||||
handleSkipDogLife() {
|
|
||||||
globalThis.handleSkipDogLife();
|
|
||||||
},
|
|
||||||
getCardImagePath(cardName) {
|
|
||||||
return `/assets/images/cards/${cardName}.png`;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
const ScoreBoardComponent = {
|
|
||||||
data() {
|
|
||||||
return {
|
|
||||||
trumpsuit: 'N/A',
|
|
||||||
playerScores: [],
|
|
||||||
};
|
|
||||||
},
|
|
||||||
template: `
|
|
||||||
<div class="score-table mt-5" id="score-table-container">
|
|
||||||
<h4 class="fw-bold mb-3 text-black">Tricks Won</h4>
|
|
||||||
|
|
||||||
<div class="d-flex justify-content-between score-header pb-1">
|
|
||||||
<div style="width: 50%">PLAYER</div>
|
|
||||||
<div style="width: 50%">TRICKS</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div id="score-table-body">
|
|
||||||
<div v-for="(player, index) in playerScores"
|
|
||||||
:key="player.name"
|
|
||||||
class="d-flex justify-content-between score-row pt-1">
|
|
||||||
|
|
||||||
<div style="width: 50%" class="text-truncate">
|
|
||||||
{{ player.name }}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div style="width: 50%">
|
|
||||||
{{ player.tricks }}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
`,
|
|
||||||
|
|
||||||
methods: {
|
|
||||||
calculateNewScores(players, tricklist) {
|
|
||||||
const playercounts = new Map();
|
|
||||||
players.forEach(player => {
|
|
||||||
playercounts.set(player, 0)
|
|
||||||
});
|
|
||||||
|
|
||||||
tricklist.forEach(playerWonTrick => {
|
|
||||||
if (playerWonTrick !== "Trick in Progress" && playercounts.has(playerWonTrick)) {
|
|
||||||
playercounts.set(playerWonTrick, playercounts.get(playerWonTrick) + 1);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
const newScores = players.map(name => ({
|
|
||||||
name: name,
|
|
||||||
tricks: playercounts.get(name) || 0,
|
|
||||||
}));
|
|
||||||
|
|
||||||
newScores.sort((a, b) => b.tricks - a.tricks);
|
|
||||||
|
|
||||||
return newScores;
|
|
||||||
},
|
|
||||||
|
|
||||||
updateNewRoundData(eventData) {
|
|
||||||
console.log("Vue Scoreboard Data Update Triggered: New Round!");
|
|
||||||
|
|
||||||
this.playerScores = eventData.players.map(player => ({
|
|
||||||
name: player,
|
|
||||||
tricks: 0,
|
|
||||||
}));
|
|
||||||
},
|
|
||||||
|
|
||||||
updateTrickEndData(eventData) {
|
|
||||||
const { playerwon, playersin, tricklist } = eventData;
|
|
||||||
|
|
||||||
console.log(`Vue Scoreboard Data Update Triggered: ${playerwon} won the trick!`);
|
|
||||||
|
|
||||||
this.playerScores = this.calculateNewScores(playersin, tricklist);
|
|
||||||
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
const GameInfoComponent = {
|
|
||||||
data() {
|
|
||||||
return {
|
|
||||||
trumpsuit: 'No Trumpsuit',
|
|
||||||
firstCardImagePath: '/assets/images/cards/1B.png',
|
|
||||||
};
|
|
||||||
},
|
|
||||||
|
|
||||||
template: `
|
|
||||||
<div>
|
|
||||||
<h4 class="fw-semibold mb-1">Trumpsuit</h4>
|
|
||||||
<p class="fs-5 text-primary" id="trumpsuit">{{ 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" id="first-card-container">
|
|
||||||
|
|
||||||
<img :src="firstCardImagePath" alt="First Card" width="80px" style="border-radius: 6px"/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
`,
|
|
||||||
|
|
||||||
methods: {
|
|
||||||
resetFirstCard(eventData) {
|
|
||||||
console.log("GameInfoComponent: Resetting First Card to placeholder.");
|
|
||||||
this.firstCardImagePath = '/assets/images/cards/1B.png';
|
|
||||||
},
|
|
||||||
updateFirstCard(eventData) {
|
|
||||||
const firstCardId = eventData.firstCard;
|
|
||||||
console.log("GameInfoComponent: Updating First Card to:", firstCardId);
|
|
||||||
|
|
||||||
let imageSource;
|
|
||||||
if (firstCardId === "BLANK" || !firstCardId) {
|
|
||||||
imageSource = "/assets/images/cards/1B.png";
|
|
||||||
} else {
|
|
||||||
imageSource = `/assets/images/cards/${firstCardId}.png`;
|
|
||||||
}
|
|
||||||
this.firstCardImagePath = imageSource;
|
|
||||||
},
|
|
||||||
updateTrumpsuit(eventData) {
|
|
||||||
this.trumpsuit = eventData.trumpsuit;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
const TrickDisplayComponent = {
|
|
||||||
data() {
|
|
||||||
return {
|
|
||||||
playedCards: [],
|
|
||||||
};
|
|
||||||
},
|
|
||||||
|
|
||||||
template: `
|
|
||||||
<div class="d-flex justify-content-center g-3" id="trick-cards-content">
|
|
||||||
<div v-for="(play, index) in playedCards" :key="index" class="col-auto">
|
|
||||||
<div class="card text-center shadow-sm border-0 bg-transparent" style="width: 7rem;
|
|
||||||
backdrop-filter: blur(4px);">
|
|
||||||
<div class="p-2">
|
|
||||||
<img :src="getCardImagePath(play.cardId)" width="100%" style="border-radius: 6px"/>
|
|
||||||
</div>
|
|
||||||
<div class="card-body p-2 bg-transparent">
|
|
||||||
<small class="fw-semibold text-secondary">{{ play.player }}</small>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
`,
|
|
||||||
|
|
||||||
methods: {
|
|
||||||
getCardImagePath(cardId) {
|
|
||||||
return `/assets/images/cards/${cardId}.png`;
|
|
||||||
},
|
|
||||||
|
|
||||||
clearPlayedCards() {
|
|
||||||
console.log("TrickDisplayComponent: Clearing played cards.");
|
|
||||||
this.playedCards = [];
|
|
||||||
},
|
|
||||||
|
|
||||||
updatePlayedCards(eventData) {
|
|
||||||
console.log("TrickDisplayComponent: Updating played cards.");
|
|
||||||
|
|
||||||
this.playedCards = eventData.playedCards;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
function formatPlayerName(player) {
|
|
||||||
let name = player.name;
|
|
||||||
if (player.dog) {
|
|
||||||
name += " 🐶";
|
|
||||||
}
|
|
||||||
return name;
|
|
||||||
}
|
|
||||||
|
|
||||||
const TurnComponent = {
|
|
||||||
data() {
|
|
||||||
return {
|
|
||||||
currentPlayerName: 'Waiting...',
|
|
||||||
nextPlayers: [],
|
|
||||||
};
|
|
||||||
},
|
|
||||||
|
|
||||||
template: `
|
|
||||||
<div class="turn-tracker-container">
|
|
||||||
<h4 class="fw-semibold mb-1">Current Player</h4>
|
|
||||||
<p class="fs-3 fw-bold text-success" id="current-player-name">{{ currentPlayerName }}</p>
|
|
||||||
|
|
||||||
<div v-if="nextPlayers.length > 0">
|
|
||||||
<h5 class="fw-semibold mt-4 mb-1" id="next-players-text">Next Players</h5>
|
|
||||||
<div id="next-players-container">
|
|
||||||
<p v-for="name in nextPlayers" :key="name" class="fs-5 text-primary">{{ name }}</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
`,
|
|
||||||
methods: {
|
|
||||||
updateTurnData(eventData) {
|
|
||||||
console.log("TurnComponent: Updating turn data.");
|
|
||||||
const { currentPlayer, nextPlayers } = eventData;
|
|
||||||
|
|
||||||
this.currentPlayerName = formatPlayerName(currentPlayer);
|
|
||||||
|
|
||||||
this.nextPlayers = nextPlayers.map(player => formatPlayerName(player));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
const LobbyComponent = {
|
|
||||||
data() {
|
|
||||||
return {
|
|
||||||
lobbyName: 'Loading...',
|
|
||||||
lobbyId: 'default',
|
|
||||||
isHost: false,
|
|
||||||
maxPlayers: 0,
|
|
||||||
players: [],
|
|
||||||
showKickedModal: false,
|
|
||||||
kickedEventData: null,
|
|
||||||
showSessionClosedModal: false,
|
|
||||||
sessionClosedEventData: null,
|
|
||||||
};
|
|
||||||
},
|
|
||||||
|
|
||||||
template: `
|
|
||||||
<main class="lobby-background vh-100" id="lobbybackground">
|
|
||||||
|
|
||||||
<div v-if="showKickedModal" class="modal fade show d-block"
|
|
||||||
tabindex="-1"
|
|
||||||
role="dialog"
|
|
||||||
aria-labelledby="kickedModalTitle"
|
|
||||||
aria-modal="true"
|
|
||||||
style="background-color: rgba(0,0,0,0.5);">
|
|
||||||
|
|
||||||
<div class="modal-dialog modal-dialog-centered" role="document">
|
|
||||||
<div class="modal-content">
|
|
||||||
<div class="modal-header">
|
|
||||||
<h5 class="modal-title" id="kickedModalTitle">Kicked</h5>
|
|
||||||
</div>
|
|
||||||
<div class="modal-body">
|
|
||||||
<p>You've been kicked from the lobby.</p>
|
|
||||||
<p class="text-muted small">You'll get redirected to the mainmenu in 5 seconds...</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
|
|
||||||
<div v-if="showSessionClosedModal" class="modal fade show d-block"
|
|
||||||
tabindex="-1"
|
|
||||||
role="dialog"
|
|
||||||
aria-labelledby="sessionClosedModalTitle"
|
|
||||||
aria-modal="true"
|
|
||||||
style="background-color: rgba(0,0,0,0.5);">
|
|
||||||
<div class="modal-dialog modal-dialog-centered" role="document">
|
|
||||||
<div class="modal-content">
|
|
||||||
<div class="modal-header">
|
|
||||||
<h5 class="modal-title" id="sessionClosedModalTitle">Session Closed</h5>
|
|
||||||
</div>
|
|
||||||
<div class="modal-body">
|
|
||||||
<p>The session was closed.</p>
|
|
||||||
<p class="text-muted small">You'll get redirected to the mainmenu in 5 seconds...</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="container d-flex flex-column" style="height: calc(100vh - 1rem);">
|
|
||||||
|
|
||||||
<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: {{ lobbyName }}
|
|
||||||
</div>
|
|
||||||
<div class="btn btn-danger ms-auto" @click="leaveGame(lobbyId)">Exit</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="row">
|
|
||||||
<div class="col">
|
|
||||||
<div class="p-3 text-center fs-4" id="playerAmount">
|
|
||||||
Players: {{ players.length }} / {{ maxPlayers }}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="row justify-content-center align-items-center flex-grow-1">
|
|
||||||
|
|
||||||
<template v-if="isHost">
|
|
||||||
<div id="players" class="justify-content-center align-items-center d-flex flex-wrap">
|
|
||||||
<div v-for="player in players" :key="player.id" class="col-auto my-auto m-3">
|
|
||||||
<div class="card" style="width: 18rem;">
|
|
||||||
<img src="/assets/images/profile.png" alt="Profile" class="card-img-top w-50 mx-auto mt-3" />
|
|
||||||
<div class="card-body">
|
|
||||||
<h5 class="card-title">
|
|
||||||
{{ player.name }} <span v-if="player.self">(You)</span>
|
|
||||||
</h5>
|
|
||||||
|
|
||||||
<template v-if="player.self">
|
|
||||||
<a href="#" class="btn btn-danger disabled" aria-disabled="true" tabindex="-1">Remove</a>
|
|
||||||
</template>
|
|
||||||
<template v-else>
|
|
||||||
<div class="btn btn-danger" @click="handleKickPlayer(player.id)">Remove</div>
|
|
||||||
</template>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="col-12 text-center mb-5">
|
|
||||||
<div class="btn btn-success" @click="startGame()">Start Game</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<template v-else>
|
|
||||||
<div id="players" class="justify-content-center align-items-center d-flex flex-wrap">
|
|
||||||
<div v-for="player in players" :key="player.id" class="col-auto my-auto m-3">
|
|
||||||
<div class="card" style="width: 18rem;">
|
|
||||||
<img src="/assets/images/profile.png" alt="Profile" class="card-img-top w-50 mx-auto mt-3" />
|
|
||||||
<div class="card-body">
|
|
||||||
<h5 class="card-title">
|
|
||||||
{{ player.name }} <span v-if="player.self">(You)</span>
|
|
||||||
</h5>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="col-12 text-center mt-3">
|
|
||||||
<p class="fs-4">Waiting for the host to start the game...</p>
|
|
||||||
<div class="spinner-border mt-1" role="status">
|
|
||||||
<span class="visually-hidden">Loading...</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</main>
|
|
||||||
`,
|
|
||||||
|
|
||||||
methods: {
|
|
||||||
updateLobbyData(eventData) {
|
|
||||||
console.log("LobbyComponent: Received Lobby Update Event.");
|
|
||||||
|
|
||||||
this.isHost = eventData.host;
|
|
||||||
this.maxPlayers = eventData.maxPlayers;
|
|
||||||
this.players = eventData.players;
|
|
||||||
},
|
|
||||||
|
|
||||||
setInitialData(name, id) {
|
|
||||||
this.lobbyName = name;
|
|
||||||
this.lobbyId = id;
|
|
||||||
},
|
|
||||||
startGame() {
|
|
||||||
globalThis.startGame()
|
|
||||||
},
|
|
||||||
leaveGame(gameId) {
|
|
||||||
//TODO: Needs implementation
|
|
||||||
},
|
|
||||||
handleKickPlayer(playerId) {
|
|
||||||
globalThis.handleKickPlayer(playerId)
|
|
||||||
},
|
|
||||||
showKickModal(eventData) {
|
|
||||||
this.showKickedModal = true;
|
|
||||||
setTimeout(() => {
|
|
||||||
this.kickedEventData = eventData;
|
|
||||||
this.showKickedModal = false;
|
|
||||||
|
|
||||||
if (typeof globalThis.receiveGameStateChange === 'function') {
|
|
||||||
globalThis.receiveGameStateChange(this.kickedEventData);
|
|
||||||
} else {
|
|
||||||
console.error("FATAL: receiveGameStateChange ist nicht global definiert.");
|
|
||||||
}
|
|
||||||
}, 5000);
|
|
||||||
},
|
|
||||||
showSessionClosedModal(eventData) {
|
|
||||||
this.sessionClosedEventData = eventData;
|
|
||||||
this.showSessionClosedModal = true;
|
|
||||||
|
|
||||||
setTimeout(() => {
|
|
||||||
this.showSessionClosedModal = false;
|
|
||||||
|
|
||||||
if (typeof globalThis.receiveGameStateChange === 'function') {
|
|
||||||
globalThis.receiveGameStateChange(this.sessionClosedEventData);
|
|
||||||
} else {
|
|
||||||
console.error("FATAL: receiveGameStateChange ist nicht global definiert.");
|
|
||||||
}
|
|
||||||
}, 5000);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
function requestCardEvent(eventData) {
|
|
||||||
//TODO: Needs correct implementation of setting the inactive class in the PlayerHandComponent
|
|
||||||
}
|
|
||||||
function receiveGameStateChange(eventData) {
|
|
||||||
const content = eventData.content;
|
|
||||||
const title = eventData.title || 'Knockout Whist';
|
|
||||||
const url = eventData.url || null;
|
|
||||||
|
|
||||||
exchangeBody(content, title, url);
|
|
||||||
}
|
|
||||||
function receiveRoundEndEvent(eventData) {
|
|
||||||
//TODO: When alert is working, set an alert that shows how won the round and with how much tricks.
|
|
||||||
}
|
|
||||||
let playerHandApp = null;
|
|
||||||
let scoreBoardApp = null;
|
|
||||||
let gameInfoApp = null;
|
|
||||||
let trickDisplayApp = null;
|
|
||||||
let turnApp = null;
|
|
||||||
globalThis.initGameVueComponents = function() {
|
|
||||||
// Initializing PlayerHandComponent
|
|
||||||
const app = Vue.createApp(PlayerHandComponent);
|
|
||||||
|
|
||||||
playerHandApp = app;
|
|
||||||
const mountedHand = app.mount('#player-hand-container');
|
|
||||||
|
|
||||||
if (mountedHand && mountedHand.updateHand) {
|
|
||||||
globalThis.updatePlayerHand = mountedHand.updateHand;
|
|
||||||
onEvent("ReceivedHandEvent", globalThis.updatePlayerHand);
|
|
||||||
console.log("PLAYER HAND SYSTEM: updatePlayerHand successfully exposed.");
|
|
||||||
} else {
|
|
||||||
console.error("FATAL ERROR: PlayerHandComponent mount failed. Check if #player-hand-container exists.");
|
|
||||||
}
|
|
||||||
|
|
||||||
// Initializing Scoreboard
|
|
||||||
if (scoreBoardApp) return
|
|
||||||
|
|
||||||
const app2 = Vue.createApp(ScoreBoardComponent)
|
|
||||||
scoreBoardApp = app2
|
|
||||||
const mountedHand2 = app2.mount('#score-table')
|
|
||||||
if (mountedHand2) {
|
|
||||||
globalThis.updateNewRoundData = mountedHand2.updateNewRoundData;
|
|
||||||
onEvent("NewRoundEvent", handleNewRoundEvent);
|
|
||||||
|
|
||||||
globalThis.updateTrickEndData = mountedHand2.updateTrickEndData;
|
|
||||||
onEvent("TrickEndEvent", globalThis.updateTrickEndData);
|
|
||||||
console.log("SCOREBOARD: updateNewRoundData successfully exposed.");
|
|
||||||
} else {
|
|
||||||
console.error("FATAL ERROR: Scoreboard mount failed. Check if #score-table exists.");
|
|
||||||
}
|
|
||||||
// Initializing Gameinfo
|
|
||||||
if (gameInfoApp) return
|
|
||||||
|
|
||||||
const app3 = Vue.createApp(GameInfoComponent)
|
|
||||||
gameInfoApp = app3
|
|
||||||
const mountedGameInfo = app3.mount('#game-info-component')
|
|
||||||
if(mountedGameInfo) {
|
|
||||||
globalThis.resetFirstCard = mountedGameInfo.resetFirstCard;
|
|
||||||
globalThis.updateFirstCard = mountedGameInfo.updateFirstCard;
|
|
||||||
globalThis.updateTrumpsuit = mountedGameInfo.updateTrumpsuit
|
|
||||||
onEvent("NewTrickEvent", handleNewTrickEvent);
|
|
||||||
console.log("GameInfo: resetFirstCard successfully exposed.");
|
|
||||||
} else {
|
|
||||||
console.error("FATAL ERROR: GameInfo mount failed. Check if #score-table exists.");
|
|
||||||
}
|
|
||||||
|
|
||||||
// Initializing TrickCardContainer
|
|
||||||
if (trickDisplayApp) return;
|
|
||||||
const app4 = Vue.createApp(TrickDisplayComponent);
|
|
||||||
trickDisplayApp = app4;
|
|
||||||
const mountedTrickDisplay = app4.mount('#trick-cards-container');
|
|
||||||
|
|
||||||
if (mountedTrickDisplay) {
|
|
||||||
globalThis.clearPlayedCards = mountedTrickDisplay.clearPlayedCards;
|
|
||||||
globalThis.updatePlayedCards = mountedTrickDisplay.updatePlayedCards;
|
|
||||||
onEvent("CardPlayedEvent", handleCardPlayedEvent)
|
|
||||||
console.log("TRICK DISPLAY: Handlers successfully exposed (clearPlayedCards, updatePlayedCards).");
|
|
||||||
} else {
|
|
||||||
console.error("FATAL ERROR: TrickDisplay mount failed. Check if #trick-cards-container exists.");
|
|
||||||
}
|
|
||||||
|
|
||||||
// Initializing TurnContainer
|
|
||||||
if (turnApp) return;
|
|
||||||
const app5 = Vue.createApp(TurnComponent)
|
|
||||||
turnApp = app5;
|
|
||||||
const mountedTurnApp = app5.mount('#turn-component')
|
|
||||||
|
|
||||||
if(mountedTurnApp) {
|
|
||||||
globalThis.updateTurnData = mountedTurnApp.updateTurnData;
|
|
||||||
onEvent("TurnEvent", globalThis.updateTurnData);
|
|
||||||
console.log("TURN DISPLAY: Handlers successfully exposed (clearPlayedCards, updatePlayedCards).");
|
|
||||||
} else {
|
|
||||||
console.error("FATAL ERROR: TURNAPP mount failed. Check if #trick-cards-container exists.");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
let lobbyApp = null;
|
|
||||||
globalThis.initLobbyVueComponents = function(initialLobbyName, initialLobbyId, initialIsHost, initialMaxPlayers, initialPlayers) {
|
|
||||||
|
|
||||||
if (lobbyApp) return;
|
|
||||||
|
|
||||||
const appLobby = Vue.createApp(LobbyComponent);
|
|
||||||
lobbyApp = appLobby;
|
|
||||||
const mountedLobby = appLobby.mount('#lobby-app-mount');
|
|
||||||
|
|
||||||
if (mountedLobby) {
|
|
||||||
mountedLobby.setInitialData(initialLobbyName, initialLobbyId);
|
|
||||||
|
|
||||||
//Damit beim erstmaligen Betreten der Lobby die Spieler etc. angezeigt werden.
|
|
||||||
mountedLobby.updateLobbyData({
|
|
||||||
host: initialIsHost,
|
|
||||||
maxPlayers: initialMaxPlayers,
|
|
||||||
players: initialPlayers
|
|
||||||
});
|
|
||||||
|
|
||||||
globalThis.updateLobbyData = mountedLobby.updateLobbyData;
|
|
||||||
globalThis.showKickModal = mountedLobby.showKickModal;
|
|
||||||
globalThis.showSessionClosedModal = mountedLobby.showSessionClosedModal;
|
|
||||||
onEvent("LobbyUpdateEvent", globalThis.updateLobbyData);
|
|
||||||
onEvent("KickEvent", globalThis.showKickModal);
|
|
||||||
onEvent("SessionClosed", globalThis.showSessionClosedModal);
|
|
||||||
console.log("LobbyComponent successfully mounted and registered events.");
|
|
||||||
} else {
|
|
||||||
console.error("FATAL ERROR: LobbyComponent mount failed.");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
function handleCardPlayedEvent(eventData) {
|
|
||||||
console.log("CardPlayedEvent received. Updating Game Info and Trick Display.");
|
|
||||||
|
|
||||||
if (typeof globalThis.updateFirstCard === 'function') {
|
|
||||||
globalThis.updateFirstCard(eventData);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (typeof globalThis.updatePlayedCards === 'function') {
|
|
||||||
globalThis.updatePlayedCards(eventData);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
function handleNewTrickEvent(eventData) {
|
|
||||||
if (typeof globalThis.resetFirstCard === 'function') {
|
|
||||||
globalThis.resetFirstCard(eventData);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (typeof globalThis.clearPlayedCards === 'function') {
|
|
||||||
globalThis.clearPlayedCards();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
function handleNewRoundEvent(eventData) {
|
|
||||||
if (typeof globalThis.updateNewRoundData === 'function') {
|
|
||||||
globalThis.updateNewRoundData(eventData);
|
|
||||||
}
|
|
||||||
if (typeof globalThis.updateTrumpsuit === 'function') {
|
|
||||||
globalThis.updateTrumpsuit(eventData);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
onEvent("GameStateChangeEvent", receiveGameStateChange)
|
|
||||||
onEvent("LeftEvent", receiveGameStateChange)
|
|
||||||
onEvent("RequestCardEvent", requestCardEvent)
|
|
||||||
onEvent("RoundEndEvent", receiveRoundEndEvent)
|
|
||||||
@@ -1,33 +0,0 @@
|
|||||||
function handlePlayCard(cardidx) {
|
|
||||||
//TODO: Needs implementation
|
|
||||||
}
|
|
||||||
|
|
||||||
function handleSkipDogLife(button) {
|
|
||||||
// TODO needs implementation
|
|
||||||
}
|
|
||||||
function startGame() {
|
|
||||||
sendEvent("StartGame")
|
|
||||||
}
|
|
||||||
|
|
||||||
function handleTrumpSelection(object) {
|
|
||||||
const $button = $(object);
|
|
||||||
const trumpIndex = parseInt($button.data('trump'));
|
|
||||||
const payload = {
|
|
||||||
suitIndex: trumpIndex
|
|
||||||
}
|
|
||||||
sendEvent("PickTrumpsuit", payload)
|
|
||||||
|
|
||||||
}
|
|
||||||
function handleKickPlayer(playerId) {
|
|
||||||
sendEvent("KickPlayer", {
|
|
||||||
playerId: playerId
|
|
||||||
})
|
|
||||||
}
|
|
||||||
function handleReturnToLobby() {
|
|
||||||
sendEvent("ReturnToLobby")
|
|
||||||
}
|
|
||||||
|
|
||||||
globalThis.startGame = startGame
|
|
||||||
globalThis.handleTrumpSelection = handleTrumpSelection
|
|
||||||
globalThis.handleKickPlayer = handleKickPlayer
|
|
||||||
globalThis.handleReturnToLobby = handleReturnToLobby
|
|
||||||
@@ -1,222 +0,0 @@
|
|||||||
/*!
|
|
||||||
* 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)
|
|
||||||
})
|
|
||||||
})
|
|
||||||
})
|
|
||||||
})()
|
|
||||||
|
|
||||||
function createGameJS() {
|
|
||||||
let lobbyName = $('#lobbyname').val();
|
|
||||||
if ($.trim(lobbyName) === "") {
|
|
||||||
lobbyName = "DefaultLobby"
|
|
||||||
}
|
|
||||||
const jsonObj = {
|
|
||||||
lobbyname: lobbyName,
|
|
||||||
playeramount: $("#playeramount").val()
|
|
||||||
}
|
|
||||||
sendGameCreationRequest(jsonObj);
|
|
||||||
}
|
|
||||||
|
|
||||||
function sendGameCreationRequest(dataObject) {
|
|
||||||
const route = jsRoutes.controllers.MainMenuController.createGame();
|
|
||||||
|
|
||||||
$.ajax({
|
|
||||||
url: route.url,
|
|
||||||
type: route.type,
|
|
||||||
contentType: 'application/json',
|
|
||||||
data: JSON.stringify(dataObject),
|
|
||||||
dataType: 'json',
|
|
||||||
success: (data => {
|
|
||||||
if (data.status === 'success') {
|
|
||||||
exchangeBody(data.content, "Knockout Whist - Lobby", data.redirectUrl);
|
|
||||||
}
|
|
||||||
}),
|
|
||||||
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 exchangeBody(content, title = "Knockout Whist", url = null) {
|
|
||||||
if (url) {
|
|
||||||
window.history.pushState({}, title, url);
|
|
||||||
}
|
|
||||||
$("#main-body").html(content);
|
|
||||||
document.title = title;
|
|
||||||
}
|
|
||||||
|
|
||||||
function login() {
|
|
||||||
const username = $('#username').val();
|
|
||||||
const password = $('#password').val();
|
|
||||||
|
|
||||||
const jsonObj = {
|
|
||||||
username: username,
|
|
||||||
password: password
|
|
||||||
};
|
|
||||||
|
|
||||||
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') {
|
|
||||||
exchangeBody(data.content, 'Knockout Whist - Create Game', data.redirectUrl);
|
|
||||||
return
|
|
||||||
}
|
|
||||||
alert('Login failed. Please check your credentials and try again.');
|
|
||||||
}),
|
|
||||||
error: ((jqXHR) => {
|
|
||||||
const errorData = JSON.parse(jqXHR.responseText);
|
|
||||||
if (errorData?.errorMessage) {
|
|
||||||
alert(`${errorData.errorMessage}`);
|
|
||||||
} else {
|
|
||||||
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?.errorMessage) {
|
|
||||||
alert(`${errorData.errorMessage}`);
|
|
||||||
} else {
|
|
||||||
alert(`An unexpected error occurred. Please try again. Status: ${jqXHR.status}`);
|
|
||||||
}
|
|
||||||
})
|
|
||||||
});
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
function navSpa(page, title) {
|
|
||||||
const route = jsRoutes.controllers.MainMenuController.navSPA(page);
|
|
||||||
$.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, title, 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?.errorMessage) {
|
|
||||||
alert(`${errorData.errorMessage}`);
|
|
||||||
} else {
|
|
||||||
alert(`An unexpected error occurred. Please try again. Status: ${jqXHR.status}`);
|
|
||||||
}
|
|
||||||
})
|
|
||||||
});
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
globalThis.exchangeBody = exchangeBody;
|
|
||||||
@@ -1,192 +0,0 @@
|
|||||||
let ws = null;
|
|
||||||
const pending = new Map();
|
|
||||||
const handlers = new Map();
|
|
||||||
|
|
||||||
let timer = null;
|
|
||||||
|
|
||||||
function setupSocketHandlers(socket) {
|
|
||||||
socket.onmessage = (event) => {
|
|
||||||
console.debug("SERVER MESSAGE:", event.data);
|
|
||||||
let msg;
|
|
||||||
try {
|
|
||||||
msg = JSON.parse(event.data);
|
|
||||||
} catch (e) {
|
|
||||||
console.debug("Non-JSON message from server:", event.data, e);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const id = msg.id;
|
|
||||||
const eventType = msg.event;
|
|
||||||
const status = msg.status;
|
|
||||||
const data = msg.data;
|
|
||||||
|
|
||||||
if (id && typeof status === "string") {
|
|
||||||
const entry = pending.get(id);
|
|
||||||
if (!entry) return;
|
|
||||||
clearTimeout(entry.timer);
|
|
||||||
pending.delete(id);
|
|
||||||
|
|
||||||
if (status === "success") {
|
|
||||||
entry.resolve(data === undefined ? {} : data);
|
|
||||||
} else {
|
|
||||||
entry.reject(new Error(msg.error || "Server returned error"));
|
|
||||||
}
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (id && eventType) {
|
|
||||||
const handler = handlers.get(eventType);
|
|
||||||
const sendResponse = (result) => {
|
|
||||||
const response = {id: id, event: eventType, status: result};
|
|
||||||
if (socket && socket.readyState === WebSocket.OPEN) {
|
|
||||||
socket.send(JSON.stringify(response));
|
|
||||||
} else {
|
|
||||||
console.warn("Cannot send response, websocket not open");
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
if (!handler) {
|
|
||||||
console.warn("No handler for event:", eventType);
|
|
||||||
sendResponse({error: "No handler for event: " + eventType});
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
Promise.resolve(handler(data === undefined ? {} : data))
|
|
||||||
.then(_ => sendResponse("success"))
|
|
||||||
.catch(_ => sendResponse("error"));
|
|
||||||
} catch (err) {
|
|
||||||
sendResponse("error");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
socket.onerror = (error) => {
|
|
||||||
console.error("WebSocket Error:", error);
|
|
||||||
if (timer) clearInterval(timer);
|
|
||||||
for (const [id, entry] of pending.entries()) {
|
|
||||||
clearTimeout(entry.timer);
|
|
||||||
entry.reject(new Error("WebSocket error/closed"));
|
|
||||||
pending.delete(id);
|
|
||||||
}
|
|
||||||
if (socket.readyState === WebSocket.OPEN) socket.close(1000, "Unexpected error.");
|
|
||||||
};
|
|
||||||
|
|
||||||
socket.onclose = (event) => {
|
|
||||||
if (timer) clearInterval(timer);
|
|
||||||
for (const [id, entry] of pending.entries()) {
|
|
||||||
clearTimeout(entry.timer);
|
|
||||||
entry.reject(new Error("WebSocket closed"));
|
|
||||||
pending.delete(id);
|
|
||||||
}
|
|
||||||
if (event.wasClean) {
|
|
||||||
console.log(`Connection closed cleanly, code=${event.code} reason=${event.reason}`);
|
|
||||||
} else {
|
|
||||||
console.warn('Connection died unexpectedly.');
|
|
||||||
}
|
|
||||||
location.href = "/mainmenu";
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
function connectWebSocket(url = null) {
|
|
||||||
if (!url) {
|
|
||||||
const loc = window.location;
|
|
||||||
const protocol = loc.protocol === "https:" ? "wss:" : "ws:";
|
|
||||||
url = protocol + "//" + loc.host + "/websocket";
|
|
||||||
}
|
|
||||||
if (ws && ws.readyState === WebSocket.OPEN) return Promise.resolve();
|
|
||||||
if (ws && ws.readyState === WebSocket.CONNECTING) {
|
|
||||||
return new Promise((resolve, reject) => {
|
|
||||||
const prevOnOpen = ws.onopen;
|
|
||||||
const prevOnError = ws.onerror;
|
|
||||||
ws.onopen = (ev) => {
|
|
||||||
if (prevOnOpen) prevOnOpen(ev);
|
|
||||||
resolve();
|
|
||||||
};
|
|
||||||
ws.onerror = (err) => {
|
|
||||||
if (prevOnError) prevOnError(err);
|
|
||||||
reject(err);
|
|
||||||
};
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
ws = new WebSocket(url);
|
|
||||||
setupSocketHandlers(ws);
|
|
||||||
|
|
||||||
return new Promise((resolve, reject) => {
|
|
||||||
ws.onopen = () => {
|
|
||||||
console.log("WebSocket connection established!");
|
|
||||||
timer = setInterval(() => {
|
|
||||||
if (ws && ws.readyState === WebSocket.OPEN) {
|
|
||||||
sendEventAndWait("ping", {}).then(
|
|
||||||
() => console.debug("PING RESPONSE RECEIVED"),
|
|
||||||
).catch(
|
|
||||||
(err) => console.warn("PING ERROR:", err.message),
|
|
||||||
);
|
|
||||||
console.debug("PING SENT");
|
|
||||||
}
|
|
||||||
}, 5000);
|
|
||||||
resolve();
|
|
||||||
};
|
|
||||||
|
|
||||||
ws.onerror = (err) => {
|
|
||||||
reject(err);
|
|
||||||
};
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function disconnectWebSocket(code = 1000, reason = "Client disconnect") {
|
|
||||||
if (timer) {
|
|
||||||
clearInterval(timer);
|
|
||||||
timer = null;
|
|
||||||
}
|
|
||||||
if (ws) {
|
|
||||||
try {
|
|
||||||
ws.close(code, reason);
|
|
||||||
} catch (e) {
|
|
||||||
}
|
|
||||||
ws = null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function sendEvent(eventType, eventData) {
|
|
||||||
if (!ws || ws.readyState !== WebSocket.OPEN) {
|
|
||||||
console.warn("WebSocket is not open. Unable to send message.");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const id = Date.now().toString(36) + Math.random().toString(36).substring(2, 9);
|
|
||||||
const message = {id: id, event: eventType, data: eventData};
|
|
||||||
ws.send(JSON.stringify(message));
|
|
||||||
console.debug("SENT:", message);
|
|
||||||
}
|
|
||||||
|
|
||||||
function sendEventAndWait(eventType, eventData, timeoutMs = 10000) {
|
|
||||||
if (!ws || ws.readyState !== WebSocket.OPEN) {
|
|
||||||
return Promise.reject(new Error("WebSocket is not open"));
|
|
||||||
}
|
|
||||||
const id = Date.now().toString(36) + Math.random().toString(36).substring(2, 9);
|
|
||||||
const message = {id: id, event: eventType, data: eventData};
|
|
||||||
const p = new Promise((resolve, reject) => {
|
|
||||||
const timerId = setTimeout(() => {
|
|
||||||
if (pending.has(id)) {
|
|
||||||
pending.delete(id);
|
|
||||||
reject(new Error(`No response within ${timeoutMs}ms for id=${id}`));
|
|
||||||
}
|
|
||||||
}, timeoutMs);
|
|
||||||
pending.set(id, {resolve, reject, timer: timerId});
|
|
||||||
});
|
|
||||||
ws.send(JSON.stringify(message));
|
|
||||||
console.debug("SENT (await):", message);
|
|
||||||
return p;
|
|
||||||
}
|
|
||||||
|
|
||||||
function onEvent(eventType, handler) {
|
|
||||||
handlers.set(eventType, handler);
|
|
||||||
}
|
|
||||||
|
|
||||||
globalThis.sendEvent = sendEvent;
|
|
||||||
globalThis.sendEventAndWait = sendEventAndWait;
|
|
||||||
globalThis.onEvent = onEvent;
|
|
||||||
globalThis.connectWebSocket = connectWebSocket;
|
|
||||||
globalThis.disconnectWebSocket = disconnectWebSocket;
|
|
||||||
globalThis.isWebSocketConnected = () => !!ws && ws.readyState === WebSocket.OPEN;
|
|
||||||