feat(ui): Fixed polling, added JQuery Ajax

Fixed Race Condition problems with polling, added JQuery
This commit is contained in:
LQ63
2025-11-19 15:14:47 +01:00
parent e60fe7c98d
commit e2a5cb9614
6 changed files with 312 additions and 312 deletions

View File

@@ -172,8 +172,7 @@ class IngameController @Inject() (
optSession.foreach(_.lock.unlock()) optSession.foreach(_.lock.unlock())
if (result.isSuccess) { if (result.isSuccess) {
Ok(Json.obj( Ok(Json.obj(
"status" -> "success", "status" -> "success"
"redirectUrl" -> routes.IngameController.game(gameId).url
)) ))
} else { } else {
val throwable = result.failed.get val throwable = result.failed.get
@@ -198,6 +197,11 @@ class IngameController @Inject() (
"status" -> "failure", "status" -> "failure",
"errorMessage" -> throwable.getMessage "errorMessage" -> throwable.getMessage
)) ))
case _: NotInteractableException =>
BadRequest(Json.obj(
"status" -> "failure",
"errorMessage" -> throwable.getMessage
))
case _ => case _ =>
InternalServerError(Json.obj( InternalServerError(Json.obj(
"status" -> "failure", "status" -> "failure",

View File

@@ -1,10 +1,11 @@
package controllers package controllers
import auth.{AuthAction, AuthenticatedRequest} import auth.{AuthAction, AuthenticatedRequest}
import controllers.PollingController.{scheduler, timeoutDuration}
import de.knockoutwhist.cards.Hand import de.knockoutwhist.cards.Hand
import logic.PodManager import logic.PodManager
import logic.game.{GameLobby, PollingEvents} import logic.game.{GameLobby, PollingEvents}
import logic.game.PollingEvents.{CardPlayed, LobbyUpdate, NewRound, ReloadEvent} import logic.game.PollingEvents.{CardPlayed, LobbyCreation, LobbyUpdate, NewRound, ReloadEvent}
import model.sessions.UserSession import model.sessions.UserSession
import model.users.User import model.users.User
import play.api.libs.json.{JsArray, JsValue, Json} import play.api.libs.json.{JsArray, JsValue, Json}
@@ -13,7 +14,12 @@ import util.WebUIUtils
import javax.inject.{Inject, Singleton} import javax.inject.{Inject, Singleton}
import scala.concurrent.{ExecutionContext, Future} import scala.concurrent.{ExecutionContext, Future}
import java.util.concurrent.{Executors, ScheduledExecutorService, TimeUnit}
import scala.concurrent.duration.*
object PollingController {
private val scheduler: ScheduledExecutorService = Executors.newSingleThreadScheduledExecutor()
private val timeoutDuration = 25.seconds
}
@Singleton @Singleton
class PollingController @Inject() ( class PollingController @Inject() (
val cc: ControllerComponents, val cc: ControllerComponents,
@@ -96,25 +102,28 @@ class PollingController @Inject() (
} }
} }
// --- Main Polling Action ---
def polling(gameId: String): Action[AnyContent] = authAction.async { implicit request: AuthenticatedRequest[AnyContent] => def polling(gameId: String): Action[AnyContent] = authAction.async { implicit request: AuthenticatedRequest[AnyContent] =>
val playerId = request.user.id val playerId = request.user.id
// 1. Safely look up the game
podManager.getGame(gameId) match { podManager.getGame(gameId) match {
case Some(game) => case Some(game) =>
val playerEventQueue = game.getEventsOfPlayer(playerId)
// 2. Short-Poll Check (Check for missed events) if (playerEventQueue.nonEmpty) {
if (game.getPollingState.nonEmpty) { val event = playerEventQueue.dequeue()
val event = game.getPollingState.dequeue()
Future.successful(handleEvent(event, game, game.getUserSession(playerId))) Future.successful(handleEvent(event, game, game.getUserSession(playerId)))
} else { } else {
val eventPromise = game.registerWaiter(playerId) val eventPromise = game.registerWaiter(playerId)
val scheduledFuture = scheduler.schedule(
new Runnable {
override def run(): Unit =
eventPromise.tryFailure(new java.util.concurrent.TimeoutException("Polling Timeout"))
},
timeoutDuration.toMillis,
TimeUnit.MILLISECONDS
)
eventPromise.future.map { event => eventPromise.future.map { event =>
scheduledFuture.cancel(false)
game.removeWaiter(playerId) game.removeWaiter(playerId)
handleEvent(event, game, game.getUserSession(playerId)) handleEvent(event, game, game.getUserSession(playerId))
}.recover { }.recover {
@@ -125,7 +134,6 @@ class PollingController @Inject() (
} }
case None => case None =>
// Game not found
Future.successful(NotFound("Game not found.")) Future.successful(NotFound("Game not found."))
} }
} }

View File

@@ -11,7 +11,7 @@ import de.knockoutwhist.player.{AbstractPlayer, PlayerFactory}
import de.knockoutwhist.rounds.{Match, Round, Trick} import de.knockoutwhist.rounds.{Match, Round, Trick}
import de.knockoutwhist.utils.events.{EventListener, SimpleEvent} import de.knockoutwhist.utils.events.{EventListener, SimpleEvent}
import exceptions.* import exceptions.*
import logic.game.PollingEvents.{CardPlayed, LobbyUpdate, NewRound, ReloadEvent} import logic.game.PollingEvents.{CardPlayed, LobbyCreation, LobbyUpdate, NewRound, ReloadEvent}
import model.sessions.{InteractionType, UserSession} import model.sessions.{InteractionType, UserSession}
import model.users.User import model.users.User
@@ -27,22 +27,40 @@ class GameLobby private(
val name: String, val name: String,
val maxPlayers: Int val maxPlayers: Int
) extends EventListener { ) extends EventListener {
logic.addListener(this)
logic.createSession()
private val users: mutable.Map[UUID, UserSession] = mutable.Map() private val users: mutable.Map[UUID, UserSession] = mutable.Map()
private val pollingState: mutable.Queue[PollingEvents] = mutable.Queue() private val eventsPerPlayer: mutable.Map[UUID, mutable.Queue[PollingEvents]] = mutable.Map()
private val waitingPromises: mutable.Map[UUID, ScalaPromise[PollingEvents]] = mutable.Map() private val waitingPromises: mutable.Map[UUID, ScalaPromise[PollingEvents]] = mutable.Map()
private val lock = new Object
lock.synchronized {
logic.addListener(this)
logic.createSession()
}
def registerWaiter(playerId: UUID): ScalaPromise[PollingEvents] = { def registerWaiter(playerId: UUID): ScalaPromise[PollingEvents] = {
val promise = ScalaPromise[PollingEvents]() val promise = ScalaPromise[PollingEvents]()
lock.synchronized {
val queue = eventsPerPlayer.getOrElseUpdate(playerId, mutable.Queue())
if (queue.nonEmpty) {
val evt = queue.dequeue()
promise.success(evt)
promise
} else {
waitingPromises.put(playerId, promise) waitingPromises.put(playerId, promise)
promise promise
} }
}
}
def removeWaiter(playerId: UUID): Unit = { def removeWaiter(playerId: UUID): Unit = {
lock.synchronized {
waitingPromises.remove(playerId) waitingPromises.remove(playerId)
} }
}
def addUser(user: User): UserSession = { def addUser(user: User): UserSession = {
if (users.size >= maxPlayers) throw new GameFullException("The game is full!") if (users.size >= maxPlayers) throw new GameFullException("The game is full!")
@@ -72,6 +90,7 @@ class GameLobby private(
} }
if (event.oldState == Lobby && event.newState == InGame) { if (event.oldState == Lobby && event.newState == InGame) {
addToQueue(ReloadEvent) addToQueue(ReloadEvent)
return
} else { } else {
addToQueue(ReloadEvent) addToQueue(ReloadEvent)
} }
@@ -84,11 +103,29 @@ class GameLobby private(
} }
private def addToQueue(event: PollingEvents): Unit = { private def addToQueue(event: PollingEvents): Unit = {
if (waitingPromises.nonEmpty) { lock.synchronized {
waitingPromises.values.foreach(_.success(event)) users.keys.foreach { playerId =>
waitingPromises.clear() val q = eventsPerPlayer.getOrElseUpdate(playerId, mutable.Queue())
} else { q.enqueue(event)
pollingState.enqueue(event) }
val waiterIds = waitingPromises.keys.toList
waiterIds.foreach { playerId =>
val q = eventsPerPlayer.getOrElseUpdate(playerId, mutable.Queue())
if (q.nonEmpty) {
val evt = q.dequeue()
val p = waitingPromises.remove(playerId)
p.foreach(_.success(evt))
}
}
}
waitingPromises.keys.foreach { playerId =>
val queue = eventsPerPlayer(playerId)
if (queue.nonEmpty) {
val promise = waitingPromises(playerId)
promise.success(queue.dequeue())
waitingPromises.remove(playerId)
}
} }
} }
@@ -218,8 +255,8 @@ class GameLobby private(
def getLogic: GameLogic = { def getLogic: GameLogic = {
logic logic
} }
def getPollingState: mutable.Queue[PollingEvents] = { def getEventsOfPlayer(playerId: UUID): mutable.Queue[PollingEvents] = {
pollingState eventsPerPlayer.getOrElseUpdate(playerId, mutable.Queue())
} }
private def getPlayerBySession(userSession: UserSession): AbstractPlayer = { private def getPlayerBySession(userSession: UserSession): AbstractPlayer = {
val playerOption = getMatch.totalplayers.find(_.id == userSession.id) val playerOption = getMatch.totalplayers.find(_.id == userSession.id)

View File

@@ -5,4 +5,5 @@ enum PollingEvents {
case NewRound case NewRound
case ReloadEvent case ReloadEvent
case LobbyUpdate case LobbyUpdate
case LobbyCreation
} }

View File

@@ -25,5 +25,6 @@
<script src="@routes.JavaScriptRoutingController.javascriptRoutes()" type="text/javascript"></script> <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("javascripts/main.js")" type="text/javascript"></script>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@@5.3.8/dist/js/bootstrap.bundle.min.js" integrity="sha384-FKyoEForCGlyvwx9Hj09JcYn3nv7wiPVlz7YYwJrWVcXK/BmnVDxM+D2scQbITxI" crossorigin="anonymous"></script> <script src="https://cdn.jsdelivr.net/npm/bootstrap@@5.3.8/dist/js/bootstrap.bundle.min.js" integrity="sha384-FKyoEForCGlyvwx9Hj09JcYn3nv7wiPVlz7YYwJrWVcXK/BmnVDxM+D2scQbITxI" crossorigin="anonymous"></script>
<script src="https://code.jquery.com/jquery-3.6.0.min.js"></script>
</body> </body>
</html> </html>

View File

@@ -80,45 +80,39 @@
})() })()
function pollForUpdates(gameId) { function pollForUpdates(gameId) {
console.log(`[DEBUG] Starting poll cycle for Game ID: ${gameId} at ${new Date().toISOString()}`);
if (!gameId) { if (!gameId) {
console.error("Game ID is missing. Stopping poll."); console.error("[DEBUG] Game ID is missing. Stopping poll.");
return; return;
} }
const element = document.getElementById('card-slide'); const $handElement = $('#card-slide');
const element2 = document.getElementById('lobbybackground'); const $lobbyElement = $('#lobbybackground');
// Safety check for the target element const $mainmenuElement = $('#main-menu-screen')
if (!element && !element2) { if (!$handElement.length && !$lobbyElement.length && !$mainmenuElement.length) {
console.error("Polling target element not found. Stopping poll.");
// Use a timeout to retry in case the DOM loads late, passing gameId.
setTimeout(() => pollForUpdates(gameId), 5000); setTimeout(() => pollForUpdates(gameId), 5000);
return; return;
} }
const route = jsRoutes.controllers.PollingController.polling(gameId); const route = jsRoutes.controllers.PollingController.polling(gameId);
$.ajax({
url: route.url,
type: 'GET',
dataType: 'json',
// Call your specific controller endpoint success: (data => {
fetch(route.url) if (!data) {
.then(response => { console.log("[DEBUG] Received 204 No Content (Timeout). Restarting poll.");
if (response.status === 204) { return;
console.log("Polling: Timeout reached. Restarting poll."); }
if (data.status === "cardPlayed" && data.handData) {
// CRITICAL: Pass gameId in the recursive call
setTimeout(() => pollForUpdates(gameId), 5000);
} else if (response.ok && response.status === 200) {
response.json().then(data => {
if (data.status === "cardPlayed" && data.handData && element) {
console.log("Event received: Card played. Redrawing hand."); console.log("Event received: Card played. Redrawing hand.");
const newHand = data.handData; const newHand = data.handData;
let newHandHTML = ''; let newHandHTML = '';
element.innerHTML = ''; $handElement.empty();
if(data.animation) { if(data.animation) {
if (!element.classList.contains('ingame-cards-slide')) { $handElement.addClass('ingame-cards-slide');
element.classList.add('ingame-cards-slide');
}
} else { } else {
element.classList.remove('ingame-cards-slide'); $handElement.removeClass('ingame-cards-slide');
} }
newHand.forEach((cardId, index) => { newHand.forEach((cardId, index) => {
@@ -136,32 +130,20 @@ function pollForUpdates(gameId) {
newHandHTML += cardHtml; newHandHTML += cardHtml;
}); });
element.innerHTML = newHandHTML; $handElement.html(newHandHTML);
$('#current-player-name').text(data.currentPlayerName)
const currentPlayerElement = document.getElementById('current-player-name'); if (data.nextPlayer) {
if (currentPlayerElement) { $('#next-player-name').text(data.nextPlayer);
currentPlayerElement.textContent = data.currentPlayerName; } else if (nextPlayerElement) {
} $('#next-player-name').text("");
const nextPlayerElement = document.getElementById('next-player-name');
if (nextPlayerElement && data.nextPlayer) {
// Use the correctly named field from the server response
nextPlayerElement.textContent = data.nextPlayer;
} else { } else {
// Case 2: Player name is empty or null (signal to clear display). console.warn("[DEBUG] 'current-player-name' element missing in DOM");
nextPlayerElement.textContent = "";
} }
$('#trump-suit').text(data.trumpSuit);
const trumpElement = document.getElementById('trump-suit'); if ($('#trick-cards-container').length) {
if (trumpElement) {
trumpElement.textContent = data.trumpSuit;
}
const trickContainer = document.getElementById('trick-cards-container');
if (trickContainer) {
let trickHTML = ''; let trickHTML = '';
// Iterate over the array of played cards received from the server
data.trickCards.forEach(trickCard => { data.trickCards.forEach(trickCard => {
// Reconstruct the HTML structure from your template
trickHTML += ` trickHTML += `
<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="card text-center shadow-sm border-0 bg-transparent" style="width: 7rem; backdrop-filter: blur(4px);">
@@ -175,10 +157,9 @@ function pollForUpdates(gameId) {
</div> </div>
`; `;
}); });
trickContainer.innerHTML = trickHTML; $('#trick-cards-container').html(trickHTML);
} }
const scoreBody = document.getElementById('score-table-body'); if ($('#score-table-body').length && data.scoreTable) {
if (scoreBody && data.scoreTable) {
let scoreHTML = ''; let scoreHTML = '';
scoreHTML += `<h4 class="fw-bold mb-3 text-black">Tricks Won</h4> scoreHTML += `<h4 class="fw-bold mb-3 text-black">Tricks Won</h4>
@@ -194,16 +175,13 @@ function pollForUpdates(gameId) {
</div> </div>
`; `;
}); });
scoreBody.innerHTML = scoreHTML; $('#score-table-body').html(scoreHTML);
} }
const firstCardContainer = document.getElementById('first-card-container'); const cardId = data.firstCardId;
const cardId = data.firstCardId; // This will be "KH", "S7", or "BLANK" if ($('#first-card-container').length) {
if (firstCardContainer) {
let imageSrc = ''; let imageSrc = '';
let altText = 'First Card'; let altText = 'First Card';
// Check if a card was actually played or if it's the start of a trick
if (cardId === "BLANK") { if (cardId === "BLANK") {
imageSrc = "/assets/images/cards/1B.png"; imageSrc = "/assets/images/cards/1B.png";
altText = "Blank Card"; altText = "Blank Card";
@@ -211,18 +189,18 @@ function pollForUpdates(gameId) {
imageSrc = `/assets/images/cards/${cardId}.png`; imageSrc = `/assets/images/cards/${cardId}.png`;
} }
// Reconstruct the image HTML (assuming the inner element needs replacement)
const newImageHTML = ` const newImageHTML = `
<img src="${imageSrc}" alt="${altText}" width="80px" style="border-radius: 6px"/> <img src="${imageSrc}" alt="${altText}" width="80px" style="border-radius: 6px"/>
`; `;
// Clear the container and insert the new image $('#first-card-container').html(newImageHTML);
firstCardContainer.innerHTML = newImageHTML;
} }
} else if (data.status === "reloadEvent") { } else if (data.status === "reloadEvent") {
console.log("[DEBUG] Reload event received. Redirecting...");
window.location.href = data.redirectUrl; window.location.href = data.redirectUrl;
} else if (data.status === "lobbyUpdate") { }
const players = document.getElementById("players"); else if (data.status === "lobbyUpdate") {
console.log("[DEBUG] Entering 'lobbyUpdate' logic.");
let newHtml = '' let newHtml = ''
if (data.host) { if (data.host) {
@@ -257,33 +235,37 @@ function pollForUpdates(gameId) {
</div>` </div>`
}) })
} }
players.innerHTML = newHtml; $("#players").html(newHtml);
}
pollForUpdates(gameId);
});
} else { } else {
// Handle network or server errors console.warn(`[DEBUG] Received unknown status: ${data.status}`);
console.error(`Polling error: Status ${response.status}`);
// Wait before retrying, passing gameId correctly
setTimeout(() => pollForUpdates(gameId), 5000);
} }
}),
error: ((jqXHR, textStatus, errorThrown) => {
if (jqXHR.status >= 400) {
console.error(`Server error: ${jqXHR.status}, ${errorThrown}`);
}
else {
console.error(`Something unexpected happened while polling. ${jqXHR.status}, ${errorThrown}`)
}
}),
complete: ((jqXHR, textStatus) => {
if (!window.location.href.includes("game")) {
console.log("[DEBUG] Page URL changed. Stopping poll restart.");
return;
}
setTimeout(() => pollForUpdates(gameId), 500);
})
}) })
.catch(error => {
console.error("Network error during polling:", error);
// Wait before retrying on network failure, passing gameId correctly
setTimeout(() => pollForUpdates(gameId), 5000);
});
} }
function createGameJS() { function createGameJS() {
let lobbyName = document.getElementById("lobbyname").value; let lobbyName = $('#lobbyname').val();
if (lobbyName === "") { if ($.trim(lobbyName) === "") {
lobbyName = "DefaultLobby" lobbyName = "DefaultLobby"
} }
const playerAmount = document.getElementById("playeramount").value;
const jsonObj = { const jsonObj = {
lobbyname: lobbyName, lobbyname: lobbyName,
playeramount: playerAmount playeramount: $("#playeramount").val()
} }
sendGameCreationRequest(jsonObj); sendGameCreationRequest(jsonObj);
} }
@@ -291,33 +273,26 @@ function createGameJS() {
function sendGameCreationRequest(dataObject) { function sendGameCreationRequest(dataObject) {
const route = jsRoutes.controllers.MainMenuController.createGame(); const route = jsRoutes.controllers.MainMenuController.createGame();
fetch(route.url, { $.ajax({
method: route.type, url: route.url,
headers: { type: route.type,
'Content-Type': 'application/json', contentType: 'application/json',
}, data: JSON.stringify(dataObject),
body: JSON.stringify(dataObject) dataType: 'json',
}) success: (data => {
.then(response => {
return response.json().then(data => {
if (!response.ok) {
return Promise.reject(data);
}
return data;
});
})
.then(data => {
if (data.status === 'success') { if (data.status === 'success') {
window.location.href = data.redirectUrl; window.location.href = data.redirectUrl;
} }
}) }),
.catch(error => { error: ((jqXHR) => {
if (error && error.errorMessage) { const errorData = JSON.parse(jqXHR.responseText);
alert(`${error.errorMessage}`); if (errorData && errorData.errorMessage) {
alert(`${errorData.errorMessage}`);
} else { } else {
alert('An unexpected error occurred. Please try again.'); alert(`An unexpected error occurred. Please try again. Status: ${jqXHR.status}`);
} }
}); })
})
} }
function startGame(gameId) { function startGame(gameId) {
sendGameStartRequest(gameId) sendGameStartRequest(gameId)
@@ -325,30 +300,24 @@ function startGame(gameId) {
function sendGameStartRequest(gameId) { function sendGameStartRequest(gameId) {
const route = jsRoutes.controllers.IngameController.startGame(gameId); const route = jsRoutes.controllers.IngameController.startGame(gameId);
fetch(route.url, { $.ajax({
method: route.type, url: route.url,
}) type: route.type,
.then(response => { dataType: 'json',
return response.json().then(data => { success: (data => {
if (!response.ok) {
return Promise.reject(data);
}
return data;
});
})
.then(data => {
// SUCCESS BLOCK: data is the { status: 'success', ... } object
if (data.status === 'success') { if (data.status === 'success') {
window.location.href = data.redirectUrl; window.location.href = data.redirectUrl;
} }
}) }),
.catch(error => { error: ((jqXHR) => {
if (error && error.errorMessage) { const errorData = JSON.parse(jqXHR.responseText);
alert(`${error.errorMessage}`); if (errorData && errorData.errorMessage) {
alert(`${errorData.errorMessage}`);
} else { } else {
alert('An unexpected error occurred. Please try again.'); alert(`An unexpected error occurred. Please try again. Status: ${jqXHR.status}`);
} }
}); })
})
} }
function removePlayer(gameid, playersessionId) { function removePlayer(gameid, playersessionId) {
sendRemovePlayerRequest(gameid, playersessionId) sendRemovePlayerRequest(gameid, playersessionId)
@@ -357,33 +326,25 @@ function removePlayer(gameid, playersessionId) {
function sendRemovePlayerRequest(gameId, playersessionId) { function sendRemovePlayerRequest(gameId, playersessionId) {
const route = jsRoutes.controllers.IngameController.kickPlayer(gameId, playersessionId); const route = jsRoutes.controllers.IngameController.kickPlayer(gameId, playersessionId);
fetch(route.url, { $.ajax({
method: route.type, url: route.url,
headers: { type: route.type,
'Content-Type': 'application/json', contentType: 'application/json',
} dataType: 'json',
}) success: (data => {
.then(response => {
return response.json().then(data => {
if (!response.ok) {
return Promise.reject(data);
}
return data;
});
})
.then(data => {
// SUCCESS BLOCK: data is the { status: 'success', ... } object
if (data.status === 'success') { if (data.status === 'success') {
window.location.href = data.redirectUrl; window.location.href = data.redirectUrl;
} }
}) }),
.catch(error => { error: ((jqXHR) => {
if (error && error.errorMessage) { const errorData = JSON.parse(jqXHR.responseText);
alert(`${error.errorMessage}`); if (errorData && errorData.errorMessage) {
alert(`${errorData.errorMessage}`);
} else { } else {
alert('An unexpected error occurred. Please try again.'); alert(`An unexpected error occurred. Please try again. Status: ${jqXHR.status}`);
} }
}); })
})
} }
function leaveGame(gameId) { function leaveGame(gameId) {
@@ -393,30 +354,24 @@ function leaveGame(gameId) {
function sendLeavePlayerRequest(gameId) { function sendLeavePlayerRequest(gameId) {
const route = jsRoutes.controllers.IngameController.leaveGame(gameId); const route = jsRoutes.controllers.IngameController.leaveGame(gameId);
fetch(route.url, { $.ajax({
method: route.type, url: route.url,
}) type: route.type,
.then(response => { dataType: 'json',
return response.json().then(data => { success: (data => {
if (!response.ok) {
return Promise.reject(data);
}
return data;
});
})
.then(data => {
// SUCCESS BLOCK: data is the { status: 'success', ... } object
if (data.status === 'success') { if (data.status === 'success') {
window.location.href = data.redirectUrl; window.location.href = data.redirectUrl;
} }
}) }),
.catch(error => { error: ((jqXHR) => {
if (error && error.errorMessage) { const errorData = JSON.parse(jqXHR.responseText);
alert(`${error.errorMessage}`); if (errorData && errorData.errorMessage) {
alert(`${errorData.errorMessage}`);
} else { } else {
alert('An unexpected error occurred. Please try again.'); alert(`An unexpected error occurred. Please try again. Status: ${jqXHR.status}`);
} }
}); })
})
} }
function handlePlayCard(cardobject, gameId) { function handlePlayCard(cardobject, gameId) {
@@ -436,37 +391,30 @@ function sendPlayCardRequest(jsonObj, gameId, cardobject) {
{ transform: 'translateX(0)' } { transform: 'translateX(0)' }
]; ];
// Define the timing options
const wiggleTiming = { const wiggleTiming = {
duration: 400, // 0.4 seconds duration: 400,
iterations: 1, iterations: 1,
easing: 'ease-in-out', easing: 'ease-in-out',
// Fill mode ensures the final state is applied until reset
fill: 'forwards' fill: 'forwards'
}; };
const route = jsRoutes.controllers.IngameController.playCard(gameId); const route = jsRoutes.controllers.IngameController.playCard(gameId);
fetch(route.url, { $.ajax({
method: route.type, url: route.url,
headers: { type: route.type,
'Content-Type': 'application/json', contentType: 'application/json',
}, dataType: 'json',
body: JSON.stringify(jsonObj) data: JSON.stringify(jsonObj),
}) success: (data => {
.then(response => {
return response.json().then(data => {
if (!response.ok) {
return Promise.reject(data);
}
return data;
});
})
.then(data => {
if (data.status === 'success') { if (data.status === 'success') {
//window.location.href = data.redirectUrl;
} }
}) }),
.catch(error => { error: (jqXHR => {
try {
error = JSON.parse(jqXHR.responseText);
} catch (e) {
console.error("Failed to parse error response:", e);
}
if (error && error.errorMessage.includes("You can't play this card!")) { if (error && error.errorMessage.includes("You can't play this card!")) {
cardobject.parentElement.animate(wiggleKeyframes, wiggleTiming); cardobject.parentElement.animate(wiggleKeyframes, wiggleTiming);
} else if (error && error.errorMessage) { } else if (error && error.errorMessage) {
@@ -474,6 +422,7 @@ function sendPlayCardRequest(jsonObj, gameId, cardobject) {
} else { } else {
alert('An unexpected error occurred. Please try again.'); alert('An unexpected error occurred. Please try again.');
} }
}); })
})
} }