feat(base): Fixed logic for websockets and added GameStateEvent. Might've caused instability on other feature branches! (#84)

Reviewed-on: #84
Reviewed-by: lq64 <lq@blackhole.local>
Co-authored-by: Janis <janis.e.20@gmx.de>
Co-committed-by: Janis <janis.e.20@gmx.de>
This commit is contained in:
2025-11-26 11:26:08 +01:00
committed by lq64
parent 52e5033afc
commit b81bb3d0ae
14 changed files with 200 additions and 134 deletions

View File

@@ -1,6 +1,7 @@
package controllers package controllers
import auth.{AuthAction, AuthenticatedRequest} import auth.{AuthAction, AuthenticatedRequest}
import de.knockoutwhist.control.GameState
import de.knockoutwhist.control.GameState.* import de.knockoutwhist.control.GameState.*
import exceptions.* import exceptions.*
import logic.PodManager import logic.PodManager
@@ -29,7 +30,7 @@ class IngameController @Inject()(
game match { game match {
case Some(g) => case Some(g) =>
val results = Try { val results = Try {
returnInnerHTML(g, request.user) IngameController.returnInnerHTML(g, g.logic.getCurrentState, request.user)
} }
if (results.isSuccess) { if (results.isSuccess) {
Ok(views.html.main("In-Game - Knockout Whist")(results.get)) Ok(views.html.main("In-Game - Knockout Whist")(results.get))
@@ -41,34 +42,6 @@ class IngameController @Inject()(
} }
} }
def returnInnerHTML(gameLobby: GameLobby, user: User): Html = {
gameLobby.logic.getCurrentState match {
case Lobby => views.html.lobby.lobby(Some(user), gameLobby)
case InGame =>
views.html.ingame.ingame(
gameLobby.getPlayerByUser(user),
gameLobby
)
case SelectTrump =>
views.html.ingame.selecttrump(
gameLobby.getPlayerByUser(user),
gameLobby
)
case TieBreak =>
views.html.ingame.tie(
gameLobby.getPlayerByUser(user),
gameLobby
)
case FinishedMatch =>
views.html.ingame.finishedMatch(
Some(user),
gameLobby
)
case _ =>
throw new IllegalStateException(s"Invalid game state for in-game view. GameId: ${gameLobby.id}" + s" State: ${gameLobby.logic.getCurrentState}")
}
}
def startGame(gameId: String): Action[AnyContent] = authAction { implicit request: AuthenticatedRequest[AnyContent] => def startGame(gameId: String): Action[AnyContent] = authAction { implicit request: AuthenticatedRequest[AnyContent] =>
val game = PodManager.getGame(gameId) val game = PodManager.getGame(gameId)
val result = Try { val result = Try {
@@ -83,7 +56,7 @@ class IngameController @Inject()(
Ok(Json.obj( Ok(Json.obj(
"status" -> "success", "status" -> "success",
"redirectUrl" -> routes.IngameController.game(gameId).url, "redirectUrl" -> routes.IngameController.game(gameId).url,
"content" -> returnInnerHTML(game.get, request.user).toString() "content" -> IngameController.returnInnerHTML(game.get, game.get.logic.getCurrentState, request.user).toString()
)) ))
} else { } else {
val throwable = result.failed.get val throwable = result.failed.get
@@ -434,5 +407,37 @@ class IngameController @Inject()(
)) ))
} }
} }
}
} 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}")
}
}
}

View File

