Compare commits
2 Commits
f48eb1396e
...
4.2.0
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
576e5af87e | ||
| 3c0828fdbe |
@@ -149,3 +149,8 @@
|
||||
|
||||
* **api:** Implement received hand event handling and UI updates ([#83](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/issues/83)) ([52e5033](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/commit/52e5033afca344ae40a644196555a9655913710a)), closes [#76](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/issues/76)
|
||||
* **base:** Fixed logic for websockets and added GameStateEvent. Might've caused instability on other feature branches! ([#84](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/issues/84)) ([b81bb3d](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/commit/b81bb3d0aeb8500a9d7417a10e24e7f8a17d71d2))
|
||||
## (2025-11-26)
|
||||
|
||||
### Features
|
||||
|
||||
* **api:** Implemented card played event via websocket ([#85](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/issues/85)) ([3c0828f](https://git.janis-eccarius.de/KnockOutWhist/KnockOutWhist-Web/commit/3c0828fdbeb507706b86f1662476c46e760533e4))
|
||||
|
||||
@@ -40,7 +40,7 @@ lazy val knockoutwhistweb = project.in(file("knockoutwhistweb"))
|
||||
libraryDependencies += "de.mkammerer" % "argon2-jvm" % "2.12",
|
||||
libraryDependencies += "com.auth0" % "java-jwt" % "4.5.0",
|
||||
libraryDependencies += "com.github.ben-manes.caffeine" % "caffeine" % "3.2.3",
|
||||
libraryDependencies += "com.fasterxml.jackson.module" %% "jackson-module-scala" % "2.16.1",
|
||||
libraryDependencies += "tools.jackson.module" %% "jackson-module-scala" % "3.0.2",
|
||||
JsEngineKeys.engineType := JsEngineKeys.EngineType.Node
|
||||
)
|
||||
|
||||
|
||||
Submodule knockoutwhist updated: afa6bc1406...c5dd02a5e8
@@ -2,7 +2,7 @@ package logic.game
|
||||
|
||||
import de.knockoutwhist.cards.{Hand, Suit}
|
||||
import de.knockoutwhist.control.GameLogic
|
||||
import de.knockoutwhist.control.GameState.{InGame, Lobby, MainMenu}
|
||||
import de.knockoutwhist.control.GameState.{Lobby, MainMenu}
|
||||
import de.knockoutwhist.control.controllerBaseImpl.sublogic.util.{MatchUtil, PlayerUtil}
|
||||
import de.knockoutwhist.events.global.{GameStateChangeEvent, SessionClosed}
|
||||
import de.knockoutwhist.events.player.PlayerEvent
|
||||
@@ -56,9 +56,6 @@ class GameLobby private(
|
||||
if (event.oldState == MainMenu && event.newState == Lobby) {
|
||||
return
|
||||
}
|
||||
if (event.oldState == Lobby && event.newState == InGame) {
|
||||
println("RECEIVED GAMESTATEEVENT")
|
||||
}
|
||||
users.values.foreach(session => session.updatePlayer(event))
|
||||
case event: SimpleEvent =>
|
||||
users.values.foreach(session => session.updatePlayer(event))
|
||||
@@ -71,7 +68,6 @@ class GameLobby private(
|
||||
* @param user the user who wants to start the game.
|
||||
*/
|
||||
def startGame(user: User): Unit = {
|
||||
println("STARTED GAME IN LOGIC")
|
||||
val sessionOpt = users.get(user.id)
|
||||
if (sessionOpt.isEmpty) {
|
||||
throw new NotInThisGameException("You are not in this game!")
|
||||
|
||||
@@ -2,11 +2,9 @@ package model.sessions
|
||||
|
||||
import de.knockoutwhist.events.player.{RequestCardEvent, RequestTieChoiceEvent, RequestTrumpSuitEvent}
|
||||
import de.knockoutwhist.utils.events.SimpleEvent
|
||||
import logic.PodManager
|
||||
import logic.game.GameLobby
|
||||
import model.users.User
|
||||
import play.api.libs.json.Format.GenericFormat
|
||||
import play.api.libs.json.{JsError, JsObject, JsResult, JsSuccess, JsValue}
|
||||
import play.api.libs.json.JsObject
|
||||
|
||||
import java.util.UUID
|
||||
import java.util.concurrent.locks.ReentrantLock
|
||||
@@ -28,7 +26,7 @@ class UserSession(val user: User, val host: Boolean, val gameLobby: GameLobby) e
|
||||
else canInteract = Some(InteractionType.Card)
|
||||
case _ =>
|
||||
}
|
||||
websocketActor.foreach(_.transmitEventToClient(event, gameLobby))
|
||||
websocketActor.foreach(_.transmitEventToClient(event))
|
||||
}
|
||||
|
||||
override def id: UUID = user.id
|
||||
@@ -39,16 +37,13 @@ class UserSession(val user: User, val host: Boolean, val gameLobby: GameLobby) e
|
||||
canInteract = None
|
||||
}
|
||||
|
||||
def handleWebResponse(eventType: String, data: JsObject, user: User, gameLobby: GameLobby): Unit = {
|
||||
def handleWebResponse(eventType: String, data: JsObject): Unit = {
|
||||
lock.lock()
|
||||
Try {
|
||||
eventType match {
|
||||
case "Ping" =>
|
||||
// No action needed for Ping
|
||||
()
|
||||
case "Start Game" =>
|
||||
println("INSIDE HANDLE WEB RESPONSE" + data)
|
||||
gameLobby.startGame(user)
|
||||
}
|
||||
}
|
||||
lock.unlock()
|
||||
|
||||
@@ -74,7 +74,7 @@ class UserWebsocketActor(
|
||||
val event = eventOpt.get
|
||||
val data = (json \ "data").asOpt[JsObject].getOrElse(Json.obj())
|
||||
val result = Try {
|
||||
session.handleWebResponse(event, data, session.user, session.gameLobby)
|
||||
session.handleWebResponse(event, data)
|
||||
}
|
||||
if (result.isSuccess) {
|
||||
transmitJsonToClient(Json.obj(
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
package model.users
|
||||
|
||||
import play.api.libs.json.{Format, Json}
|
||||
|
||||
import java.util.UUID
|
||||
|
||||
case class User(
|
||||
@@ -18,4 +16,5 @@ case class User(
|
||||
private def withPasswordHash(newPasswordHash: String): User = {
|
||||
this.copy(passwordHash = newPasswordHash)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -6,7 +6,7 @@ import model.sessions.UserSession
|
||||
import play.api.libs.json.{JsValue, Json}
|
||||
import tools.jackson.databind.json.JsonMapper
|
||||
import tools.jackson.module.scala.ScalaModule
|
||||
import util.mapper.{GameStateEventMapper, ReceivedHandEventMapper, SimpleEventMapper}
|
||||
import util.mapper.{CardPlayedEventMapper, GameStateEventMapper, ReceivedHandEventMapper, SimpleEventMapper}
|
||||
|
||||
object WebsocketEventMapper {
|
||||
|
||||
@@ -26,11 +26,8 @@ object WebsocketEventMapper {
|
||||
// Register all custom mappers here
|
||||
registerCustomMapper(ReceivedHandEventMapper)
|
||||
registerCustomMapper(GameStateEventMapper)
|
||||
registerCustomMapper(NewRoundEventMapper)
|
||||
registerCustomMapper(NewTrickEventMapper)
|
||||
registerCustomMapper(TrickEndEventMapper)
|
||||
registerCustomMapper(RequestCardEventMapper)
|
||||
|
||||
registerCustomMapper(CardPlayedEventMapper)
|
||||
|
||||
def toJson(obj: SimpleEvent, session: UserSession): JsValue = {
|
||||
val data: Option[JsValue] = if (customMappers.contains(obj.id)) {
|
||||
Some(customMappers(obj.id).toJson(obj, session))
|
||||
|
||||
20
knockoutwhistweb/app/util/mapper/CardPlayedEventMapper.scala
Normal file
20
knockoutwhistweb/app/util/mapper/CardPlayedEventMapper.scala
Normal file
@@ -0,0 +1,20 @@
|
||||
package util.mapper
|
||||
|
||||
import de.knockoutwhist.events.global.CardPlayedEvent
|
||||
import model.sessions.UserSession
|
||||
import play.api.libs.json.{JsArray, JsObject, Json}
|
||||
import util.WebUIUtils
|
||||
|
||||
object CardPlayedEventMapper extends SimpleEventMapper[CardPlayedEvent]{
|
||||
|
||||
override def id: String = "CardPlayedEvent"
|
||||
|
||||
override def toJson(event: CardPlayedEvent, session: UserSession): JsObject = {
|
||||
Json.obj(
|
||||
"firstCard" -> (if (event.trick.firstCard.isDefined) WebUIUtils.cardtoString(event.trick.firstCard.get) else "BLANK"),
|
||||
"playedCards" -> JsArray(event.trick.cards.map { case (card, player) =>
|
||||
Json.obj("cardId" -> WebUIUtils.cardtoString(card), "player" -> player.name)
|
||||
}.toList)
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -1,18 +0,0 @@
|
||||
package util.mapper
|
||||
|
||||
import de.knockoutwhist.events.global.GameStateChangeEvent
|
||||
import logic.game.GameLobby
|
||||
import play.api.libs.json.{JsObject, Json}
|
||||
|
||||
object GameStateChangeEventMapper extends SimpleEventMapper[GameStateChangeEvent]{
|
||||
override def id: String = "GameStateChangeEvent"
|
||||
|
||||
override def toJson(event: GameStateChangeEvent, gameLobby: GameLobby): JsObject = {
|
||||
println("CALLED toJSON FOR GAMESTATECHANGE")
|
||||
Json.obj(
|
||||
"oldState" -> event.oldState.toString,
|
||||
"newState" -> event.newState.toString,
|
||||
"gameLobby" -> gameLobby.id
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -1,16 +0,0 @@
|
||||
package util.mapper
|
||||
|
||||
import de.knockoutwhist.events.global.NewRoundEvent
|
||||
import logic.game.GameLobby
|
||||
import play.api.libs.json.{JsObject, Json}
|
||||
|
||||
object NewRoundEventMapper extends SimpleEventMapper[NewRoundEvent]{
|
||||
override def id: String = "NewRoundEvent"
|
||||
|
||||
override def toJson(event: NewRoundEvent, gameLobby: GameLobby): JsObject = {
|
||||
Json.obj(
|
||||
"trumpsuit" -> gameLobby.getLogic.getCurrentRound.get.trumpSuit.toString,
|
||||
"players" -> gameLobby.getLogic.getCurrentMatch.get.playersIn.map(player => player.toString)
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -1,13 +0,0 @@
|
||||
package util.mapper
|
||||
|
||||
import de.knockoutwhist.events.global.NewTrickEvent
|
||||
import logic.game.GameLobby
|
||||
import play.api.libs.json.{JsObject, Json}
|
||||
|
||||
object NewTrickEventMapper extends SimpleEventMapper[NewTrickEvent]{
|
||||
override def id: String = "NewTrickEvent"
|
||||
|
||||
override def toJson(event: NewTrickEvent, gameLobby: GameLobby): JsObject = {
|
||||
Json.obj()
|
||||
}
|
||||
}
|
||||
@@ -1,15 +0,0 @@
|
||||
package util.mapper
|
||||
|
||||
import de.knockoutwhist.events.player.RequestCardEvent
|
||||
import logic.game.GameLobby
|
||||
import play.api.libs.json.{JsObject, Json}
|
||||
|
||||
object RequestCardEventMapper extends SimpleEventMapper[RequestCardEvent]{
|
||||
override def id: String = "RequestCardEvent"
|
||||
|
||||
override def toJson(event: RequestCardEvent, gameLobby: GameLobby): JsObject = {
|
||||
Json.obj(
|
||||
"player" -> event.player.name
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -1,18 +0,0 @@
|
||||
package util.mapper
|
||||
|
||||
import de.knockoutwhist.events.global.TrickEndEvent
|
||||
import de.knockoutwhist.rounds.Trick
|
||||
import logic.game.GameLobby
|
||||
import play.api.libs.json.{JsObject, Json}
|
||||
|
||||
object TrickEndEventMapper extends SimpleEventMapper[TrickEndEvent]{
|
||||
override def id: String = "TrickEndEvent"
|
||||
|
||||
override def toJson(event: TrickEndEvent, gameLobby: GameLobby): JsObject = {
|
||||
Json.obj(
|
||||
"playerwon" -> event.winner.name,
|
||||
"playersin" -> gameLobby.getLogic.getCurrentMatch.get.playersIn.map(player => player.name),
|
||||
"tricklist" -> gameLobby.getLogic.getCurrentRound.get.tricklist.map(trick => trick.winner.map(player => player.name).getOrElse("Trick in Progress"))
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -40,7 +40,7 @@
|
||||
}
|
||||
</div>
|
||||
<div class="col-12 text-center mb-5">
|
||||
<div class="btn btn-success" onclick="startGame('@gamelobby.id', '@user.get.id', '@user.get.name','@user.get.passwordHash', @user.get.internalId)">Start Game</div>
|
||||
<div class="btn btn-success" onclick="startGame('@gamelobby.id')">Start Game</div>
|
||||
</div>
|
||||
} else {
|
||||
<div id="players" class="justify-content-center align-items-center d-flex">
|
||||
@@ -69,7 +69,6 @@
|
||||
</div>
|
||||
</main>
|
||||
<script>
|
||||
/*
|
||||
function waitForFunction(name, checkInterval = 100) {
|
||||
return new Promise(resolve => {
|
||||
const timer = setInterval(() => {
|
||||
@@ -81,6 +80,5 @@
|
||||
});
|
||||
}
|
||||
waitForFunction("pollForUpdates").then(fn => fn('@gamelobby.id'));
|
||||
*/
|
||||
connectWebSocket()
|
||||
</script>
|
||||
@@ -30,4 +30,4 @@
|
||||
</main>
|
||||
<script>
|
||||
disconnectWebSocket();
|
||||
</script>
|
||||
</script>
|
||||
@@ -1,30 +1,3 @@
|
||||
function alertMessage(message) {
|
||||
let newHtml = '';
|
||||
const alertId = `alert-${Date.now()}`;
|
||||
const fadeTime = 500;
|
||||
const duration = 5000;
|
||||
newHtml += `
|
||||
<div class="fixed-top d-flex justify-content-center mt-3" style="z-index: 1050;">
|
||||
<div
|
||||
id="${alertId}" class="alert alert-primary d-flex align-items-center p-2 mb-0 w-auto" role="alert" style="display: none;">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" class="bi flex-shrink-0 me-2" viewBox="0 0 16 16" role="img" aria-label="Warning:">
|
||||
<path d="M8.982 1.566a1.13 1.13 0 0 0-1.96 0L.165 13.233c-.457.778.091 1.767.98 1.767h13.713c.889 0 1.438-.99.98-1.767L8.982 1.566zM8 5c.535 0 .954.462.9.995l-.35 3.507a.552.552 0 0 1-1.1 0L7.1 5.995A.905.905 0 0 1 8 5zm.002 6a1 1 0 1 1 0 2 1 1 0 0 1 0-2z"/>
|
||||
</svg>
|
||||
<div class="small">
|
||||
<small>${message}</small>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
$('#main-body').prepend(newHtml);
|
||||
const $notice = $(`#${alertId}`);
|
||||
$notice.fadeIn(fadeTime);
|
||||
setTimeout(function() {
|
||||
$notice.fadeOut(fadeTime, function() {
|
||||
$(this).parent().remove();
|
||||
});
|
||||
}, duration);
|
||||
}
|
||||
function receiveHandEvent(eventData) {
|
||||
//Data
|
||||
const dog = eventData.dog;
|
||||
@@ -65,101 +38,6 @@ function receiveHandEvent(eventData) {
|
||||
}
|
||||
handElement.html(newHtml);
|
||||
}
|
||||
function newRoundEvent(eventData) {
|
||||
const trumpsuit = eventData.trumpsuit;
|
||||
const players = eventData.players;
|
||||
|
||||
const tableElement = $('#score-table-body');
|
||||
|
||||
|
||||
let tablehtml = `
|
||||
<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>
|
||||
`;
|
||||
|
||||
players.forEach(
|
||||
tablehtml += `
|
||||
<div class="d-flex justify-content-between score-row pt-1">
|
||||
<div style="width: 50%" class="text-truncate">'${players}'</div>
|
||||
<div style="width: 50%">
|
||||
0
|
||||
</div>
|
||||
</div>
|
||||
`
|
||||
);
|
||||
tableElement.html(tablehtml);
|
||||
|
||||
const trumpsuitClass = $('#trumpsuit');
|
||||
trumpsuitClass.html(trumpsuit);
|
||||
|
||||
}
|
||||
function trickEndEvent(eventData) {
|
||||
const winner = eventData.playerwon;
|
||||
const players = eventData.playersin;
|
||||
const tricklist = eventData.tricklist;
|
||||
|
||||
let newHtml = '';
|
||||
|
||||
let tricktable = $('#score-table-body');
|
||||
|
||||
newHtml += `
|
||||
<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>
|
||||
`;
|
||||
let playercounts = new Map();
|
||||
|
||||
players.forEach( player => {
|
||||
playercounts.set(player, 0)
|
||||
});
|
||||
tricklist.forEach( player => {
|
||||
if ( player !== "Trick in Progress" && playercounts.has(player)) {
|
||||
playercounts.set(player, playercounts.get(player) + 1)
|
||||
}
|
||||
}
|
||||
)
|
||||
const playerorder = players.sort((playerA, playerB) => {
|
||||
const countA = playercounts.get(playerA.name) || 0;
|
||||
const countB = playercounts.get(playerB.name) || 0;
|
||||
return countB - countA;
|
||||
});
|
||||
playerorder.forEach( player => {
|
||||
newHtml += `
|
||||
<div class="d-flex justify-content-between score-row pt-1">
|
||||
<div style="width: 50%" class="text-truncate">'${player}'</div>
|
||||
<div style="width: 50%">
|
||||
'${playercounts.get(player)}'
|
||||
</div>
|
||||
</div>
|
||||
`
|
||||
});
|
||||
tricktable.html(newHtml);
|
||||
}
|
||||
function newTrickEvent() {
|
||||
const firstCardContainer = $('first-card-container');
|
||||
|
||||
let newHtml = '';
|
||||
|
||||
newHtml += `
|
||||
<img src="images/cards/1B.png" alt="Blank Card" width="80px"/>
|
||||
`;
|
||||
|
||||
firstCardContainer.html(newHtml);
|
||||
}
|
||||
function requestCardEvent(eventData) {
|
||||
const player = eventData.player;
|
||||
const handElement = $('#card-slide')
|
||||
handElement.removeClass('inactive');
|
||||
}
|
||||
//alertMessage("It worked!")
|
||||
|
||||
|
||||
function receiveGameStateChange(eventData) {
|
||||
const content = eventData.content;
|
||||
@@ -167,10 +45,45 @@ function receiveGameStateChange(eventData) {
|
||||
|
||||
exchangeBody(content, title);
|
||||
}
|
||||
function receiveCardPlayedEvent(eventData) {
|
||||
const firstCard = eventData.firstCard;
|
||||
const playedCards = eventData.playedCards;
|
||||
|
||||
const trickCardsContainer = $('#trick-cards-container');
|
||||
const firstCardContainer = $('#first-card-container')
|
||||
|
||||
let trickHTML = '';
|
||||
playedCards.forEach(cardCombo => {
|
||||
trickHTML += `
|
||||
<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">
|
||||
<img src="/assets/images/cards/${cardCombo.cardId}.png" width="100%" alt="${cardCombo.cardId}"/>
|
||||
</div>
|
||||
<div class="card-body p-2 bg-transparent">
|
||||
<small class="fw-semibold text-secondary">${cardCombo.player}</small>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
});
|
||||
trickCardsContainer.html(trickHTML);
|
||||
|
||||
let altText;
|
||||
let imageSrc;
|
||||
if (firstCard === "BLANK") {
|
||||
imageSrc = "/assets/images/cards/1B.png";
|
||||
altText = "Blank Card";
|
||||
} else {
|
||||
imageSrc = `/assets/images/cards/${firstCard}.png`;
|
||||
altText = `Card ${firstCard}`;
|
||||
}
|
||||
|
||||
const newFirstCardHTML = `
|
||||
<img src="${imageSrc}" alt="${altText}" width="80px" style="border-radius: 6px"/>
|
||||
`;
|
||||
firstCardContainer.html(newFirstCardHTML);
|
||||
}
|
||||
onEvent("ReceivedHandEvent", receiveHandEvent)
|
||||
onEvent("GameStateChangeEvent", receiveGameStateChange)
|
||||
onEvent("NewRoundEvent", newRoundEvent)
|
||||
onEvent("TrickEndEvent", trickEndEvent)
|
||||
onEvent("NewTrickEvent", newTrickEvent)
|
||||
onEvent("RequestCardEvent", requestCardEvent)
|
||||
onEvent("CardPlayedEvent", receiveCardPlayedEvent)
|
||||
@@ -4,8 +4,4 @@ function handlePlayCard(card, dog) {
|
||||
|
||||
function handleSkipDogLife(button) {
|
||||
// TODO needs implementation
|
||||
}
|
||||
function startGame() {
|
||||
console.log("CLICKED START GAME, SENDING EVENT...")
|
||||
sendEvent("Start Game")
|
||||
}
|
||||
@@ -1,3 +1,3 @@
|
||||
MAJOR=4
|
||||
MINOR=1
|
||||
MINOR=2
|
||||
PATCH=0
|
||||
|
||||
Reference in New Issue
Block a user