diff --git a/src/client/scripts/esm/chess/logic/gamefile.js b/src/client/scripts/esm/chess/logic/gamefile.js index 9a7234521..005fd73c8 100644 --- a/src/client/scripts/esm/chess/logic/gamefile.js +++ b/src/client/scripts/esm/chess/logic/gamefile.js @@ -143,7 +143,20 @@ function gamefile(metadata, { moves = [], variantOptions, gameConclusion, clockV terminateIfGenerating: () => { if (this.mesh.isGenerating) this.mesh.terminate = true; }, /** A flag the mesh generation reads to know whether to terminate or not. * Do ***NOT*** set manually, call `terminateIfGenerating()` instead. */ - terminate: false + terminate: false, + /** A list of functions to execute as soon as the mesh is unlocked. @type {(gamefile => {})[]} */ + callbacksOnUnlock: [], + /** + * Releases a single lock off of the mesh. + * If there are zero locks, we execute all functions in callbacksOnUnlock + */ + releaseLock: () => { + this.mesh.locked--; + if (this.mesh.locked > 0) return; // Still Locked + // Fully Unlocked + this.mesh.callbacksOnUnlock.forEach(callback => callback(this)); + this.mesh.callbacksOnUnlock.length = 0; + } }; /** The object that contains the buffer model to render the voids */ @@ -248,7 +261,8 @@ function gamefile(metadata, { moves = [], variantOptions, gameConclusion, clockV movepiece.makeAllMovesInGame(this, moves); /** The game's conclusion, if it is over. For example, `'white checkmate'` * Server's gameConclusion should overwrite preexisting gameConclusion. */ - this.gameConclusion = gameConclusion || this.gameConclusion; + if (gameConclusion) this.gameConclusion = gameConclusion; + else gamefileutility.doGameOverChecks(this); organizedlines.addMoreUndefineds(this, { regenModel: false }); diff --git a/src/client/scripts/esm/chess/logic/movepiece.js b/src/client/scripts/esm/chess/logic/movepiece.js index 289b351b6..ad61f9b83 100644 --- a/src/client/scripts/esm/chess/logic/movepiece.js +++ b/src/client/scripts/esm/chess/logic/movepiece.js @@ -337,14 +337,12 @@ function makeAllMovesInGame(gamefile, moves) { // Make the move in the game! - const isLastMove = i === moves.length - 1; - const animate = isLastMove; - makeMove(gamefile, move, { pushClock: false, updateData: false, concludeGameIfOver: false, doGameOverChecks: false, animate }); + // const isLastMove = i === moves.length - 1; + // const animate = isLastMove; + makeMove(gamefile, move, { pushClock: false, updateData: false, concludeGameIfOver: false, doGameOverChecks: false, animate: false }); } if (moves.length === 0) updateInCheck(gamefile, false); - - gamefileutility.doGameOverChecks(gamefile); // Update the gameConclusion } /** @@ -404,6 +402,11 @@ function calculateMoveFromShortmove(gamefile, shortmove) { */ function forwardToFront(gamefile, { flipTurn = true, animateLastMove = true, updateData = true, updateProperties = true, simulated = false } = {}) { + if (updateData && gamefile.mesh.locked > 0) { // The mesh is locked (we cannot forward moves right now) + // Call this function again with the same arguments as soon as the mesh is unlocked + gamefile.mesh.callbacksOnUnlock.push(gamefile => forwardToFront(gamefile, { flipTurn, animateLastMove, updateData, updateProperties, simulated })); + return; + } while (true) { // For as long as we have moves to forward... const nextIndex = gamefile.moveIndex + 1; diff --git a/src/client/scripts/esm/chess/logic/wincondition.js b/src/client/scripts/esm/chess/logic/wincondition.js index bf74e9e12..f0fe4fe53 100644 --- a/src/client/scripts/esm/chess/logic/wincondition.js +++ b/src/client/scripts/esm/chess/logic/wincondition.js @@ -34,6 +34,8 @@ const kothCenterSquares = [[4,4],[5,4],[4,5],[5,5]]; * @returns {string | false} The conclusion string, if the game is over. For example, "white checkmate", or "draw stalemate". If the game isn't over, this returns *false*. */ function getGameConclusion(gamefile) { + if (!moveutil.areWeViewingLatestMove(gamefile)) throw new Error("Cannot perform game over checks when we're not on the last move."); + return detectAllpiecescaptured(gamefile) || detectRoyalCapture(gamefile) || detectAllroyalscaptured(gamefile) diff --git a/src/client/scripts/esm/game/chess/game.js b/src/client/scripts/esm/game/chess/game.js index d35b42f93..8348d2a92 100644 --- a/src/client/scripts/esm/game/chess/game.js +++ b/src/client/scripts/esm/game/chess/game.js @@ -37,6 +37,8 @@ import winconutil from '../../chess/util/winconutil.js'; import sound from '../misc/sound.js'; import spritesheet from '../rendering/spritesheet.js'; import loadingscreen from '../gui/loadingscreen.js'; +import movepiece from '../../chess/logic/movepiece.js'; +import frametracker from '../rendering/frametracker.js'; // Import End /** @@ -63,6 +65,19 @@ let gamefile; */ let gameIsLoading = false; +/** + * The timeout id of the timer that animates the latest-played + * move when rejoining a game, after a short delay + */ +let animateLastMoveTimeoutID; +/** + * The delay, in millis, until the latest-played + * move is animated, after rejoining a game. + */ +const delayOfLatestMoveAnimationOnRejoinMillis = 150; + + + /** * Returns the gamefile currently loaded * @returns {gamefile} The current gamefile @@ -199,9 +214,19 @@ async function loadGamefile(newGamefile) { guinavigation.update_MoveButtons(); guigameinfo.updateWhosTurn(gamefile); - await spritesheet.initSpritesheetForGame(gl, gamefile); + try { + await spritesheet.initSpritesheetForGame(gl, gamefile); + } catch (e) { // An error ocurred during the fetching of piece svgs and spritesheet gen + loadingscreen.onError(); + } guipromotion.initUI(gamefile.gameRules.promotionsAllowed); + // Rewind one move so that we can animate the very final move. + if (newGamefile.moveIndex > -1) movepiece.rewindMove(newGamefile, { updateData: false, removeMove: false, animate: false }); + // A small delay to animate the very last move, so the loading screen + // spinny pawn animation has time to fade away. + animateLastMoveTimeoutID = setTimeout(movepiece.forwardToFront, delayOfLatestMoveAnimationOnRejoinMillis, gamefile, { flipTurn: false, updateProperties: false }); + // Disable miniimages and arrows if there's over 50K pieces. They render too slow. if (newGamefile.startSnapshot.pieceCount >= gamefileutility.pieceCountToDisableCheckmate) { miniimage.disable(); @@ -221,6 +246,8 @@ async function loadGamefile(newGamefile) { gameIsLoading = false; loadingscreen.close(); + // Required so the first frame of the game & tiles is rendered once the animation page fades away + frametracker.onVisualChange(); } /** The canvas will no longer render the current game */ @@ -239,6 +266,10 @@ function unloadGame() { spritesheet.deleteSpritesheet(); guipromotion.resetUI(); + + // Stop the timer that animates the latest-played move when rejoining a game, after a short delay + clearTimeout(animateLastMoveTimeoutID); + animateLastMoveTimeoutID = undefined; } /** Called when a game is loaded, loads the event listeners for when we are in a game. */ diff --git a/src/client/scripts/esm/game/rendering/board.js b/src/client/scripts/esm/game/rendering/board.js index d454f014c..50f1be796 100644 --- a/src/client/scripts/esm/game/rendering/board.js +++ b/src/client/scripts/esm/game/rendering/board.js @@ -155,13 +155,11 @@ function recalcTile_MouseOver() { const tile_MouseOver_IntAndFloat = getTileMouseOver(); tile_MouseOver_Float = tile_MouseOver_IntAndFloat.tile_Float; - if (options.isDebugModeOn()) console.log("Set tile_MouseOver_Float: " + JSON.stringify(tile_MouseOver_Float)); tile_MouseOver_Int = tile_MouseOver_IntAndFloat.tile_Int; } function setTile_MouseOverToUndefined() { tile_MouseOver_Float = undefined; - if (options.isDebugModeOn()) console.log("Set tile_MouseOver_Float: " + JSON.stringify(tile_MouseOver_Float)); tile_MouseOver_Int = undefined; } @@ -171,7 +169,6 @@ function recalcTile_CrosshairOver() { const coords = space.convertWorldSpaceToCoords(input.getMouseWorldLocation()); tile_MouseOver_Float = coords; - if (options.isDebugModeOn()) console.log("Set tile_MouseOver_Float: " + JSON.stringify(tile_MouseOver_Float)); tile_MouseOver_Int = [Math.floor(coords[0] + squareCenter), Math.floor(coords[1] + squareCenter)]; } @@ -206,6 +203,8 @@ function getTileMouseOver() { const mouseWorld = input.getMouseWorldLocation(); // [x, y] const tile_Float = space.convertWorldSpaceToCoords(mouseWorld); const tile_Int = [Math.floor(tile_Float[0] + squareCenter), Math.floor(tile_Float[1] + squareCenter)]; + + if (options.isDebugModeOn()) console.log("Getting tile mouse over: " + JSON.stringify(mouseWorld) + " " + JSON.stringify(tile_Float) + " " + JSON.stringify(tile_Int)); return { tile_Float, tile_Int }; } diff --git a/src/client/scripts/esm/game/rendering/piecesmodel.js b/src/client/scripts/esm/game/rendering/piecesmodel.js index 47bb6c213..2cf1e0310 100644 --- a/src/client/scripts/esm/game/rendering/piecesmodel.js +++ b/src/client/scripts/esm/game/rendering/piecesmodel.js @@ -182,7 +182,7 @@ async function regenModel(gamefile, colorArgs, giveStatus) { // giveStatus can b if (gamefile.mesh.terminate) { console.log("Mesh generation terminated."); gamefile.mesh.terminate = false; - gamefile.mesh.locked--; + gamefile.mesh.releaseLock(); gamefile.mesh.isGenerating--; return; } @@ -198,7 +198,7 @@ async function regenModel(gamefile, colorArgs, giveStatus) { // giveStatus can b if (gamefile.mesh.terminate) { gamefile.mesh.terminate = false; - gamefile.mesh.locked--; + gamefile.mesh.releaseLock(); gamefile.mesh.isGenerating--; return; } @@ -209,7 +209,7 @@ async function regenModel(gamefile, colorArgs, giveStatus) { // giveStatus can b frametracker.onVisualChange(); - gamefile.mesh.locked--; + gamefile.mesh.releaseLock(); gamefile.mesh.isGenerating--; } @@ -516,7 +516,7 @@ async function initRotatedPiecesModel(gamefile, ignoreGenerating = false) { console.log("Mesh generation terminated."); stats.hideRotateMesh(); if (!ignoreGenerating) gamefile.mesh.terminate = false; - gamefile.mesh.locked--; + gamefile.mesh.releaseLock(); gamefile.mesh.isGenerating--; return; } @@ -525,7 +525,7 @@ async function initRotatedPiecesModel(gamefile, ignoreGenerating = false) { console.log("Mesh generation terminated."); stats.hideRotateMesh(); if (!ignoreGenerating) gamefile.mesh.terminate = false; - gamefile.mesh.locked--; + gamefile.mesh.releaseLock(); gamefile.mesh.isGenerating--; return; } @@ -684,7 +684,7 @@ async function initRotatedPiecesModel(gamefile, ignoreGenerating = false) { gamefile.mesh.rotatedModel = gamefile.mesh.usingColoredTextures ? buffermodel.createModel_ColorTextured(gamefile.mesh.rotatedData32, 2, "TRIANGLES", spritesheet.getSpritesheet()) : buffermodel.createModel_Textured(gamefile.mesh.rotatedData32, 2, "TRIANGLES", spritesheet.getSpritesheet()); - gamefile.mesh.locked--; + gamefile.mesh.releaseLock(); gamefile.mesh.isGenerating--; frametracker.onVisualChange(); } diff --git a/src/client/scripts/esm/game/rendering/spritesheet.ts b/src/client/scripts/esm/game/rendering/spritesheet.ts index 44e94ac2b..47e37989d 100644 --- a/src/client/scripts/esm/game/rendering/spritesheet.ts +++ b/src/client/scripts/esm/game/rendering/spritesheet.ts @@ -169,6 +169,8 @@ async function fetchAllPieceSVGs(types: string[]) { }) .catch(error => { console.error(`Failed to fetch ${pieceType}:`, error); // Log specific error + // Propagate the error so that Promise.all() can reject + throw error; }); });