@@ -16,8 +16,7 @@ import javax.inject.*
@Singleton @Singleton
class MainMenuController @Inject()( class MainMenuController @Inject()(
val controllerComponents: ControllerComponents, val controllerComponents: ControllerComponents,
val authAction: AuthAction, val authAction: AuthAction
val ingameController: IngameController
) extends BaseController { ) extends BaseController {
// Pass the request-handling function directly to authAction (no nested Action) // Pass the request-handling function directly to authAction (no nested Action)
@@ -46,7 +45,7 @@ class MainMenuController @Inject()(
Ok(Json.obj( Ok(Json.obj(
"status" -> "success", "status" -> "success",
"redirectUrl" -> routes.IngameController.game(gameLobby.id).url, "redirectUrl" -> routes.IngameController.game(gameLobby.id).url,
"content" -> ingameController.returnInnerHTML(gameLobby, request.user).toString "content" -> IngameController.returnInnerHTML(gameLobby, gameLobby.logic.getCurrentState, request.user).toString
)) ))
} else { } else {
BadRequest(Json.obj( BadRequest(Json.obj(
@@ -70,7 +69,7 @@ class MainMenuController @Inject()(
Ok(Json.obj( Ok(Json.obj(
"status" -> "success", "status" -> "success",
"redirectUrl" -> routes.IngameController.game(g.id).url, "redirectUrl" -> routes.IngameController.game(g.id).url,
"content" -> ingameController.returnInnerHTML(g, request.user).toString "content" -> IngameController.returnInnerHTML(g, g.logic.getCurrentState, request.user).toString
)) ))
case None => case None =>
NotFound(Json.obj( NotFound(Json.obj(

View File

@@ -12,13 +12,19 @@ class UserWebsocketActor(
session: UserSession session: UserSession
) extends Actor { ) extends Actor {
if (session.websocketActor.isDefined) { {
session.websocketActor.foreach(actor => actor.transmitTextToClient("Error: Multiple websocket connections detected. Closing this connection.")) session.lock.lock()
context.stop(self) if (session.websocketActor.isDefined) {
} else { val otherWebsocket = session.websocketActor.get
otherWebsocket.transmitTextToClient("Error: Multiple websocket connections detected. Closing your connection.")
context.stop(otherWebsocket.self)
transmitTextToClient("Previous websocket connection closed. You are now connected.")
}
session.websocketActor = Some(this) session.websocketActor = Some(this)
session.lock.unlock()
} }
override def receive: Receive = { override def receive: Receive = {
case msg: String => case msg: String =>
val jsonObject = Try { val jsonObject = Try {
@@ -86,12 +92,12 @@ class UserWebsocketActor(
} }
} }
def transmitJsonToClient(jsonObj: JsObject): Unit = { def transmitJsonToClient(jsonObj: JsValue): Unit = {
transmitTextToClient(jsonObj.toString()) transmitTextToClient(jsonObj.toString())
} }
def transmitEventToClient(event: SimpleEvent): Unit = { def transmitEventToClient(event: SimpleEvent): Unit = {
transmitJsonToClient(WebsocketEventMapper.toJson(event)) transmitJsonToClient(WebsocketEventMapper.toJson(event, session))
} }
} }

View File

@@ -2,10 +2,11 @@ package util
import de.knockoutwhist.utils.events.SimpleEvent import de.knockoutwhist.utils.events.SimpleEvent
import logic.game.GameLobby import logic.game.GameLobby
import model.sessions.UserSession
import play.api.libs.json.{JsValue, Json} import play.api.libs.json.{JsValue, Json}
import tools.jackson.databind.json.JsonMapper import tools.jackson.databind.json.JsonMapper
import tools.jackson.module.scala.ScalaModule import tools.jackson.module.scala.ScalaModule
import util.mapper.{ReceivedHandEventMapper, SimpleEventMapper} import util.mapper.{GameStateEventMapper, ReceivedHandEventMapper, SimpleEventMapper}
object WebsocketEventMapper { object WebsocketEventMapper {
@@ -24,10 +25,11 @@ object WebsocketEventMapper {
// Register all custom mappers here // Register all custom mappers here
registerCustomMapper(ReceivedHandEventMapper) registerCustomMapper(ReceivedHandEventMapper)
registerCustomMapper(GameStateEventMapper)
def toJson(obj: SimpleEvent, gameLobby: GameLobby): JsValue = { def toJson(obj: SimpleEvent, session: UserSession): JsValue = {
val data: Option[JsValue] = if (customMappers.contains(obj.id)) { val data: Option[JsValue] = if (customMappers.contains(obj.id)) {
Some(customMappers(obj.id).toJson(obj)) Some(customMappers(obj.id).toJson(obj, session))
}else { }else {
None None
} }

View File

@@ -0,0 +1,18 @@
package util.mapper
import controllers.IngameController
import de.knockoutwhist.events.global.GameStateChangeEvent
import model.sessions.UserSession
import play.api.libs.json.{JsObject, Json}
object GameStateEventMapper extends SimpleEventMapper[GameStateChangeEvent] {
override def id: String = "GameStateChangeEvent"
override def toJson(event: GameStateChangeEvent, session: UserSession): JsObject = {
Json.obj(
//Title
"content" -> IngameController.returnInnerHTML(session.gameLobby, event.newState, session.user).toString
)
}
}

View File

@@ -1,14 +1,14 @@
package util.mapper package util.mapper
import de.knockoutwhist.events.player.ReceivedHandEvent import de.knockoutwhist.events.player.ReceivedHandEvent
import logic.game.GameLobby import model.sessions.UserSession
import play.api.libs.json.{JsArray, JsObject, Json} import play.api.libs.json.{JsObject, Json}
import util.WebUIUtils import util.WebUIUtils
object ReceivedHandEventMapper extends SimpleEventMapper[ReceivedHandEvent] { object ReceivedHandEventMapper extends SimpleEventMapper[ReceivedHandEvent] {
override def id: String = "ReceivedHandEvent" override def id: String = "ReceivedHandEvent"
override def toJson(event: ReceivedHandEvent, gameLobby: GameLobby): JsObject = { override def toJson(event: ReceivedHandEvent, session: UserSession): JsObject = {
Json.obj( Json.obj(
"dog" -> event.player.isInDogLife, "dog" -> event.player.isInDogLife,
"hand" -> event.player.currentHand().map(hand => WebUIUtils.handToJson(hand)) "hand" -> event.player.currentHand().map(hand => WebUIUtils.handToJson(hand))

View File

@@ -2,11 +2,12 @@ package util.mapper
import de.knockoutwhist.utils.events.SimpleEvent import de.knockoutwhist.utils.events.SimpleEvent
import logic.game.GameLobby import logic.game.GameLobby
import model.sessions.UserSession
import play.api.libs.json.JsObject import play.api.libs.json.JsObject
trait SimpleEventMapper[T <: SimpleEvent] { trait SimpleEventMapper[T <: SimpleEvent] {
def id: String def id: String
def toJson(event: T, gameLobby: GameLobby): JsObject def toJson(event: T, session: UserSession): JsObject
} }

View File

@@ -10,8 +10,12 @@
<div class="row ms-4 me-4"> <div class="row ms-4 me-4">
<div class="col-4 mt-5 text-start"> <div class="col-4 mt-5 text-start">
<h4 class="fw-semibold mb-1">Current Player</h4> <h4 class="fw-semibold mb-1">Current Player</h4>
<p class="fs-5 text-primary" id="current-player-name">@gamelobby.getLogic.getCurrentPlayer.get.name</p> @if(gamelobby.getLogic.getCurrentPlayer.isDefined) {
@if(!TrickUtil.isOver(gamelobby.getLogic.getCurrentMatch.get, gamelobby.getLogic.getPlayerQueue.get)) { <p class="fs-5 text-primary" id="current-player-name">@gamelobby.getLogic.getCurrentPlayer.get.name</p>
}else {
<p class="fs-5 text-primary" id="current-player-name">---</p>
}
@if(gamelobby.getLogic.getPlayerQueue.isDefined && gamelobby.getLogic.getCurrentMatch && !TrickUtil.isOver(gamelobby.getLogic.getCurrentMatch.get, gamelobby.getLogic.getPlayerQueue.get)) {
<h4 class="fw-semibold mb-1">Next Player</h4> <h4 class="fw-semibold mb-1">Next Player</h4>
@for(nextplayer <- gamelobby.getLogic.getPlayerQueue.get.duplicate()) { @for(nextplayer <- gamelobby.getLogic.getPlayerQueue.get.duplicate()) {
<p class="fs-5 text-primary" id="next-player-name">@nextplayer</p> <p class="fs-5 text-primary" id="next-player-name">@nextplayer</p>
@@ -29,41 +33,56 @@
<div style="width: 50%">TRICKS</div> <div style="width: 50%">TRICKS</div>
</div> </div>
@for(player <- gamelobby.getLogic.getPlayerQueue.get.toList.sortBy { p => @if(gamelobby.getLogic.getPlayerQueue.isDefined) {
-(gamelobby.getLogic.getCurrentRound.get.tricklist.filter { trick => trick.winner.contains(p) }.size) @for(player <- gamelobby.getLogic.getPlayerQueue.get.toList.sortBy { p =>
}) { -(gamelobby.getLogic.getCurrentRound.get.tricklist.filter { trick => trick.winner.contains(p) }.size)
<div class="d-flex justify-content-between score-row pt-1"> }) {
<div style="width: 50%" class="text-truncate">@player.name</div> <div class="d-flex justify-content-between score-row pt-1">
<div style="width: 50%"> <div style="width: 50%" class="text-truncate">@player.name</div>
@(gamelobby.getLogic.getCurrentRound.get.tricklist.filter { trick => trick.winner.contains(player) }.size) <div style="width: 50%">
@(gamelobby.getLogic.getCurrentRound.get.tricklist.filter { trick => trick.winner.contains(player) }.size)
</div>
</div> </div>
}
}else{
<div class="d-flex justify-content-between score-row pt-1">
</div> </div>
} }
</div> </div>
<div class="d-flex justify-content-center g-3 mb-5" id="trick-cards-container"> <div class="d-flex justify-content-center g-3 mb-5" id="trick-cards-container">
@for((cardplayed, player) <- gamelobby.getLogic.getCurrentTrick.get.cards) { @if(gamelobby.getLogic.getCurrentTrick.isEmpty || gamelobby.getLogic.getCurrentTrick.get.cards.isEmpty) {
<div class="col-auto"> <div 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">
@util.WebUIUtils.cardtoImage(cardplayed) width="100%"/>
</div>
<div class="card-body p-2 bg-transparent">
<small class="fw-semibold text-secondary">@player</small>
</div>
</div> </div>
</div> } else {
} @for((cardplayed, player) <- gamelobby.getLogic.getCurrentTrick.get.cards) {
<div 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">
@util.WebUIUtils.cardtoImage(cardplayed) width="100%"/>
</div>
<div class="card-body p-2 bg-transparent">
<small class="fw-semibold text-secondary">@player</small>
</div>
</div>
</div>
}
}
</div> </div>
</div> </div>
<div class="col-4 mt-5 text-end"> <div class="col-4 mt-5 text-end">
<h4 class="fw-semibold mb-1">Trumpsuit</h4> <h4 class="fw-semibold mb-1">Trumpsuit</h4>
<p class="fs-5 text-primary" id="trumpsuit">@gamelobby.getLogic.getCurrentRound.get.trumpSuit</p> @if(gamelobby.getLogic.getCurrentRound.isEmpty) {
<p class="fs-5 text-primary" id="trumpsuit">No Trumpsuit</p>
}else {
<p class="fs-5 text-primary" id="trumpsuit">@gamelobby.getLogic.getCurrentRound.get.trumpSuit</p>
}
<h5 class="fw-semibold mt-4 mb-1">First Card</h5> <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"> <div class="d-inline-block border rounded shadow-sm p-1 bg-light" id="first-card-container">
@if(gamelobby.getLogic.getCurrentTrick.get.firstCard.isDefined) { @if(gamelobby.getLogic.getCurrentTrick.isDefined && gamelobby.getLogic.getCurrentTrick.get.firstCard.isDefined) {
@util.WebUIUtils.cardtoImage(gamelobby.getLogic.getCurrentTrick.get.firstCard.get) @util.WebUIUtils.cardtoImage(gamelobby.getLogic.getCurrentTrick.get.firstCard.get)
width="80px"/> width="80px"/>
} else { } else {
@@ -79,19 +98,23 @@
<div class="row justify-content-center ingame-cards-slide @{ <div class="row justify-content-center ingame-cards-slide @{
!gamelobby.logic.getCurrentPlayer.contains(player) ? "inactive" |: "" !gamelobby.logic.getCurrentPlayer.contains(player) ? "inactive" |: ""
}" id="card-slide"> }" id="card-slide">
@for(i <- player.currentHand().get.cards.indices) { @if(player.currentHand().isEmpty || player.currentHand().get.cards.isEmpty) {
<div class="col-auto handcard" style="border-radius: 6px">
<div class="btn btn-outline-light p-0 border-0 shadow-none" data-card-id="@i" style="border-radius: 6px" onclick="handlePlayCard(this, '@gamelobby.id', '@player.isInDogLife')"> } else {
@util.WebUIUtils.cardtoImage(player.currentHand().get.cards(i)) width="120px" style="border-radius: 6px"/> @for(i <- player.currentHand().get.cards.indices) {
</div> <div class="col-auto handcard" style="border-radius: 6px">
@if(player.isInDogLife) { <div class="btn btn-outline-light p-0 border-0 shadow-none" data-card-id="@i" style="border-radius: 6px" onclick="handlePlayCard(this, '@gamelobby.id', '@player.isInDogLife')">
<div class="mt-2"> @util.WebUIUtils.cardtoImage(player.currentHand().get.cards(i)) width="120px" style="border-radius: 6px"/>
<button class="btn btn-danger" onclick="handleSkipDogLife(this, '@gamelobby.id')"> </div>
Skip Dog Life</button> @if(player.isInDogLife) {
<div class="mt-2">
<button class="btn btn-danger" onclick="handleSkipDogLife(this, '@gamelobby.id')">
Skip Dog Life</button>
</div>
}
</div> </div>
} }
</div> }
}
</div> </div>
</div> </div>
</div> </div>

View File

@@ -10,49 +10,51 @@
<h3 class="mb-0">Select Trump Suit</h3> <h3 class="mb-0">Select Trump Suit</h3>
</div> </div>
<div class="card-body"> <div class="card-body">
@if(player.equals(gamelobby.logic.getCurrentMatch.get.roundlist.last.winner.get)) { @if(gamelobby.logic.getCurrentMatch.isDefined) {
<div class="alert alert-info" role="alert" aria-live="polite"> @if(player.equals(gamelobby.logic.getCurrentMatch.get.roundlist.last.winner.get)) {
You (@player.toString) won the last round. Choose the trump suit for the next round. <div class="alert alert-info" role="alert" aria-live="polite">
</div> 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="row justify-content-center col-auto mb-5">
<div class="col-auto handcard"> <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, '@gamelobby.id')"> <div class="btn btn-outline-light p-0 border-0 shadow-none" data-trump="0" style="border-radius: 6px" onclick="handleTrumpSelection(this, '@gamelobby.id')">
@util.WebUIUtils.cardtoImage(de.knockoutwhist.cards.Card(de.knockoutwhist.cards.CardValue.Ace, de.knockoutwhist.cards.Suit.Spades)) @util.WebUIUtils.cardtoImage(de.knockoutwhist.cards.Card(de.knockoutwhist.cards.CardValue.Ace, de.knockoutwhist.cards.Suit.Spades))
width="120px" style="border-radius: 6px"/> 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, '@gamelobby.id')">
@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, '@gamelobby.id')">
@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, '@gamelobby.id')">
@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> </div>
<div class="col-auto handcard"> <div class="row justify-content-center ingame-cards-slide" id="card-slide">
<div class="btn btn-outline-light p-0 border-0 shadow-none" data-trump="1" style="border-radius: 6px" onclick="handleTrumpSelection(this, '@gamelobby.id')"> @for(i <- player.currentHand().get.cards.indices) {
@util.WebUIUtils.cardtoImage(de.knockoutwhist.cards.Card(de.knockoutwhist.cards.CardValue.Ace, de.knockoutwhist.cards.Suit.Hearts)) <div class="col-auto" style="border-radius: 6px">
width="120px" style="border-radius: 6px"/> @util.WebUIUtils.cardtoImage(player.currentHand().get.cards(i)) width="120px" style="border-radius: 6px"/>
</div> </div>
}
</div> </div>
<div class="col-auto handcard"> } else {
<div class="btn btn-outline-light p-0 border-0 shadow-none" data-trump="2" style="border-radius: 6px" onclick="handleTrumpSelection(this, '@gamelobby.id')"> <div class="alert alert-warning" role="alert" aria-live="polite">
@util.WebUIUtils.cardtoImage(de.knockoutwhist.cards.Card(de.knockoutwhist.cards.CardValue.Ace, de.knockoutwhist.cards.Suit.Diamonds)) @gamelobby.logic.getCurrentMatch.get.roundlist.last.winner.get.name
width="120px" style="border-radius: 6px"/> is choosing a trumpsuit. The new round will start once a suit is picked.
</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, '@gamelobby.id')">
@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>
} }
</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>

View File

@@ -18,16 +18,17 @@
<link href="https://cdn.jsdelivr.net/npm/bootstrap@@5.3.8/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-sRIl4kxILFvY47J16cr9ZwB07vP4J8+LH7qKQnuqkuIAvNWLzeN8tE5YBujZqJLB" crossorigin="anonymous"> <link href="https://cdn.jsdelivr.net/npm/bootstrap@@5.3.8/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-sRIl4kxILFvY47J16cr9ZwB07vP4J8+LH7qKQnuqkuIAvNWLzeN8tE5YBujZqJLB" crossorigin="anonymous">
</head> </head>
<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="@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"> <body class="d-flex flex-column min-vh-100" id="main-body">
@* And here's where we render the `Html` object containing @* And here's where we render the `Html` object containing
* the page content. *@ * the page content. *@
@content @content
</body> </body>
<script src="@routes.JavaScriptRoutingController.javascriptRoutes()" type="text/javascript"></script>
<script src="@routes.Assets.versioned("javascripts/main.js")" type="text/javascript"></script>
<script src="@routes.Assets.versioned("../../public/javascripts/websocket.js")" type="text/javascript"></script>
<script src="@routes.Assets.versioned("../../public/javascripts/events.js")" type="text/javascript"></script>
<script src="@routes.Assets.versioned("../../public/javascripts/interact.js")" type="text/javascript"></script>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@@5.3.8/dist/js/bootstrap.bundle.min.js" integrity="sha384-FKyoEForCGlyvwx9Hj09JcYn3nv7wiPVlz7YYwJrWVcXK/BmnVDxM+D2scQbITxI" crossorigin="anonymous"></script>
<script src="https://code.jquery.com/jquery-3.6.0.min.js"></script>
</html> </html>

View File

@@ -39,4 +39,12 @@ function receiveHandEvent(eventData) {
handElement.html(newHtml); handElement.html(newHtml);
} }
onEvent("ReceivedHandEvent", receiveHandEvent) function receiveGameStateChange(eventData) {
const content = eventData.content;
const title = eventData.title || 'Knockout Whist';
exchangeBody(content, title);
}
onEvent("ReceivedHandEvent", receiveHandEvent)
onEvent("GameStateChangeEvent", receiveGameStateChange)

View File

@@ -216,4 +216,7 @@ function navSpa(page, title) {
}) })
}); });
return false return false
} }
globalThis.exchangeBody = exchangeBody;

View File

@@ -1,10 +1,7 @@
type EventHandler = (data: any) => any | Promise<any>;
// javascript // javascript
let ws = null; // will be created by connectWebSocket() let ws = null; // will be created by connectWebSocket()
const pending: Map<string, any> = new Map(); // id -> { resolve, reject, timer } const pending = new Map(); // id -> { resolve, reject, timer }
const handlers: Map<string, EventHandler> = new Map(); // eventType -> handler(data) -> (value|Promise) const handlers = new Map(); // eventType -> handler(data) -> (value|Promise)
let timer = null; let timer = null;
@@ -52,6 +49,7 @@ function setupSocketHandlers(socket) {
if (!handler) { if (!handler) {
// no handler: respond with an error object in data so server can fail it // no handler: respond with an error object in data so server can fail it
console.warn("No handler for event:", eventType);
sendResponse({error: "No handler for event: " + eventType}); sendResponse({error: "No handler for event: " + eventType});
return; return;
} }
@@ -182,7 +180,7 @@ function sendEventAndWait(eventType, eventData, timeoutMs = 10000) {
return p; return p;
} }
function onEvent(eventType: string, handler: EventHandler) { function onEvent(eventType, handler) {
handlers.set(eventType, handler); handlers.set(eventType, handler);
} }