feat(ui): Implement countless feature using the SJWP (#89)
Co-authored-by: LQ63 <lkhermann@web.de> Reviewed-on: #89
This commit is contained in:
@@ -4,7 +4,7 @@ import de.knockoutwhist.events.player.{RequestCardEvent, RequestTieChoiceEvent,
|
||||
import de.knockoutwhist.utils.events.SimpleEvent
|
||||
import logic.game.GameLobby
|
||||
import model.users.User
|
||||
import play.api.libs.json.JsObject
|
||||
import play.api.libs.json.{JsObject, JsValue}
|
||||
|
||||
import java.util.UUID
|
||||
import java.util.concurrent.locks.ReentrantLock
|
||||
@@ -39,14 +39,38 @@ class UserSession(val user: User, val host: Boolean, val gameLobby: GameLobby) e
|
||||
|
||||
def handleWebResponse(eventType: String, data: JsObject): Unit = {
|
||||
lock.lock()
|
||||
Try {
|
||||
val result = Try {
|
||||
eventType match {
|
||||
case "Ping" =>
|
||||
// No action needed for Ping
|
||||
()
|
||||
case "Start Game" =>
|
||||
gameLobby.startGame(user)
|
||||
case "play Card" =>
|
||||
val maybeCardIndex: Option[Int] = (data \ "cardindex").asOpt[Int]
|
||||
maybeCardIndex match {
|
||||
case Some(index) =>
|
||||
val session = gameLobby.getUserSession(user.id)
|
||||
gameLobby.playCard(session, index)
|
||||
case None =>
|
||||
println("Card Index not found or is not a number.")
|
||||
}
|
||||
case "Picked Trumpsuit" =>
|
||||
val maybeSuitIndex: Option[Int] = (data \ "suitIndex").asOpt[Int]
|
||||
maybeSuitIndex match {
|
||||
case Some(index) =>
|
||||
val session = gameLobby.getUserSession(user.id)
|
||||
gameLobby.selectTrump(session, index)
|
||||
case None =>
|
||||
println("Card Index not found or is not a number.")
|
||||
}
|
||||
}
|
||||
}
|
||||
lock.unlock()
|
||||
if (result.isFailure) {
|
||||
val throwable = result.failed.get
|
||||
throw throwable
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
package model.users
|
||||
|
||||
|
||||
import java.util.UUID
|
||||
|
||||
case class User(
|
||||
@@ -16,5 +17,4 @@ case class User(
|
||||
private def withPasswordHash(newPasswordHash: String): User = {
|
||||
this.copy(passwordHash = newPasswordHash)
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
@@ -1,8 +1,8 @@
|
||||
package util
|
||||
|
||||
import de.knockoutwhist.cards.{Card, Hand}
|
||||
import de.knockoutwhist.cards.CardValue.*
|
||||
import de.knockoutwhist.cards.Suit.{Clubs, Diamonds, Hearts, Spades}
|
||||
import de.knockoutwhist.cards.{Card, Hand}
|
||||
import play.api.libs.json.{JsArray, Json}
|
||||
import play.twirl.api.Html
|
||||
import scalafx.scene.image.Image
|
||||
|
||||
@@ -1,12 +1,11 @@
|
||||
package util
|
||||
|
||||
import de.knockoutwhist.utils.events.SimpleEvent
|
||||
import logic.game.GameLobby
|
||||
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.{CardPlayedEventMapper, GameStateEventMapper, KickEventMapper, LeftEventMapper, LobbyUpdateEventMapper, ReceivedHandEventMapper, SessionClosedMapper, SimpleEventMapper, TurnEventMapper}
|
||||
import util.mapper.*
|
||||
|
||||
object WebsocketEventMapper {
|
||||
|
||||
@@ -27,6 +26,10 @@ object WebsocketEventMapper {
|
||||
registerCustomMapper(ReceivedHandEventMapper)
|
||||
registerCustomMapper(GameStateEventMapper)
|
||||
registerCustomMapper(CardPlayedEventMapper)
|
||||
registerCustomMapper(NewRoundEventMapper)
|
||||
registerCustomMapper(NewTrickEventMapper)
|
||||
registerCustomMapper(TrickEndEventMapper)
|
||||
registerCustomMapper(RequestCardEventMapper)
|
||||
registerCustomMapper(LobbyUpdateEventMapper)
|
||||
registerCustomMapper(LeftEventMapper)
|
||||
registerCustomMapper(KickEventMapper)
|
||||
|
||||
18
knockoutwhistweb/app/util/mapper/NewRoundEventMapper.scala
Normal file
18
knockoutwhistweb/app/util/mapper/NewRoundEventMapper.scala
Normal file
@@ -0,0 +1,18 @@
|
||||
package util.mapper
|
||||
|
||||
import de.knockoutwhist.events.global.NewRoundEvent
|
||||
import logic.game.GameLobby
|
||||
import model.sessions.UserSession
|
||||
import play.api.libs.json.{JsObject, Json}
|
||||
|
||||
object NewRoundEventMapper extends SimpleEventMapper[NewRoundEvent]{
|
||||
override def id: String = "NewRoundEvent"
|
||||
|
||||
override def toJson(event: NewRoundEvent, session: UserSession): JsObject = {
|
||||
val gameLobby = session.gameLobby
|
||||
Json.obj(
|
||||
"trumpsuit" -> gameLobby.getLogic.getCurrentRound.get.trumpSuit.toString,
|
||||
"players" -> gameLobby.getLogic.getCurrentMatch.get.playersIn.map(player => player.toString)
|
||||
)
|
||||
}
|
||||
}
|
||||
14
knockoutwhistweb/app/util/mapper/NewTrickEventMapper.scala
Normal file
14
knockoutwhistweb/app/util/mapper/NewTrickEventMapper.scala
Normal file
@@ -0,0 +1,14 @@
|
||||
package util.mapper
|
||||
|
||||
import de.knockoutwhist.events.global.NewTrickEvent
|
||||
import logic.game.GameLobby
|
||||
import model.sessions.UserSession
|
||||
import play.api.libs.json.{JsObject, Json}
|
||||
|
||||
object NewTrickEventMapper extends SimpleEventMapper[NewTrickEvent]{
|
||||
override def id: String = "NewTrickEvent"
|
||||
|
||||
override def toJson(event: NewTrickEvent, session: UserSession): JsObject = {
|
||||
Json.obj()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
package util.mapper
|
||||
|
||||
import de.knockoutwhist.events.player.RequestCardEvent
|
||||
import logic.game.GameLobby
|
||||
import model.sessions.UserSession
|
||||
import play.api.libs.json.{JsObject, Json}
|
||||
|
||||
object RequestCardEventMapper extends SimpleEventMapper[RequestCardEvent]{
|
||||
override def id: String = "RequestCardEvent"
|
||||
|
||||
override def toJson(event: RequestCardEvent, session: UserSession): JsObject = {
|
||||
Json.obj(
|
||||
"player" -> event.player.name
|
||||
)
|
||||
}
|
||||
}
|
||||
20
knockoutwhistweb/app/util/mapper/TrickEndEventMapper.scala
Normal file
20
knockoutwhistweb/app/util/mapper/TrickEndEventMapper.scala
Normal file
@@ -0,0 +1,20 @@
|
||||
package util.mapper
|
||||
|
||||
import de.knockoutwhist.events.global.TrickEndEvent
|
||||
import de.knockoutwhist.rounds.Trick
|
||||
import logic.game.GameLobby
|
||||
import model.sessions.UserSession
|
||||
import play.api.libs.json.{JsObject, Json}
|
||||
|
||||
object TrickEndEventMapper extends SimpleEventMapper[TrickEndEvent]{
|
||||
override def id: String = "TrickEndEvent"
|
||||
|
||||
override def toJson(event: TrickEndEvent, session: UserSession): JsObject = {
|
||||
val gameLobby = session.gameLobby
|
||||
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"))
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -107,7 +107,7 @@
|
||||
} else {
|
||||
@for(i <- player.currentHand().get.cards.indices) {
|
||||
<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')">
|
||||
<div class="btn btn-outline-light p-0 border-0 shadow-none" id="@i" data-card-id="@i" style="border-radius: 6px" onclick="handlePlayCard(@i, '@player.isInDogLife')">
|
||||
@util.WebUIUtils.cardtoImage(player.currentHand().get.cards(i)) width="120px" style="border-radius: 6px"/>
|
||||
</div>
|
||||
@if(player.isInDogLife) {
|
||||
|
||||
@@ -18,25 +18,25 @@
|
||||
|
||||
<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, '@gamelobby.id')">
|
||||
<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, '@gamelobby.id')">
|
||||
<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, '@gamelobby.id')">
|
||||
<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, '@gamelobby.id')">
|
||||
<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>
|
||||
|
||||
@@ -69,7 +69,7 @@
|
||||
}
|
||||
</div>
|
||||
<div class="col-12 text-center mb-5">
|
||||
<div class="btn btn-success" onclick="startGame('@gamelobby.id')">Start Game</div>
|
||||
<div class="btn btn-success" onclick="startGame()">Start Game</div>
|
||||
</div>
|
||||
} else {
|
||||
<div id="players" class="justify-content-center align-items-center d-flex">
|
||||
@@ -98,6 +98,7 @@
|
||||
</div>
|
||||
</main>
|
||||
<script>
|
||||
/*
|
||||
function waitForFunction(name, checkInterval = 100) {
|
||||
return new Promise(resolve => {
|
||||
const timer = setInterval(() => {
|
||||
@@ -109,5 +110,6 @@
|
||||
});
|
||||
}
|
||||
waitForFunction("pollForUpdates").then(fn => fn('@gamelobby.id'));
|
||||
*/
|
||||
connectWebSocket()
|
||||
</script>
|
||||
@@ -30,4 +30,4 @@
|
||||
</main>
|
||||
<script>
|
||||
disconnectWebSocket();
|
||||
</script>
|
||||
</script>
|
||||
@@ -1,3 +1,30 @@
|
||||
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;
|
||||
@@ -38,6 +65,100 @@ 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');
|
||||
const emptyHtml = '';
|
||||
let newHtml = '';
|
||||
newHtml += `
|
||||
<img src="/assets/images/cards/1B.png" alt="Blank Card" width="80px" style="border-radius: 6px"/>
|
||||
`;
|
||||
firstCardContainer.html(newHtml);
|
||||
const playedCardsContainer = $('#trick-cards-container')
|
||||
playedCardsContainer.html(emptyHtml)
|
||||
}
|
||||
function requestCardEvent(eventData) {
|
||||
const player = eventData.player;
|
||||
const handElement = $('#card-slide')
|
||||
handElement.removeClass('inactive');
|
||||
}
|
||||
|
||||
|
||||
function receiveGameStateChange(eventData) {
|
||||
const content = eventData.content;
|
||||
@@ -85,37 +206,6 @@ function receiveCardPlayedEvent(eventData) {
|
||||
`;
|
||||
firstCardContainer.html(newFirstCardHTML);
|
||||
}
|
||||
function receiveTurnEvent(eventData) {
|
||||
const currentPlayer = eventData.currentPlayer;
|
||||
const nextPlayers = eventData.nextPlayers;
|
||||
|
||||
const currentPlayerNameContainer = $('#current-player-name');
|
||||
const nextPlayersContainer = $('#next-players-container');
|
||||
const nextPlayerText = $('#next-players-section');
|
||||
|
||||
let currentPlayerName = currentPlayer.name;
|
||||
if (currentPlayer.dog) {
|
||||
currentPlayerName += " 🐶";
|
||||
}
|
||||
currentPlayerNameContainer.text(currentPlayerName);
|
||||
|
||||
if (nextPlayers.length === 0) {
|
||||
nextPlayerText.hide();
|
||||
nextPlayersContainer.html('');
|
||||
} else {
|
||||
nextPlayerText.show();
|
||||
let nextPlayersHtml = '';
|
||||
nextPlayers.forEach((player) => {
|
||||
let playerName = player.name;
|
||||
if (player.dog) {
|
||||
playerName += " 🐶";
|
||||
}
|
||||
nextPlayersHtml += `<p className="fs-5 text-primary">${playerName}</p>`;
|
||||
});
|
||||
nextPlayersContainer.html(nextPlayersHtml);
|
||||
}
|
||||
}
|
||||
|
||||
function receiveLobbyUpdateEvent(eventData) {
|
||||
const host = eventData.host;
|
||||
const maxPlayers = eventData.maxPlayers;
|
||||
@@ -218,9 +308,15 @@ function receiveTurnEvent(eventData) {
|
||||
|
||||
onEvent("ReceivedHandEvent", receiveHandEvent)
|
||||
onEvent("GameStateChangeEvent", receiveGameStateChange)
|
||||
onEvent("NewRoundEvent", newRoundEvent)
|
||||
onEvent("TrickEndEvent", trickEndEvent)
|
||||
onEvent("NewTrickEvent", newTrickEvent)
|
||||
onEvent("RequestCardEvent", requestCardEvent)
|
||||
onEvent("CardPlayedEvent", receiveCardPlayedEvent)
|
||||
onEvent("LobbyUpdateEvent", receiveLobbyUpdateEvent)
|
||||
onEvent("LeftEvent", receiveGameStateChange)
|
||||
onEvent("KickEvent", receiveKickEvent)
|
||||
onEvent("SessionClosed", receiveSessionClosedEvent)
|
||||
onEvent("TurnEvent", receiveTurnEvent)
|
||||
onEvent("TurnEvent", receiveTurnEvent)
|
||||
|
||||
globalThis.alertMessage = alertMessage
|
||||
@@ -1,10 +1,58 @@
|
||||
function handlePlayCard(card, dog) {
|
||||
// TODO needs implementation
|
||||
function handlePlayCard(cardIndex, dog) {
|
||||
const cardslide = $('#card-slide')
|
||||
cardslide.addClass("inactive")
|
||||
|
||||
const payload = {
|
||||
cardindex: cardIndex,
|
||||
isDog: dog
|
||||
}
|
||||
sendEventAndWait("play Card", payload).then(
|
||||
() => {
|
||||
console.debug("play card successful")
|
||||
const datacardid = $(`#${cardIndex}`)
|
||||
datacardid.parent('.handcard').remove();
|
||||
cardslide.find('.handcard').each(function(newIndex) {
|
||||
|
||||
const $innerButton = $(this).find('.btn');
|
||||
|
||||
$innerButton.attr('id', newIndex);
|
||||
$innerButton.attr('data-card-id', newIndex);
|
||||
|
||||
const isInDogLife = $innerButton.attr('onclick').includes("'true'") ? 'true' : 'false';
|
||||
$innerButton.attr('onclick', `handlePlayCard(${newIndex}, '${isInDogLife}')`);
|
||||
|
||||
console.debug(`Re-indexed card: Old index was ${$innerButton.attr('data-card-id')}, New index is ${newIndex}`);
|
||||
});
|
||||
}
|
||||
).catch(
|
||||
(err) => {
|
||||
const cardslide = $('#card-slide')
|
||||
console.warn("play card was not successful")
|
||||
if (err.message === "You can't play this card!") {
|
||||
cardslide.removeClass("inactive")
|
||||
}
|
||||
alertMessage("You aren't allowed to play this card")
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
function handleSkipDogLife(button) {
|
||||
// TODO needs implementation
|
||||
}
|
||||
function startGame() {
|
||||
sendEvent("Start Game")
|
||||
}
|
||||
|
||||
function handleTrumpSelection(object) {
|
||||
const $button = $(object);
|
||||
const trumpIndex = parseInt($button.data('trump'));
|
||||
const payload = {
|
||||
suitIndex: trumpIndex
|
||||
}
|
||||
console.log("SENDING TRUMP SUIT SELECTION:", payload);
|
||||
sendEvent("Picked Trumpsuit", payload)
|
||||
|
||||
}
|
||||
function handleKickPlayer(playerId) {
|
||||
// TODO needs implementation
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user