From d06c3fcfb70cf91964143faeabcd1d463197c356 Mon Sep 17 00:00:00 2001 From: Naviary2 Date: Wed, 1 Jan 2025 22:37:40 -0700 Subject: [PATCH 001/131] Converted clock.js to typescript --- .../esm/chess/logic/{clock.js => clock.ts} | 129 +++++++++++------- 1 file changed, 77 insertions(+), 52 deletions(-) rename src/client/scripts/esm/chess/logic/{clock.js => clock.ts} (54%) diff --git a/src/client/scripts/esm/chess/logic/clock.js b/src/client/scripts/esm/chess/logic/clock.ts similarity index 54% rename from src/client/scripts/esm/chess/logic/clock.js rename to src/client/scripts/esm/chess/logic/clock.ts index 2572470f0..9382e5c93 100644 --- a/src/client/scripts/esm/chess/logic/clock.js +++ b/src/client/scripts/esm/chess/logic/clock.ts @@ -1,31 +1,49 @@ -// Import Start +/** + * This script keeps track of both players timer, + * updates them each frame, + * and the update() method will return the loser + * if somebody loses on time. + */ + +// @ts-ignore import onlinegame from '../../game/misc/onlinegame.js'; +// @ts-ignore import moveutil from '../util/moveutil.js'; +// @ts-ignore import clockutil from '../util/clockutil.js'; +// @ts-ignore import timeutil from '../../util/timeutil.js'; +// @ts-ignore import gamefileutility from '../util/gamefileutility.js'; +// @ts-ignore import pingManager from '../../util/pingManager.js'; +// @ts-ignore import options from '../../game/rendering/options.js'; -// Import End -/** - * @typedef {import('./gamefile.js').gamefile} gamefile - */ -"use strict"; +// Type Definitions --------------------------------------------------------------- + +// @ts-ignore +import type gamefile from './gamefile.js'; + +/** An object containing each color in the game for the keys, and that color's time left in milliseconds for the values. */ +interface ClockValues { [color: string]: number }; + + +// Functions ----------------------------------------------------------------------- + + + + /** - * This script keeps track of both players timer, updates them, - * and ends game if somebody loses on time. - */ -/** - * Sets the clocks. - * @param {gamefile} gamefile - * @param {string} clock - The clock value (e.g. "600+5" => 10m+5s). - * @param {Object} [currentTimes] - Optional. An object containing the properties `timerWhite`, `timerBlack`, and `accountForPing` (if an online game) for the current time of the players. Often used if we re-joining an online game. + * Sets the clocks. If no current clock values are specified, clocks will + * be set to the starting values, according to the game's TimeControl metadata. + * @param gamefile + * @param [currentTimes] Optional. An object containing the current times of the players. Often used if we re-joining an online game. */ -function set(gamefile, currentTimes) { +function set(gamefile: gamefile, currentTimes?: ClockValues) { const clock = gamefile.metadata.TimeControl; // "600+5" const clocks = gamefile.clocks; @@ -36,7 +54,7 @@ function set(gamefile, currentTimes) { const clockPartsSplit = clockutil.getMinutesAndIncrementFromClock(clock); // { minutes, increment } if (clockPartsSplit !== null) { clocks.startTime.minutes = clockPartsSplit.minutes; - clocks.startTime.millis = timeutil.minutesToMillis(clocks.startTime.minutes); + clocks.startTime.millis = timeutil.minutesToMillis(clocks.startTime.minutes!); clocks.startTime.increment = clockPartsSplit.increment; } @@ -45,8 +63,9 @@ function set(gamefile, currentTimes) { // Edit the closk if we're re-loading an online game if (currentTimes) edit(gamefile, currentTimes); else { // No current time specified, start both players with the default. - clocks.currentTime.white = clocks.startTime.millis; - clocks.currentTime.black = clocks.startTime.millis; + gamefile.gameRules.turnOrder.forEach(color => { + clocks.currentTime[color] = clocks.startTime.millis; + }); } clocks.untimed = clockutil.isClockValueInfinite(clock); @@ -56,11 +75,8 @@ function set(gamefile, currentTimes) { * Updates the gamefile with new clock information received from the server. * @param {gamefile} gamefile - The current game state object containing clock information. * @param {object} clockValues - An object containing the updated clock values. - * @param {number} clockValues.timerWhite - White's current time, in milliseconds. - * @param {number} clockValues.timerBlack - Black's current time, in milliseconds. - * @param {number} clockValues.accountForPing - True if it's an online game */ -function edit(gamefile, clockValues) { +function edit(gamefile: gamefile, clockValues: ClockValues) { if (!clockValues) return; // Likely a no-timed game const { timerWhite, timerBlack } = clockValues; const clocks = gamefile.clocks; @@ -72,21 +88,36 @@ function edit(gamefile, clockValues) { clocks.timeAtTurnStart = now; if (clockValues.accountForPing && moveutil.isGameResignable(gamefile) && !gamefileutility.isGameOver(gamefile)) { - // Ping is round-trip time (RTT), So divided by two to get the approximate - // time that has elapsed since the server sent us the correct clock values - const halfPing = pingManager.getHalfPing(); - clocks.currentTime[gamefile.whosTurn] -= halfPing; - if (halfPing > 2500) console.error("Ping is above 5000 milliseconds!!! This is a lot to adjust the clock values!"); - if (options.isDebugModeOn()) console.log(`Ping is ${halfPing * 2}. Subtracted ${halfPing} millis from ${gamefile.whosTurn}'s clock.`); + // // Ping is round-trip time (RTT), So divided by two to get the approximate + // // time that has elapsed since the server sent us the correct clock values + // const halfPing = pingManager.getHalfPing(); + // clocks.currentTime[gamefile.whosTurn] -= halfPing; + // if (halfPing > 2500) console.error("Ping is above 5000 milliseconds!!! This is a lot to adjust the clock values!"); + // if (options.isDebugModeOn()) console.log(`Ping is ${halfPing * 2}. Subtracted ${halfPing} millis from ${gamefile.whosTurn}'s clock.`); } clocks.timeRemainAtTurnStart = clocks.colorTicking === 'white' ? clocks.currentTime.white : clocks.currentTime.black; } +/** + * Modifies the clock values to account for ping. + */ +function adjustClockValuesForPing(clockValues: ClockValues, whosTurn: string) { + // Ping is round-trip time (RTT), So divided by two to get the approximate + // time that has elapsed since the server sent us the correct clock values + const halfPing = pingManager.getHalfPing(); + if (halfPing > 2500) console.error("Ping is above 5000 milliseconds!!! This is a lot to adjust the clock values!"); + if (options.isDebugModeOn()) console.log(`Ping is ${halfPing * 2}. Subtracted ${halfPing} millis from ${whosTurn}'s clock.`); + + if (clockValues[whosTurn] === undefined) throw Error(`Invalid color "${whosTurn}" to modify clock value to account for ping.`); + clockValues[whosTurn] -= halfPing; + + return clockValues; +} + /** * Call after flipping whosTurn. Flips colorTicking in local games. - * @param {gamefile} gamefile */ -function push(gamefile) { +function push(gamefile: gamefile) { const clocks = gamefile.clocks; if (onlinegame.areInOnlineGame()) return; // Only the server can push clocks if (clocks.untimed) return; @@ -97,14 +128,14 @@ function push(gamefile) { // Add increment if the last move has a clock ticking if (clocks.timeAtTurnStart !== undefined) { const prevcolor = moveutil.getWhosTurnAtMoveIndex(gamefile, gamefile.moves.length - 2); - clocks.currentTime[prevcolor] += timeutil.secondsToMillis(clocks.startTime.increment); + clocks.currentTime[prevcolor]! += timeutil.secondsToMillis(clocks.startTime.increment!); } - clocks.timeRemainAtTurnStart = clocks.currentTime[clocks.colorTicking]; + clocks.timeRemainAtTurnStart = clocks.currentTime[clocks.colorTicking]!; clocks.timeAtTurnStart = Date.now(); } -function endGame(gamefile) { +function endGame(gamefile: gamefile) { const clocks = gamefile.clocks; clocks.timeRemainAtTurnStart = undefined; clocks.timeAtTurnStart = undefined; @@ -112,36 +143,30 @@ function endGame(gamefile) { /** * Called every frame, updates values. - * @param {gamefile} gamefile - * @returns {undefined | string} undefined if clocks still have time, otherwise it's the color who won. + * @param gamefile + * @returns undefined if clocks still have time, otherwise it's the color who won. */ -function update(gamefile) { +function update(gamefile: gamefile): string | undefined { const clocks = gamefile.clocks; if (clocks.untimed || gamefileutility.isGameOver(gamefile) || !moveutil.isGameResignable(gamefile) || clocks.timeAtTurnStart === undefined) return; // Update current values const timePassedSinceTurnStart = Date.now() - clocks.timeAtTurnStart; - clocks.currentTime[clocks.colorTicking] = Math.ceil(clocks.timeRemainAtTurnStart - timePassedSinceTurnStart); + clocks.currentTime[clocks.colorTicking] = Math.ceil(clocks.timeRemainAtTurnStart! - timePassedSinceTurnStart); // Has either clock run out of time? if (onlinegame.areInOnlineGame()) return; // Don't conclude game by time if in an online game, only the server does that. - // TODO: update when lose conditions are added - if (clocks.currentTime.white <= 0) { - clocks.currentTime.white = 0; - return 'black'; - } - else if (clocks.currentTime.black <= 0) { - clocks.currentTime.black = 0; - return 'white'; + + for (const [color,time] of Object.entries(clocks.currentTime)) { + if (time! <= 0) { + clocks.currentTime[color] = 0; + return color; + } } } -/** - * - * @param {gamefile} gamefile - */ -function printClocks(gamefile) { +function printClocks(gamefile: gamefile) { const clocks = gamefile.clocks; for (const color in clocks.currentTime) { console.log(`${color} time: ${clocks.currentTime[color]}`); @@ -152,9 +177,8 @@ function printClocks(gamefile) { /** * Returns true if the current game is untimed (infinite clocks) - * @param {gamefile} gamefile */ -function isGameUntimed(gamefile) { +function isGameUntimed(gamefile: gamefile): boolean { return gamefile.clocks.untimed; } @@ -166,4 +190,5 @@ export default { push, printClocks, isGameUntimed, + adjustClockValuesForPing, }; \ No newline at end of file From ddd93397d8fab3af322e2ec17ebf88a654426c32 Mon Sep 17 00:00:00 2001 From: Naviary2 Date: Wed, 1 Jan 2025 23:27:11 -0700 Subject: [PATCH 002/131] Added getVariantTurnOrder() --- .../scripts/esm/chess/variants/variant.ts | 45 ++++++++++++++----- 1 file changed, 34 insertions(+), 11 deletions(-) diff --git a/src/client/scripts/esm/chess/variants/variant.ts b/src/client/scripts/esm/chess/variants/variant.ts index 33e7cac30..bd00a4fc1 100644 --- a/src/client/scripts/esm/chess/variants/variant.ts +++ b/src/client/scripts/esm/chess/variants/variant.ts @@ -319,7 +319,6 @@ function getStartSnapshotPosition({ positionString, startingPosition, specialRig * Returns the variant's gamerules at the provided date in time. * @param options - An object containing the metadata `Variant`, and if desired, `Date`. * @param options.Variant - The name of the variant for which to get the gamerules. - * @param [position] - The starting position of the game, organized by key `{ '1,2': 'queensB' }`, if it's already known. If not provided, it will be calculated. * @returns The gamerules object for the variant. */ function getGameRulesOfVariant({ Variant, UTCDate = timeutil.getCurrentUTCDate(), UTCTime = timeutil.getCurrentUTCTime() }: { @@ -328,21 +327,29 @@ function getGameRulesOfVariant({ Variant, UTCDate = timeutil.getCurrentUTCDate() UTCTime: string }, position: { [coordKey: string]: string }): GameRules { if (!isVariantValid(Variant)) throw new Error(`Cannot get starting position of invalid variant "${Variant}"!`); - const variantEntry: Variant = variantDictionary[Variant]!; - let gameruleModifications: GameRuleModifications; - // gameruleModifications + const gameruleModifications: GameRuleModifications = getVariantGameRuleModifications({ Variant, UTCDate, UTCTime }); + + return getGameRules(gameruleModifications, position); +} - // Does the gameruleModifications entry have multiple UTC timestamps? Or just one? +function getVariantGameRuleModifications({ Variant, UTCDate = timeutil.getCurrentUTCDate(), UTCTime = timeutil.getCurrentUTCTime() }: { + Variant: string, + UTCDate: string, + UTCTime: string +}): GameRuleModifications { + + const variantEntry: Variant = variantDictionary[Variant]; + if (!variantEntry) throw Error(`Cannot get gameruleModifications of invalid variant "${Variant}".`); + // Does the gameruleModifications entry have multiple UTC timestamps? Or just one? + // We use hasOwnProperty() because it is true even if the property is set as `undefined`, which in this case would mean zero gamerule modifications. if (variantEntry.gameruleModifications?.hasOwnProperty(0)) { // Multiple UTC timestamps - gameruleModifications = getApplicableTimestampEntry(variantEntry.gameruleModifications, { UTCDate, UTCTime }); + return getApplicableTimestampEntry(variantEntry.gameruleModifications, { UTCDate, UTCTime }); } else { // Just one gameruleModifications entry - gameruleModifications = variantEntry.gameruleModifications; + return variantEntry.gameruleModifications; } - - return getGameRules(gameruleModifications, position); } /** @@ -371,6 +378,21 @@ function getGameRules(modifications: GameRuleModifications = {}, position?: { [c return jsutil.deepCopyObject(gameRules) as GameRules; // Copy it so the game doesn't modify the values in this module. } +/** + * Returns the turnOrder of the provided variant at the date (if specified). + */ +function getVariantTurnOrder({ Variant, UTCDate = timeutil.getCurrentUTCDate(), UTCTime = timeutil.getCurrentUTCTime() }: { + Variant: string, + UTCDate: string, + UTCTime: string +}): GameRules['turnOrder'] { + + const gameruleModifications = getVariantGameRuleModifications({ Variant, UTCDate, UTCTime }); + // If the gamerule modifications have a turnOrder modification, return that, + // otherwise return the default instead. + return gameruleModifications.turnOrder || defaultTurnOrder; +} + /** * Returns the `promotionsAllowed` property of the variant's gamerules. * You can promote to whatever pieces the game starts with. @@ -378,7 +400,7 @@ function getGameRules(modifications: GameRuleModifications = {}, position?: { [c * @param promotionRanks - The `promotionRanks` gamerule of the variant. If one side's promotion rank is `null`, then we won't add legal promotions for them. * @returns The gamefile's `promotionsAllowed` gamerule. */ -function getPromotionsAllowed(position: { [coordKey: string]: string }, promotionRanks: (number | null)[]): ColorVariantProperty { +function getPromotionsAllowed(position: { [coordKey: string]: string }, promotionRanks: GameRules['promotionRanks']): ColorVariantProperty { console.log("Parsing position to get the promotionsAllowed gamerule.."); // We can't promote to royals or pawns, whether we started the game with them. @@ -435,7 +457,7 @@ function getApplicableTimestampEntry(object: TimeVariantProperty, /** * Gets the piece movesets for the given variant and time, such that each piece contains a function returning a copy of its moveset (to avoid modifying originals) - * @param options - An object containing the metadata `Variant`, and if desired, `Date`. + * @param options - An object containing the metadata `Variant`, and if desired, `UTCDate` & `UTCTime`. * @param options.Variant - The name of the variant for which to get the moveset. * @param [options.UTCDate] - Optional. The UTCDate metadata for which to get the moveset, in the format `YYYY.MM.DD`. Defaults to the current date. * @param [options.UTCTime] - Optional. The UTCTime metadata for which to get the moveset, in the format `HH:MM:SS`. Defaults to the current time. @@ -499,6 +521,7 @@ export default { isVariantValid, getStartingPositionOfVariant, getGameRulesOfVariant, + getVariantTurnOrder, getPromotionsAllowed, getMovesetsOfVariant, }; From e7bbce62ae862de07b5f403d31883e13351c25a7 Mon Sep 17 00:00:00 2001 From: Naviary2 Date: Wed, 1 Jan 2025 23:46:59 -0700 Subject: [PATCH 003/131] Corrected all call arguments for clock.ts --- src/client/scripts/esm/chess/logic/clock.ts | 4 ++ .../scripts/esm/chess/logic/gamefile.js | 43 ++++++------------- src/client/scripts/esm/chess/util/metadata.ts | 4 +- src/client/scripts/esm/game/chess/gameslot.ts | 5 ++- 4 files changed, 24 insertions(+), 32 deletions(-) diff --git a/src/client/scripts/esm/chess/logic/clock.ts b/src/client/scripts/esm/chess/logic/clock.ts index 9382e5c93..558bfe179 100644 --- a/src/client/scripts/esm/chess/logic/clock.ts +++ b/src/client/scripts/esm/chess/logic/clock.ts @@ -191,4 +191,8 @@ export default { printClocks, isGameUntimed, adjustClockValuesForPing, +}; + +export type { + ClockValues, }; \ No newline at end of file diff --git a/src/client/scripts/esm/chess/logic/gamefile.js b/src/client/scripts/esm/chess/logic/gamefile.js index 92fab7ab8..d689785d7 100644 --- a/src/client/scripts/esm/chess/logic/gamefile.js +++ b/src/client/scripts/esm/chess/logic/gamefile.js @@ -17,40 +17,27 @@ import gamerules from '../variants/gamerules.js'; /** @typedef {import('../util/moveutil.js').Move} Move */ /** @typedef {import('../../game/rendering/buffermodel.js').BufferModel} BufferModel */ /** @typedef {import('../variants/gamerules.js').GameRules} GameRules */ +/** @typedef {import('../util/metadata.js').MetaData} MetaData */ +/** @typedef {import('./clock.js').ClockValues} ClockValues */ 'use strict'; /** * Constructs a gamefile from provided arguments. Use the *new* keyword. - * @param {Object} metadata - An object containing the property `Variant`, and optionally `UTCDate` and `UTCTime`, which can be used to extract the version of the variant. Without the date, the latest version will be used. + * @param {MetaData} metadata - An object containing the property `Variant`, and optionally `UTCDate` and `UTCTime`, which can be used to extract the version of the variant. Without the date, the latest version will be used. * @param {Object} [options] - Options for constructing the gamefile. * @param {string[]} [options.moves=[]] - Existing moves, if any, to forward to the front of the game. Should be specified if reconnecting to an online game or pasting a game. Each move should be in the most compact notation, e.g., `['1,2>3,4','10,7>10,8Q']`. * @param {Object} [options.variantOptions] - If a custom position is needed, for instance, when pasting a game, then these options should be included. * @param {Object} [options.gameConclusion] - The conclusion of the game, if loading an online game that has already ended. - * @param {Object} [options.clockValues] - Any already existing clock values for the gamefile, in the format `{ timerWhite, timerBlack, accountForPing }` + * @param {ClockValues} [options.clockValues] - Any already existing clock values for the gamefile, in the format `{ timerWhite, timerBlack, accountForPing }` * @returns {Object} The gamefile */ function gamefile(metadata, { moves = [], variantOptions, gameConclusion, clockValues } = {}) { // Everything for JSDoc stuff... - /** Information about the game */ - this.metadata = { - Variant: undefined, - White: undefined, - Black: undefined, - TimeControl: undefined, - UTCDate: undefined, - UTCTime: undefined, - /** 1-0 = White won */ - Result: undefined, - /** What caused the game to end, in spoken language. For example, "Time forfeit". This will always be the win condition that concluded the game. */ - Termination: undefined, - /** What kind of game (rated/casual), and variant, in spoken language. For example, "Casual local Classical infinite chess game" */ - Event: undefined, - /** What website hosted the game. "https://www.infinitechess.org/" */ - Site: undefined, - }; + /** Information about the game @type {MetaData} */ + this.metadata = metadata; /** Information about the beginning of the game (position, positionString, specialRights, turn) */ this.startSnapshot = { @@ -189,8 +176,9 @@ function gamefile(metadata, { moves = [], variantOptions, gameConclusion, clockV /** Contains the methods for undo'ing special moves for this game. */ this.specialUndos = undefined; + /** The clocks of the game, if the game is timed. */ this.clocks = { - /** The time each player has remaining, in milliseconds. */ + /** The time each player has remaining, in milliseconds. @type {{ [color: string]: number | null }}*/ currentTime: { white: undefined, black: undefined, @@ -198,30 +186,27 @@ function gamefile(metadata, { moves = [], variantOptions, gameConclusion, clockV /** Contains information about the start time of the game. */ startTime: { - /** The number of minutes both sides started with. */ + /** The number of minutes both sides started with. @type {null | number} */ minutes: undefined, - /** The number of miliseconds both sides started with. */ + /** The number of miliseconds both sides started with. @type {null | number} */ millis: undefined, - /** The increment used, in milliseconds. */ + /** The increment used, in milliseconds. @type {null | number} */ increment: undefined, }, /** We need this separate from gamefile's "whosTurn", because when we are * in an online game and we make a move, we want our Clock to continue - * ticking until we receive the Clock information back from the server! */ + * ticking until we receive the Clock information back from the server! @type {string} */ colorTicking: undefined, /** The amount of time in millis the current player had at the beginning of their turn, in milliseconds. - * When set to undefined no clocks are ticking */ + * When set to undefined no clocks are ticking @type {number | undefined} */ timeRemainAtTurnStart: undefined, - /** The time at the beginning of the current player's turn, in milliseconds elapsed since the Unix epoch. */ + /** The time at the beginning of the current player's turn, in milliseconds elapsed since the Unix epoch. @type {number | undefined} */ timeAtTurnStart: undefined, /** True if the game is not timed. @type {Boolean}*/ untimed: undefined, }; // JSDoc stuff over... - // this.metadata = metadata; // Breaks the above JSDoc - jsutil.copyPropertiesToObject(metadata, this.metadata); - // Init things related to the variant, and the startSnapshot of the position initvariant.setupVariant(this, metadata, variantOptions); // Initiates startSnapshot, gameRules, and pieceMovesets /** The number of half-moves played since the last capture or pawn push. */ diff --git a/src/client/scripts/esm/chess/util/metadata.ts b/src/client/scripts/esm/chess/util/metadata.ts index ea2f266b0..83920822d 100644 --- a/src/client/scripts/esm/chess/util/metadata.ts +++ b/src/client/scripts/esm/chess/util/metadata.ts @@ -10,7 +10,7 @@ interface MetaData { - /** This phrase goes: "Casual/Rated variantName infinite chess game."" */ + /** What kind of game (rated/casual), and variant, in spoken language. For example, "Casual local Classical infinite chess game". This phrase goes: "Casual/Rated variantName infinite chess game." */ Event: string, /** What website the game was played on. Right now this has no application because infinitechess.org is the ONLY site you can play this game on. */ Site: 'https://www.infinitechess.org/', @@ -37,7 +37,7 @@ interface MetaData { BlackID?: string, /** How many points each side received from the game (e.g. `"1-0"` means white won, `"1/2-1/2"` means a draw) */ Result?: string, - /** What caused the game to end? Whether it's the wincondition, time, resignation, moverule, repetition, draw agreement, etc. */ + /** What caused the game to end, in spoken language. For example, "Time forfeit". This will always be the win condition that concluded the game. */ Termination: string, } diff --git a/src/client/scripts/esm/game/chess/gameslot.ts b/src/client/scripts/esm/game/chess/gameslot.ts index 3ca1f2375..ec555a512 100644 --- a/src/client/scripts/esm/game/chess/gameslot.ts +++ b/src/client/scripts/esm/game/chess/gameslot.ts @@ -46,6 +46,7 @@ import winconutil from "../../chess/util/winconutil.js"; // @ts-ignore import moveutil from "../../chess/util/moveutil.js"; // @ts-ignore +// eslint-disable-next-line no-unused-vars import clock from "../../chess/logic/clock.js"; // @ts-ignore import guigameinfo from "../gui/guigameinfo.js"; @@ -61,6 +62,8 @@ import spritesheet from "../rendering/spritesheet.js"; import type { MetaData } from "../../chess/util/metadata.js"; +// eslint-disable-next-line no-unused-vars +import type { ClockValues } from "../../chess/logic/clock.js"; // Variables --------------------------------------------------------------- @@ -125,7 +128,7 @@ function isLoadedGameViewingWhitePerspective() { * @param {string[]} [options.moves] - Existing moves, if any, to forward to the front of the game. Should be specified if reconnecting to an online game or pasting a game. Each move should be in the most compact notation, e.g., `['1,2>3,4','10,7>10,8Q']`. * @param {Object} [options.variantOptions] - If a custom position is needed, for instance, when pasting a game, then these options should be included. * @param {Object} [options.gameConclusion] - The conclusion of the game, if loading an online game that has already ended. - * @param {Object} [options.clockValues] - Any already existing clock values for the gamefile, in the format `{ timerWhite, timerBlack, accountForPing }` + * @param {ClockValues} [options.clockValues] - Any already existing clock values for the gamefile, in the format `{ timerWhite, timerBlack, accountForPing }` */ async function loadGamefile( metadata: MetaData, From 5f154d7c63a20421cdf8c398ef74ed52d1a2b8b2 Mon Sep 17 00:00:00 2001 From: Naviary2 Date: Wed, 1 Jan 2025 23:47:19 -0700 Subject: [PATCH 004/131] Added getWhosTurnAtFrom_ByMoveCountAndTurnOrder() --- src/client/scripts/esm/chess/util/moveutil.js | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/src/client/scripts/esm/chess/util/moveutil.js b/src/client/scripts/esm/chess/util/moveutil.js index 017ab70e5..7b50db30b 100644 --- a/src/client/scripts/esm/chess/util/moveutil.js +++ b/src/client/scripts/esm/chess/util/moveutil.js @@ -9,9 +9,9 @@ import coordutil from './coordutil.js'; /** * Type Definitions * @typedef {import('../logic/gamefile.js').gamefile} gamefile + * @typedef {import('../variants/gamerules.js').GameRules} GameRules */ - "use strict"; // Custom type definitions... @@ -183,6 +183,19 @@ function getWhosTurnAtFront(gamefile) { return getWhosTurnAtMoveIndex(gamefile, gamefile.moves.length - 1); } +/** + * Returns whos turn it is at the front of the game, + * provided the only information you have is the existing moves list + * and the turnOrder gamerule. + * + * You may need this if the gamefile hasn't actually been contructed yet. + * @param {number} numberOfMoves - The number of moves played in the game so far (length of the current moves list). + * @param {GameRules['turnOrder']} turnOrder - The order of colors turns in the game. + */ +function getWhosTurnAtFrom_ByMoveCountAndTurnOrder(numberOfMoves, turnOrder) { + return turnOrder[numberOfMoves % turnOrder]; +} + /** * Returns total ply count (or half-moves) of the game so far. * @param {Move[]} moves - The moves list @@ -314,6 +327,8 @@ function doesAnyPlayerGet2TurnsInARow(gamefile) { return false; } + + // Type export DO NOT USE export { Move }; @@ -327,6 +342,7 @@ export default { areWeViewingLatestMove, isIndexTheLastMove, getWhosTurnAtFront, + getWhosTurnAtFrom_ByMoveCountAndTurnOrder, getPlyCount, hasPieceMoved, deleteLastMove, From 5e441d49e9f85b2d01698b24726fc8159267b9fb Mon Sep 17 00:00:00 2001 From: Naviary2 Date: Wed, 1 Jan 2025 23:51:38 -0700 Subject: [PATCH 005/131] Fixed typescript errors --- src/client/scripts/esm/chess/logic/clock.ts | 8 +++++--- src/client/scripts/esm/chess/variants/variant.ts | 2 +- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/src/client/scripts/esm/chess/logic/clock.ts b/src/client/scripts/esm/chess/logic/clock.ts index 558bfe179..90ed02705 100644 --- a/src/client/scripts/esm/chess/logic/clock.ts +++ b/src/client/scripts/esm/chess/logic/clock.ts @@ -63,7 +63,7 @@ function set(gamefile: gamefile, currentTimes?: ClockValues) { // Edit the closk if we're re-loading an online game if (currentTimes) edit(gamefile, currentTimes); else { // No current time specified, start both players with the default. - gamefile.gameRules.turnOrder.forEach(color => { + gamefile.gameRules.turnOrder.forEach((color: string) => { clocks.currentTime[color] = clocks.startTime.millis; }); } @@ -87,7 +87,7 @@ function edit(gamefile: gamefile, clockValues: ClockValues) { const now = Date.now(); clocks.timeAtTurnStart = now; - if (clockValues.accountForPing && moveutil.isGameResignable(gamefile) && !gamefileutility.isGameOver(gamefile)) { + if (clockValues['accountForPing'] && moveutil.isGameResignable(gamefile) && !gamefileutility.isGameOver(gamefile)) { // // Ping is round-trip time (RTT), So divided by two to get the approximate // // time that has elapsed since the server sent us the correct clock values // const halfPing = pingManager.getHalfPing(); @@ -159,11 +159,13 @@ function update(gamefile: gamefile): string | undefined { if (onlinegame.areInOnlineGame()) return; // Don't conclude game by time if in an online game, only the server does that. for (const [color,time] of Object.entries(clocks.currentTime)) { - if (time! <= 0) { + if (time as number <= 0) { clocks.currentTime[color] = 0; return color; } } + + return; // Without this, typescript complains not all code paths return a value. } function printClocks(gamefile: gamefile) { diff --git a/src/client/scripts/esm/chess/variants/variant.ts b/src/client/scripts/esm/chess/variants/variant.ts index bd00a4fc1..6595b86e4 100644 --- a/src/client/scripts/esm/chess/variants/variant.ts +++ b/src/client/scripts/esm/chess/variants/variant.ts @@ -339,7 +339,7 @@ function getVariantGameRuleModifications({ Variant, UTCDate = timeutil.getCurren UTCTime: string }): GameRuleModifications { - const variantEntry: Variant = variantDictionary[Variant]; + const variantEntry = variantDictionary[Variant]; if (!variantEntry) throw Error(`Cannot get gameruleModifications of invalid variant "${Variant}".`); // Does the gameruleModifications entry have multiple UTC timestamps? Or just one? From 5709afaaf4086484f07936518d1156896c13aa8d Mon Sep 17 00:00:00 2001 From: Naviary2 Date: Thu, 2 Jan 2025 20:50:28 -0700 Subject: [PATCH 006/131] Server now, when it sends clock information, includes what color is currently ticking, if there is one --- src/server/game/gamemanager/gameutility.js | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/src/server/game/gamemanager/gameutility.js b/src/server/game/gamemanager/gameutility.js index 00115b0d4..32f31bce0 100644 --- a/src/server/game/gamemanager/gameutility.js +++ b/src/server/game/gamemanager/gameutility.js @@ -643,10 +643,17 @@ function sendUpdatedClockToColor(game, color) { */ function getGameClockValues(game) { updateClockValues(game); - return { - timerWhite: game.timerWhite, - timerBlack: game.timerBlack, + const clockValues = { + white: game.timerWhite, + black: game.timerBlack, }; + + // Let the client know which clock is ticking so that they can immediately adjust for ping. + // * If less than 2 moves have been played, no color is considered ticking. + // * If the game is over, no color is considered ticking. + if (isGameResignable(game) && !isGameOver(game)) clockValues.colorTicking = game.whosTurn; + + return clockValues; } /** From cbb4aae49b69109b88b21a72c1af919d636cb45a Mon Sep 17 00:00:00 2001 From: Naviary2 Date: Fri, 3 Jan 2025 00:58:24 -0700 Subject: [PATCH 007/131] gameloader.ts is no longer crashing, but not yet finished. --- src/client/scripts/esm/chess/logic/clock.ts | 64 ++++--- .../scripts/esm/chess/logic/gamefile.js | 4 +- src/client/scripts/esm/chess/util/moveutil.js | 2 +- .../scripts/esm/chess/variants/variant.ts | 30 ++-- .../scripts/esm/game/chess/gameloader.ts | 162 +++++++++++++++--- src/client/scripts/esm/game/chess/gameslot.ts | 29 ++-- src/client/scripts/esm/game/gui/guiplay.js | 127 +++++--------- src/client/scripts/esm/game/misc/invites.js | 23 ++- .../scripts/esm/game/misc/onlinegame.js | 19 +- src/server/config/config.js | 1 + src/server/game/gamemanager/gameutility.js | 13 +- 11 files changed, 300 insertions(+), 174 deletions(-) diff --git a/src/client/scripts/esm/chess/logic/clock.ts b/src/client/scripts/esm/chess/logic/clock.ts index 90ed02705..cb8d2575e 100644 --- a/src/client/scripts/esm/chess/logic/clock.ts +++ b/src/client/scripts/esm/chess/logic/clock.ts @@ -27,8 +27,26 @@ import options from '../../game/rendering/options.js'; // @ts-ignore import type gamefile from './gamefile.js'; -/** An object containing each color in the game for the keys, and that color's time left in milliseconds for the values. */ -interface ClockValues { [color: string]: number }; +/** An object containg the values of each color's clock, and which one is currently counting down, if any. */ +interface ClockValues { + /** The actual clock values. An object containing each color in the game for the keys, and that color's time left in milliseconds for the values. */ + clocks: { [color: string]: number } + /** + * If a player's timer is currently counting down, this should be specified. + * No clock is ticking if less than 2 moves are played, or if game is over. + * + * The color specified should have their time immediately accomodated for ping. + */ + colorTicking?: string, + /** + * The timestamp the color ticking (if there is one) will lose by timeout. + * This should be calulated AFTER we adjust the clock values for ping. + * + * The server should NOT specify this when sending the clock information + * to the client, because the server and client's clocks are not always in sync. + */ + timeColorTickingLosesAt?: number, +}; // Functions ----------------------------------------------------------------------- @@ -76,40 +94,46 @@ function set(gamefile: gamefile, currentTimes?: ClockValues) { * @param {gamefile} gamefile - The current game state object containing clock information. * @param {object} clockValues - An object containing the updated clock values. */ -function edit(gamefile: gamefile, clockValues: ClockValues) { +function edit(gamefile: gamefile, clockValues?: ClockValues) { if (!clockValues) return; // Likely a no-timed game - const { timerWhite, timerBlack } = clockValues; const clocks = gamefile.clocks; - clocks.colorTicking = gamefile.whosTurn; - clocks.currentTime.white = timerWhite; - clocks.currentTime.black = timerBlack; + const colorTicking = gamefile.whosTurn; + + if (clockValues.colorTicking !== undefined) { + // Adjust the clock value according to the precalculated time they will lost by timeout. + if (clockValues.timeColorTickingLosesAt === undefined) throw Error('clockValues should have been modified to account for ping BEFORE editing the clocks. Use adjustClockValuesForPing() beore edit()'); + const colorTickingTrueTimeRemaining = clockValues.timeColorTickingLosesAt - Date.now(); + clockValues.clocks[colorTicking] = colorTickingTrueTimeRemaining; + } + + clocks.colorTicking = colorTicking; + clocks.currentTime = { ...clockValues.clocks }; + const now = Date.now(); clocks.timeAtTurnStart = now; - if (clockValues['accountForPing'] && moveutil.isGameResignable(gamefile) && !gamefileutility.isGameOver(gamefile)) { - // // Ping is round-trip time (RTT), So divided by two to get the approximate - // // time that has elapsed since the server sent us the correct clock values - // const halfPing = pingManager.getHalfPing(); - // clocks.currentTime[gamefile.whosTurn] -= halfPing; - // if (halfPing > 2500) console.error("Ping is above 5000 milliseconds!!! This is a lot to adjust the clock values!"); - // if (options.isDebugModeOn()) console.log(`Ping is ${halfPing * 2}. Subtracted ${halfPing} millis from ${gamefile.whosTurn}'s clock.`); - } clocks.timeRemainAtTurnStart = clocks.colorTicking === 'white' ? clocks.currentTime.white : clocks.currentTime.black; } /** * Modifies the clock values to account for ping. */ -function adjustClockValuesForPing(clockValues: ClockValues, whosTurn: string) { +function adjustClockValuesForPing(clockValues: ClockValues): ClockValues { + if (!clockValues.colorTicking) return clockValues; // No clock is ticking (< 2 moves, or game is over), don't adjust for ping + // Ping is round-trip time (RTT), So divided by two to get the approximate // time that has elapsed since the server sent us the correct clock values const halfPing = pingManager.getHalfPing(); if (halfPing > 2500) console.error("Ping is above 5000 milliseconds!!! This is a lot to adjust the clock values!"); - if (options.isDebugModeOn()) console.log(`Ping is ${halfPing * 2}. Subtracted ${halfPing} millis from ${whosTurn}'s clock.`); + if (options.isDebugModeOn()) console.log(`Ping is ${halfPing * 2}. Subtracted ${halfPing} millis from ${clockValues.colorTicking}'s clock.`); + + if (clockValues.clocks[clockValues.colorTicking] === undefined) throw Error(`Invalid color "${clockValues.colorTicking}" to modify clock value to account for ping.`); + clockValues.clocks[clockValues.colorTicking]! -= halfPing; - if (clockValues[whosTurn] === undefined) throw Error(`Invalid color "${whosTurn}" to modify clock value to account for ping.`); - clockValues[whosTurn] -= halfPing; + // Flag what time the player who's clock is ticking will lose on time. + // Do this because while while the gamefile is being constructed, the time left may become innacurate. + clockValues.timeColorTickingLosesAt = Date.now() + clockValues.clocks[clockValues.colorTicking]!; return clockValues; } @@ -137,7 +161,7 @@ function push(gamefile: gamefile) { function endGame(gamefile: gamefile) { const clocks = gamefile.clocks; - clocks.timeRemainAtTurnStart = undefined; + clocks.timeRemainAtTurnStart = null; clocks.timeAtTurnStart = undefined; } diff --git a/src/client/scripts/esm/chess/logic/gamefile.js b/src/client/scripts/esm/chess/logic/gamefile.js index d689785d7..4c4a898df 100644 --- a/src/client/scripts/esm/chess/logic/gamefile.js +++ b/src/client/scripts/esm/chess/logic/gamefile.js @@ -29,7 +29,7 @@ import gamerules from '../variants/gamerules.js'; * @param {string[]} [options.moves=[]] - Existing moves, if any, to forward to the front of the game. Should be specified if reconnecting to an online game or pasting a game. Each move should be in the most compact notation, e.g., `['1,2>3,4','10,7>10,8Q']`. * @param {Object} [options.variantOptions] - If a custom position is needed, for instance, when pasting a game, then these options should be included. * @param {Object} [options.gameConclusion] - The conclusion of the game, if loading an online game that has already ended. - * @param {ClockValues} [options.clockValues] - Any already existing clock values for the gamefile, in the format `{ timerWhite, timerBlack, accountForPing }` + * @param {ClockValues} [options.clockValues] - Any already existing clock values for the gamefile * @returns {Object} The gamefile */ function gamefile(metadata, { moves = [], variantOptions, gameConclusion, clockValues } = {}) { @@ -198,7 +198,7 @@ function gamefile(metadata, { moves = [], variantOptions, gameConclusion, clockV * ticking until we receive the Clock information back from the server! @type {string} */ colorTicking: undefined, /** The amount of time in millis the current player had at the beginning of their turn, in milliseconds. - * When set to undefined no clocks are ticking @type {number | undefined} */ + * When set to undefined no clocks are ticking @type {number | null} */ timeRemainAtTurnStart: undefined, /** The time at the beginning of the current player's turn, in milliseconds elapsed since the Unix epoch. @type {number | undefined} */ timeAtTurnStart: undefined, diff --git a/src/client/scripts/esm/chess/util/moveutil.js b/src/client/scripts/esm/chess/util/moveutil.js index 7b50db30b..2a66621bb 100644 --- a/src/client/scripts/esm/chess/util/moveutil.js +++ b/src/client/scripts/esm/chess/util/moveutil.js @@ -193,7 +193,7 @@ function getWhosTurnAtFront(gamefile) { * @param {GameRules['turnOrder']} turnOrder - The order of colors turns in the game. */ function getWhosTurnAtFrom_ByMoveCountAndTurnOrder(numberOfMoves, turnOrder) { - return turnOrder[numberOfMoves % turnOrder]; + return turnOrder[numberOfMoves % turnOrder.length]; } /** diff --git a/src/client/scripts/esm/chess/variants/variant.ts b/src/client/scripts/esm/chess/variants/variant.ts index 6595b86e4..c06e148e5 100644 --- a/src/client/scripts/esm/chess/variants/variant.ts +++ b/src/client/scripts/esm/chess/variants/variant.ts @@ -378,20 +378,20 @@ function getGameRules(modifications: GameRuleModifications = {}, position?: { [c return jsutil.deepCopyObject(gameRules) as GameRules; // Copy it so the game doesn't modify the values in this module. } -/** - * Returns the turnOrder of the provided variant at the date (if specified). - */ -function getVariantTurnOrder({ Variant, UTCDate = timeutil.getCurrentUTCDate(), UTCTime = timeutil.getCurrentUTCTime() }: { - Variant: string, - UTCDate: string, - UTCTime: string -}): GameRules['turnOrder'] { - - const gameruleModifications = getVariantGameRuleModifications({ Variant, UTCDate, UTCTime }); - // If the gamerule modifications have a turnOrder modification, return that, - // otherwise return the default instead. - return gameruleModifications.turnOrder || defaultTurnOrder; -} +// /** +// * Returns the turnOrder of the provided variant at the date (if specified). +// */ +// function getVariantTurnOrder({ Variant, UTCDate = timeutil.getCurrentUTCDate(), UTCTime = timeutil.getCurrentUTCTime() }: { +// Variant: string, +// UTCDate: string, +// UTCTime: string +// }): GameRules['turnOrder'] { + +// const gameruleModifications = getVariantGameRuleModifications({ Variant, UTCDate, UTCTime }); +// // If the gamerule modifications have a turnOrder modification, return that, +// // otherwise return the default instead. +// return gameruleModifications.turnOrder || defaultTurnOrder; +// } /** * Returns the `promotionsAllowed` property of the variant's gamerules. @@ -521,7 +521,7 @@ export default { isVariantValid, getStartingPositionOfVariant, getGameRulesOfVariant, - getVariantTurnOrder, + // getVariantTurnOrder, getPromotionsAllowed, getMovesetsOfVariant, }; diff --git a/src/client/scripts/esm/game/chess/gameloader.ts b/src/client/scripts/esm/game/chess/gameloader.ts index 44b6e9427..45a639157 100644 --- a/src/client/scripts/esm/game/chess/gameloader.ts +++ b/src/client/scripts/esm/game/chess/gameloader.ts @@ -1,8 +1,13 @@ /** - * This script loads and unloads gamefiles, not only handling the logic stuff, - * but also initiating and opening the gui elements for the game, - * such as the navigation and gameinfo bars. + * This script contains the logic for loading any kind of game onto our game board: + * * Local + * * Online + * * Analysis Board (in the future) + * * Board Editor (in the future) + * + * It not only handles the logic of the gamefile, + * but also prepares and opens the UI elements for that type of game. */ // @ts-ignore @@ -17,12 +22,138 @@ import guinavigation from "../gui/guinavigation.js"; import sound from '../misc/sound.js'; // @ts-ignore import onlinegame from "../misc/onlinegame.js"; +// @ts-ignore +import gui from "../gui/gui.js"; +// @ts-ignore +import drawoffers from "../misc/drawoffers.js"; +// @ts-ignore +import localstorage from "../../util/localstorage.js"; +// @ts-ignore +import jsutil from "../../util/jsutil.js"; import gameslot from "./gameslot.js"; +import clock from "../../chess/logic/clock.js"; + + +// Type Definitions -------------------------------------------------------------------- // @ts-ignore import type { GameRules } from "../../chess/variants/gamerules.js"; import type { MetaData } from "../../chess/util/metadata.js"; +import type { Coords, CoordsKey } from "../../chess/util/coordutil.js"; +import type { ClockValues } from "../../chess/logic/clock.js"; + +/** + * Variant options that can be used to load a custom game, + * whether local or online, instead of one of the default variants. + */ +interface VariantOptions { + /** + * The full move number of the turn at the provided position. Default: 1. + * Can be higher if you copy just the positional information in a game with some moves played already. + */ + fullMove: number, + /** The square enpassant capture is allowed, in the starting position specified (not after all moves are played). */ + enpassant?: Coords, + gameRules: GameRules, + /** If the move moveRule gamerule is present, this is a string of its current state and the move rule number (e.g. `"0/100"`) */ + moveRule?: `${number}/${number}`, + /** A position in ICN notation (e.g. `"P1,2+|P2,2+|..."`) */ + positionString: string, + /** + * The starting position object, containing the pieces organized by key. + * The key of the object is the coordinates of the piece as a string, + * and the value is the type of piece on that coordinate (e.g. `"pawnsW"`) + */ + startingPosition: { [key: CoordsKey]: string } + /** The special rights object of the gamefile at the starting position provided, NOT after the moves provided have been played. */ + specialRights: { [key: CoordsKey]: true }, +} + + +// Type Definitions -------------------------------------------------------------------- + + +/** Starts a local game according to the options provided. */ +async function startLocalGame(options: { + /** Must be one of the valid variants in variant.ts */ + Variant: string, + TimeControl: MetaData['TimeControl'], +}) { + // console.log("Starting local game with invite options:"); + // console.log(options); + + gui.setScreen('game local'); // Change screen location + + // [Event "Casual Space Classic infinite chess game"] [Site "https://www.infinitechess.org/"] [Round "-"] + const gameOptions = { + metadata: { + Event: `Casual local ${translations[options.Variant]} infinite chess game`, + Site: "https://www.infinitechess.org/", + Round: "-", + Variant: options.Variant, + TimeControl: options.TimeControl + } + }; + + guigameinfo.hidePlayerNames(); + // @ts-ignore + loadGame(gameOptions, true, true); +} + +/** + * Starts an online game according to the options provided by the server. + */ +async function startOnlineGame(options: { + clock: MetaData['TimeControl'], + drawOffer: { + /** True if our opponent has extended a draw offer we haven't yet confirmed/denied */ + unconfirmed: boolean, + /** The move ply WE HAVE last offered a draw, if we have, otherwise undefined. */ + lastOfferPly?: number, + }, + gameConclusion: string | false, + id: string, + metadata: MetaData, + /** Existing moves, if any, to forward to the front of the game. Should be specified if reconnecting to an online. Each move should be in the most compact notation, e.g., `['1,2>3,4','10,7>10,8Q']`. */ + moves: string[], + publicity: 'public' | 'private', + variantOptions?: VariantOptions, + youAreColor: 'white' | 'black', + /** Provide if the game is timed. */ + clockValues?: ClockValues, +}) { + console.log("Starting online game with invite options:"); + console.log(jsutil.deepCopyObject(options)); + + // If the clock values are provided, adjust the timer of whos turn it is depending on ping. + if (options.clockValues) options.clockValues = clock.adjustClockValuesForPing(options.clockValues); + + gui.setScreen('game online'); // Change screen location + // Must be set BEFORE loading the game, because the mesh generation relies on the color we are. + onlinegame.setColorAndGameID(options); + options.variantOptions = generateVariantOptionsIfReloadingPrivateCustomGame(); + const fromWhitePerspective = options.youAreColor === 'white'; + await loadGame(options, fromWhitePerspective, false); + + onlinegame.initOnlineGame(options); + guigameinfo.setAndRevealPlayerNames(options); + drawoffers.set(options.drawOffer); +} + + + + + +function generateVariantOptionsIfReloadingPrivateCustomGame() { + if (!onlinegame.getIsPrivate()) return; // Can't play/paste custom position in public matches. + const gameID = onlinegame.getGameID(); + if (!gameID) return console.error("Can't generate variant options when reloading private custom game because gameID isn't defined yet."); + return localstorage.loadItem(gameID); +} + + + @@ -36,11 +167,7 @@ async function loadGame( gameOptions: { metadata: MetaData, /** Should be provided if we're rejoining an online game. */ - clockValues?: { - timerWhite: number, - timerBlack: number, - accountForPing: boolean - }, + clockValues?: ClockValues, /** Should be provided if we're rejoining an online game. */ gameConclusion?: string | false, /** @@ -55,22 +182,7 @@ async function loadGame( * * Should be provided if we're pasting a game, or rejoining a custom online private game. */ - variantOptions?: { - fullMove: number, - gameRules: GameRules, - /** If the move ruleRule gamerule is present, this is a string of its current state and the move rule number (e.g. `"0/100"`) */ - moveRule?: string, - /** A position in ICN notation (e.g. `"P1,2+|P2,2+|..."`) */ - positionString: string, - /** - * The starting position object, containing the pieces organized by key. - * The key of the object is the coordinates of the piece as a string, - * and the value is the type of piece on that coordinate (e.g. `"pawnsW"`) - */ - startingPosition: { [coordsKey: string]: string } - /** The special rights object of the gamefile at the starting position provided, NOT after the moves provided have been played. */ - specialRights: { [coordsKey: string]: true }, - }, + variantOptions?: VariantOptions, }, fromWhitePerspective: boolean, allowEditCoords: boolean @@ -105,6 +217,8 @@ function unloadGame() { export default { + startLocalGame, + startOnlineGame, loadGame, unloadGame, }; \ No newline at end of file diff --git a/src/client/scripts/esm/game/chess/gameslot.ts b/src/client/scripts/esm/game/chess/gameslot.ts index ec555a512..f6a7193cd 100644 --- a/src/client/scripts/esm/game/chess/gameslot.ts +++ b/src/client/scripts/esm/game/chess/gameslot.ts @@ -122,13 +122,13 @@ function isLoadedGameViewingWhitePerspective() { * Loads a gamefile onto the board. * Generates the gamefile and organizes its lines. Inits the promotion UI, * mesh of all the pieces, and toggles miniimage rendering. (everything visual) - * @param {Object} metadata - An object containing the property `Variant`, and optionally `UTCDate` and `UTCTime`, which can be used to extract the version of the variant. Without the date, the latest version will be used. - * @param {Object} viewWhitePerspective - True if we should be viewing the game from white's perspective, false for black's perspective. - * @param {Object} [options] - Options for constructing the gamefile. - * @param {string[]} [options.moves] - Existing moves, if any, to forward to the front of the game. Should be specified if reconnecting to an online game or pasting a game. Each move should be in the most compact notation, e.g., `['1,2>3,4','10,7>10,8Q']`. - * @param {Object} [options.variantOptions] - If a custom position is needed, for instance, when pasting a game, then these options should be included. - * @param {Object} [options.gameConclusion] - The conclusion of the game, if loading an online game that has already ended. - * @param {ClockValues} [options.clockValues] - Any already existing clock values for the gamefile, in the format `{ timerWhite, timerBlack, accountForPing }` + * @param metadata - An object containing the property `Variant`, and optionally `UTCDate` and `UTCTime`, which can be used to extract the version of the variant. Without the date, the latest version will be used. + * @param viewWhitePerspective - True if we should be viewing the game from white's perspective, false for black's perspective. + * @param [options] - Options for constructing the gamefile. + * @param [options.moves] - Existing moves, if any, to forward to the front of the game. Should be specified if reconnecting to an online game or pasting a game. Each move should be in the most compact notation, e.g., `['1,2>3,4','10,7>10,8Q']`. + * @param [options.variantOptions] - If a custom position is needed, for instance, when pasting a game, then these options should be included. + * @param [options.gameConclusion] - The conclusion of the game, if loading an online game that has already ended. + * @param [options.clockValues] - Any already existing clock values for the gamefile. */ async function loadGamefile( metadata: MetaData, @@ -137,7 +137,7 @@ async function loadGamefile( moves?: string[], variantOptions?: any, gameConclusion?: string | false, - clockValues?: any, + clockValues?: ClockValues, } = {} ) { @@ -164,7 +164,6 @@ async function loadGamefile( // spinny pawn animation has time to fade away. animateLastMoveTimeoutID = setTimeout(movepiece.forwardToFront, delayOfLatestMoveAnimationOnRejoinMillis, newGamefile, { flipTurn: false, updateProperties: false }); - loadedGamefile = newGamefile; youAreColor = viewWhitePerspective ? 'white' : 'black'; // If the game has more lines than this, then we turn off arrows at the start to prevent a lag spike. @@ -178,18 +177,20 @@ async function loadGamefile( arrows.setMode(0); } + // The only time the document should listen for us pasting a game, is when a game is already loaded. + // If a game WASN'T loaded, then we wouldn't be on a screen that COULD load a game!! + initCopyPastGameListeners(); + + loadedGamefile = newGamefile; + // Immediately conclude the game if we loaded a game that's over already if (gamefileutility.isGameOver(newGamefile)) { concludeGame(); onlinegame.requestRemovalFromPlayersInActiveGames(); } - - // The only time the document should listen for us pasting a game, is when a game is already loaded. - // If a game WASN'T loaded, then we wouldn't be on a screen that COULD load a game!! - initCopyPastGameListeners(); - // Has to be awaited to give the document a chance to repaint. await loadingscreen.close(); + startStartingTransition(); console.log('Finished loading'); diff --git a/src/client/scripts/esm/game/gui/guiplay.js b/src/client/scripts/esm/game/gui/guiplay.js index 6a9029834..28644e74e 100644 --- a/src/client/scripts/esm/game/gui/guiplay.js +++ b/src/client/scripts/esm/game/gui/guiplay.js @@ -1,24 +1,36 @@ // Import Start import websocket from '../websocket.js'; -import guigameinfo from './guigameinfo.js'; -import onlinegame from '../misc/onlinegame.js'; import localstorage from '../../util/localstorage.js'; import style from './style.js'; -import game from '../chess/game.js'; -import sound from '../misc/sound.js'; -import movement from '../rendering/movement.js'; -import options from '../rendering/options.js'; import statustext from './statustext.js'; import invites from '../misc/invites.js'; import gui from './gui.js'; -import drawoffers from '../misc/drawoffers.js'; import guititle from './guititle.js'; import timeutil from '../../util/timeutil.js'; import docutil from '../../util/docutil.js'; import gameloader from '../chess/gameloader.js'; // Import End + +// Type Definitions -------------------------------------------------------------------- + +/** @typedef {import('../../chess/util/metadata.js').MetaData} MetaData*/ + +/** + * An object containing the values of each of the invite options on the invite creation screen. + * @typedef {Object} InviteOptions + * @property {string} variant + * @property {MetaData['TimeControl']} clock + * @property {'White' | 'Black' | 'Random'} color + * @property {'public' | 'private'} private + * @property {'casual'} rated + */ + + +// Variables -------------------------------------------------------------------- + + "use strict"; /** @@ -197,23 +209,37 @@ function callback_local() { // Also starts local games function callback_createInvite() { - const gameOptions = { - variant: element_optionVariant.value, - clock: element_optionClock.value, - color: element_optionColor.value, - rated: element_optionRated.value, - publicity: element_optionPrivate.value - }; + const inviteOptions = getInviteOptions(); if (modeSelected === 'local') { - close(); - startLocalGame(gameOptions); + // Load options the game loader needs to load a local loaded game + const options = { + Variant: inviteOptions.variant, + TimeControl: inviteOptions.clock, + }; + close(); // Close the invite creation screen + gameloader.startLocalGame(options); // Actually load the game } else if (modeSelected === 'online') { if (invites.doWeHave()) invites.cancel(); - else invites.create(gameOptions); + else invites.create(inviteOptions); } } + +/** + * Returns an object containing the values of each of the invite options on the invite creation screen. + * @returns {InviteOptions} + */ +function getInviteOptions() { + return { + variant: element_optionVariant.value, + clock: element_optionClock.value, + color: element_optionColor.value, + private: element_optionPrivate.value, + rated: element_optionRated.value, + }; +} + // Call whenever the Clock or Color inputs change, or play mode changes function callback_updateOptions() { @@ -300,72 +326,6 @@ function callback_inviteClicked(event) { invites.click(event.currentTarget); } -/** - * Starts a local game according to the options provided. - * @param {Object} inviteOptions - An object that contains the invite properties `variant`, `clock`, `color`, `publicity`, `rated`. - */ -async function startLocalGame(inviteOptions) { - // console.log("Starting local game with invite options:") - // console.log(inviteOptions); - gui.setScreen('game local'); // Change screen location - - // [Event "Casual Space Classic infinite chess game"] [Site "https://www.infinitechess.org/"] [Round "-"] - const gameOptions = { - metadata: { - Event: `Casual local ${translations[inviteOptions.variant]} infinite chess game`, - Site: "https://www.infinitechess.org/", - Round: "-", - Variant: inviteOptions.variant, - TimeControl: inviteOptions.clock - } - }; - - guigameinfo.hidePlayerNames(); - gameloader.loadGame(gameOptions, true, true); -} - -/** - * Starts an online game according to the options provided by the server. - * @param {Object} gameOptions - An object that contains the properties - * `metadata`, `clockValues`, `id`, `publicity`, `youAreColor`, `moves`, `millisUntilAutoAFKResign`, - * `disconnect`, `gameConclusion`, `serverRestartingAt`, `drawOffer` - * - * The `metadata` property contains the properties `Variant`, `White`, `Black`, `TimeControl`, `UTCDate`, `UTCTime`, `Rated`. - * The `clockValues` property contains the properties `timerWhite`, `timerBlack`, `accountForPing`. - */ -async function startOnlineGame(gameOptions) { - if (gameOptions.clockValues !== undefined) gameOptions.clockValues.accountForPing = true; // Set this to true so our clock knows to account for ping. - gui.setScreen('game online'); // Change screen location - // Must be set BEFORE loading the game, because the mesh generation relies on the color we are. - onlinegame.setColorAndGameID(gameOptions); - gameOptions.variantOptions = generateVariantOptionsIfReloadingPrivateCustomGame(); - const fromWhitePerspective = gameOptions.youAreColor === 'white'; - await gameloader.loadGame(gameOptions, fromWhitePerspective, false); - - onlinegame.initOnlineGame(gameOptions); - guigameinfo.setAndRevealPlayerNames(gameOptions); - drawoffers.set(gameOptions.drawOffer); -} - -function generateVariantOptionsIfReloadingPrivateCustomGame() { - if (!onlinegame.getIsPrivate()) return; // Can't play/paste custom position in public matches. - const gameID = onlinegame.getGameID(); - if (!gameID) return console.error("Can't generate variant options when reloading private custom game because gameID isn't defined yet."); - return localstorage.loadItem(gameID); - - // The variant options passed into the variant loader needs to contain the following properties: - // `fullMove`, `enpassant`, `moveRule`, `positionString`, `startingPosition`, `specialRights`, `gameRules`. - // const variantOptions = { - // fullMove: longformat.fullMove, - // enpassant: longformat.enpassant, - // moveRule: longformat.moveRule, - // positionString: longformat.shortposition, - // startingPosition: longformat.startingPosition, - // specialRights: longformat.specialRights, - // gameRules: longformat.gameRules - // } -} - /** * Locks the create invite button to disable it. @@ -455,7 +415,6 @@ export default { getModeSelected, open, close, - startOnlineGame, setElement_CreateInviteTextContent, initListeners_Invites, closeListeners_Invites, diff --git a/src/client/scripts/esm/game/misc/invites.js b/src/client/scripts/esm/game/misc/invites.js index 426a7df3b..3dbfe489e 100644 --- a/src/client/scripts/esm/game/misc/invites.js +++ b/src/client/scripts/esm/game/misc/invites.js @@ -15,6 +15,7 @@ import validatorama from '../../util/validatorama.js'; "use strict"; + /** * @typedef {Object} Invite - The invite object. NOT an HTML object. * @property {string} name - Who owns the invite. If it's a guest, then "(Guest)". If it's us, we like to change this to "(You)" @@ -27,6 +28,9 @@ import validatorama from '../../util/validatorama.js'; * @property {string} rated - rated/casual */ +/** @typedef {import('../gui/guiplay.js').InviteOptions} InviteOptions */ + + /** This script manages the invites on the Play page. */ const invitesContainer = document.getElementById('invites'); @@ -79,9 +83,21 @@ function onmessage(data) { // { sub, action, value, id } } } -function create(inviteOptions) { // { variant, clock, color, rated, publicity } +/** + * Sends the create invite request message from the given InviteOptions specified on the invite creation screen. + * @param {InviteOptions} variantOptions + */ +function create(variantOptions) { if (weHaveInvite) return console.error("We already have an existing invite, can't create more."); + const inviteOptions = { + variant: variantOptions.variant, + clock: variantOptions.clock, + color: variantOptions.color, + publicity: variantOptions.private, // Only the `private` property is changed to `publicity` + rated: variantOptions.rated, + }; + generateTagForInvite(inviteOptions); guiplay.lockCreateInviteButton(); @@ -89,6 +105,9 @@ function create(inviteOptions) { // { variant, clock, color, rated, publicity } // The function to execute when we hear back the server's response const onreplyFunc = guiplay.unlockCreateInviteButton; + // console.log("Invite options before sending create invite:"); + // console.log(inviteOptions); + websocket.sendmessage("invites", "createinvite", inviteOptions, true, onreplyFunc); } @@ -375,7 +394,7 @@ function updatePrivateInviteCode(privateInviteID) { // If undefined, we know we } function updateActiveGameCount(newCount) { - if (newCount == null) return; + if (newCount === undefined) throw Error('Need to specify active game count'); element_joinExisting.textContent = `${translations.invites.join_existing_active_games} ${newCount}`; } diff --git a/src/client/scripts/esm/game/misc/onlinegame.js b/src/client/scripts/esm/game/misc/onlinegame.js index 5865b9d0a..ee315f8ae 100644 --- a/src/client/scripts/esm/game/misc/onlinegame.js +++ b/src/client/scripts/esm/game/misc/onlinegame.js @@ -279,8 +279,9 @@ function onmessage(data) { // { sub, action, value, id } case "clock": { // Contain this case in a block so that it's variables are not hoisted if (!inOnlineGame) return; const message = data.value; // { clockValues: { timerWhite, timerBlack } } - message.clockValues.accountForPing = true; // We are in an online game so we need to inform the clock script to account for ping const gamefile = gameslot.getGamefile(); + // Adjust the timer whos turn it is depending on ping. + if (message.clockValues) message.clockValues = clock.adjustClockValuesForPing(message.clockValues); clock.edit(gamefile, message.clockValues); // Edit the clocks guiclock.edit(gamefile); break; @@ -409,8 +410,8 @@ function handleJoinGame(message) { // The server's message looks like: // { // metadata: { Variant, White, Black, TimeControl, UTCDate, UTCTime, Rated }, - // clockValues: { timerWhite, timerBlack } - // id, clock, publicity, youAreColor, , moves, millisUntilAutoAFKResign, disconnect, gameConclusion, drawOffer, + // clockValues: ClockValues, + // id, clock, publicity, youAreColor, moves, millisUntilAutoAFKResign, disconnect, gameConclusion, drawOffer, // } // We were auto-unsubbed from the invites list, BUT we want to keep open the socket!! @@ -420,7 +421,7 @@ function handleJoinGame(message) { inSync = true; guititle.close(); guiplay.close(); - guiplay.startOnlineGame(message); + gameloader.startOnlineGame(message); } /** @@ -475,7 +476,9 @@ function handleOpponentsMove(message) { // { move, gameConclusion, moveNumber, c selection.reselectPiece(); // Reselect the currently selected piece. Recalc its moves and recolor it if needed. // Edit the clocks - if (message.clockValues !== undefined) message.clockValues.accountForPing = true; // Set this to true so our clock knows to account for ping. + + // Adjust the timer whos turn it is depending on ping. + if (message.clockValues) message.clockValues = clock.adjustClockValuesForPing(message.clockValues, gamefile.whosTurn); clock.edit(gamefile, message.clockValues); guiclock.edit(gamefile); @@ -529,9 +532,8 @@ function resyncToGame() { * @param {Object} messageContents - The contents of the server message, with the properties: * `gameConclusion`, `clockValues`, `moves`, `millisUntilAutoAFKResign`, `offerDraw` */ -function handleServerGameUpdate(messageContents) { // { gameConclusion, clockValues: { timerWhite, timerBlack }, moves, millisUntilAutoAFKResign, offerDraw } +function handleServerGameUpdate(messageContents) { // { gameConclusion, clockValues: ClockValues, moves, millisUntilAutoAFKResign, offerDraw } if (!inOnlineGame) return; - if (messageContents.clockValues !== undefined) messageContents.clockValues.accountForPing = true; // Set this too true so our clock knows to account for ping const gamefile = gameslot.getGamefile(); const claimedGameConclusion = messageContents.gameConclusion; @@ -563,7 +565,8 @@ function handleServerGameUpdate(messageContents) { // { gameConclusion, clockVal // Must be set before editing the clocks. gamefile.gameConclusion = claimedGameConclusion; - // When the game has ended by time/disconnect/resignation/aborted + // Adjust the timer whos turn it is depending on ping. + if (messageContents.clockValues) messageContents.clockValues = clock.adjustClockValuesForPing(messageContents.clockValues); clock.edit(gamefile, messageContents.clockValues); if (gamefileutility.isGameOver(gamefile)) { diff --git a/src/server/config/config.js b/src/server/config/config.js index eea2793f2..2390e1f09 100644 --- a/src/server/config/config.js +++ b/src/server/config/config.js @@ -29,6 +29,7 @@ if (!DEV_BUILD && !ARE_RATE_LIMITING) throw new Error("ARE_RATE_LIMITING must be * I recommend 2 seconds of latency for testing slow networks. */ const simulatedWebsocketLatencyMillis = 0; +// const simulatedWebsocketLatencyMillis = 1000; // 1 Second // const simulatedWebsocketLatencyMillis = 2000; // 2 Seconds if (!DEV_BUILD && simulatedWebsocketLatencyMillis !== 0) throw new Error("simulatedWebsocketLatencyMillis must be 0 in production!!"); diff --git a/src/server/game/gamemanager/gameutility.js b/src/server/game/gamemanager/gameutility.js index 32f31bce0..3b9f1351c 100644 --- a/src/server/game/gamemanager/gameutility.js +++ b/src/server/game/gamemanager/gameutility.js @@ -36,6 +36,7 @@ import metadata from '../../../client/scripts/esm/chess/util/metadata.js'; /** * @typedef {import('../TypeDefinitions.js').Game} Game * @typedef {import('../../../client/scripts/esm/chess/variants/gamerules.js').GameRules} GameRules + * @typedef {import('../../../client/scripts/esm/chess/logic/clock.js').ClockValues} ClockValues */ /** @typedef {import("../../socket/socketUtility.js").CustomWebSocket} CustomWebSocket */ @@ -638,16 +639,20 @@ function sendUpdatedClockToColor(game, color) { /** * Return the clock values of the game that can be sent to a client. - * This also updates the clocks, as the players current time should not be the same as when they return first started + * It also includes who's clock is currently counting down, if one is. + * This also updates the clocks, as the players current time should not be the same as when their turn firs started. * @param {Game} game - The game + * @returns {ClockValues} */ function getGameClockValues(game) { updateClockValues(game); const clockValues = { - white: game.timerWhite, - black: game.timerBlack, + clocks: { + white: game.timerWhite, + black: game.timerBlack, + } }; - + // Let the client know which clock is ticking so that they can immediately adjust for ping. // * If less than 2 moves have been played, no color is considered ticking. // * If the game is over, no color is considered ticking. From a64b47be4fc21092052831b99a04b4cbdbc84c9e Mon Sep 17 00:00:00 2001 From: Naviary2 Date: Fri, 3 Jan 2025 04:21:06 -0700 Subject: [PATCH 008/131] Removed the need for gui.screen location --- src/client/scripts/esm/game/chess/game.ts | 4 +- .../scripts/esm/game/chess/gameloader.ts | 47 ++++++++++++++++--- src/client/scripts/esm/game/gui/gui.js | 25 +++++----- src/client/scripts/esm/game/gui/guiplay.js | 10 ---- src/client/scripts/esm/game/gui/guititle.js | 12 ++--- src/client/scripts/esm/game/misc/invites.js | 6 +-- 6 files changed, 62 insertions(+), 42 deletions(-) diff --git a/src/client/scripts/esm/game/chess/game.ts b/src/client/scripts/esm/game/chess/game.ts index 96da30445..541460d0c 100644 --- a/src/client/scripts/esm/game/chess/game.ts +++ b/src/client/scripts/esm/game/chess/game.ts @@ -73,6 +73,8 @@ import type gamefile from '../../chess/logic/gamefile.js'; function init() { options.initTheme(); + gui.prepareForOpen(); + guititle.open(); board.recalcTileWidth_Pixels(); // Without this, the first touch tile is NaN @@ -80,7 +82,7 @@ function init() { // Update the game every single frame function update() { - if (gui.getScreen() === 'title play') invites.update(); + invites.update(); const gamefile = gameslot.getGamefile(); if (!gamefile) return updateSelectionScreen(); diff --git a/src/client/scripts/esm/game/chess/gameloader.ts b/src/client/scripts/esm/game/chess/gameloader.ts index 45a639157..2288ace86 100644 --- a/src/client/scripts/esm/game/chess/gameloader.ts +++ b/src/client/scripts/esm/game/chess/gameloader.ts @@ -23,13 +23,15 @@ import sound from '../misc/sound.js'; // @ts-ignore import onlinegame from "../misc/onlinegame.js"; // @ts-ignore -import gui from "../gui/gui.js"; -// @ts-ignore import drawoffers from "../misc/drawoffers.js"; // @ts-ignore import localstorage from "../../util/localstorage.js"; // @ts-ignore import jsutil from "../../util/jsutil.js"; +// @ts-ignore +import perspective from "../rendering/perspective.js"; +// @ts-ignore +import gui from "../gui/gui.js"; import gameslot from "./gameslot.js"; import clock from "../../chess/logic/clock.js"; @@ -43,6 +45,10 @@ import type { MetaData } from "../../chess/util/metadata.js"; import type { Coords, CoordsKey } from "../../chess/util/coordutil.js"; import type { ClockValues } from "../../chess/logic/clock.js"; + +// Type Definitions -------------------------------------------------------------------- + + /** * Variant options that can be used to load a custom game, * whether local or online, instead of one of the default variants. @@ -71,7 +77,32 @@ interface VariantOptions { } -// Type Definitions -------------------------------------------------------------------- +// Variables -------------------------------------------------------------------- + + +/** + * True if we are in ANY type of game, whether local, online, analysis, or editor. + * + * If we're on the title screen or the lobby, this will be false. + */ +let inAGame: boolean = false; + + +// Functions -------------------------------------------------------------------- + +/** + * Returns true if we are in ANY type of game, whether local, online, analysis, or editor. + * + * If we're on the title screen or the lobby, this will be false. + */ +function areInAGame(): boolean { + return inAGame; +} + + + + + /** Starts a local game according to the options provided. */ @@ -83,8 +114,6 @@ async function startLocalGame(options: { // console.log("Starting local game with invite options:"); // console.log(options); - gui.setScreen('game local'); // Change screen location - // [Event "Casual Space Classic infinite chess game"] [Site "https://www.infinitechess.org/"] [Round "-"] const gameOptions = { metadata: { @@ -129,7 +158,6 @@ async function startOnlineGame(options: { // If the clock values are provided, adjust the timer of whos turn it is depending on ping. if (options.clockValues) options.clockValues = clock.adjustClockValuesForPing(options.clockValues); - gui.setScreen('game online'); // Change screen location // Must be set BEFORE loading the game, because the mesh generation relies on the color we are. onlinegame.setColorAndGameID(options); options.variantOptions = generateVariantOptionsIfReloadingPrivateCustomGame(); @@ -184,6 +212,7 @@ async function loadGame( */ variantOptions?: VariantOptions, }, + /** If false, we'll be viewing black's perspective. */ fromWhitePerspective: boolean, allowEditCoords: boolean ) { @@ -207,16 +236,22 @@ async function loadGame( guigameinfo.updateWhosTurn(gamefile); sound.playSound_gamestart(); + + inAGame = true; } function unloadGame() { onlinegame.closeOnlineGame(); guinavigation.close(); gameslot.unloadGame(); + perspective.disable(); + gui.prepareForOpen(); + inAGame = false; } export default { + areInAGame, startLocalGame, startOnlineGame, loadGame, diff --git a/src/client/scripts/esm/game/gui/gui.js b/src/client/scripts/esm/game/gui/gui.js index b3fc4950f..0c01ae86f 100644 --- a/src/client/scripts/esm/game/gui/gui.js +++ b/src/client/scripts/esm/game/gui/gui.js @@ -1,11 +1,10 @@ -// Import Start import selection from '../chess/selection.js'; import guipromotion from './guipromotion.js'; import style from './style.js'; import statustext from './statustext.js'; import frametracker from '../rendering/frametracker.js'; -// Import End +import movement from '../rendering/movement.js'; "use strict"; @@ -17,8 +16,6 @@ import frametracker from '../rendering/frametracker.js'; // Variables -let screen = ''; // Current screen location in the game. title/online/computer/local/board - const element_overlay = document.getElementById('overlay'); element_overlay.addEventListener('click', callback_CancelPromotionIfUIOpen); @@ -31,13 +28,6 @@ function callback_CancelPromotionIfUIOpen() { // Functions -function getScreen() { - return screen; -} - -function setScreen(value) { - screen = value; -} // Fades-in the overlay element over 1 second function fadeInOverlay1s() { @@ -56,10 +46,19 @@ function makeOverlaySelectable() { element_overlay.classList.remove('unselectable'); } +/** + * Call when we first load the page, or leave any game. This prepares the board + * for either the title screen or lobby (any screen that's not in a game) + */ +function prepareForOpen() { + // Randomize pan velocity direction for the title screen and lobby menus + movement.randomizePanVelDir(); + movement.setBoardScale(1.8); // 1.8 +} + export default { + prepareForOpen, fadeInOverlay1s, - getScreen, - setScreen, callback_featurePlanned, makeOverlayUnselectable, makeOverlaySelectable diff --git a/src/client/scripts/esm/game/gui/guiplay.js b/src/client/scripts/esm/game/gui/guiplay.js index 28644e74e..4f7945b69 100644 --- a/src/client/scripts/esm/game/gui/guiplay.js +++ b/src/client/scripts/esm/game/gui/guiplay.js @@ -108,7 +108,6 @@ function showElement_inviteCode() { style.revealElement(element_inviteCode); } function open() { pageIsOpen = true; - gui.setScreen('title play'); style.revealElement(element_PlaySelection); style.revealElement(element_menuExternalLinks); changePlayMode('online'); @@ -398,14 +397,6 @@ function onSocketClose() { unlockAcceptInviteButton(); } -/** - * Returns *true* if we are on the play page. - * @returns {boolean} - */ -function onPlayPage() { - return gui.getScreen() === 'title play'; -} - export default { isOpen, hideElement_joinPrivate, @@ -418,7 +409,6 @@ export default { setElement_CreateInviteTextContent, initListeners_Invites, closeListeners_Invites, - onPlayPage, lockCreateInviteButton, unlockCreateInviteButton, isCreateInviteButtonLocked, diff --git a/src/client/scripts/esm/game/gui/guititle.js b/src/client/scripts/esm/game/gui/guititle.js index 448f7ff10..4bca6ddb3 100644 --- a/src/client/scripts/esm/game/gui/guititle.js +++ b/src/client/scripts/esm/game/gui/guititle.js @@ -4,7 +4,6 @@ import style from './style.js'; import gui from './gui.js'; import movement from '../rendering/movement.js'; import guiguide from './guiguide.js'; -import perspective from '../rendering/perspective.js'; import guiplay from './guiplay.js'; // Import End @@ -29,20 +28,15 @@ const element_menuExternalLinks = document.getElementById('menu-external-links') // Call when title screen is loaded function open() { - perspective.disable(); - if (!gui.getScreen()?.includes('title')) movement.randomizePanVelDir(); // Randomize pan velocity direction - gui.setScreen('title'); - movement.setBoardScale(1.8); // 1.8 style.revealElement(titleElement); style.revealElement(element_menuExternalLinks); - initListeners(); // These need to be canceled when leaving screen -} + initListeners(); +}; function close() { - // Cancel all title screen button event listeners to save cpu... - closeListeners(); style.hideElement(titleElement); style.hideElement(element_menuExternalLinks); + closeListeners(); } function initListeners() { diff --git a/src/client/scripts/esm/game/misc/invites.js b/src/client/scripts/esm/game/misc/invites.js index 3dbfe489e..a9ead7970 100644 --- a/src/client/scripts/esm/game/misc/invites.js +++ b/src/client/scripts/esm/game/misc/invites.js @@ -51,7 +51,7 @@ function gelement_iCodeCode() { } function update() { - if (!guiplay.onPlayPage()) return; // Not on the play screen + if (!guiplay.isOpen()) return; // Not on the play screen if (loadbalancer.gisHibernating()) statustext.showStatus(translations.invites.move_mouse, false, 0.1); } @@ -276,7 +276,7 @@ function clear({ recentUsersInLastList = false } = {}) { // Deletes all invites and resets create invite button if on play page function clearIfOnPlayPage() { - if (!guiplay.onPlayPage()) return; // Not on the play screen + if (!guiplay.isOpen()) return; // Not on the play screen clear(); updateCreateInviteButton(); } @@ -407,7 +407,7 @@ function doWeHave() { * @param {ignoreAlreadySubbed} *true* If the socket closed unexpectedly and we need to resub. subs.invites will already be true so we ignore that. * */ async function subscribeToInvites(ignoreAlreadySubbed) { // Set to true when we are restarting the connection and need to resub to everything we were to before. - if (!guiplay.onPlayPage()) return; // Don't subscribe to invites if we're not on the play page! + if (!guiplay.isOpen()) return; // Don't subscribe to invites if we're not on the play page! const subs = websocket.getSubs(); if (!ignoreAlreadySubbed && subs.invites) return; // console.log("Subbing to invites!"); From 33ee12d828d552aeb17b874ad2bb02c9a47d9712 Mon Sep 17 00:00:00 2001 From: Naviary2 Date: Fri, 3 Jan 2025 04:23:00 -0700 Subject: [PATCH 009/131] Converted guititle.js to typescript --- .../esm/game/gui/{guititle.js => guititle.ts} | 37 +++++++++++-------- 1 file changed, 21 insertions(+), 16 deletions(-) rename src/client/scripts/esm/game/gui/{guititle.js => guititle.ts} (69%) diff --git a/src/client/scripts/esm/game/gui/guititle.js b/src/client/scripts/esm/game/gui/guititle.ts similarity index 69% rename from src/client/scripts/esm/game/gui/guititle.js rename to src/client/scripts/esm/game/gui/guititle.ts index 4bca6ddb3..9da59edf0 100644 --- a/src/client/scripts/esm/game/gui/guititle.js +++ b/src/client/scripts/esm/game/gui/guititle.ts @@ -1,30 +1,33 @@ -// Import Start +/** + * This script handles our Title Screen + */ + +// @ts-ignore import style from './style.js'; +// @ts-ignore import gui from './gui.js'; -import movement from '../rendering/movement.js'; +// @ts-ignore import guiguide from './guiguide.js'; +// @ts-ignore import guiplay from './guiplay.js'; -// Import End -"use strict"; -/** - * This script handles our Title Screen - */ +// Variables ---------------------------------------------------------------------------- -// Variables // Title Screen const boardVel = 0.6; // Speed at which board slowly moves while on title screen -const titleElement = document.getElementById('title'); // Visible when on the title screen -const element_play = document.getElementById('play'); -const element_guide = document.getElementById('rules'); -const element_boardEditor = document.getElementById('board-editor'); -const element_menuExternalLinks = document.getElementById('menu-external-links'); +const titleElement = document.getElementById('title')!; // Visible when on the title screen +const element_play = document.getElementById('play')!; +const element_guide = document.getElementById('rules')!; +const element_boardEditor = document.getElementById('board-editor')!; +const element_menuExternalLinks = document.getElementById('menu-external-links')!; + + +// Functions ---------------------------------------------------------------------------- -// Functions // Call when title screen is loaded function open() { @@ -51,16 +54,18 @@ function closeListeners() { element_boardEditor.removeEventListener('click', gui.callback_featurePlanned); } -function callback_Play(event) { +function callback_Play(event: Event) { close(); guiplay.open(); } -function callback_Guide(event) { +function callback_Guide(event: Event) { close(); guiguide.open(); } + + export default { boardVel, open, From 9fe5e015c484eb7a2c520715eadb826c177ea79e Mon Sep 17 00:00:00 2001 From: Naviary2 Date: Fri, 3 Jan 2025 04:31:19 -0700 Subject: [PATCH 010/131] Converted gui.js to typescript --- src/client/scripts/esm/game/chess/game.ts | 2 - .../scripts/esm/game/chess/gameloader.ts | 1 - .../scripts/esm/game/gui/{gui.js => gui.ts} | 62 +++++++++---------- src/client/scripts/esm/game/gui/guiplay.js | 4 +- src/client/scripts/esm/game/gui/guititle.ts | 4 +- src/client/scripts/esm/game/input.js | 2 - 6 files changed, 35 insertions(+), 40 deletions(-) rename src/client/scripts/esm/game/gui/{gui.js => gui.ts} (58%) diff --git a/src/client/scripts/esm/game/chess/game.ts b/src/client/scripts/esm/game/chess/game.ts index 541460d0c..b9b0497de 100644 --- a/src/client/scripts/esm/game/chess/game.ts +++ b/src/client/scripts/esm/game/chess/game.ts @@ -7,7 +7,6 @@ // @ts-ignore import onlinegame from '../misc/onlinegame.js'; -// @ts-ignore import gui from '../gui/gui.js'; // @ts-ignore import arrows from '../rendering/arrows.js'; @@ -15,7 +14,6 @@ import arrows from '../rendering/arrows.js'; import pieces from '../rendering/pieces.js'; // @ts-ignore import invites from '../misc/invites.js'; -// @ts-ignore import guititle from '../gui/guititle.js'; // @ts-ignore import guipause from '../gui/guipause.js'; diff --git a/src/client/scripts/esm/game/chess/gameloader.ts b/src/client/scripts/esm/game/chess/gameloader.ts index 2288ace86..1fabd074b 100644 --- a/src/client/scripts/esm/game/chess/gameloader.ts +++ b/src/client/scripts/esm/game/chess/gameloader.ts @@ -30,7 +30,6 @@ import localstorage from "../../util/localstorage.js"; import jsutil from "../../util/jsutil.js"; // @ts-ignore import perspective from "../rendering/perspective.js"; -// @ts-ignore import gui from "../gui/gui.js"; import gameslot from "./gameslot.js"; import clock from "../../chess/logic/clock.js"; diff --git a/src/client/scripts/esm/game/gui/gui.js b/src/client/scripts/esm/game/gui/gui.ts similarity index 58% rename from src/client/scripts/esm/game/gui/gui.js rename to src/client/scripts/esm/game/gui/gui.ts index 0c01ae86f..6d415b1d3 100644 --- a/src/client/scripts/esm/game/gui/gui.js +++ b/src/client/scripts/esm/game/gui/gui.ts @@ -1,22 +1,33 @@ +/** + * This script adds event listeners for our main overlay html element that + * contains all of our gui pages. + * + * We also prepare the board here whenever ANY gui page (non-game) is opened. + */ + +// @ts-ignore import selection from '../chess/selection.js'; +// @ts-ignore import guipromotion from './guipromotion.js'; +// @ts-ignore import style from './style.js'; +// @ts-ignore import statustext from './statustext.js'; +// @ts-ignore import frametracker from '../rendering/frametracker.js'; +// @ts-ignore import movement from '../rendering/movement.js'; -"use strict"; -/** - * This is the parent gui script of all gui scripts. - * Here we remember what page we're on, - * and we have a reference to the overlay element above the entire canvas. - */ +// Variables ------------------------------------------------------------------------------ + + +const element_overlay = document.getElementById('overlay')!; + -// Variables +// Functions ------------------------------------------------------------------------------ -const element_overlay = document.getElementById('overlay'); element_overlay.addEventListener('click', callback_CancelPromotionIfUIOpen); @@ -26,26 +37,6 @@ function callback_CancelPromotionIfUIOpen() { frametracker.onVisualChange(); } -// Functions - - -// Fades-in the overlay element over 1 second -function fadeInOverlay1s() { - style.fadeIn1s(element_overlay); -} - -function callback_featurePlanned() { - statustext.showStatus(translations.planned_feature); -} - -function makeOverlayUnselectable() { - element_overlay.classList.add('unselectable'); -} - -function makeOverlaySelectable() { - element_overlay.classList.remove('unselectable'); -} - /** * Call when we first load the page, or leave any game. This prepares the board * for either the title screen or lobby (any screen that's not in a game) @@ -56,10 +47,19 @@ function prepareForOpen() { movement.setBoardScale(1.8); // 1.8 } +// Fades-in the overlay element over 1 second +function fadeInOverlay1s() { + style.fadeIn1s(element_overlay); +} + +/** Displays the status message on screen "Feature is planned". */ +function displayStatus_FeaturePlanned() { + statustext.showStatus(translations['planned_feature']); +} + + export default { prepareForOpen, fadeInOverlay1s, - callback_featurePlanned, - makeOverlayUnselectable, - makeOverlaySelectable + displayStatus_FeaturePlanned, }; \ No newline at end of file diff --git a/src/client/scripts/esm/game/gui/guiplay.js b/src/client/scripts/esm/game/gui/guiplay.js index 4f7945b69..7257db1b7 100644 --- a/src/client/scripts/esm/game/gui/guiplay.js +++ b/src/client/scripts/esm/game/gui/guiplay.js @@ -130,7 +130,7 @@ function initListeners() { element_playBack.addEventListener('click', callback_playBack); element_online.addEventListener('click', callback_online); element_local.addEventListener('click', callback_local); - element_computer.addEventListener('click', gui.callback_featurePlanned); + element_computer.addEventListener('click', gui.displayStatus_FeaturePlanned); element_createInvite.addEventListener('click', callback_createInvite); element_optionColor.addEventListener('change', callback_updateOptions); element_optionClock.addEventListener('change', callback_updateOptions); @@ -143,7 +143,7 @@ function closeListeners() { element_playBack.removeEventListener('click', callback_playBack); element_online.removeEventListener('click', callback_online); element_local.removeEventListener('click', callback_local); - element_computer.removeEventListener('click', gui.callback_featurePlanned); + element_computer.removeEventListener('click', gui.displayStatus_FeaturePlanned); element_createInvite.removeEventListener('click', callback_createInvite); element_optionColor.removeEventListener('change', callback_updateOptions); element_optionClock.removeEventListener('change', callback_updateOptions); diff --git a/src/client/scripts/esm/game/gui/guititle.ts b/src/client/scripts/esm/game/gui/guititle.ts index 9da59edf0..ab10edfa3 100644 --- a/src/client/scripts/esm/game/gui/guititle.ts +++ b/src/client/scripts/esm/game/gui/guititle.ts @@ -45,13 +45,13 @@ function close() { function initListeners() { element_play.addEventListener('click', callback_Play); element_guide.addEventListener('click', callback_Guide); - element_boardEditor.addEventListener('click', gui.callback_featurePlanned); + element_boardEditor.addEventListener('click', gui.displayStatus_FeaturePlanned); } function closeListeners() { element_play.removeEventListener('click', callback_Play); element_guide.removeEventListener('click', callback_Guide); - element_boardEditor.removeEventListener('click', gui.callback_featurePlanned); + element_boardEditor.removeEventListener('click', gui.displayStatus_FeaturePlanned); } function callback_Play(event: Event) { diff --git a/src/client/scripts/esm/game/input.js b/src/client/scripts/esm/game/input.js index 3caba508e..03e047195 100644 --- a/src/client/scripts/esm/game/input.js +++ b/src/client/scripts/esm/game/input.js @@ -375,7 +375,6 @@ function initListeners_Mouse() { if (ignoreMouseDown) return; if (event.target.id === 'overlay') event.preventDefault(); - // if (clickedOverlay) gui.makeOverlayUnselectable(); pushMouseDown(event); @@ -402,7 +401,6 @@ function initListeners_Mouse() { overlayElement.addEventListener("mouseup", (event) => { event = event || window.event; - // gui.makeOverlaySelectable(); removeMouseHeld(event); setTimeout(perspective.relockMouse, 1); // 1 millisecond, to give time for pause listener to fire From 899925464803f76ecdd49dd0b6b9658990014d8b Mon Sep 17 00:00:00 2001 From: Naviary2 Date: Fri, 3 Jan 2025 04:34:46 -0700 Subject: [PATCH 011/131] Converted frametracker.js to typescript --- src/client/scripts/esm/game/gui/gui.ts | 3 +-- src/client/scripts/esm/game/gui/guinavigation.ts | 1 - .../game/rendering/{frametracker.js => frametracker.ts} | 8 ++++++-- .../esm/game/rendering/highlights/legalmovehighlights.ts | 1 - 4 files changed, 7 insertions(+), 6 deletions(-) rename src/client/scripts/esm/game/rendering/{frametracker.js => frametracker.ts} (81%) diff --git a/src/client/scripts/esm/game/gui/gui.ts b/src/client/scripts/esm/game/gui/gui.ts index 6d415b1d3..772a08fae 100644 --- a/src/client/scripts/esm/game/gui/gui.ts +++ b/src/client/scripts/esm/game/gui/gui.ts @@ -14,7 +14,6 @@ import guipromotion from './guipromotion.js'; import style from './style.js'; // @ts-ignore import statustext from './statustext.js'; -// @ts-ignore import frametracker from '../rendering/frametracker.js'; // @ts-ignore import movement from '../rendering/movement.js'; @@ -23,7 +22,7 @@ import movement from '../rendering/movement.js'; // Variables ------------------------------------------------------------------------------ -const element_overlay = document.getElementById('overlay')!; +const element_overlay: HTMLElement = document.getElementById('overlay')!; // Functions ------------------------------------------------------------------------------ diff --git a/src/client/scripts/esm/game/gui/guinavigation.ts b/src/client/scripts/esm/game/gui/guinavigation.ts index 52dbda03c..46f1ef2c6 100644 --- a/src/client/scripts/esm/game/gui/guinavigation.ts +++ b/src/client/scripts/esm/game/gui/guinavigation.ts @@ -25,7 +25,6 @@ import stats from './stats.js'; import movepiece from '../../chess/logic/movepiece.js'; // @ts-ignore import selection from '../chess/selection.js'; -// @ts-ignore import frametracker from '../rendering/frametracker.js'; // @ts-ignore import guigameinfo from './guigameinfo.js'; diff --git a/src/client/scripts/esm/game/rendering/frametracker.js b/src/client/scripts/esm/game/rendering/frametracker.ts similarity index 81% rename from src/client/scripts/esm/game/rendering/frametracker.js rename to src/client/scripts/esm/game/rendering/frametracker.ts index 8fd5e94a8..8ebb487c5 100644 --- a/src/client/scripts/esm/game/rendering/frametracker.js +++ b/src/client/scripts/esm/game/rendering/frametracker.ts @@ -8,7 +8,7 @@ */ /** Whether there has been a visual change on-screen the past frame. */ -let hasBeenVisualChange = true; +let hasBeenVisualChange: boolean = true; /** The next frame will be rendered. Compute can be saved if nothing has visibly changed on-screen. */ @@ -21,12 +21,16 @@ function doWeRenderNextFrame() { return hasBeenVisualChange; } -/** Resets {@link hasBeenVisualChange} to false, to prepare for next frame. */ +/** + * Resets {@link hasBeenVisualChange} to false, to prepare for next frame. + * Call right after we finish a render frame. + */ function onFrameRender() { hasBeenVisualChange = false; } + export default { onVisualChange, doWeRenderNextFrame, diff --git a/src/client/scripts/esm/game/rendering/highlights/legalmovehighlights.ts b/src/client/scripts/esm/game/rendering/highlights/legalmovehighlights.ts index c02f1793b..cd1b4d4f0 100644 --- a/src/client/scripts/esm/game/rendering/highlights/legalmovehighlights.ts +++ b/src/client/scripts/esm/game/rendering/highlights/legalmovehighlights.ts @@ -21,7 +21,6 @@ import camera from '../camera.js'; import board from '../board.js'; // @ts-ignore import math, { BoundingBox } from '../../../util/math.js'; -// @ts-ignore import frametracker from '../frametracker.js'; // @ts-ignore import preferences from '../../../components/header/preferences.js'; From 8a689b9246b7199ab2493deb0366b42cae6c0861 Mon Sep 17 00:00:00 2001 From: Naviary2 Date: Fri, 3 Jan 2025 04:57:28 -0700 Subject: [PATCH 012/131] Changed guiloading.js to typescript --- src/client/css/play.css | 4 ++- src/client/scripts/esm/game/gui/gui.ts | 6 ----- src/client/scripts/esm/game/gui/guiloading.js | 27 ------------------- src/client/scripts/esm/game/gui/guiloading.ts | 24 +++++++++++++++++ src/client/views/play.ejs | 2 +- 5 files changed, 28 insertions(+), 35 deletions(-) delete mode 100644 src/client/scripts/esm/game/gui/guiloading.js create mode 100644 src/client/scripts/esm/game/gui/guiloading.ts diff --git a/src/client/css/play.css b/src/client/css/play.css index b9f0f920e..cbd327ebe 100644 --- a/src/client/css/play.css +++ b/src/client/css/play.css @@ -43,6 +43,9 @@ button { /* Loading Page. A COUPLE OF THSEE CLASSES are also used for the game's loading animation page! */ .animation-container { + transition: opacity 0.4s; + z-index: 1; + pointer-events: none; display: flex; background-color: black; justify-content: center; /* Center horizontally */ @@ -52,7 +55,6 @@ button { bottom: 0; left: 0; right: 0; - z-index: -1; overflow: hidden; } diff --git a/src/client/scripts/esm/game/gui/gui.ts b/src/client/scripts/esm/game/gui/gui.ts index 772a08fae..6bd4c4cbb 100644 --- a/src/client/scripts/esm/game/gui/gui.ts +++ b/src/client/scripts/esm/game/gui/gui.ts @@ -46,11 +46,6 @@ function prepareForOpen() { movement.setBoardScale(1.8); // 1.8 } -// Fades-in the overlay element over 1 second -function fadeInOverlay1s() { - style.fadeIn1s(element_overlay); -} - /** Displays the status message on screen "Feature is planned". */ function displayStatus_FeaturePlanned() { statustext.showStatus(translations['planned_feature']); @@ -59,6 +54,5 @@ function displayStatus_FeaturePlanned() { export default { prepareForOpen, - fadeInOverlay1s, displayStatus_FeaturePlanned, }; \ No newline at end of file diff --git a/src/client/scripts/esm/game/gui/guiloading.js b/src/client/scripts/esm/game/gui/guiloading.js deleted file mode 100644 index 30d7f6b04..000000000 --- a/src/client/scripts/esm/game/gui/guiloading.js +++ /dev/null @@ -1,27 +0,0 @@ - -// Import Start -import gui from './gui.js'; -import camera from '../rendering/camera.js'; -import style from './style.js'; -// Import End - -"use strict"; - -/** This script is able to stop the loading animation as soon as the page fully loads. */ - -// Loading Animation Before Page Load -const element_loadingAnimation = document.getElementById('loading-animation'); -const element_loadingText = document.getElementById('loading-text'); - -/** Stops the loading screen animation. */ -function closeAnimation() { - // Fade in the canvas (which is hidden by default because it renders grey over the loading animation) - style.fadeIn1s(camera.canvas); - // Fade in the overlay which contains all our html elements overtop our canvas - gui.fadeInOverlay1s(); - setTimeout(style.hideElement, 1000, element_loadingAnimation); -} - -export default { - closeAnimation -}; \ No newline at end of file diff --git a/src/client/scripts/esm/game/gui/guiloading.ts b/src/client/scripts/esm/game/gui/guiloading.ts new file mode 100644 index 000000000..8b374c986 --- /dev/null +++ b/src/client/scripts/esm/game/gui/guiloading.ts @@ -0,0 +1,24 @@ + +/** + * This script hides the loading animation when the page fully loads. + * */ + +// Loading Animation Before Page Load +const element_loadingAnimation = document.getElementById('loading-animation')!; +const element_loadingText = document.getElementById('loading-text')!; + +/** THIS SHOULD MATCH THE transition time declared in the css stylesheet!! */ +const durationOfFadeOutMillis = 400; + +/** Stops the loading screen animation. */ +function closeAnimation() { + setTimeout(() => { + element_loadingAnimation.classList.add('hidden'); + }, durationOfFadeOutMillis); + + element_loadingAnimation.style.opacity = '0'; +} + +export default { + closeAnimation +}; \ No newline at end of file diff --git a/src/client/views/play.ejs b/src/client/views/play.ejs index f9040d6a2..942e48ab2 100644 --- a/src/client/views/play.ejs +++ b/src/client/views/play.ejs @@ -41,7 +41,7 @@ - + From 89caa867921a78fc76b1527dfc7e9f0148870b45 Mon Sep 17 00:00:00 2001 From: Naviary2 Date: Fri, 3 Jan 2025 05:01:30 -0700 Subject: [PATCH 013/131] These fade in fade out methods are completely unnecessary. In the css just add an opacity transition duration --- src/client/scripts/esm/game/gui/style.js | 36 ------------------------ 1 file changed, 36 deletions(-) diff --git a/src/client/scripts/esm/game/gui/style.js b/src/client/scripts/esm/game/gui/style.js index 4c9346219..472211bfe 100644 --- a/src/client/scripts/esm/game/gui/style.js +++ b/src/client/scripts/esm/game/gui/style.js @@ -48,40 +48,6 @@ function revealElement(element) { removeClass(element, "hidden"); } -// Animate elements - -// Fades in the element over the span of 1 second -function fadeIn1s(element) { - revealElement(element); // Make sure the element no longer has the 'display: none' property. - reinstateClass(element, 'fade-in-2_3s'); // This class contain the fade-in animation that begins immediately upon receiving this property - - if (!element.fadeIn1sLayers) element.fadeIn1sLayers = 1; - else element.fadeIn1sLayers++; - - setTimeout(() => { // After that 1 second, remove this no longer needed animation class from them. - element.fadeIn1sLayers--; - if (element.fadeIn1sLayers > 0) return; // The fade-in-1s animation was RENEWED - delete element.fadeIn1sLayers; - removeClass(element, 'fade-in-2_3s'); - }, 1000); -} - -// Fades out the element over the span of 1 second -function fadeOut1s(element) { - revealElement(element); - reinstateClass(element,'fade-out-2_3s'); // This class contain the fade-out animation that begins immediately upon receiving this property. - - if (!element.fadeOut1sLayers) element.fadeOut1sLayers = 1; - else element.fadeOut1sLayers++; - - setTimeout(() => { // After that 1 second, remove this no longer needed animation class from them. - element.fadeOut1sLayers--; - if (element.fadeOut1sLayers > 0) return; // The fade-in-1s animation was RENEWED - delete element.fadeOut1sLayers; - removeClass(element, 'fade-out-2_3s'); - hideElement(element); - }, 1000); -} // Other operations @@ -144,8 +110,6 @@ export default { hideElement, revealElement, setNavStyle, - fadeIn1s, - fadeOut1s, getChildrenTextContents, arrayToCssColor, }; \ No newline at end of file From 1a8543c6f56a90acbf82321e27bc630bfaa4c2c6 Mon Sep 17 00:00:00 2001 From: Naviary2 Date: Fri, 3 Jan 2025 05:13:20 -0700 Subject: [PATCH 014/131] Removed the need for revealElement() and hideElement() --- src/client/scripts/esm/game/gui/guiclock.js | 4 +-- .../scripts/esm/game/gui/guidrawoffer.js | 8 +++--- .../scripts/esm/game/gui/guigameinfo.js | 14 +++++----- src/client/scripts/esm/game/gui/guiguide.js | 12 ++++----- .../scripts/esm/game/gui/guinavigation.ts | 4 +-- src/client/scripts/esm/game/gui/guipause.js | 4 +-- src/client/scripts/esm/game/gui/guiplay.js | 16 +++++------ .../scripts/esm/game/gui/guipromotion.ts | 12 ++++----- src/client/scripts/esm/game/gui/guititle.ts | 8 +++--- src/client/scripts/esm/game/gui/stats.js | 27 +++++++------------ src/client/scripts/esm/game/gui/style.js | 17 ------------ 11 files changed, 50 insertions(+), 76 deletions(-) diff --git a/src/client/scripts/esm/game/gui/guiclock.js b/src/client/scripts/esm/game/gui/guiclock.js index e13582751..ad5627372 100644 --- a/src/client/scripts/esm/game/gui/guiclock.js +++ b/src/client/scripts/esm/game/gui/guiclock.js @@ -63,13 +63,13 @@ const countdown = { function hideClocks() { for (const color in element_timers) { - style.hideElement(element_timers[color].container); + element_timers[color].container.classList.add('hidden'); } } function showClocks() { for (const color in element_timers) { - style.revealElement(element_timers[color].container); + element_timers[color].container.classList.remove('hidden'); } } diff --git a/src/client/scripts/esm/game/gui/guidrawoffer.js b/src/client/scripts/esm/game/gui/guidrawoffer.js index bbf06c88f..d09d3bb5a 100644 --- a/src/client/scripts/esm/game/gui/guidrawoffer.js +++ b/src/client/scripts/esm/game/gui/guidrawoffer.js @@ -30,8 +30,8 @@ let drawOfferUICramped = false; /** Reveals the draw offer UI on the bottom navigation bar */ function open() { - style.revealElement(element_draw_offer_ui); - style.hideElement(element_whosturn); + element_draw_offer_ui.classList.remove('hidden'); + element_whosturn.classList.add('hidden'); initDrawOfferListeners(); // Do the names and clocks need to be hidden to make room for the draw offer UI? updateVisibilityOfNamesAndClocksWithDrawOffer(); @@ -39,8 +39,8 @@ function open() { /** Hides the draw offer UI on the bottom navigation bar */ function close() { - style.hideElement(element_draw_offer_ui); - style.revealElement(element_whosturn); + element_draw_offer_ui.classList.add('hidden'); + element_whosturn.classList.remove('hidden'); closeDrawOfferListeners(); if (!drawOfferUICramped) return; diff --git a/src/client/scripts/esm/game/gui/guigameinfo.js b/src/client/scripts/esm/game/gui/guigameinfo.js index 9b5bcd779..663cb09c7 100644 --- a/src/client/scripts/esm/game/gui/guigameinfo.js +++ b/src/client/scripts/esm/game/gui/guigameinfo.js @@ -31,12 +31,12 @@ const element_playerBlack = document.getElementById('playerblack'); function open() { if (gameslot.getGamefile().gameConclusion) return; - style.revealElement(element_dot); + element_dot.classList.remove('hidden'); } function hidePlayerNames() { - style.hideElement(element_playerWhite); - style.hideElement(element_playerBlack); + element_playerWhite.classList.add('hidden'); + element_playerBlack.classList.add('hidden'); } function setAndRevealPlayerNames(gameOptions) { @@ -47,8 +47,8 @@ function setAndRevealPlayerNames(gameOptions) { element_playerWhite.textContent = onlinegame.areWeColor('white') && white === translations.guest_indicator ? translations.you_indicator : white; element_playerBlack.textContent = onlinegame.areWeColor('black') && black === translations.guest_indicator ? translations.you_indicator : black; } - style.revealElement(element_playerWhite); - style.revealElement(element_playerBlack); + element_playerWhite.classList.remove('hidden'); + element_playerBlack.classList.remove('hidden'); } /** @@ -74,7 +74,7 @@ function updateWhosTurn(gamefile) { element_whosturn.textContent = textContent; - style.revealElement(element_dot); + element_dot.classList.remove('hidden'); if (color === 'white') { element_dot.classList.remove('dotblack'); element_dot.classList.add('dotwhite'); @@ -90,7 +90,7 @@ function gameEnd(conclusion) { const { victor, condition } = winconutil.getVictorAndConditionFromGameConclusion(conclusion); const resultTranslations = translations.results; - style.hideElement(element_dot); + element_dot.classList.add('hidden'); if (onlinegame.areInOnlineGame()) { diff --git a/src/client/scripts/esm/game/gui/guiguide.js b/src/client/scripts/esm/game/gui/guiguide.js index c4935dba8..bc365be7e 100644 --- a/src/client/scripts/esm/game/gui/guiguide.js +++ b/src/client/scripts/esm/game/gui/guiguide.js @@ -22,13 +22,13 @@ let fairyIndex = 0; const maxFairyIndex = element_FairyImg.querySelectorAll('picture').length - 1; function open() { - style.revealElement(element_Guide); + element_Guide.classList.remove('hidden'); initListeners(); loadAllImages(); } function close() { - style.hideElement(element_Guide); + element_Guide.classList.add('hidden'); closeListeners(); } @@ -75,21 +75,21 @@ function callback_FairyForward(event) { function hideCurrentFairy() { const allFairyImgs = element_FairyImg.querySelectorAll('picture'); const targetFairyImg = allFairyImgs[fairyIndex]; - style.hideElement(targetFairyImg); + targetFairyImg.classList.add('hidden'); const allFairyCards = element_FairyCard.querySelectorAll('.fairy-card-desc'); const targetFairyCard = allFairyCards[fairyIndex]; - style.hideElement(targetFairyCard); + targetFairyCard.classList.add('hidden'); } function revealCurrentFairy() { const allFairyImgs = element_FairyImg.querySelectorAll('picture'); const targetFairyImg = allFairyImgs[fairyIndex]; - style.revealElement(targetFairyImg); + targetFairyImg.classList.remove('hidden'); const allFairyCards = element_FairyCard.querySelectorAll('.fairy-card-desc'); const targetFairyCard = allFairyCards[fairyIndex]; - style.revealElement(targetFairyCard); + targetFairyCard.classList.remove('hidden'); } function updateArrowTransparency() { diff --git a/src/client/scripts/esm/game/gui/guinavigation.ts b/src/client/scripts/esm/game/gui/guinavigation.ts index 46f1ef2c6..47e4192cb 100644 --- a/src/client/scripts/esm/game/gui/guinavigation.ts +++ b/src/client/scripts/esm/game/gui/guinavigation.ts @@ -116,7 +116,7 @@ function onToggleNavigationBar() { function open(gamefile: gamefile, { allowEditCoords = true }: { allowEditCoords?: boolean } = {}) { activeGamefile = gamefile; - style.revealElement(element_Navigation); + element_Navigation.classList.remove('hidden'); initListeners_Navigation(); update_MoveButtons(); initCoordinates({ allowEditCoords }); @@ -139,7 +139,7 @@ function initCoordinates({ allowEditCoords }: { allowEditCoords: boolean }) { function close() { activeGamefile = undefined; - style.hideElement(element_Navigation); + element_Navigation.classList.add('hidden'); closeListeners_Navigation(); navigationOpen = false; } diff --git a/src/client/scripts/esm/game/gui/guipause.js b/src/client/scripts/esm/game/gui/guipause.js index 5e2eabbd8..e64aab51d 100644 --- a/src/client/scripts/esm/game/gui/guipause.js +++ b/src/client/scripts/esm/game/gui/guipause.js @@ -48,7 +48,7 @@ function open() { updateTextOfMainMenuButton(); updatePasteButtonTransparency(); updateDrawOfferButton(); - style.revealElement(element_pauseUI); + element_pauseUI.classList.remove('hidden'); initListeners(); } @@ -142,7 +142,7 @@ function closeListeners() { function callback_Resume() { if (!isPaused) return; isPaused = false; - style.hideElement(element_pauseUI); + element_pauseUI.classList.add('hidden'); closeListeners(); frametracker.onVisualChange(); } diff --git a/src/client/scripts/esm/game/gui/guiplay.js b/src/client/scripts/esm/game/gui/guiplay.js index 7257db1b7..4d0dfda5d 100644 --- a/src/client/scripts/esm/game/gui/guiplay.js +++ b/src/client/scripts/esm/game/gui/guiplay.js @@ -101,15 +101,15 @@ function isOpen() { return pageIsOpen; } */ function getModeSelected() { return modeSelected; } -function hideElement_joinPrivate() { style.hideElement(element_joinPrivate); } -function showElement_joinPrivate() { style.revealElement(element_joinPrivate); } -function hideElement_inviteCode() { style.hideElement(element_inviteCode); } -function showElement_inviteCode() { style.revealElement(element_inviteCode); } +function hideElement_joinPrivate() { element_joinPrivate.classList.add('hidden'); } +function showElement_joinPrivate() { element_joinPrivate.classList.remove('hidden'); } +function hideElement_inviteCode() { element_inviteCode.classList.add('hidden'); } +function showElement_inviteCode() { element_inviteCode.classList.remove('hidden'); } function open() { pageIsOpen = true; - style.revealElement(element_PlaySelection); - style.revealElement(element_menuExternalLinks); + element_PlaySelection.classList.remove('hidden'); + element_menuExternalLinks.classList.remove('hidden'); changePlayMode('online'); initListeners(); invites.subscribeToInvites(); // Subscribe to the invites list subscription service! @@ -117,8 +117,8 @@ function open() { function close() { pageIsOpen = false; - style.hideElement(element_PlaySelection); - style.hideElement(element_menuExternalLinks); + element_PlaySelection.classList.add('hidden'); + element_menuExternalLinks.classList.add('hidden'); hideElement_inviteCode(); closeListeners(); // This will auto-cancel our existing invite diff --git a/src/client/scripts/esm/game/gui/guipromotion.ts b/src/client/scripts/esm/game/gui/guipromotion.ts index a3ee9347e..eba16bad4 100644 --- a/src/client/scripts/esm/game/gui/guipromotion.ts +++ b/src/client/scripts/esm/game/gui/guipromotion.ts @@ -32,18 +32,18 @@ function isUIOpen() { return selectionOpen; } function open(color: string) { selectionOpen = true; - style.revealElement(element_Promote!); - if (color === 'white') style.revealElement(element_PromoteWhite!); - else if (color === 'black') style.revealElement(element_PromoteBlack!); + element_Promote?.classList.remove('hidden'); + if (color === 'white') element_PromoteWhite?.classList.remove('hidden'); + else if (color === 'black') element_PromoteBlack?.classList.remove('hidden'); else throw new Error(`Promotion UI does not support color "${color}"`); } /** Closes the promotion UI */ function close() { selectionOpen = false; - style.hideElement(element_PromoteWhite!); - style.hideElement(element_PromoteBlack!); - style.hideElement(element_Promote!); + element_PromoteWhite?.classList.add('hidden'); + element_PromoteBlack?.classList.add('hidden'); + element_Promote?.classList.add('hidden'); } /** diff --git a/src/client/scripts/esm/game/gui/guititle.ts b/src/client/scripts/esm/game/gui/guititle.ts index ab10edfa3..57034986e 100644 --- a/src/client/scripts/esm/game/gui/guititle.ts +++ b/src/client/scripts/esm/game/gui/guititle.ts @@ -31,14 +31,14 @@ const element_menuExternalLinks = document.getElementById('menu-external-links') // Call when title screen is loaded function open() { - style.revealElement(titleElement); - style.revealElement(element_menuExternalLinks); + titleElement.classList.remove('hidden'); + element_menuExternalLinks.classList.remove('hidden'); initListeners(); }; function close() { - style.hideElement(titleElement); - style.hideElement(element_menuExternalLinks); + titleElement.classList.add('hidden'); + element_menuExternalLinks.classList.add('hidden'); closeListeners(); } diff --git a/src/client/scripts/esm/game/gui/stats.js b/src/client/scripts/esm/game/gui/stats.js index 92f9dd67b..41b9e34f0 100644 --- a/src/client/scripts/esm/game/gui/stats.js +++ b/src/client/scripts/esm/game/gui/stats.js @@ -43,12 +43,12 @@ function showMoves(durationSecs = 2.5) { setTextContentOfMoves(); setTimeout(hideMoves, durationSecs * 1000); - if (visibilityWeight === 1) style.revealElement(elementStatusMoves); + if (visibilityWeight === 1) elementStatusMoves.classList.remove('hidden'); } function hideMoves() { visibilityWeight--; - if (visibilityWeight === 0) style.hideElement(elementStatusMoves); + if (visibilityWeight === 0) elementStatusMoves.classList.add('hidden'); } function setTextContentOfMoves() { @@ -65,7 +65,7 @@ function updateStatsCSS() { function showPiecesMesh() { if (config.VIDEO_MODE) return; - style.revealElement(elementStatusPiecesMesh); + elementStatusPiecesMesh.classList.remove('hidden'); } function updatePiecesMesh(percent) { @@ -74,16 +74,16 @@ function updatePiecesMesh(percent) { } function hidePiecesMesh() { - style.hideElement(elementStatusPiecesMesh); + elementStatusPiecesMesh.classList.add('hidden'); } function showFPS() { if (config.VIDEO_MODE) return; - style.revealElement(elementStatusFPS); + elementStatusFPS.classList.remove('hidden'); } function hideFPS() { - style.hideElement(elementStatusFPS); + elementStatusFPS.classList.add('hidden'); } function updateFPS(fps) { @@ -94,7 +94,7 @@ function updateFPS(fps) { function showRotateMesh() { if (config.VIDEO_MODE) return; - style.revealElement(elementStatusRotateMesh); + elementStatusRotateMesh.classList.remove('hidden'); } function updateRotateMesh(percent) { @@ -103,19 +103,10 @@ function updateRotateMesh(percent) { } function hideRotateMesh() { - style.hideElement(elementStatusRotateMesh); + elementStatusRotateMesh.classList.add('hidden'); } -// NO LONGER USED. These were for the aynchronious checkmate algorithm. -// showMoveLooking() { -// if (config.VIDEO_MODE) return; -// style.revealElement(elementStatusMoveLooking); -// }, -// updateMoveLooking(percent) { -// const percentString = math.decimalToPercent(percent); -// showMoveLooking(); -// elementStatusMoveLooking.textContent = `Looking for moves... ${percentString}`; -// }, + export default { showMoves, diff --git a/src/client/scripts/esm/game/gui/style.js b/src/client/scripts/esm/game/gui/style.js index 472211bfe..d9d4523c4 100644 --- a/src/client/scripts/esm/game/gui/style.js +++ b/src/client/scripts/esm/game/gui/style.js @@ -32,21 +32,6 @@ function reinstateClass(element, className) { // Hide and show elements... -/** - * Hides the provided document element by giving it a class with the property "display: none". - * @param {HTMLElement} element - The document element - */ -function hideElement(element) { - addClass(element, "hidden"); -} - -/** - * Reveals the provided document element by **removing** the class with the property "display: none". - * @param {HTMLElement} element - The document element - */ -function revealElement(element) { - removeClass(element, "hidden"); -} // Other operations @@ -107,8 +92,6 @@ function arrayToCssColor(colorArray) { export default { - hideElement, - revealElement, setNavStyle, getChildrenTextContents, arrayToCssColor, From c44aa831e46a06baf97793f54e8bede202a2e960 Mon Sep 17 00:00:00 2001 From: Naviary2 Date: Fri, 3 Jan 2025 05:18:11 -0700 Subject: [PATCH 015/131] Not needed classes --- src/client/css/play.css | 8 -------- 1 file changed, 8 deletions(-) diff --git a/src/client/css/play.css b/src/client/css/play.css index cbd327ebe..1d53785a2 100644 --- a/src/client/css/play.css +++ b/src/client/css/play.css @@ -1210,14 +1210,6 @@ a { -webkit-tap-highlight-color: rgba(0, 0, 0, 0.099); } -.unavailable { - color: rgb(190, 190, 190); -} - -.flex { - display: flex; -} - .hidden { display: none; } From 5d888a39a7f126d455f0864a7d44bce7988b8c6f Mon Sep 17 00:00:00 2001 From: Naviary2 Date: Fri, 3 Jan 2025 05:22:16 -0700 Subject: [PATCH 016/131] Use classList.add() instead of className = --- src/client/scripts/esm/views/index.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/client/scripts/esm/views/index.ts b/src/client/scripts/esm/views/index.ts index ab6ced270..0b5339103 100644 --- a/src/client/scripts/esm/views/index.ts +++ b/src/client/scripts/esm/views/index.ts @@ -36,14 +36,14 @@ interface Contributor { iconImg.src = contributor.iconUrl; const githubStatsContainer = document.createElement("div"); - githubStatsContainer.className = "github-stats"; + githubStatsContainer.classList.add("github-stats"); const name = document.createElement("p"); - name.className = "name"; + name.classList.add("name"); name.innerText = contributor.name; const paragraph = document.createElement("p"); - paragraph.className = "contribution-count"; + paragraph.classList.add("contribution-count"); paragraph.innerText = `${translations['contribution_count']?.[0] || ""}${contributor.contributionCount}${translations['contribution_count']?.[1] || ""}`; githubStatsContainer.appendChild(name); From b90bf3f63eabf0bc7ec409d4df712624410cf883 Mon Sep 17 00:00:00 2001 From: Naviary2 Date: Fri, 3 Jan 2025 05:34:58 -0700 Subject: [PATCH 017/131] gameloader.ts is now in charge of telling every other script whether we're in an online game or not --- src/client/scripts/esm/chess/logic/clock.ts | 7 +++--- .../scripts/esm/chess/logic/movepiece.js | 4 +-- .../scripts/esm/game/chess/copypastegame.js | 6 ++--- .../scripts/esm/game/chess/gameloader.ts | 17 +++++++++++++ src/client/scripts/esm/game/chess/gameslot.ts | 5 ++-- .../scripts/esm/game/chess/selection.js | 11 ++++---- src/client/scripts/esm/game/gui/guiclock.js | 11 ++++---- .../scripts/esm/game/gui/guigameinfo.js | 6 ++--- .../scripts/esm/game/gui/guinavigation.ts | 7 ++---- src/client/scripts/esm/game/gui/guipause.js | 5 ++-- src/client/scripts/esm/game/input.js | 3 ++- .../scripts/esm/game/misc/drawoffers.js | 3 ++- .../scripts/esm/game/misc/onlinegame.js | 25 +++++++------------ .../scripts/esm/game/rendering/arrows.js | 3 ++- .../scripts/esm/game/rendering/options.js | 5 ++-- 15 files changed, 64 insertions(+), 54 deletions(-) diff --git a/src/client/scripts/esm/chess/logic/clock.ts b/src/client/scripts/esm/chess/logic/clock.ts index cb8d2575e..da698f32e 100644 --- a/src/client/scripts/esm/chess/logic/clock.ts +++ b/src/client/scripts/esm/chess/logic/clock.ts @@ -6,8 +6,6 @@ * if somebody loses on time. */ -// @ts-ignore -import onlinegame from '../../game/misc/onlinegame.js'; // @ts-ignore import moveutil from '../util/moveutil.js'; // @ts-ignore @@ -20,6 +18,7 @@ import gamefileutility from '../util/gamefileutility.js'; import pingManager from '../../util/pingManager.js'; // @ts-ignore import options from '../../game/rendering/options.js'; +import gameloader from '../../game/chess/gameloader.js'; // Type Definitions --------------------------------------------------------------- @@ -143,7 +142,7 @@ function adjustClockValuesForPing(clockValues: ClockValues): ClockValues { */ function push(gamefile: gamefile) { const clocks = gamefile.clocks; - if (onlinegame.areInOnlineGame()) return; // Only the server can push clocks + if (gameloader.areInOnlineGame()) return; // Only the server can push clocks if (clocks.untimed) return; if (!moveutil.isGameResignable(gamefile)) return; // Don't push unless atleast 2 moves have been played @@ -180,7 +179,7 @@ function update(gamefile: gamefile): string | undefined { clocks.currentTime[clocks.colorTicking] = Math.ceil(clocks.timeRemainAtTurnStart! - timePassedSinceTurnStart); // Has either clock run out of time? - if (onlinegame.areInOnlineGame()) return; // Don't conclude game by time if in an online game, only the server does that. + if (gameloader.areInOnlineGame()) return; // Don't conclude game by time if in an online game, only the server does that. for (const [color,time] of Object.entries(clocks.currentTime)) { if (time as number <= 0) { diff --git a/src/client/scripts/esm/chess/logic/movepiece.js b/src/client/scripts/esm/chess/logic/movepiece.js index 8e0037333..6a12065d5 100644 --- a/src/client/scripts/esm/chess/logic/movepiece.js +++ b/src/client/scripts/esm/chess/logic/movepiece.js @@ -19,8 +19,8 @@ import jsutil from '../../util/jsutil.js'; import coordutil from '../util/coordutil.js'; import frametracker from '../../game/rendering/frametracker.js'; import stats from '../../game/gui/stats.js'; -import onlinegame from '../../game/misc/onlinegame.js'; import gameslot from '../../game/chess/gameslot.js'; +import gameloader from '../../game/chess/gameloader.js'; // Import End /** @@ -93,7 +93,7 @@ function makeMove(gamefile, move, { flipTurn = true, recordMove = true, pushCloc updateInCheck(gamefile, recordMove); if (doGameOverChecks) { gamefileutility.doGameOverChecks(gamefile); - if (!simulated && concludeGameIfOver && gamefile.gameConclusion && !onlinegame.areInOnlineGame()) gameslot.concludeGame(); + if (!simulated && concludeGameIfOver && gamefile.gameConclusion && !gameloader.areInOnlineGame()) gameslot.concludeGame(); } if (updateData) { diff --git a/src/client/scripts/esm/game/chess/copypastegame.js b/src/client/scripts/esm/game/chess/copypastegame.js index f0272c9e6..9823635e5 100644 --- a/src/client/scripts/esm/game/chess/copypastegame.js +++ b/src/client/scripts/esm/game/chess/copypastegame.js @@ -103,10 +103,10 @@ async function callbackPaste(event) { if (guinavigation.isCoordinateActive()) return; // Make sure we're not in a public match - if (onlinegame.areInOnlineGame() && !onlinegame.getIsPrivate()) return statustext.showStatus(translations.copypaste.cannot_paste_in_public); + if (gameloader.areInOnlineGame() && !onlinegame.getIsPrivate()) return statustext.showStatus(translations.copypaste.cannot_paste_in_public); // Make sure it's legal in a private match - if (onlinegame.areInOnlineGame() && onlinegame.getIsPrivate() && gameslot.getGamefile().moves.length > 0) return statustext.showStatus(translations.copypaste.cannot_paste_after_moves); + if (gameloader.areInOnlineGame() && onlinegame.getIsPrivate() && gameslot.getGamefile().moves.length > 0) return statustext.showStatus(translations.copypaste.cannot_paste_after_moves); // Do we have clipboard permission? let clipboard; @@ -256,7 +256,7 @@ async function pasteGame(longformat) { // game: { startingPosition (key-list), p gameRules: longformat.gameRules }; - if (onlinegame.areInOnlineGame() && onlinegame.getIsPrivate()) { + if (gameloader.areInOnlineGame() && onlinegame.getIsPrivate()) { // Playing a custom private game! Save the pasted position in browser // storage so that we can remember it upon refreshing. const gameID = onlinegame.getGameID(); diff --git a/src/client/scripts/esm/game/chess/gameloader.ts b/src/client/scripts/esm/game/chess/gameloader.ts index 1fabd074b..b78295a6f 100644 --- a/src/client/scripts/esm/game/chess/gameloader.ts +++ b/src/client/scripts/esm/game/chess/gameloader.ts @@ -86,9 +86,13 @@ interface VariantOptions { */ let inAGame: boolean = false; +/** The type of game we are in, whether local or online, if we are in a game. */ +let typeOfGameWeAreIn: undefined | 'local' | 'online'; + // Functions -------------------------------------------------------------------- + /** * Returns true if we are in ANY type of game, whether local, online, analysis, or editor. * @@ -98,6 +102,14 @@ function areInAGame(): boolean { return inAGame; } +function areInLocalGame(): boolean { + return typeOfGameWeAreIn === 'local'; +} + +function areInOnlineGame(): boolean { + return typeOfGameWeAreIn === 'online'; +} + @@ -127,6 +139,7 @@ async function startLocalGame(options: { guigameinfo.hidePlayerNames(); // @ts-ignore loadGame(gameOptions, true, true); + typeOfGameWeAreIn = 'local'; } /** @@ -162,6 +175,7 @@ async function startOnlineGame(options: { options.variantOptions = generateVariantOptionsIfReloadingPrivateCustomGame(); const fromWhitePerspective = options.youAreColor === 'white'; await loadGame(options, fromWhitePerspective, false); + typeOfGameWeAreIn = 'online'; onlinegame.initOnlineGame(options); guigameinfo.setAndRevealPlayerNames(options); @@ -246,11 +260,14 @@ function unloadGame() { perspective.disable(); gui.prepareForOpen(); inAGame = false; + typeOfGameWeAreIn = undefined; } export default { areInAGame, + areInLocalGame, + areInOnlineGame, startLocalGame, startOnlineGame, loadGame, diff --git a/src/client/scripts/esm/game/chess/gameslot.ts b/src/client/scripts/esm/game/chess/gameslot.ts index f6a7193cd..2d2d46332 100644 --- a/src/client/scripts/esm/game/chess/gameslot.ts +++ b/src/client/scripts/esm/game/chess/gameslot.ts @@ -46,7 +46,6 @@ import winconutil from "../../chess/util/winconutil.js"; // @ts-ignore import moveutil from "../../chess/util/moveutil.js"; // @ts-ignore -// eslint-disable-next-line no-unused-vars import clock from "../../chess/logic/clock.js"; // @ts-ignore import guigameinfo from "../gui/guigameinfo.js"; @@ -62,8 +61,8 @@ import spritesheet from "../rendering/spritesheet.js"; import type { MetaData } from "../../chess/util/metadata.js"; -// eslint-disable-next-line no-unused-vars import type { ClockValues } from "../../chess/logic/clock.js"; +import gameloader from "./gameloader.js"; // Variables --------------------------------------------------------------- @@ -267,7 +266,7 @@ function concludeGame() { onlinegame.onGameConclude(); const delayToPlayConcludeSoundSecs = 0.65; - if (!onlinegame.areInOnlineGame()) { + if (!gameloader.areInOnlineGame()) { if (!loadedGamefile.gameConclusion.includes('draw')) sound.playSound_win(delayToPlayConcludeSoundSecs); else sound.playSound_draw(delayToPlayConcludeSoundSecs); } else { // In online game diff --git a/src/client/scripts/esm/game/chess/selection.js b/src/client/scripts/esm/game/chess/selection.js index df8134c9d..c60c142b3 100644 --- a/src/client/scripts/esm/game/chess/selection.js +++ b/src/client/scripts/esm/game/chess/selection.js @@ -25,6 +25,7 @@ import draganimation from '../rendering/draganimation.js'; import space from '../misc/space.js'; import preferences from '../../components/header/preferences.js'; import gameslot from './gameslot.js'; +import gameloader from './gameloader.js'; // Import End /** @@ -122,7 +123,7 @@ function promoteToType(type) { promoteTo = type; } function update() { // Guard clauses... const gamefile = gameslot.getGamefile(); - // if (onlinegame.areInOnlineGame() && !onlinegame.isItOurTurn(gamefile)) return; // Not our turn + // if (gameloader.areInOnlineGame() && !onlinegame.isItOurTurn(gamefile)) return; // Not our turn if (input.isMouseDown_Right()) return unselectPiece(); // Right-click deselects everything if (pawnIsPromoting) { // Do nothing else this frame but wait for a promotion piece to be selected if (promoteTo) makePromotionMove(); @@ -298,9 +299,9 @@ function selectPiece(type, index, coords) { legalMoves = legalmoves.calculate(gameslot.getGamefile(), pieceSelected); const pieceColor = colorutil.getPieceColorFromType(pieceSelected.type); - isOpponentPiece = onlinegame.areInOnlineGame() ? pieceColor !== onlinegame.getOurColor() + isOpponentPiece = gameloader.areInOnlineGame() ? pieceColor !== onlinegame.getOurColor() /* Local Game */ : pieceColor !== gameslot.getGamefile().whosTurn; - isPremove = !isOpponentPiece && onlinegame.areInOnlineGame() && !onlinegame.isItOurTurn(); + isPremove = !isOpponentPiece && gameloader.areInOnlineGame() && !onlinegame.isItOurTurn(); legalmovehighlights.onPieceSelected(pieceSelected, legalMoves); // Generate the buffer model for the blue legal move fields. } @@ -400,10 +401,10 @@ function canMovePieceType(pieceType) { if (!pieceType || pieceType === 'voidsN') return false; // Never move voids else if (options.getEM()) return true; //Edit mode allows pieces to be moved on any turn. const pieceColor = colorutil.getPieceColorFromType(pieceType); - const isOpponentPiece = onlinegame.areInOnlineGame() ? pieceColor !== onlinegame.getOurColor() + const isOpponentPiece = gameloader.areInOnlineGame() ? pieceColor !== onlinegame.getOurColor() /* Local Game */ : pieceColor !== gameslot.getGamefile().whosTurn; if (isOpponentPiece) return false; // Don't move opponent pieces - const isPremove = !isOpponentPiece && onlinegame.areInOnlineGame() && !onlinegame.isItOurTurn(); + const isPremove = !isOpponentPiece && gameloader.areInOnlineGame() && !onlinegame.isItOurTurn(); return (!isPremove /*|| premovesEnabled*/); } diff --git a/src/client/scripts/esm/game/gui/guiclock.js b/src/client/scripts/esm/game/gui/guiclock.js index ad5627372..dc326104e 100644 --- a/src/client/scripts/esm/game/gui/guiclock.js +++ b/src/client/scripts/esm/game/gui/guiclock.js @@ -1,9 +1,10 @@ -import style from "./style.js"; + import moveutil from "../../chess/util/moveutil.js"; import onlinegame from "../misc/onlinegame.js"; import sound from "../misc/sound.js"; import clockutil from "../../chess/util/clockutil.js"; import gamefileutility from "../../chess/util/gamefileutility.js"; +import gameloader from "../chess/gameloader.js"; /** * @typedef {import('../../chess/logic/gamefile.js').gamefile} gamefile @@ -201,7 +202,7 @@ function updateTextContent(gamefile) { function rescheduleMinuteTick(gamefile) { if (gamefile.clocks.startTime.minutes < lowtimeNotif.clockMinsRequiredToUse) return; // 1 minute lowtime notif is not used in bullet games. clearTimeout(lowtimeNotif.timeoutID); - if (onlinegame.areInOnlineGame() && gamefile.clocks.colorTicking !== onlinegame.getOurColor()) return; // Don't play the sound effect for our opponent. + if (gameloader.areInOnlineGame() && gamefile.clocks.colorTicking !== onlinegame.getOurColor()) return; // Don't play the sound effect for our opponent. if (lowtimeNotif.colorsNotified.has(gamefile.clocks.colorTicking)) return; const timeRemainAtTurnStart = gamefile.clocks.timeRemainAtTurnStart; const timeRemain = timeRemainAtTurnStart - lowtimeNotif.timeToStartFromEnd; // Time remaining until sound it should start playing @@ -250,7 +251,7 @@ function push(gamefile) { function rescheduleDrum(gamefile) { clearTimeout(countdown.drum.timeoutID); - if (onlinegame.areInOnlineGame() && gamefile.clocks.colorTicking !== onlinegame.getOurColor()) return; // Don't play the sound effect for our opponent. + if (gameloader.areInOnlineGame() && gamefile.clocks.colorTicking !== onlinegame.getOurColor()) return; // Don't play the sound effect for our opponent. const timeUntil10SecsRemain = gamefile.clocks.currentTime[gamefile.clocks.colorTicking] - 10000; let timeNextDrum = timeUntil10SecsRemain; let secsRemaining = 10; @@ -265,7 +266,7 @@ function rescheduleDrum(gamefile) { function rescheduleTicking(gamefile) { clearTimeout(countdown.ticking.timeoutID); countdown.ticking.sound?.fadeOut(countdown.ticking.fadeOutDuration); - if (onlinegame.areInOnlineGame() && gamefile.clocks.colorTicking !== onlinegame.getOurColor()) return; // Don't play the sound effect for our opponent. + if (gameloader.areInOnlineGame() && gamefile.clocks.colorTicking !== onlinegame.getOurColor()) return; // Don't play the sound effect for our opponent. if (gamefile.clocks.timeAtTurnStart < 10000) return; const timeRemain = gamefile.clocks.currentTime[gamefile.clocks.colorTicking] - countdown.ticking.timeToStartFromEnd; if (timeRemain > 0) countdown.ticking.timeoutID = setTimeout(playTickingEffect, timeRemain); @@ -279,7 +280,7 @@ function rescheduleTicking(gamefile) { function rescheduleTick(gamefile) { clearTimeout(countdown.tick.timeoutID); countdown.tick.sound?.fadeOut(countdown.tick.fadeOutDuration); - if (onlinegame.areInOnlineGame() && gamefile.clocks.colorTicking !== onlinegame.getOurColor()) return; // Don't play the sound effect for our opponent. + if (gameloader.areInOnlineGame() && gamefile.clocks.colorTicking !== onlinegame.getOurColor()) return; // Don't play the sound effect for our opponent. const timeRemain = gamefile.clocks.currentTime[gamefile.clocks.colorTicking] - countdown.tick.timeToStartFromEnd; if (timeRemain > 0) countdown.tick.timeoutID = setTimeout(playTickEffect, timeRemain); else { diff --git a/src/client/scripts/esm/game/gui/guigameinfo.js b/src/client/scripts/esm/game/gui/guigameinfo.js index 663cb09c7..eee5b5a72 100644 --- a/src/client/scripts/esm/game/gui/guigameinfo.js +++ b/src/client/scripts/esm/game/gui/guigameinfo.js @@ -1,10 +1,10 @@ // Import Start -import style from './style.js'; import onlinegame from '../misc/onlinegame.js'; import winconutil from '../../chess/util/winconutil.js'; import gamefileutility from '../../chess/util/gamefileutility.js'; import gameslot from '../chess/gameslot.js'; +import gameloader from '../chess/gameloader.js'; // Import End /** @@ -67,7 +67,7 @@ function updateWhosTurn(gamefile) { throw new Error(`Cannot set the document element text showing whos turn it is when color is neither white nor black! ${color}`); let textContent = ""; - if (onlinegame.areInOnlineGame()) { + if (gameloader.areInOnlineGame()) { const ourTurn = onlinegame.isItOurTurn(gamefile); textContent = ourTurn ? translations.your_move : translations.their_move; } else textContent = color === "white" ? translations.white_to_move : translations.black_to_move; @@ -92,7 +92,7 @@ function gameEnd(conclusion) { const resultTranslations = translations.results; element_dot.classList.add('hidden'); - if (onlinegame.areInOnlineGame()) { + if (gameloader.areInOnlineGame()) { if (onlinegame.areWeColor(victor)) element_whosturn.textContent = condition === 'checkmate' ? resultTranslations.you_checkmate : condition === 'time' ? resultTranslations.you_time diff --git a/src/client/scripts/esm/game/gui/guinavigation.ts b/src/client/scripts/esm/game/gui/guinavigation.ts index 47e4192cb..72e06fcf9 100644 --- a/src/client/scripts/esm/game/gui/guinavigation.ts +++ b/src/client/scripts/esm/game/gui/guinavigation.ts @@ -6,8 +6,6 @@ import moveutil from '../../chess/util/moveutil.js'; // @ts-ignore import movement from '../rendering/movement.js'; // @ts-ignore -import style from './style.js'; -// @ts-ignore import input from '../input.js'; // @ts-ignore import guipause from './guipause.js'; @@ -29,14 +27,13 @@ import frametracker from '../rendering/frametracker.js'; // @ts-ignore import guigameinfo from './guigameinfo.js'; // @ts-ignore -import onlinegame from '../misc/onlinegame.js'; -// @ts-ignore import camera from '../rendering/camera.js'; import gameslot from '../chess/gameslot.js'; // @ts-ignore // eslint-disable-next-line no-unused-vars import type gamefile from '../../chess/logic/gamefile.js'; +import gameloader from '../chess/gameloader.js'; /** * This script handles the navigation bar, in a game, @@ -105,7 +102,7 @@ function onToggleNavigationBar() { const gamefile = gameslot.getGamefile(); if (!gamefile) throw Error("Should not have toggled navigation bar when there's no game. The listener should have been closed."); if (navigationOpen) { - open(gamefile, { allowEditCoords: !onlinegame.areInOnlineGame() }); + open(gamefile, { allowEditCoords: !gameloader.areInOnlineGame() }); guigameinfo.open(); } else close(); diff --git a/src/client/scripts/esm/game/gui/guipause.js b/src/client/scripts/esm/game/gui/guipause.js index e64aab51d..7dfee8cb9 100644 --- a/src/client/scripts/esm/game/gui/guipause.js +++ b/src/client/scripts/esm/game/gui/guipause.js @@ -1,7 +1,6 @@ // Import Start import onlinegame from '../misc/onlinegame.js'; -import style from './style.js'; import arrows from '../rendering/arrows.js'; import statustext from './statustext.js'; import copypastegame from '../chess/copypastegame.js'; @@ -61,7 +60,7 @@ function updatePasteButtonTransparency() { const moves = gameslot.getGamefile().moves; const legalInPrivateMatch = onlinegame.getIsPrivate() && moves.length === 0; - if (onlinegame.areInOnlineGame() && !legalInPrivateMatch) element_pastegame.classList.add('opacity-0_5'); + if (gameloader.areInOnlineGame() && !legalInPrivateMatch) element_pastegame.classList.add('opacity-0_5'); else element_pastegame.classList.remove('opacity-0_5'); } @@ -100,7 +99,7 @@ function onReceiveOpponentsMove() { function updateTextOfMainMenuButton({ freezeResignButtonIfNoLongerAbortable } = {}) { if (!isPaused) return; - if (!onlinegame.areInOnlineGame() || onlinegame.hasGameConcluded()) return element_mainmenu.textContent = translations.main_menu; + if (!gameloader.areInOnlineGame() || onlinegame.hasGameConcluded()) return element_mainmenu.textContent = translations.main_menu; if (moveutil.isGameResignable(gameslot.getGamefile())) { // If the text currently says "Abort Game", freeze the button for 1 second in case the user clicked it RIGHT after it switched text! They may have tried to abort and actually not want to resign. diff --git a/src/client/scripts/esm/game/input.js b/src/client/scripts/esm/game/input.js index 03e047195..5ec5b2250 100644 --- a/src/client/scripts/esm/game/input.js +++ b/src/client/scripts/esm/game/input.js @@ -16,6 +16,7 @@ import space from './misc/space.js'; import frametracker from './rendering/frametracker.js'; import docutil from '../util/docutil.js'; import gameslot from './chess/gameslot.js'; +import gameloader from './chess/gameloader.js'; // Import End "use strict"; @@ -726,7 +727,7 @@ function moveMouse(touch1, touch2) { // touch2 optional. If provided, will take setTouchesChangeInXYTo0(touch2); } - const oneOrNegOne = onlinegame.areInOnlineGame() && onlinegame.areWeColor('black') ? -1 : 1; + const oneOrNegOne = gameloader.areInOnlineGame() && onlinegame.areWeColor('black') ? -1 : 1; mouseWorldLocation[0] -= touchMovementX * dampeningToMoveMouseInTouchMode * oneOrNegOne; mouseWorldLocation[1] -= touchMovementY * dampeningToMoveMouseInTouchMode * oneOrNegOne; diff --git a/src/client/scripts/esm/game/misc/drawoffers.js b/src/client/scripts/esm/game/misc/drawoffers.js index 32d4ca96d..0fc696c8b 100644 --- a/src/client/scripts/esm/game/misc/drawoffers.js +++ b/src/client/scripts/esm/game/misc/drawoffers.js @@ -8,6 +8,7 @@ import sound from './sound.js'; import moveutil from '../../chess/util/moveutil.js'; import onlinegame from './onlinegame.js'; import gameslot from '../chess/gameslot.js'; +import gameloader from '../chess/gameloader.js'; // Import End 'use strict'; @@ -42,7 +43,7 @@ let isAcceptingDraw = false; */ function isOfferingDrawLegal() { const gamefile = gameslot.getGamefile(); - if (!onlinegame.areInOnlineGame()) return false; // Can't offer draws in local games + if (!gameloader.areInOnlineGame()) return false; // Can't offer draws in local games if (!moveutil.isGameResignable(gamefile)) return false; // Not atleast 2+ moves if (onlinegame.hasGameConcluded()) return false; // Can't offer draws after the game has ended if (isTooSoonToOfferDraw()) return false; // It's been too soon since our last offer diff --git a/src/client/scripts/esm/game/misc/onlinegame.js b/src/client/scripts/esm/game/misc/onlinegame.js index ee315f8ae..2c594ba1d 100644 --- a/src/client/scripts/esm/game/misc/onlinegame.js +++ b/src/client/scripts/esm/game/misc/onlinegame.js @@ -41,8 +41,6 @@ import gameloader from '../chess/gameloader.js'; /** This module keeps trap of the data of the onlinegame we are currently in. */ -/** Whether we are currently in an online game. */ -let inOnlineGame = false; /** The id of the online game we are in, if we are in one. @type {string} */ let gameID; /** Whether the game is a private one (joined from an invite code). */ @@ -133,7 +131,7 @@ function addWarningLeaveGamePopupsToHyperlinks() { function confirmNavigationAwayFromGame(event) { // Check if Command (Meta) or Ctrl key is held down if (event.metaKey || event.ctrlKey) return; // Allow opening in a new tab without confirmation - if (!inOnlineGame || gamefileutility.isGameOver(gameslot.getGamefile())) return; + if (!gameloader.areInOnlineGame() || gamefileutility.isGameOver(gameslot.getGamefile())) return; const userConfirmed = confirm('Are you sure you want to leave the game?'); if (userConfirmed) return; // Follow link like normal. Server starts a 20-second auto-resign timer for disconnecting on purpose. @@ -159,8 +157,6 @@ function confirmNavigationAwayFromGame(event) { */ function getGameID() { return gameID; } -function areInOnlineGame() { return inOnlineGame; } - function getIsPrivate() { return isPrivate; } function getOurColor() { return ourColor; } @@ -175,7 +171,7 @@ function hasGameConcluded() { return gameHasConcluded; } function setInSyncFalse() { inSync = false; } function update() { - if (!inOnlineGame) return; + if (!gameloader.areInOnlineGame()) return; updateAFK(); } @@ -277,7 +273,7 @@ function onmessage(data) { // { sub, action, value, id } handleOpponentsMove(data.value); break; case "clock": { // Contain this case in a block so that it's variables are not hoisted - if (!inOnlineGame) return; + if (!gameloader.areInOnlineGame()) return; const message = data.value; // { clockValues: { timerWhite, timerBlack } } const gamefile = gameslot.getGamefile(); // Adjust the timer whos turn it is depending on ping. @@ -431,7 +427,7 @@ function handleJoinGame(message) { * @param {Object} message - The server's socket message, with the properties `move`, `gameConclusion`, `moveNumber`, `clockValues`. */ function handleOpponentsMove(message) { // { move, gameConclusion, moveNumber, clockValues } - if (!inOnlineGame) return; + if (!gameloader.areInOnlineGame()) return; const moveAndConclusion = { move: message.move, gameConclusion: message.gameConclusion }; // Make sure the move number matches the expected. @@ -521,7 +517,7 @@ function cancelMoveSound() { } function resyncToGame() { - if (!inOnlineGame) return; + if (!gameloader.areInOnlineGame()) return; function onReplyFunc() { inSync = true; } websocket.sendmessage('game', 'resync', gameID, false, onReplyFunc); } @@ -533,7 +529,7 @@ function resyncToGame() { * `gameConclusion`, `clockValues`, `moves`, `millisUntilAutoAFKResign`, `offerDraw` */ function handleServerGameUpdate(messageContents) { // { gameConclusion, clockValues: ClockValues, moves, millisUntilAutoAFKResign, offerDraw } - if (!inOnlineGame) return; + if (!gameloader.areInOnlineGame()) return; const gamefile = gameslot.getGamefile(); const claimedGameConclusion = messageContents.gameConclusion; @@ -678,7 +674,6 @@ function reportOpponentsMove(reason) { * @param {Object} gameOptions - An object that contains the properties `id`, `publicity`, `youAreColor`, `millisUntilAutoAFKResign`, `disconnect`, `serverRestartingAt` */ function setColorAndGameID(gameOptions) { - inOnlineGame = true; ourColor = gameOptions.youAreColor; gameID = gameOptions.id; isPrivate = gameOptions.publicity === 'private'; @@ -706,7 +701,6 @@ function initOnlineGame(gameOptions) { // Call when we leave an online game function closeOnlineGame() { - inOnlineGame = false; gameID = undefined; isPrivate = undefined; ourColor = undefined; @@ -744,7 +738,7 @@ function isItOurTurn() { return gameslot.getGamefile().whosTurn === ourColor; } function areWeColor(color) { return color === ourColor; } function sendMove() { - if (!inOnlineGame || !inSync) return; // Don't do anything if it's a local game + if (!gameloader.areInOnlineGame() || !inSync) return; // Don't do anything if it's a local game if (config.DEV_BUILD) console.log("Sending our move.."); const gamefile = gameslot.getGamefile(); @@ -768,7 +762,7 @@ function sendMove() { // Aborts / Resigns function onMainMenuPress() { - if (!inOnlineGame) return; + if (!gameloader.areInOnlineGame()) return; const gamefile = gameslot.getGamefile(); if (gameHasConcluded) { // The server has concluded the game, not us if (websocket.getSubs().game) { @@ -819,7 +813,7 @@ async function askServerIfWeAreInGame() { * and the server may change the players elos! */ function requestRemovalFromPlayersInActiveGames() { - if (!inOnlineGame) return; + if (!gameloader.areInOnlineGame()) return; websocket.sendmessage('game', 'removefromplayersinactivegames'); } @@ -879,7 +873,6 @@ function onGameConclude() { export default { onmessage, - areInOnlineGame, getIsPrivate, getOurColor, setInSyncFalse, diff --git a/src/client/scripts/esm/game/rendering/arrows.js b/src/client/scripts/esm/game/rendering/arrows.js index 63a76ce26..d35bc6f0d 100644 --- a/src/client/scripts/esm/game/rendering/arrows.js +++ b/src/client/scripts/esm/game/rendering/arrows.js @@ -23,6 +23,7 @@ import coordutil from '../../chess/util/coordutil.js'; import space from '../misc/space.js'; import spritesheet from './spritesheet.js'; import gameslot from '../chess/gameslot.js'; +import gameloader from '../chess/gameloader.js'; // Import End /** @@ -437,7 +438,7 @@ function onPieceIndicatorHover(type, pieceCoords, direction) { // Determine what color the legal move highlights should be... const pieceColor = colorutil.getPieceColorFromType(type); - const opponentColor = onlinegame.areInOnlineGame() ? colorutil.getOppositeColor(onlinegame.getOurColor()) : colorutil.getOppositeColor(gamefile.whosTurn); + const opponentColor = gameloader.areInOnlineGame() ? colorutil.getOppositeColor(onlinegame.getOurColor()) : colorutil.getOppositeColor(gamefile.whosTurn); const isOpponentPiece = pieceColor === opponentColor; const isOurTurn = gamefile.whosTurn === pieceColor; const color = options.getLegalMoveHighlightColor({ isOpponentPiece, isPremove: !isOurTurn }); diff --git a/src/client/scripts/esm/game/rendering/options.js b/src/client/scripts/esm/game/rendering/options.js index cece3d8c1..81c907fee 100644 --- a/src/client/scripts/esm/game/rendering/options.js +++ b/src/client/scripts/esm/game/rendering/options.js @@ -15,6 +15,7 @@ import timeutil from '../../util/timeutil.js'; import themes from '../../components/header/themes.js'; import preferences from '../../components/header/preferences.js'; import gameslot from '../chess/gameslot.js'; +import gameloader from '../chess/gameloader.js'; // Import End "use strict"; @@ -85,8 +86,8 @@ function isFPSOn() { function toggleEM() { // Make sure it's legal - const legalInPrivate = onlinegame.areInOnlineGame() && onlinegame.getIsPrivate() && input.isKeyHeld('0'); - if (onlinegame.areInOnlineGame() && !legalInPrivate) return; // Don't toggle if in an online game + const legalInPrivate = gameloader.areInOnlineGame() && onlinegame.getIsPrivate() && input.isKeyHeld('0'); + if (gameloader.areInOnlineGame() && !legalInPrivate) return; // Don't toggle if in an online game frametracker.onVisualChange(); // Visual change, render the screen this frame em = !em; From 492163d320177528fce6468f5027ce41e12725fd Mon Sep 17 00:00:00 2001 From: Naviary2 Date: Fri, 3 Jan 2025 14:35:34 -0700 Subject: [PATCH 018/131] Polished gameloader.ts a bit more (not done with it) --- src/client/scripts/esm/chess/util/metadata.ts | 2 +- .../scripts/esm/game/chess/gameloader.ts | 31 +++++++------------ .../scripts/esm/game/misc/onlinegame.js | 5 +++ 3 files changed, 17 insertions(+), 21 deletions(-) diff --git a/src/client/scripts/esm/chess/util/metadata.ts b/src/client/scripts/esm/chess/util/metadata.ts index 83920822d..8917c09f1 100644 --- a/src/client/scripts/esm/chess/util/metadata.ts +++ b/src/client/scripts/esm/chess/util/metadata.ts @@ -38,7 +38,7 @@ interface MetaData { /** How many points each side received from the game (e.g. `"1-0"` means white won, `"1/2-1/2"` means a draw) */ Result?: string, /** What caused the game to end, in spoken language. For example, "Time forfeit". This will always be the win condition that concluded the game. */ - Termination: string, + Termination?: string, } // getMetadataOfGame() diff --git a/src/client/scripts/esm/game/chess/gameloader.ts b/src/client/scripts/esm/game/chess/gameloader.ts index b78295a6f..a6db17c3d 100644 --- a/src/client/scripts/esm/game/chess/gameloader.ts +++ b/src/client/scripts/esm/game/chess/gameloader.ts @@ -90,7 +90,7 @@ let inAGame: boolean = false; let typeOfGameWeAreIn: undefined | 'local' | 'online'; -// Functions -------------------------------------------------------------------- +// Getters -------------------------------------------------------------------- /** @@ -111,9 +111,7 @@ function areInOnlineGame(): boolean { } - - - +// Start Game -------------------------------------------------------------------- /** Starts a local game according to the options provided. */ @@ -122,22 +120,19 @@ async function startLocalGame(options: { Variant: string, TimeControl: MetaData['TimeControl'], }) { - // console.log("Starting local game with invite options:"); - // console.log(options); - - // [Event "Casual Space Classic infinite chess game"] [Site "https://www.infinitechess.org/"] [Round "-"] const gameOptions = { metadata: { + ...options, Event: `Casual local ${translations[options.Variant]} infinite chess game`, Site: "https://www.infinitechess.org/", Round: "-", - Variant: options.Variant, - TimeControl: options.TimeControl - } + UTCDate: timeutil.getCurrentUTCDate(), + UTCTime: timeutil.getCurrentUTCTime() + } as MetaData }; - guigameinfo.hidePlayerNames(); - // @ts-ignore + guigameinfo.hidePlayerNames(); // -------------------------- + loadGame(gameOptions, true, true); typeOfGameWeAreIn = 'local'; } @@ -164,16 +159,16 @@ async function startOnlineGame(options: { /** Provide if the game is timed. */ clockValues?: ClockValues, }) { - console.log("Starting online game with invite options:"); - console.log(jsutil.deepCopyObject(options)); + // console.log("Starting online game with invite options:"); + // console.log(jsutil.deepCopyObject(options)); // If the clock values are provided, adjust the timer of whos turn it is depending on ping. if (options.clockValues) options.clockValues = clock.adjustClockValuesForPing(options.clockValues); // Must be set BEFORE loading the game, because the mesh generation relies on the color we are. - onlinegame.setColorAndGameID(options); options.variantOptions = generateVariantOptionsIfReloadingPrivateCustomGame(); const fromWhitePerspective = options.youAreColor === 'white'; + await loadGame(options, fromWhitePerspective, false); typeOfGameWeAreIn = 'online'; @@ -232,10 +227,6 @@ async function loadGame( // console.log("Loading game with game options:"); // console.log(gameOptions); - // If the date is not already specified, set that here. - gameOptions.metadata['UTCDate'] = gameOptions.metadata['UTCDate'] || timeutil.getCurrentUTCDate(); - gameOptions.metadata['UTCTime'] = gameOptions.metadata['UTCTime'] || timeutil.getCurrentUTCTime(); - await gameslot.loadGamefile(gameOptions.metadata, fromWhitePerspective, { // Pass in the pre-existing moves moves: gameOptions.moves, variantOptions: gameOptions.variantOptions, diff --git a/src/client/scripts/esm/game/misc/onlinegame.js b/src/client/scripts/esm/game/misc/onlinegame.js index 2c594ba1d..db6aa7cbb 100644 --- a/src/client/scripts/esm/game/misc/onlinegame.js +++ b/src/client/scripts/esm/game/misc/onlinegame.js @@ -685,6 +685,11 @@ function setColorAndGameID(gameOptions) { * @param {Object} gameOptions - An object that contains the properties `id`, `publicity`, `youAreColor`, `millisUntilAutoAFKResign`, `disconnect`, `serverRestartingAt` */ function initOnlineGame(gameOptions) { + ourColor = gameOptions.youAreColor; + gameID = gameOptions.id; + isPrivate = gameOptions.publicity === 'private'; + gameHasConcluded = false; + rescheduleAlertServerWeAFK(); // If Opponent is currently afk, display that countdown if (gameOptions.millisUntilAutoAFKResign !== undefined) startOpponentAFKCountdown(gameOptions.millisUntilAutoAFKResign); From 17a6ad6ce516355e0aa2b516ad69d3ccd3f37b1e Mon Sep 17 00:00:00 2001 From: Naviary Alt Date: Fri, 3 Jan 2025 17:36:41 -0700 Subject: [PATCH 019/131] Converted jsutil.js to typescript --- .../scripts/esm/chess/logic/movepiece.js | 3 +- .../scripts/esm/chess/util/gamefileutility.js | 3 +- .../scripts/esm/chess/variants/variant.ts | 1 - src/client/scripts/esm/game/chess/game.ts | 1 - .../scripts/esm/game/chess/gameloader.ts | 1 - .../scripts/esm/game/misc/loadbalancer.js | 2 +- .../scripts/esm/game/rendering/spritesheet.ts | 1 - src/client/scripts/esm/util/jsutil.js | 226 ------------------ src/client/scripts/esm/util/jsutil.ts | 214 +++++++++++++++++ src/server/middleware/rateLimit.js | 4 +- src/server/socket/socketUtility.ts | 1 - 11 files changed, 219 insertions(+), 238 deletions(-) delete mode 100644 src/client/scripts/esm/util/jsutil.js create mode 100644 src/client/scripts/esm/util/jsutil.ts diff --git a/src/client/scripts/esm/chess/logic/movepiece.js b/src/client/scripts/esm/chess/logic/movepiece.js index 6a12065d5..4baf5bea5 100644 --- a/src/client/scripts/esm/chess/logic/movepiece.js +++ b/src/client/scripts/esm/chess/logic/movepiece.js @@ -222,8 +222,7 @@ function addPiece(gamefile, type, coords, desiredIndex, { updateData = true } = if (isPieceAtCoords) throw new Error("Can't add a piece on top of another piece!"); // Remove the undefined from the undefineds list - const deleteSuccussful = jsutil.deleteValueFromOrganizedArray(gamefile.ourPieces[type].undefineds, desiredIndex) !== false; - if (!deleteSuccussful) throw new Error("Index to add a piece has an existing piece on it!"); + gamefile.ourPieces[type].undefineds = jsutil.deleteElementFromOrganizedArray(gamefile.ourPieces[type].undefineds, desiredIndex) !== false; list[desiredIndex] = coords; } diff --git a/src/client/scripts/esm/chess/util/gamefileutility.js b/src/client/scripts/esm/chess/util/gamefileutility.js index bf33c0584..f5766a8f4 100644 --- a/src/client/scripts/esm/chess/util/gamefileutility.js +++ b/src/client/scripts/esm/chess/util/gamefileutility.js @@ -224,8 +224,7 @@ function getPieceCount_IncludingUndefineds(gamefile) { function deleteIndexFromPieceList(list, pieceIndex) { list[pieceIndex] = undefined; // Keep track of where the undefined indices are! Have an "undefineds" array property. - const undefinedsInsertIndex = jsutil.binarySearch_findSplitPoint(list.undefineds, pieceIndex); - list.undefineds.splice(undefinedsInsertIndex, 0, pieceIndex); + list.undefineds = jsutil.addElementToOrganizedArray(list.undefineds, pieceIndex); } diff --git a/src/client/scripts/esm/chess/variants/variant.ts b/src/client/scripts/esm/chess/variants/variant.ts index c06e148e5..7c2b1a9dd 100644 --- a/src/client/scripts/esm/chess/variants/variant.ts +++ b/src/client/scripts/esm/chess/variants/variant.ts @@ -14,7 +14,6 @@ import omega4generator from './omega4generator.js'; import colorutil from '../util/colorutil.js'; // @ts-ignore import typeutil from '../util/typeutil.js'; -// @ts-ignore import jsutil from '../../util/jsutil.js'; // @ts-ignore import timeutil from '../../util/timeutil.js'; diff --git a/src/client/scripts/esm/game/chess/game.ts b/src/client/scripts/esm/game/chess/game.ts index b9b0497de..5ca09dd41 100644 --- a/src/client/scripts/esm/game/chess/game.ts +++ b/src/client/scripts/esm/game/chess/game.ts @@ -51,7 +51,6 @@ import dragAnimation from '../rendering/draganimation.js'; import piecesmodel from '../rendering/piecesmodel.js'; // @ts-ignore import loadbalancer from '../misc/loadbalancer.js'; -// @ts-ignore import jsutil from '../../util/jsutil.js'; import highlights from '../rendering/highlights/highlights.js'; import gameslot from './gameslot.js'; diff --git a/src/client/scripts/esm/game/chess/gameloader.ts b/src/client/scripts/esm/game/chess/gameloader.ts index a6db17c3d..b6cb7dd2c 100644 --- a/src/client/scripts/esm/game/chess/gameloader.ts +++ b/src/client/scripts/esm/game/chess/gameloader.ts @@ -26,7 +26,6 @@ import onlinegame from "../misc/onlinegame.js"; import drawoffers from "../misc/drawoffers.js"; // @ts-ignore import localstorage from "../../util/localstorage.js"; -// @ts-ignore import jsutil from "../../util/jsutil.js"; // @ts-ignore import perspective from "../rendering/perspective.js"; diff --git a/src/client/scripts/esm/game/misc/loadbalancer.js b/src/client/scripts/esm/game/misc/loadbalancer.js index ed41577e6..735c54957 100644 --- a/src/client/scripts/esm/game/misc/loadbalancer.js +++ b/src/client/scripts/esm/game/misc/loadbalancer.js @@ -114,7 +114,7 @@ function trimFrames() { const splitPoint = runTime - fpsWindow; // Use binary search to find the split point. - const indexToSplit = jsutil.binarySearch_findValue(frames, splitPoint); + const indexToSplit = jsutil.findIndexOfPointInOrganizedArray(frames, splitPoint); // This will not delete a timestamp if it falls exactly on the split point. frames.splice(0, indexToSplit); diff --git a/src/client/scripts/esm/game/rendering/spritesheet.ts b/src/client/scripts/esm/game/rendering/spritesheet.ts index 095207e10..1bcd606c7 100644 --- a/src/client/scripts/esm/game/rendering/spritesheet.ts +++ b/src/client/scripts/esm/game/rendering/spritesheet.ts @@ -11,7 +11,6 @@ import { generateSpritesheet } from '../../chess/rendering/spritesheetGenerator. import { convertSVGsToImages } from '../../chess/rendering/svgtoimageconverter.js'; // @ts-ignore import typeutil from '../../chess/util/typeutil.js'; -// @ts-ignore import jsutil from '../../util/jsutil.js'; // @ts-ignore import texture from './texture.js'; diff --git a/src/client/scripts/esm/util/jsutil.js b/src/client/scripts/esm/util/jsutil.js deleted file mode 100644 index 15a749dad..000000000 --- a/src/client/scripts/esm/util/jsutil.js +++ /dev/null @@ -1,226 +0,0 @@ - -/** - * This scripts contains utility methods for working with javascript objects. - * - * ZERO dependancies. - */ - -/** - * Deep copies an entire object, no matter how deep its nested. - * No properties will contain references to the source object. - * Use this instead of structuredClone() because of browser support, - * or when that throws an error due to functions contained within the src. - * - * SLOW. Avoid using for very massive objects. - * @param {Object | string | number | bigint | boolean} src - The source object - * @returns {Object | string | number | bigint | boolean} The copied object - */ -function deepCopyObject(src) { - if (typeof src !== "object" || src === null) return src; - - const copy = Array.isArray(src) ? [] : {}; // Create an empty array or object - - for (const key in src) { - const value = src[key]; - copy[key] = deepCopyObject(value); // Recursively copy each property - } - - return copy; // Return the copied object -} - -/** - * Deep copies a Float32Array. - * @param {Float32Array} src - The source Float32Array - * @returns {Float32Array} The copied Float32Array - */ -function copyFloat32Array(src) { - if (!src || !(src instanceof Float32Array)) { - throw new Error('Invalid input: must be a Float32Array'); - } - - const copy = new Float32Array(src.length); - - for (let i = 0; i < src.length; i++) { - copy[i] = src[i]; - } - - return copy; -} - -/** - * Performs a binary search on a sorted array to find the index where a given value could be inserted, - * maintaining the array's sorted order. MUST NOT ALREADY CONTAIN THE VALUE!! - * @param {number[]} sortedArray - The array to search, which must be sorted in ascending order. - * @param {number} value - The value to search for. - * @returns {number} The index where the value can be inserted, maintaining order. - */ -function binarySearch_findSplitPoint(sortedArray, value) { - if (value === undefined) throw new Error('Cannot binary search when value is undefined!'); - - let left = 0; - let right = sortedArray.length - 1; - - while (left <= right) { - const mid = Math.floor((left + right) / 2); - const midValue = sortedArray[mid]; - - if (value < midValue) right = mid - 1; - else if (value > midValue) left = mid + 1; - else if (midValue === value) { - throw new(`Cannot find split point of sortedArray when it already contains the value! ${value}. List: ${JSON.stringify(sortedArray)}`); - } - } - - // The left is the index at which you could insert the new value at the correct location! - return left; -} - -/** - * Calculates the index at which you could insert the given value - * and keep the array organized, OR returns the index of the given value. - * @param {number[]} sortedArray - An Array of NUMBERS. If not all numbers, this will crash. - * @param {number} value - The number to find the split point of, or exact index position of. - * @returns {number} The index - */ -function binarySearch_findValue(sortedArray, value) { - let left = 0; - let right = sortedArray.length - 1; - - while (left <= right) { - const mid = Math.floor((left + right) / 2); - const midValue = sortedArray[mid]; - - if (value < midValue) right = mid - 1; - else if (value > midValue) left = mid + 1; - else if (midValue === value) return mid; - } - - // The left is the index at which you could insert the new value at the correct location! - return left; -} - -// Returns the index if deletion was successful. -// false if not found -function deleteValueFromOrganizedArray(sortedArray, value) { // object can't be an array - - let left = 0; - let right = sortedArray.length - 1; - - while (left <= right) { - const mid = Math.floor((left + right) / 2); - const midValue = sortedArray[mid]; - - if (value === midValue) { - sortedArray.splice(mid, 1); - return mid; - } else if (value < midValue) { // Set the new left - right = mid - 1; - } else if (value > midValue) { - left = mid + 1; - } - } -} - -// Removes specified object from given array. Throws error if it fails. The object cannot be an object or array, only a single value. -function removeObjectFromArray(array, object) { // object can't be an array - const index = array.indexOf(object); - if (index !== -1) array.splice(index, 1); - else throw new Error(`Could not delete object from array, not found! Array: ${JSON.stringify(array)}. Object: ${object}`); -} - -// Returns true if provided object is a float32array -function isFloat32Array(param) { - return param instanceof Float32Array; -} - -/** - * Copies the properties from one object to another, - * without overwriting the existing properties on the destination object. - * @param {Object} objSrc - The source object - * @param {Object} objDest - The destination object - */ -function copyPropertiesToObject(objSrc, objDest) { - const objSrcKeys = Object.keys(objSrc); - for (let i = 0; i < objSrcKeys.length; i++) { - const key = objSrcKeys[i]; - objDest[key] = objSrc[key]; - } -} - -/** - * O(1) method of checking if an object/dict is empty - * I think??? I may be wrong. I think before the first iteration of - * a for-in loop the program still has to calculate the keys... - * @param {Object} obj - * @returns {Boolean} - */ -function isEmpty(obj) { - for (const prop in obj) { - if (Object.prototype.hasOwnProperty.call(obj, prop)) { - return false; - } - } - - return true; -} - -/** - * Tests if a string is in valid JSON format, and can thus be parsed into an object. - * @param {string} str - The string to test - * @returns {boolean} *true* if the string is in valid JSON fromat - */ -function isJson(str) { - try { - JSON.parse(str); - } catch { - return false; - } - return true; -} - -/** - * Returns a new object with the keys being the values of the provided object, and the values being the keys. - * @param {Object} obj - The object to invert - * @returns {Object} The inverted object - */ -function invertObj(obj) { - const inv = {}; - for (const key in obj) { - inv[obj[key]] = key; - } - return inv; -} - -/** - * Checks if array1 contains all the strings that array2 has and returns a list of missing strings. - * @param {string[]} array1 - The first array to check against. - * @param {string[]} array2 - The second array whose elements need to be present in array1. - * @returns {string[]} - Returns an array of missing strings from array1. If none are missing, returns an empty array. - */ -function getMissingStringsFromArray(array1, array2) { - // Convert array1 to a Set for efficient lookup - const set1 = new Set(array1); - const missing = []; - - // Check if each element in array2 is present in set1 - for (const item of array2) { - if (!set1.has(item)) missing.push(item); // If element from array2 is missing in array1, add it to the missing list - } - - return missing; // Return the list of missing strings -} - -export default { - deepCopyObject, - copyFloat32Array, - binarySearch_findSplitPoint, - binarySearch_findValue, - deleteValueFromOrganizedArray, - isFloat32Array, - copyPropertiesToObject, - isEmpty, - isJson, - invertObj, - removeObjectFromArray, - getMissingStringsFromArray, -}; \ No newline at end of file diff --git a/src/client/scripts/esm/util/jsutil.ts b/src/client/scripts/esm/util/jsutil.ts new file mode 100644 index 000000000..a9741f4ff --- /dev/null +++ b/src/client/scripts/esm/util/jsutil.ts @@ -0,0 +1,214 @@ + +/** + * This scripts contains utility methods for working with javascript objects. + * + * ZERO dependancies. + */ + + + +/** All types that are deep-copyable by {@link deepCopyObject} */ +type DeepCopyable = object | string | number | bigint | boolean | null; + +/** + * Deep copies an entire object, no matter how deep its nested. + * No properties will contain references to the source object. + * Use this instead of structuredClone() because of browser support, + * or when that throws an error due to functions contained within the src. + * + * SLOW. Avoid using for very massive objects. + */ +function deepCopyObject(src: DeepCopyable): DeepCopyable { + if (typeof src !== "object" || src === null) return src; + + const copy = Array.isArray(src) ? [] : {}; // Create an empty array or object + + for (const key in src) { + const value = src[key]; + copy[key] = deepCopyObject(value); // Recursively copy each property + } + + return copy; // Return the copied object +} + +/** + * Deep copies a Float32Array. + */ +function copyFloat32Array(src: Float32Array): Float32Array { + if (!src || !(src instanceof Float32Array)) { + throw new Error('Invalid input: must be a Float32Array'); + } + + const copy = new Float32Array(src.length); + + for (let i = 0; i < src.length; i++) { + copy[i] = src[i]; + } + + return copy; +} + +/** + * Searches an organized array and returns an object telling + * you the index the element could be added at for the array to remain + * organized, and whether the element was already found in the array. + * @param sortedArray - The array sorted in ascending order + * @param value - The value to find in the array. + * @returns An object telling you whether the value was found, and the index of that value, or where it can be inserted to remain organized. + */ +function binarySearch(sortedArray: number[], value: number): { found: boolean, index: number } { + let left = 0; + let right = sortedArray.length - 1; + + while (left <= right) { + const mid = Math.floor((left + right) / 2); + const midValue = sortedArray[mid]; + + if (value < midValue) right = mid - 1; + else if (value > midValue) left = mid + 1; + else return { found: true, index: mid }; + } + + // The left is the correct index to insert at, while retaining order! + return { found: false, index: left }; +} + +/** + * Uses binary search to quickly find and insert the given number in the + * organized array. + * + * MUST NOT ALREADY CONTAIN THE VALUE!! + * @param sortedArray - The array to search, which must be sorted in ascending order. + * @param value - The value add in the correct place, retaining order. + * @returns The new array with the sorted element. + */ +function addElementToOrganizedArray(sortedArray: number[], value: number): number[] { + const { found, index } = binarySearch(sortedArray, value); + if (found) throw Error(`Cannot add element to sorted array when it already contains the value! ${value}. List: ${JSON.stringify(sortedArray)}`); + sortedArray.splice(index, 0, value); + return sortedArray; +} + +/** + * Calculates the index in the given organized array at which you could insert + * the point and the array would still be organized. + * @param sortedArray - An array of numbers organized in ascending order. + * @param point - The point in the array to find the index for. + * @returns The index + */ +function findIndexOfPointInOrganizedArray(sortedArray: number[], point: number): number { + return binarySearch(sortedArray, point).index; +} + +/** + * Deletes an element from an organized array. MUST CONTAIN THE ELEMENT. + * @param sortedArray - An array of numbers organized in ascending order. + * @param value - The value to search for and delete + * @returns The new array with the element deleted + */ +function deleteElementFromOrganizedArray(sortedArray: number[], value: number): number[] { + const { found, index } = binarySearch(sortedArray, value); + if (!found) throw Error(`Cannot delete value "${value}" from organized array (not found). Array: ${JSON.stringify(sortedArray)}`); + sortedArray.splice(index, 1); + return sortedArray; +} + +// Removes specified object from given array. Throws error if it fails. The object cannot be an object or array, only a single value. +function removeObjectFromArray(array: Array, object: any) { // object can't be an array + const index = array.indexOf(object); + if (index !== -1) array.splice(index, 1); + else throw Error(`Could not delete object from array, not found! Array: ${JSON.stringify(array)}. Object: ${object}`); +} + +// Returns true if provided object is a float32array +function isFloat32Array(param: any) { + return param instanceof Float32Array; +} + +/** + * Copies the properties from one object to another, + * without overwriting the existing properties on the destination object. + * @param objSrc - The source object + * @param objDest - The destination object + */ +function copyPropertiesToObject(objSrc: object, objDest: object) { + const objSrcKeys = Object.keys(objSrc); + for (let i = 0; i < objSrcKeys.length; i++) { + const key = objSrcKeys[i]; + objDest[key] = objSrc[key]; + } +} + +/** + * O(1) method of checking if an object/dict is empty + * I think??? I may be wrong. I think before the first iteration of + * a for-in loop the program still has to calculate the keys... + */ +function isEmpty(obj: object): boolean { + for (const prop in obj) { + if (Object.prototype.hasOwnProperty.call(obj, prop)) return false; + } + + return true; +} + +/** + * Tests if a string is in valid JSON format, and can thus be parsed into an object. + */ +function isJson(str: string): boolean { + try { + JSON.parse(str); + } catch { + return false; + } + return true; +} + +/** + * Returns a new object with the keys being the values of the provided object, and the values being the keys. + * @param obj - The object to invert + * @returns The inverted object + */ +function invertObj(obj: object): object { + const inv = {}; + for (const key in obj) { + inv[obj[key]] = key; + } + return inv; +} + +/** + * Checks if array1 contains all the strings that array2 has and returns a list of missing strings. + * @param array1 - The first array to check against. + * @param array2 - The second array whose elements need to be present in array1. + * @returns - An array of missing strings from array1. If none are missing, returns an empty array. + */ +function getMissingStringsFromArray(array1: string[], array2: string[]): string[] { + // Convert array1 to a Set for efficient lookup + const set1 = new Set(array1); + const missing = []; + + // Check if each element in array2 is present in set1 + for (const item of array2) { + if (!set1.has(item)) missing.push(item); // If element from array2 is missing in array1, add it to the missing list + } + + return missing; // Return the list of missing strings +} + + + +export default { + deepCopyObject, + copyFloat32Array, + addElementToOrganizedArray, + findIndexOfPointInOrganizedArray, + deleteElementFromOrganizedArray, + isFloat32Array, + copyPropertiesToObject, + isEmpty, + isJson, + invertObj, + removeObjectFromArray, + getMissingStringsFromArray, +}; \ No newline at end of file diff --git a/src/server/middleware/rateLimit.js b/src/server/middleware/rateLimit.js index cd46c9a22..cfe8860be 100644 --- a/src/server/middleware/rateLimit.js +++ b/src/server/middleware/rateLimit.js @@ -200,7 +200,7 @@ setInterval(() => { } // Use binary search to find the index to split at - const indexToSplitAt = jsutil.binarySearch_findValue(timestamps, currentTimeMillis - minuteInMillis); + const indexToSplitAt = jsutil.findIndexOfPointInOrganizedArray(timestamps, currentTimeMillis - minuteInMillis); // Remove all timestamps to the left of the found index timestamps.splice(0, indexToSplitAt); @@ -231,7 +231,7 @@ function countRecentRequests() { setInterval(() => { // Delete recent requests longer than 2 seconds ago const twoSecondsAgo = Date.now() - requestWindowToToggleAttackModeMillis; - const indexToSplitAt = jsutil.binarySearch_findValue(recentRequests, twoSecondsAgo); + const indexToSplitAt = jsutil.findIndexOfPointInOrganizedArray(recentRequests, twoSecondsAgo); recentRequests.splice(0, indexToSplitAt + 1); if (recentRequests.length > requestCapToToggleAttackMode) { diff --git a/src/server/socket/socketUtility.ts b/src/server/socket/socketUtility.ts index 10d67fbbc..efef942dd 100644 --- a/src/server/socket/socketUtility.ts +++ b/src/server/socket/socketUtility.ts @@ -3,7 +3,6 @@ // @ts-ignore import { ensureJSONString } from '../utility/JSONUtils.js'; -// @ts-ignore import jsutil from '../../client/scripts/esm/util/jsutil.js'; From 65f0718c93e9a7dc72b8b8ac1f6f62bc423ea97e Mon Sep 17 00:00:00 2001 From: Naviary Alt Date: Fri, 3 Jan 2025 19:10:08 -0700 Subject: [PATCH 020/131] Converted localstorage.js to typescript --- src/client/scripts/esm/game/main.js | 2 - src/client/scripts/esm/util/localstorage.ts | 105 ++++++++++++++++++++ 2 files changed, 105 insertions(+), 2 deletions(-) create mode 100644 src/client/scripts/esm/util/localstorage.ts diff --git a/src/client/scripts/esm/game/main.js b/src/client/scripts/esm/game/main.js index 2ba310ff3..349f87c97 100644 --- a/src/client/scripts/esm/game/main.js +++ b/src/client/scripts/esm/game/main.js @@ -41,8 +41,6 @@ function start() { onlinegame.askServerIfWeAreInGame(); - localstorage.eraseExpiredItems(); - gameLoop(); // Update & draw the scene repeatedly } diff --git a/src/client/scripts/esm/util/localstorage.ts b/src/client/scripts/esm/util/localstorage.ts new file mode 100644 index 000000000..9c870fcb0 --- /dev/null +++ b/src/client/scripts/esm/util/localstorage.ts @@ -0,0 +1,105 @@ + +/** + * This script handles reading, saving, and deleting expired + * browser local storage data for us! + * Without it, things we save NEVER expire or are deleted. + * (unless the user clears their browser cache) + */ + + +/** An entry in local storage */ +interface Entry { + /** The actual value of the entry */ + value: any, + /** The timestamp the entry will become stale, at which point it should be deleted. */ + expires: number +} + +/** For debugging. This prints to the console all save and delete operations. */ +const printSavesAndDeletes = false; + +const defaultExpiryTimeMillis = 1000 * 60 * 60 * 24; // 24 hours +// const defaultExpiryTimeMillis = 1000 * 20; // 20 seconds + +// Do this on load every time +eraseExpiredItems(); + +/** + * Saves an item in browser local storage + * @param key - The key-name to give this entry. + * @param value - What to save + * @param [expiryMillis] How long until this entry should be auto-deleted for being stale + */ +function saveItem(key: string, value: any, expiryMillis: number = defaultExpiryTimeMillis) { + if (printSavesAndDeletes) console.log(`Saving key to local storage: ${key}`); + const timeExpires = Date.now() + expiryMillis; + const save: Entry = { value, expires: timeExpires }; + const stringifiedSave = JSON.stringify(save); + localStorage.setItem(key, stringifiedSave); +} + +/** + * Loads an item from browser local storage + * @param key - The name/key of the item in storage + * @returns The entry + */ +function loadItem(key: string): any { + const stringifiedSave: string | null = localStorage.getItem(key); // "{ value, expiry }" + if (stringifiedSave === null) return; + let save: Entry | any; + try { + save = JSON.parse(stringifiedSave); // { value, expires } + } catch (e) { // Value wasn't in json format, just delete it. They have to be in json because we always store the 'expiry' property. + deleteItem(key); + return; + } + if (hasItemExpired(save)) { + deleteItem(key); + return; + } + // Not expired... + return save.value; +} + +/** + * Deletes an item from browser local storage + * @param key The name/key of the item in storage + */ +function deleteItem(key: string) { + if (printSavesAndDeletes) console.log(`Deleting local storage item with key '${key}!'`); + localStorage.removeItem(key); +} + +function hasItemExpired(save: Entry | any) { + if (save.expires === undefined) { + console.log(`Local storage item was in an old format. Deleting it! Value: ${JSON.stringify(save)}}`); + return true; + } + return Date.now() >= save.expires; +} + +function eraseExpiredItems() { + const keys = Object.keys(localStorage); + + // if (keys.length > 0) console.log(`Items in local storage: ${JSON.stringify(keys)}`); + + for (const key of keys) { + loadItem(key); // Auto-deletes expired items + } +} + +function eraseAll() { + console.log("Erasing ALL items in local storage..."); + const keys = Object.keys(localStorage); + for (const key of keys) { + deleteItem(key); // Auto-deletes expired items + } +} + +export default { + saveItem, + loadItem, + deleteItem, + eraseExpiredItems, + eraseAll +}; \ No newline at end of file From 8f5ea691e00d5c7fd6c09ae29ac67f40c9998c21 Mon Sep 17 00:00:00 2001 From: Naviary Alt Date: Fri, 3 Jan 2025 19:26:57 -0700 Subject: [PATCH 021/131] Converted docutil.js to typescript --- .../esm/util/{docutil.js => docutil.ts} | 56 ++++++++----------- 1 file changed, 22 insertions(+), 34 deletions(-) rename src/client/scripts/esm/util/{docutil.js => docutil.ts} (66%) diff --git a/src/client/scripts/esm/util/docutil.js b/src/client/scripts/esm/util/docutil.ts similarity index 66% rename from src/client/scripts/esm/util/docutil.js rename to src/client/scripts/esm/util/docutil.ts index 31556df58..65dbb7c20 100644 --- a/src/client/scripts/esm/util/docutil.js +++ b/src/client/scripts/esm/util/docutil.ts @@ -7,9 +7,9 @@ /** * Determines if the current page is running on a local environment (localhost or local IP). - * @returns {boolean} *true* if the page is running locally, *false* otherwise. + * @returns *true* if the page is running locally, *false* otherwise. */ -function isLocalEnvironment() { +function isLocalEnvironment(): boolean { const hostname = window.location.hostname; // Check for common localhost hostnames and local IP ranges @@ -24,9 +24,9 @@ function isLocalEnvironment() { /** * Copies the provided text to the operating system's clipboard. - * @param {string} text - The text to copy + * @param text - The text to copy */ -function copyToClipboard(text) { +function copyToClipboard(text: string) { navigator.clipboard.writeText(text) .then(() => { console.log('Copied to clipboard'); }) .catch((error) => { console.error('Failed to copy to clipboard', error); }); @@ -34,9 +34,8 @@ function copyToClipboard(text) { /** * Returns true if the current device has a mouse pointer. - * @returns {boolean} */ -function isMouseSupported() { +function isMouseSupported(): boolean { // "pointer: coarse" are devices will less pointer accuracy (not "fine" like a mouse) // See W3 documentation: https://www.w3.org/TR/mediaqueries-4/#mf-interaction return window.matchMedia("(pointer: fine)").matches; @@ -44,18 +43,16 @@ function isMouseSupported() { /** * Returns true if the current device supports touch events. - * @returns {boolean} */ -function isTouchSupported() { +function isTouchSupported(): boolean { return 'ontouchstart' in window || navigator.maxTouchPoints > 0 || window.matchMedia("(pointer: coarse)").matches; } /** * Gets the last segment of the current URL without query parameters. * "/member/jacob?lng=en-US" ==> "jacob" - * @returns {string} - The last segment of the URL. */ -function getLastSegmentOfURL() { +function getLastSegmentOfURL(): string { const url = new URL(window.location.href); const pathname = url.pathname; const segments = pathname.split('/'); @@ -65,34 +62,20 @@ function getLastSegmentOfURL() { /** * Extracts the pathname from a given href. * (e.g. "https://www.infinitechess.org/news?lng=en-US" ==> "/news") - * @param {string} href - The href to extract the pathname from. Can be a relative or absolute URL. - * @returns {string} The pathname of the href (e.g., '/news'). + * @param href - The href to extract the pathname from. Can be a relative or absolute URL. + * @returns The pathname of the href (e.g., '/news'). */ -function getPathnameFromHref(href) { +function getPathnameFromHref(href: string) { const url = new URL(href, window.location.origin); return url.pathname; } -/** - * Fetches data from a given endpoint after removing any query parameters from the URL. - * - * @param {string} member - The member identifier to include in the URL. - * @param {Object} config - The configuration object for the fetch request. - * @returns {Promise} - The fetch response promise. - */ -function removeQueryParamsFromLink(link) { - const url = new URL(link, window.location.origin); - // Remove query parameters - url.search = ''; - return url.toString(); -} - /** * Searches the document for the specified cookie, and returns it if found. - * @param {string} cookieName The name of the cookie you would like to retrieve. - * @returns {string | undefined} The cookie, if it exists, otherwise, undefined. + * @param cookieName The name of the cookie you would like to retrieve. + * @returns The cookie, if it exists, otherwise, undefined. */ -function getCookieValue(cookieName) { +function getCookieValue(cookieName: string): string | undefined { const cookieArray = document.cookie.split("; "); for (let i = 0; i < cookieArray.length; i++) { @@ -101,7 +84,13 @@ function getCookieValue(cookieName) { } } -function updateCookie(cookieName, value, days) { +/** + * Sets a cookie in the document + * @param cookieName - The name of the cookie + * @param value - The value of the cookie + * @param days - How many days until the cookie should expire. + */ +function updateCookie(cookieName: string, value: string, days: number) { let expires = ""; if (days) { const date = new Date(); @@ -113,9 +102,9 @@ function updateCookie(cookieName, value, days) { /** * Deletes a document cookie. - * @param {string} cookieName - The name of the cookie you would like to delete. + * @param cookieName - The name of the cookie you would like to delete. */ -function deleteCookie(cookieName) { +function deleteCookie(cookieName: string) { document.cookie = cookieName + '=; Max-Age=-99999999;'; } @@ -126,7 +115,6 @@ export default { isTouchSupported, getLastSegmentOfURL, getPathnameFromHref, - removeQueryParamsFromLink, getCookieValue, updateCookie, deleteCookie, From c1f69c036c6fe338c338b30cd5b6652dde1edf69 Mon Sep 17 00:00:00 2001 From: Naviary Alt Date: Fri, 3 Jan 2025 19:28:21 -0700 Subject: [PATCH 022/131] This should have been deleted --- src/client/scripts/esm/util/localstorage.js | 79 --------------------- 1 file changed, 79 deletions(-) delete mode 100644 src/client/scripts/esm/util/localstorage.js diff --git a/src/client/scripts/esm/util/localstorage.js b/src/client/scripts/esm/util/localstorage.js deleted file mode 100644 index 1e90c71ea..000000000 --- a/src/client/scripts/esm/util/localstorage.js +++ /dev/null @@ -1,79 +0,0 @@ - -'use strict'; - -/** - * This script handles reading, saving, and deleting expired - * browser local storage data for us! - * Without it, things we save NEVER expire or are deleted. - * (unless the user clears their browser cache) - */ - -const printSavesAndDeletes = false; - -const defaultExpiryTimeMillis = 1000 * 60 * 60 * 24; // 24 hours -// const defaultExpiryTimeMillis = 1000 * 20; // 20 seconds - -function saveItem(key, value, expiryMillis = defaultExpiryTimeMillis) { - if (printSavesAndDeletes) console.log(`Saving key to local storage: ${key}`); - const timeExpires = Date.now() + expiryMillis; - const save = { value, expires: timeExpires }; - const stringifiedSave = JSON.stringify(save); - localStorage.setItem(key, stringifiedSave); -} - -function loadItem(key) { - const stringifiedSave = localStorage.getItem(key); // "{ value, expiry }" - if (stringifiedSave === null) return; - let save; - try { - save = JSON.parse(stringifiedSave); // { value, expires } - } catch (e) { // Value wasn't in json format, just delete it. They have to be in json because we always store the 'expiry' property. - deleteItem(key); - return; - } - if (hasItemExpired(save)) { - deleteItem(key); - return; - } - // Not expired... - return save.value; -} - -function deleteItem(key) { - if (printSavesAndDeletes) console.log(`Deleting local storage item with key '${key}!'`); - localStorage.removeItem(key); -} - -function hasItemExpired(save) { - if (save.expires === undefined) { - console.log(`Local storage item was in an old format. Deleting it! Value: ${JSON.stringify(save)}}`); - return true; - } - return Date.now() >= save.expires; -} - -function eraseExpiredItems() { - const keys = Object.keys(localStorage); - - // if (keys.length > 0) console.log(`Items in local storage: ${JSON.stringify(keys)}`); - - for (const key of keys) { - loadItem(key); // Auto-deletes expired items - } -} - -function eraseAll() { - console.log("Erasing ALL items in local storage..."); - const keys = Object.keys(localStorage); - for (const key of keys) { - deleteItem(key); // Auto-deletes expired items - } -} - -export default { - saveItem, - loadItem, - deleteItem, - eraseExpiredItems, - eraseAll -}; \ No newline at end of file From 12d87a1846a7ff16d3af8dc2d64e2ae4f5c7beea Mon Sep 17 00:00:00 2001 From: Naviary2 Date: Sat, 4 Jan 2025 00:12:23 -0700 Subject: [PATCH 023/131] Fixed crashes --- .../scripts/esm/chess/logic/movepiece.js | 5 +- .../scripts/esm/chess/logic/movesets.ts | 3 +- .../scripts/esm/chess/variants/variant.ts | 2 +- .../scripts/esm/game/misc/loadbalancer.js | 2 +- src/client/scripts/esm/util/jsutil.ts | 63 +++++++++---------- 5 files changed, 35 insertions(+), 40 deletions(-) diff --git a/src/client/scripts/esm/chess/logic/movepiece.js b/src/client/scripts/esm/chess/logic/movepiece.js index 4baf5bea5..1672925da 100644 --- a/src/client/scripts/esm/chess/logic/movepiece.js +++ b/src/client/scripts/esm/chess/logic/movepiece.js @@ -27,6 +27,7 @@ import gameloader from '../../game/chess/gameloader.js'; * Type Definitions * @typedef {import('./gamefile.js').gamefile} gamefile * @typedef {import('../util/moveutil.js').Move} Move + * @typedef {import('../util/coordutil.js').Coords} Coords */ "use strict"; @@ -40,7 +41,7 @@ import gameloader from '../../game/chess/gameloader.js'; * The Piece Object. * @typedef {Object} Piece * @property {string} type - The type of the piece (e.g. `queensW`). - * @property {[number,number]} coords - The coordinates of the piece: `[x,y]` + * @property {Coords} coords - The coordinates of the piece: `[x,y]` * @property {number} index - The index of the piece within the gamefile's piece list. */ @@ -222,7 +223,7 @@ function addPiece(gamefile, type, coords, desiredIndex, { updateData = true } = if (isPieceAtCoords) throw new Error("Can't add a piece on top of another piece!"); // Remove the undefined from the undefineds list - gamefile.ourPieces[type].undefineds = jsutil.deleteElementFromOrganizedArray(gamefile.ourPieces[type].undefineds, desiredIndex) !== false; + gamefile.ourPieces[type].undefineds = jsutil.deleteElementFromOrganizedArray(gamefile.ourPieces[type].undefineds, desiredIndex); list[desiredIndex] = coords; } diff --git a/src/client/scripts/esm/chess/logic/movesets.ts b/src/client/scripts/esm/chess/logic/movesets.ts index 9cce72fca..1d7f556c1 100644 --- a/src/client/scripts/esm/chess/logic/movesets.ts +++ b/src/client/scripts/esm/chess/logic/movesets.ts @@ -97,8 +97,7 @@ type BlockingFunction = (friendlyColor: string, blockingPiece: Piece, gamefile?: /** The default blocking function of each piece's sliding moves, if not specified. */ -// eslint-disable-next-line no-unused-vars -function defaultBlockingFunction(friendlyColor: string, blockingPiece: Piece, gamefile?: gamefile): 0 | 1 | 2 { +function defaultBlockingFunction(friendlyColor: string, blockingPiece: Piece): 0 | 1 | 2 { const colorOfBlockingPiece = colorutil.getPieceColorFromType(blockingPiece.type); const isVoid = blockingPiece.type === 'voidsN'; if (friendlyColor === colorOfBlockingPiece || isVoid) return 1; // Block where it is if it is a friendly OR a void square. diff --git a/src/client/scripts/esm/chess/variants/variant.ts b/src/client/scripts/esm/chess/variants/variant.ts index 7c2b1a9dd..e7b37d1e0 100644 --- a/src/client/scripts/esm/chess/variants/variant.ts +++ b/src/client/scripts/esm/chess/variants/variant.ts @@ -508,7 +508,7 @@ function getMovesets(movesetModifications: Movesets = {}, defaultSlideLimitForOl } = {}; for (const [piece, moves] of Object.entries(origMoveset)) { - pieceMovesets[piece] = movesetModifications[piece] ? () => jsutil.deepCopyObject(movesetModifications[piece]) + pieceMovesets[piece] = movesetModifications[piece] ? () => jsutil.deepCopyObject(movesetModifications[piece]!) : () => jsutil.deepCopyObject(moves); } diff --git a/src/client/scripts/esm/game/misc/loadbalancer.js b/src/client/scripts/esm/game/misc/loadbalancer.js index 735c54957..e96b04067 100644 --- a/src/client/scripts/esm/game/misc/loadbalancer.js +++ b/src/client/scripts/esm/game/misc/loadbalancer.js @@ -108,7 +108,7 @@ function updateDeltaTime(runtime) { lastFrameTime = runTime; } -// Deletes frame timestamps from out list over 1 second ago +// Deletes frame timestamps from our list over 1 second ago function trimFrames() { // What time was it 1 second ago const splitPoint = runTime - fpsWindow; diff --git a/src/client/scripts/esm/util/jsutil.ts b/src/client/scripts/esm/util/jsutil.ts index a9741f4ff..d61c09896 100644 --- a/src/client/scripts/esm/util/jsutil.ts +++ b/src/client/scripts/esm/util/jsutil.ts @@ -5,11 +5,6 @@ * ZERO dependancies. */ - - -/** All types that are deep-copyable by {@link deepCopyObject} */ -type DeepCopyable = object | string | number | bigint | boolean | null; - /** * Deep copies an entire object, no matter how deep its nested. * No properties will contain references to the source object. @@ -18,17 +13,17 @@ type DeepCopyable = object | string | number | bigint | boolean | null; * * SLOW. Avoid using for very massive objects. */ -function deepCopyObject(src: DeepCopyable): DeepCopyable { +function deepCopyObject(src: T): T { if (typeof src !== "object" || src === null) return src; - const copy = Array.isArray(src) ? [] : {}; // Create an empty array or object + const copy: any = Array.isArray(src) ? [] : {}; // Create an empty array or object for (const key in src) { const value = src[key]; copy[key] = deepCopyObject(value); // Recursively copy each property } - return copy; // Return the copied object + return copy as T; // Return the copied object } /** @@ -42,7 +37,7 @@ function copyFloat32Array(src: Float32Array): Float32Array { const copy = new Float32Array(src.length); for (let i = 0; i < src.length; i++) { - copy[i] = src[i]; + copy[i]! = src[i]!; } return copy; @@ -57,20 +52,20 @@ function copyFloat32Array(src: Float32Array): Float32Array { * @returns An object telling you whether the value was found, and the index of that value, or where it can be inserted to remain organized. */ function binarySearch(sortedArray: number[], value: number): { found: boolean, index: number } { - let left = 0; - let right = sortedArray.length - 1; + let left = 0; + let right = sortedArray.length - 1; - while (left <= right) { - const mid = Math.floor((left + right) / 2); - const midValue = sortedArray[mid]; + while (left <= right) { + const mid = Math.floor((left + right) / 2); + const midValue = sortedArray[mid]!; - if (value < midValue) right = mid - 1; - else if (value > midValue) left = mid + 1; - else return { found: true, index: mid }; - } + if (value < midValue) right = mid - 1; + else if (value > midValue) left = mid + 1; + else return { found: true, index: mid }; + } // The left is the correct index to insert at, while retaining order! - return { found: false, index: left }; + return { found: false, index: left }; } /** @@ -83,10 +78,10 @@ function binarySearch(sortedArray: number[], value: number): { found: boolean, i * @returns The new array with the sorted element. */ function addElementToOrganizedArray(sortedArray: number[], value: number): number[] { - const { found, index } = binarySearch(sortedArray, value); - if (found) throw Error(`Cannot add element to sorted array when it already contains the value! ${value}. List: ${JSON.stringify(sortedArray)}`); - sortedArray.splice(index, 0, value); - return sortedArray; + const { found, index } = binarySearch(sortedArray, value); + if (found) throw Error(`Cannot add element to sorted array when it already contains the value! ${value}. List: ${JSON.stringify(sortedArray)}`); + sortedArray.splice(index, 0, value); + return sortedArray; } /** @@ -107,14 +102,14 @@ function findIndexOfPointInOrganizedArray(sortedArray: number[], point: number): * @returns The new array with the element deleted */ function deleteElementFromOrganizedArray(sortedArray: number[], value: number): number[] { - const { found, index } = binarySearch(sortedArray, value); - if (!found) throw Error(`Cannot delete value "${value}" from organized array (not found). Array: ${JSON.stringify(sortedArray)}`); - sortedArray.splice(index, 1); - return sortedArray; + const { found, index } = binarySearch(sortedArray, value); + if (!found) throw Error(`Cannot delete value "${value}" from organized array (not found). Array: ${JSON.stringify(sortedArray)}`); + sortedArray.splice(index, 1); + return sortedArray; } // Removes specified object from given array. Throws error if it fails. The object cannot be an object or array, only a single value. -function removeObjectFromArray(array: Array, object: any) { // object can't be an array +function removeObjectFromArray(array: any[], object: any) { // object can't be an array const index = array.indexOf(object); if (index !== -1) array.splice(index, 1); else throw Error(`Could not delete object from array, not found! Array: ${JSON.stringify(array)}. Object: ${object}`); @@ -131,10 +126,10 @@ function isFloat32Array(param: any) { * @param objSrc - The source object * @param objDest - The destination object */ -function copyPropertiesToObject(objSrc: object, objDest: object) { +function copyPropertiesToObject(objSrc: Record, objDest: Record) { const objSrcKeys = Object.keys(objSrc); for (let i = 0; i < objSrcKeys.length; i++) { - const key = objSrcKeys[i]; + const key = objSrcKeys[i]!; objDest[key] = objSrc[key]; } } @@ -169,10 +164,10 @@ function isJson(str: string): boolean { * @param obj - The object to invert * @returns The inverted object */ -function invertObj(obj: object): object { - const inv = {}; +function invertObj(obj: Record): Record { + const inv: Record = {}; for (const key in obj) { - inv[obj[key]] = key; + inv[obj[key]!] = key; } return inv; } @@ -186,7 +181,7 @@ function invertObj(obj: object): object { function getMissingStringsFromArray(array1: string[], array2: string[]): string[] { // Convert array1 to a Set for efficient lookup const set1 = new Set(array1); - const missing = []; + const missing: string[] = []; // Check if each element in array2 is present in set1 for (const item of array2) { From 91a7e1e9a9cd8331d083488343337ed8476fc8a4 Mon Sep 17 00:00:00 2001 From: Naviary2 Date: Sat, 4 Jan 2025 00:23:12 -0700 Subject: [PATCH 024/131] Fixed crashes --- src/client/scripts/esm/util/docutil.ts | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/src/client/scripts/esm/util/docutil.ts b/src/client/scripts/esm/util/docutil.ts index 65dbb7c20..1ae0a13c8 100644 --- a/src/client/scripts/esm/util/docutil.ts +++ b/src/client/scripts/esm/util/docutil.ts @@ -18,7 +18,7 @@ function isLocalEnvironment(): boolean { hostname === '127.0.0.1' || // Loopback IP address hostname.startsWith('192.168.') || // Private IPv4 address space hostname.startsWith('10.') || // Private IPv4 address space - hostname.startsWith('172.') && parseInt(hostname.split('.')[1], 10) >= 16 && parseInt(hostname.split('.')[1], 10) <= 31 // Private IPv4 address space + hostname.startsWith('172.') && parseInt(hostname.split('.')[1]!, 10) >= 16 && parseInt(hostname.split('.')[1]!, 10) <= 31 // Private IPv4 address space ); } @@ -55,8 +55,8 @@ function isTouchSupported(): boolean { function getLastSegmentOfURL(): string { const url = new URL(window.location.href); const pathname = url.pathname; - const segments = pathname.split('/'); - return segments[segments.length - 1] || segments[segments.length - 2]; // Handle situation if trailing '/' is present + const segments = pathname.split('/').filter(Boolean); // Remove empty segments caused by leading/trailing slashes + return segments[segments.length - 1] ?? ''; // Fallback to an empty string if no segment exists } /** @@ -79,9 +79,11 @@ function getCookieValue(cookieName: string): string | undefined { const cookieArray = document.cookie.split("; "); for (let i = 0; i < cookieArray.length; i++) { - const cookiePair = cookieArray[i].split("="); + const cookiePair = cookieArray[i]!.split("="); if (cookiePair[0] === cookieName) return cookiePair[1]; } + + return; // Typescript is angry without this } /** From 95d580b39d22916ee2df70f765e29a5a9dab6c9f Mon Sep 17 00:00:00 2001 From: Naviary2 Date: Sun, 5 Jan 2025 22:27:33 -0700 Subject: [PATCH 025/131] Deleted en-US.unknown_action_received_1 and unknown_action_received_2 keys --- translation/changes.json | 4 ++++ translation/en-US.toml | 4 +--- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/translation/changes.json b/translation/changes.json index ed7be2554..0f5dfef4d 100644 --- a/translation/changes.json +++ b/translation/changes.json @@ -1,4 +1,8 @@ { + "28": { + "note": "Deleted unknown_action_received_1 & unknown_action_received_2 on lines 346-347.", + "changes": [] + }, "27": { "note": "Added index.github_title and index.javascript.contribution_count, lines 57-60", "changes": [] diff --git a/translation/en-US.toml b/translation/en-US.toml index 68a7f543e..8a1c00473 100644 --- a/translation/en-US.toml +++ b/translation/en-US.toml @@ -1,6 +1,6 @@ name = "English" # Name of language direction = "ltr" # Change to "rtl" for right to left languages -version = "27" +version = "28" maintainer = "Naviary" [header] @@ -343,8 +343,6 @@ regenerated_pieces = "Regenerated pieces." [play.javascript.invites] move_mouse = "Move the mouse to reconnect." -unknown_action_received_1 = "Unknown action" -unknown_action_received_2 = "received from the server in the invites subscription!" cannot_cancel = "Cannot cancel invite of undefined ID." you_indicator = "(You)" you_are_white = "You're: White" From 0cdf3e1c1f72ed8c882b6e4bc85d6782342fa392 Mon Sep 17 00:00:00 2001 From: Naviary2 Date: Sun, 5 Jan 2025 23:15:25 -0700 Subject: [PATCH 026/131] Deleted play.footer in the TOML --- translation/changes.json | 5 ++++- translation/en-US.toml | 5 ----- 2 files changed, 4 insertions(+), 6 deletions(-) diff --git a/translation/changes.json b/translation/changes.json index 0f5dfef4d..082bf2481 100644 --- a/translation/changes.json +++ b/translation/changes.json @@ -1,6 +1,9 @@ { "28": { - "note": "Deleted unknown_action_received_1 & unknown_action_received_2 on lines 346-347.", + "note": [ + "Deleted unknown_action_received_1 & unknown_action_received_2 on lines 346-347", + "Deleted all of play.footer, lines 264-267" + ], "changes": [] }, "27": { diff --git a/translation/en-US.toml b/translation/en-US.toml index 8a1c00473..64ea4b01a 100644 --- a/translation/en-US.toml +++ b/translation/en-US.toml @@ -261,11 +261,6 @@ rewind_move = "Rewind move" forward_move = "Forward move" pause = "Pause" -[play.footer] -white_to_move = "White to move" -player_white = "Player white" -player_black = "Player black" - [play.pause] title = "Paused" resume = "Resume" From 072c1281167cc6b119fff0dc223a576be8e94817 Mon Sep 17 00:00:00 2001 From: Naviary2 Date: Sun, 5 Jan 2025 23:18:10 -0700 Subject: [PATCH 027/131] Added generic player names for white and black in the TOML --- translation/changes.json | 3 ++- translation/en-US.toml | 2 ++ 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/translation/changes.json b/translation/changes.json index 082bf2481..f7a045af3 100644 --- a/translation/changes.json +++ b/translation/changes.json @@ -2,7 +2,8 @@ "28": { "note": [ "Deleted unknown_action_received_1 & unknown_action_received_2 on lines 346-347", - "Deleted all of play.footer, lines 264-267" + "Deleted all of play.footer, lines 264-267", + "Added player_name_white_generic and player_name_black_generic, lines 280 & 281" ], "changes": [] }, diff --git a/translation/en-US.toml b/translation/en-US.toml index 64ea4b01a..914ea05e8 100644 --- a/translation/en-US.toml +++ b/translation/en-US.toml @@ -277,6 +277,8 @@ question = "Accept draw offer?" [play.javascript] # Not text that's included in the html, but text that scripts use! guest_indicator = "(Guest)" you_indicator = "(You)" +player_name_white_generic = "White" +player_name_black_generic = "Black" white_to_move = "White to move" black_to_move = "Black to move" your_move = "Your move" From 87570d741697b7cd4e0b0db802cd650679fcc85d Mon Sep 17 00:00:00 2001 From: Naviary2 Date: Tue, 7 Jan 2025 14:34:07 -0700 Subject: [PATCH 028/131] Split onlinegame.js into several smaller files --- src/client/css/play.css | 18 +- src/client/scripts/esm/chess/logic/clock.ts | 5 +- .../scripts/esm/chess/logic/legalmoves.js | 2 +- .../scripts/esm/chess/logic/movepiece.js | 3 +- .../scripts/esm/chess/util/colorutil.ts | 2 +- .../scripts/esm/game/chess/copypastegame.js | 8 +- src/client/scripts/esm/game/chess/game.ts | 38 +- .../scripts/esm/game/chess/gameloader.ts | 65 +- src/client/scripts/esm/game/chess/gameslot.ts | 4 +- .../scripts/esm/game/chess/selection.js | 12 +- src/client/scripts/esm/game/gui/guiclock.js | 10 +- .../scripts/esm/game/gui/guidrawoffer.js | 29 +- .../gui/{guigameinfo.js => guigameinfo.ts} | 124 ++- .../scripts/esm/game/gui/guinavigation.ts | 29 +- src/client/scripts/esm/game/gui/guipause.js | 6 +- src/client/scripts/esm/game/input.js | 4 +- src/client/scripts/esm/game/main.js | 2 +- .../scripts/esm/game/misc/drawoffers.js | 12 +- .../scripts/esm/game/misc/loadbalancer.js | 2 +- .../esm/game/misc/localgame/localgame.ts | 21 + .../scripts/esm/game/misc/onlinegame.js | 900 ------------------ .../scripts/esm/game/misc/onlinegame/afk.ts | 214 +++++ .../esm/game/misc/onlinegame/disconnect.ts | 71 ++ .../esm/game/misc/onlinegame/onlinegame.ts | 475 +++++++++ .../esm/game/misc/onlinegame/serverrestart.ts | 70 ++ .../esm/game/misc/onlinegame/tabnameflash.ts | 85 ++ .../scripts/esm/game/misc/onlinegamerouter.ts | 405 ++++++++ .../scripts/esm/game/rendering/arrows.js | 4 +- .../scripts/esm/game/rendering/options.js | 6 +- .../scripts/esm/game/rendering/perspective.js | 2 +- .../scripts/esm/game/rendering/pieces.js | 4 +- src/client/scripts/esm/game/websocket.js | 26 +- src/client/scripts/esm/util/jsutil.ts | 57 ++ src/client/views/play.ejs | 127 ++- src/server/game/gamemanager/gameutility.js | 5 +- 35 files changed, 1696 insertions(+), 1151 deletions(-) rename src/client/scripts/esm/game/gui/{guigameinfo.js => guigameinfo.ts} (73%) create mode 100644 src/client/scripts/esm/game/misc/localgame/localgame.ts delete mode 100644 src/client/scripts/esm/game/misc/onlinegame.js create mode 100644 src/client/scripts/esm/game/misc/onlinegame/afk.ts create mode 100644 src/client/scripts/esm/game/misc/onlinegame/disconnect.ts create mode 100644 src/client/scripts/esm/game/misc/onlinegame/onlinegame.ts create mode 100644 src/client/scripts/esm/game/misc/onlinegame/serverrestart.ts create mode 100644 src/client/scripts/esm/game/misc/onlinegame/tabnameflash.ts create mode 100644 src/client/scripts/esm/game/misc/onlinegamerouter.ts diff --git a/src/client/css/play.css b/src/client/css/play.css index 1d53785a2..5e231e0b7 100644 --- a/src/client/css/play.css +++ b/src/client/css/play.css @@ -748,7 +748,7 @@ button.join-button, button.copy-button { /* Top Navigation: Zoom buttons, coordinates, rewind/forward game, pause */ -.navigation { +.navigation-bar { position: absolute; top: 0; width: 100%; @@ -828,7 +828,7 @@ button.join-button, button.copy-button { margin: 0; } -.navigation .button { +.navigation-bar .button { position: relative; width: 0.74em; height: 0.74em; @@ -841,28 +841,28 @@ button.join-button, button.copy-button { -webkit-tap-highlight-color: transparent; /* No more blue highlight when tapping buttons on mobile */ } -.navigation .button:hover { +.navigation-bar .button:hover { transform: scale(1.07); } -.navigation .button:active { +.navigation-bar .button:active { transform: scale(1); } -.navigation svg { +.navigation-bar svg { position: absolute; } /* Start shrinking top navigation bar */ @media only screen and (max-width: 700px) { /* 700px needs to be updated within camera.updatePIXEL_HEIGHT_OF_NAVS() !!! */ - .navigation { + .navigation-bar { font-size: 12vw; /* Update with doc!! */ } } /* Small screens. HIDE the coords and make the buttons bigger! */ @media only screen and (max-width: 550px) { /* 550 needs to be updated within camera.updatePIXEL_HEIGHT_OF_NAVS() !!! */ - .navigation { + .navigation-bar { justify-content: space-between; font-size: 66px; /* Update with doc!! */ } @@ -874,7 +874,7 @@ button.join-button, button.copy-button { /* Mobile screen, start shrinking the size again */ @media only screen and (max-width: 368px) { /* 368 needs to be updated within camera.updatePIXEL_HEIGHT_OF_NAVS() !!! */ - .navigation { + .navigation-bar { font-size: 17.9vw; /* Update with doc!! */ } } @@ -883,7 +883,7 @@ button.join-button, button.copy-button { /* Bottom Navigation: Color to move, clocks, player names, draw offer UI */ -.footer { +.game-info-bar { position: absolute; bottom: 0; width: 100%; diff --git a/src/client/scripts/esm/chess/logic/clock.ts b/src/client/scripts/esm/chess/logic/clock.ts index da698f32e..2a7a764c9 100644 --- a/src/client/scripts/esm/chess/logic/clock.ts +++ b/src/client/scripts/esm/chess/logic/clock.ts @@ -25,6 +25,7 @@ import gameloader from '../../game/chess/gameloader.js'; // @ts-ignore import type gamefile from './gamefile.js'; +import onlinegame from '../../game/misc/onlinegame/onlinegame.js'; /** An object containg the values of each color's clock, and which one is currently counting down, if any. */ interface ClockValues { @@ -142,7 +143,7 @@ function adjustClockValuesForPing(clockValues: ClockValues): ClockValues { */ function push(gamefile: gamefile) { const clocks = gamefile.clocks; - if (gameloader.areInOnlineGame()) return; // Only the server can push clocks + if (onlinegame.areInOnlineGame()) return; // Only the server can push clocks if (clocks.untimed) return; if (!moveutil.isGameResignable(gamefile)) return; // Don't push unless atleast 2 moves have been played @@ -179,7 +180,7 @@ function update(gamefile: gamefile): string | undefined { clocks.currentTime[clocks.colorTicking] = Math.ceil(clocks.timeRemainAtTurnStart! - timePassedSinceTurnStart); // Has either clock run out of time? - if (gameloader.areInOnlineGame()) return; // Don't conclude game by time if in an online game, only the server does that. + if (onlinegame.areInOnlineGame()) return; // Don't conclude game by time if in an online game, only the server does that. for (const [color,time] of Object.entries(clocks.currentTime)) { if (time as number <= 0) { diff --git a/src/client/scripts/esm/chess/logic/legalmoves.js b/src/client/scripts/esm/chess/logic/legalmoves.js index 0b129e46e..e0bd7cbc4 100644 --- a/src/client/scripts/esm/chess/logic/legalmoves.js +++ b/src/client/scripts/esm/chess/logic/legalmoves.js @@ -295,7 +295,7 @@ function checkIfMoveLegal(legalMoves, startCoords, endCoords, { ignoreIndividual * Tests if the provided move is legal to play in this game. * This accounts for the piece color AND legal promotions, AND their claimed game conclusion. * @param {gamefile} gamefile - The gamefile - * @param {Move} move - The move, with the bare minimum properties: `{ startCoords, endCoords, promotion }` + * @param {Move | undefined} move - The move, with the bare minimum properties: `{ startCoords, endCoords, promotion }` * @returns {boolean | string} *true* If the move is legal, otherwise a string containing why it is illegal. */ function isOpponentsMoveLegal(gamefile, move, claimedGameConclusion) { diff --git a/src/client/scripts/esm/chess/logic/movepiece.js b/src/client/scripts/esm/chess/logic/movepiece.js index 1672925da..07517b5f5 100644 --- a/src/client/scripts/esm/chess/logic/movepiece.js +++ b/src/client/scripts/esm/chess/logic/movepiece.js @@ -21,6 +21,7 @@ import frametracker from '../../game/rendering/frametracker.js'; import stats from '../../game/gui/stats.js'; import gameslot from '../../game/chess/gameslot.js'; import gameloader from '../../game/chess/gameloader.js'; +import onlinegame from '../../game/misc/onlinegame/onlinegame.js'; // Import End /** @@ -94,7 +95,7 @@ function makeMove(gamefile, move, { flipTurn = true, recordMove = true, pushCloc updateInCheck(gamefile, recordMove); if (doGameOverChecks) { gamefileutility.doGameOverChecks(gamefile); - if (!simulated && concludeGameIfOver && gamefile.gameConclusion && !gameloader.areInOnlineGame()) gameslot.concludeGame(); + if (!simulated && concludeGameIfOver && gamefile.gameConclusion && !onlinegame.areInOnlineGame()) gameslot.concludeGame(); } if (updateData) { diff --git a/src/client/scripts/esm/chess/util/colorutil.ts b/src/client/scripts/esm/chess/util/colorutil.ts index 36f518f47..867b6401d 100644 --- a/src/client/scripts/esm/chess/util/colorutil.ts +++ b/src/client/scripts/esm/chess/util/colorutil.ts @@ -93,7 +93,7 @@ function getColorFromExtension(colorExtention: string): string { * @param {string} color - "White" / "Black" * @returns {string} The opposite color, "White" / "Black" */ -function getOppositeColor(color: string): string { +function getOppositeColor(color: string): 'white' | 'black' { if (color === 'white') return 'black'; else if (color === 'black') return 'white'; else throw new Error(`Cannot return the opposite color of color ${color}!`); diff --git a/src/client/scripts/esm/game/chess/copypastegame.js b/src/client/scripts/esm/game/chess/copypastegame.js index 9823635e5..d19b738db 100644 --- a/src/client/scripts/esm/game/chess/copypastegame.js +++ b/src/client/scripts/esm/game/chess/copypastegame.js @@ -5,7 +5,7 @@ */ // Import Start -import onlinegame from '../misc/onlinegame.js'; +import onlinegame from '../misc/onlinegame/onlinegame.js'; import localstorage from '../../util/localstorage.js'; import formatconverter from '../../chess/logic/formatconverter.js'; import backcompatible from '../../chess/logic/backcompatible.js'; @@ -103,10 +103,10 @@ async function callbackPaste(event) { if (guinavigation.isCoordinateActive()) return; // Make sure we're not in a public match - if (gameloader.areInOnlineGame() && !onlinegame.getIsPrivate()) return statustext.showStatus(translations.copypaste.cannot_paste_in_public); + if (onlinegame.areInOnlineGame() && !onlinegame.getIsPrivate()) return statustext.showStatus(translations.copypaste.cannot_paste_in_public); // Make sure it's legal in a private match - if (gameloader.areInOnlineGame() && onlinegame.getIsPrivate() && gameslot.getGamefile().moves.length > 0) return statustext.showStatus(translations.copypaste.cannot_paste_after_moves); + if (onlinegame.areInOnlineGame() && onlinegame.getIsPrivate() && gameslot.getGamefile().moves.length > 0) return statustext.showStatus(translations.copypaste.cannot_paste_after_moves); // Do we have clipboard permission? let clipboard; @@ -256,7 +256,7 @@ async function pasteGame(longformat) { // game: { startingPosition (key-list), p gameRules: longformat.gameRules }; - if (gameloader.areInOnlineGame() && onlinegame.getIsPrivate()) { + if (onlinegame.areInOnlineGame() && onlinegame.getIsPrivate()) { // Playing a custom private game! Save the pasted position in browser // storage so that we can remember it upon refreshing. const gameID = onlinegame.getGameID(); diff --git a/src/client/scripts/esm/game/chess/game.ts b/src/client/scripts/esm/game/chess/game.ts index 5ca09dd41..818b1d121 100644 --- a/src/client/scripts/esm/game/chess/game.ts +++ b/src/client/scripts/esm/game/chess/game.ts @@ -5,9 +5,18 @@ * And contains our main update() and render() methods */ + // @ts-ignore -import onlinegame from '../misc/onlinegame.js'; +import type gamefile from '../../chess/logic/gamefile.js'; + + import gui from '../gui/gui.js'; +import jsutil from '../../util/jsutil.js'; +import highlights from '../rendering/highlights/highlights.js'; +import gameslot from './gameslot.js'; +import guinavigation from '../gui/guinavigation.js'; +// @ts-ignore +import onlinegame from '../misc/onlinegame/onlinegame.js'; // @ts-ignore import arrows from '../rendering/arrows.js'; // @ts-ignore @@ -51,17 +60,11 @@ import dragAnimation from '../rendering/draganimation.js'; import piecesmodel from '../rendering/piecesmodel.js'; // @ts-ignore import loadbalancer from '../misc/loadbalancer.js'; -import jsutil from '../../util/jsutil.js'; -import highlights from '../rendering/highlights/highlights.js'; -import gameslot from './gameslot.js'; -import guinavigation from '../gui/guinavigation.js'; - - -// Type Definitions ------------------------------------------------------------------------------- - - // @ts-ignore -import type gamefile from '../../chess/logic/gamefile.js'; +import camera from '../rendering/camera.js'; +// @ts-ignore +import guigameinfo from '../gui/guigameinfo.js'; +import gameloader from './gameloader.js'; // Functions ------------------------------------------------------------------------------- @@ -88,14 +91,17 @@ function update() { if (!guinavigation.isCoordinateActive()) { if (input.isKeyDown('`')) options.toggleDeveloperMode(); - if (input.isKeyDown('2')) console.log(jsutil.deepCopyObject(gamefile)); + if (input.isKeyDown('2')) { + console.log(jsutil.deepCopyObject(gamefile)); + console.log('Estimated gamefile memory usage: ' + jsutil.estimateMemorySizeOf(gamefile)); + } if (input.isKeyDown('m')) options.toggleFPS(); if (gamefile.mesh.locked && input.isKeyDown('z')) loadbalancer.setForceCalc(true); } updateBoard(gamefile); // Other screen, board is visible, update everything board related - onlinegame.update(); + gameloader.update(); // Updates whatever game is currently loaded. guinavigation.updateElement_Coords(); // Update the division on the screen displaying your current coordinates } @@ -113,7 +119,11 @@ function updateBoard(gamefile: gamefile) { if (input.isKeyDown('escape')) guipause.toggle(); if (input.isKeyDown('tab')) guipause.callback_TogglePointers(); if (input.isKeyDown('r')) piecesmodel.regenModel(gamefile, options.getPieceRegenColorArgs(), true); - if (input.isKeyDown('n')) guinavigation.toggleNavigationBar(); + if (input.isKeyDown('n')) { + guinavigation.toggle(); + guigameinfo.toggle(); + camera.updatePIXEL_HEIGHT_OF_NAVS(); + } } const timeWinner = clock.update(gamefile); diff --git a/src/client/scripts/esm/game/chess/gameloader.ts b/src/client/scripts/esm/game/chess/gameloader.ts index b6cb7dd2c..7b90cc524 100644 --- a/src/client/scripts/esm/game/chess/gameloader.ts +++ b/src/client/scripts/esm/game/chess/gameloader.ts @@ -21,12 +21,11 @@ import guinavigation from "../gui/guinavigation.js"; // @ts-ignore import sound from '../misc/sound.js'; // @ts-ignore -import onlinegame from "../misc/onlinegame.js"; +import onlinegame from "../misc/onlinegame/onlinegame.js"; // @ts-ignore import drawoffers from "../misc/drawoffers.js"; // @ts-ignore import localstorage from "../../util/localstorage.js"; -import jsutil from "../../util/jsutil.js"; // @ts-ignore import perspective from "../rendering/perspective.js"; import gui from "../gui/gui.js"; @@ -42,6 +41,8 @@ import type { GameRules } from "../../chess/variants/gamerules.js"; import type { MetaData } from "../../chess/util/metadata.js"; import type { Coords, CoordsKey } from "../../chess/util/coordutil.js"; import type { ClockValues } from "../../chess/logic/clock.js"; +import type { DisconnectInfo, DrawOfferInfo } from "../misc/onlinegamerouter.js"; +import localgame from "../misc/localgame/localgame.js"; // Type Definitions -------------------------------------------------------------------- @@ -101,12 +102,11 @@ function areInAGame(): boolean { return inAGame; } -function areInLocalGame(): boolean { - return typeOfGameWeAreIn === 'local'; -} - -function areInOnlineGame(): boolean { - return typeOfGameWeAreIn === 'online'; +/** + * Updates whatever game is currently loaded, for what needs to be updated. + */ +function update() { + if (typeOfGameWeAreIn === 'online') onlinegame.update(); } @@ -123,31 +123,24 @@ async function startLocalGame(options: { metadata: { ...options, Event: `Casual local ${translations[options.Variant]} infinite chess game`, - Site: "https://www.infinitechess.org/", - Round: "-", + Site: 'https://www.infinitechess.org/' as 'https://www.infinitechess.org/', + Round: '-' as '-', UTCDate: timeutil.getCurrentUTCDate(), UTCTime: timeutil.getCurrentUTCTime() - } as MetaData + } }; - guigameinfo.hidePlayerNames(); // -------------------------- - - loadGame(gameOptions, true, true); + await loadGame(gameOptions, true, true); typeOfGameWeAreIn = 'local'; + localgame.initLocalGame(); } /** * Starts an online game according to the options provided by the server. */ async function startOnlineGame(options: { - clock: MetaData['TimeControl'], - drawOffer: { - /** True if our opponent has extended a draw offer we haven't yet confirmed/denied */ - unconfirmed: boolean, - /** The move ply WE HAVE last offered a draw, if we have, otherwise undefined. */ - lastOfferPly?: number, - }, gameConclusion: string | false, + /** The id of the online game */ id: string, metadata: MetaData, /** Existing moves, if any, to forward to the front of the game. Should be specified if reconnecting to an online. Each move should be in the most compact notation, e.g., `['1,2>3,4','10,7>10,8Q']`. */ @@ -157,6 +150,16 @@ async function startOnlineGame(options: { youAreColor: 'white' | 'black', /** Provide if the game is timed. */ clockValues?: ClockValues, + drawOffer: DrawOfferInfo, + /** If our opponent has disconnected, this will be present. */ + disconnect?: DisconnectInfo, + /** + * If our opponent is afk, this is how many millseconds left until they will be auto-resigned, + * at the time the server sent the message. Subtract half our ping to get the correct estimated value! + */ + millisUntilAutoAFKResign?: number, + /** If the server us restarting soon for maintenance, this is the time (on the server's machine) that it will be restarting. */ + serverRestartingAt?: number, }) { // console.log("Starting online game with invite options:"); // console.log(jsutil.deepCopyObject(options)); @@ -165,30 +168,18 @@ async function startOnlineGame(options: { if (options.clockValues) options.clockValues = clock.adjustClockValuesForPing(options.clockValues); // Must be set BEFORE loading the game, because the mesh generation relies on the color we are. - options.variantOptions = generateVariantOptionsIfReloadingPrivateCustomGame(); + if (options.publicity === 'private') options.variantOptions = localstorage.loadItem(options.id); const fromWhitePerspective = options.youAreColor === 'white'; await loadGame(options, fromWhitePerspective, false); typeOfGameWeAreIn = 'online'; - onlinegame.initOnlineGame(options); - guigameinfo.setAndRevealPlayerNames(options); - drawoffers.set(options.drawOffer); } -function generateVariantOptionsIfReloadingPrivateCustomGame() { - if (!onlinegame.getIsPrivate()) return; // Can't play/paste custom position in public matches. - const gameID = onlinegame.getGameID(); - if (!gameID) return console.error("Can't generate variant options when reloading private custom game because gameID isn't defined yet."); - return localstorage.loadItem(gameID); -} - - - @@ -235,8 +226,8 @@ async function loadGame( const gamefile = gameslot.getGamefile()!; guinavigation.open(gamefile, { allowEditCoords }); // Editing your coords allowed in local games + guigameinfo.open(gameOptions.metadata); guiclock.set(gamefile); - guigameinfo.updateWhosTurn(gamefile); sound.playSound_gamestart(); @@ -246,6 +237,7 @@ async function loadGame( function unloadGame() { onlinegame.closeOnlineGame(); guinavigation.close(); + guigameinfo.close(); gameslot.unloadGame(); perspective.disable(); gui.prepareForOpen(); @@ -256,8 +248,7 @@ function unloadGame() { export default { areInAGame, - areInLocalGame, - areInOnlineGame, + update, startLocalGame, startOnlineGame, loadGame, diff --git a/src/client/scripts/esm/game/chess/gameslot.ts b/src/client/scripts/esm/game/chess/gameslot.ts index 2d2d46332..1a486bb21 100644 --- a/src/client/scripts/esm/game/chess/gameslot.ts +++ b/src/client/scripts/esm/game/chess/gameslot.ts @@ -20,7 +20,7 @@ import copypastegame from "./copypastegame.js"; // @ts-ignore import gamefileutility from "../../chess/util/gamefileutility.js"; // @ts-ignore -import onlinegame from "../misc/onlinegame.js"; +import onlinegame from "../misc/onlinegame/onlinegame.js"; // @ts-ignore import piecesmodel from "../rendering/piecesmodel.js"; // @ts-ignore @@ -266,7 +266,7 @@ function concludeGame() { onlinegame.onGameConclude(); const delayToPlayConcludeSoundSecs = 0.65; - if (!gameloader.areInOnlineGame()) { + if (!onlinegame.areInOnlineGame()) { if (!loadedGamefile.gameConclusion.includes('draw')) sound.playSound_win(delayToPlayConcludeSoundSecs); else sound.playSound_draw(delayToPlayConcludeSoundSecs); } else { // In online game diff --git a/src/client/scripts/esm/game/chess/selection.js b/src/client/scripts/esm/game/chess/selection.js index c60c142b3..98396ce08 100644 --- a/src/client/scripts/esm/game/chess/selection.js +++ b/src/client/scripts/esm/game/chess/selection.js @@ -3,7 +3,7 @@ import guipause from '../gui/guipause.js'; import legalmoves from '../../chess/logic/legalmoves.js'; import input from '../input.js'; -import onlinegame from '../misc/onlinegame.js'; +import onlinegame from '../misc/onlinegame/onlinegame.js'; import movepiece from '../../chess/logic/movepiece.js'; import gamefileutility from '../../chess/util/gamefileutility.js'; import specialdetect from '../../chess/logic/specialdetect.js'; @@ -123,7 +123,7 @@ function promoteToType(type) { promoteTo = type; } function update() { // Guard clauses... const gamefile = gameslot.getGamefile(); - // if (gameloader.areInOnlineGame() && !onlinegame.isItOurTurn(gamefile)) return; // Not our turn + // if (onlinegame.areInOnlineGame() && !onlinegame.isItOurTurn(gamefile)) return; // Not our turn if (input.isMouseDown_Right()) return unselectPiece(); // Right-click deselects everything if (pawnIsPromoting) { // Do nothing else this frame but wait for a promotion piece to be selected if (promoteTo) makePromotionMove(); @@ -299,9 +299,9 @@ function selectPiece(type, index, coords) { legalMoves = legalmoves.calculate(gameslot.getGamefile(), pieceSelected); const pieceColor = colorutil.getPieceColorFromType(pieceSelected.type); - isOpponentPiece = gameloader.areInOnlineGame() ? pieceColor !== onlinegame.getOurColor() + isOpponentPiece = onlinegame.areInOnlineGame() ? pieceColor !== onlinegame.getOurColor() /* Local Game */ : pieceColor !== gameslot.getGamefile().whosTurn; - isPremove = !isOpponentPiece && gameloader.areInOnlineGame() && !onlinegame.isItOurTurn(); + isPremove = !isOpponentPiece && onlinegame.areInOnlineGame() && !onlinegame.isItOurTurn(); legalmovehighlights.onPieceSelected(pieceSelected, legalMoves); // Generate the buffer model for the blue legal move fields. } @@ -401,10 +401,10 @@ function canMovePieceType(pieceType) { if (!pieceType || pieceType === 'voidsN') return false; // Never move voids else if (options.getEM()) return true; //Edit mode allows pieces to be moved on any turn. const pieceColor = colorutil.getPieceColorFromType(pieceType); - const isOpponentPiece = gameloader.areInOnlineGame() ? pieceColor !== onlinegame.getOurColor() + const isOpponentPiece = onlinegame.areInOnlineGame() ? pieceColor !== onlinegame.getOurColor() /* Local Game */ : pieceColor !== gameslot.getGamefile().whosTurn; if (isOpponentPiece) return false; // Don't move opponent pieces - const isPremove = !isOpponentPiece && gameloader.areInOnlineGame() && !onlinegame.isItOurTurn(); + const isPremove = !isOpponentPiece && onlinegame.areInOnlineGame() && !onlinegame.isItOurTurn(); return (!isPremove /*|| premovesEnabled*/); } diff --git a/src/client/scripts/esm/game/gui/guiclock.js b/src/client/scripts/esm/game/gui/guiclock.js index dc326104e..8be0b40a0 100644 --- a/src/client/scripts/esm/game/gui/guiclock.js +++ b/src/client/scripts/esm/game/gui/guiclock.js @@ -1,6 +1,6 @@ import moveutil from "../../chess/util/moveutil.js"; -import onlinegame from "../misc/onlinegame.js"; +import onlinegame from "../misc/onlinegame/onlinegame.js"; import sound from "../misc/sound.js"; import clockutil from "../../chess/util/clockutil.js"; import gamefileutility from "../../chess/util/gamefileutility.js"; @@ -202,7 +202,7 @@ function updateTextContent(gamefile) { function rescheduleMinuteTick(gamefile) { if (gamefile.clocks.startTime.minutes < lowtimeNotif.clockMinsRequiredToUse) return; // 1 minute lowtime notif is not used in bullet games. clearTimeout(lowtimeNotif.timeoutID); - if (gameloader.areInOnlineGame() && gamefile.clocks.colorTicking !== onlinegame.getOurColor()) return; // Don't play the sound effect for our opponent. + if (onlinegame.areInOnlineGame() && gamefile.clocks.colorTicking !== onlinegame.getOurColor()) return; // Don't play the sound effect for our opponent. if (lowtimeNotif.colorsNotified.has(gamefile.clocks.colorTicking)) return; const timeRemainAtTurnStart = gamefile.clocks.timeRemainAtTurnStart; const timeRemain = timeRemainAtTurnStart - lowtimeNotif.timeToStartFromEnd; // Time remaining until sound it should start playing @@ -251,7 +251,7 @@ function push(gamefile) { function rescheduleDrum(gamefile) { clearTimeout(countdown.drum.timeoutID); - if (gameloader.areInOnlineGame() && gamefile.clocks.colorTicking !== onlinegame.getOurColor()) return; // Don't play the sound effect for our opponent. + if (onlinegame.areInOnlineGame() && gamefile.clocks.colorTicking !== onlinegame.getOurColor()) return; // Don't play the sound effect for our opponent. const timeUntil10SecsRemain = gamefile.clocks.currentTime[gamefile.clocks.colorTicking] - 10000; let timeNextDrum = timeUntil10SecsRemain; let secsRemaining = 10; @@ -266,7 +266,7 @@ function rescheduleDrum(gamefile) { function rescheduleTicking(gamefile) { clearTimeout(countdown.ticking.timeoutID); countdown.ticking.sound?.fadeOut(countdown.ticking.fadeOutDuration); - if (gameloader.areInOnlineGame() && gamefile.clocks.colorTicking !== onlinegame.getOurColor()) return; // Don't play the sound effect for our opponent. + if (onlinegame.areInOnlineGame() && gamefile.clocks.colorTicking !== onlinegame.getOurColor()) return; // Don't play the sound effect for our opponent. if (gamefile.clocks.timeAtTurnStart < 10000) return; const timeRemain = gamefile.clocks.currentTime[gamefile.clocks.colorTicking] - countdown.ticking.timeToStartFromEnd; if (timeRemain > 0) countdown.ticking.timeoutID = setTimeout(playTickingEffect, timeRemain); @@ -280,7 +280,7 @@ function rescheduleTicking(gamefile) { function rescheduleTick(gamefile) { clearTimeout(countdown.tick.timeoutID); countdown.tick.sound?.fadeOut(countdown.tick.fadeOutDuration); - if (gameloader.areInOnlineGame() && gamefile.clocks.colorTicking !== onlinegame.getOurColor()) return; // Don't play the sound effect for our opponent. + if (onlinegame.areInOnlineGame() && gamefile.clocks.colorTicking !== onlinegame.getOurColor()) return; // Don't play the sound effect for our opponent. const timeRemain = gamefile.clocks.currentTime[gamefile.clocks.colorTicking] - countdown.tick.timeToStartFromEnd; if (timeRemain > 0) countdown.tick.timeoutID = setTimeout(playTickEffect, timeRemain); else { diff --git a/src/client/scripts/esm/game/gui/guidrawoffer.js b/src/client/scripts/esm/game/gui/guidrawoffer.js index d09d3bb5a..87e6b7ebd 100644 --- a/src/client/scripts/esm/game/gui/guidrawoffer.js +++ b/src/client/scripts/esm/game/gui/guidrawoffer.js @@ -46,6 +46,7 @@ function close() { if (!drawOfferUICramped) return; // We had hid the names and clocks to make room for the UI, reveal them here! // console.log("revealing"); + throw Error("Don't know how"); guigameinfo.setAndRevealPlayerNames(); guiclock.showClocks(); drawOfferUICramped = false; // Reset for next draw offer UI opening @@ -68,20 +69,22 @@ function closeDrawOfferListeners() { */ function updateVisibilityOfNamesAndClocksWithDrawOffer() { if (!drawoffers.areWeAcceptingDraw()) return; // No open draw offer + + throw Error("Don't know how to hide or show player names to make room for draw offer."); - if (isDrawOfferUICramped()) { // Hide the player names and clocks - if (drawOfferUICramped) return; // Already hidden - // console.log("hiding"); - drawOfferUICramped = true; - guigameinfo.hidePlayerNames(); - guiclock.hideClocks(); - } else { // We have space now, reveal them! - if (!drawOfferUICramped) return; // Already revealed - // console.log("revealing"); - drawOfferUICramped = false; - guigameinfo.setAndRevealPlayerNames(); - guiclock.showClocks(); - } + // if (isDrawOfferUICramped()) { // Hide the player names and clocks + // if (drawOfferUICramped) return; // Already hidden + // // console.log("hiding"); + // drawOfferUICramped = true; + // guigameinfo.hidePlayerNames(); + // guiclock.hideClocks(); + // } else { // We have space now, reveal them! + // if (!drawOfferUICramped) return; // Already revealed + // // console.log("revealing"); + // drawOfferUICramped = false; + // guigameinfo.setAndRevealPlayerNames(); + // guiclock.showClocks(); + // } } /** diff --git a/src/client/scripts/esm/game/gui/guigameinfo.js b/src/client/scripts/esm/game/gui/guigameinfo.ts similarity index 73% rename from src/client/scripts/esm/game/gui/guigameinfo.js rename to src/client/scripts/esm/game/gui/guigameinfo.ts index eee5b5a72..8b26a5cd2 100644 --- a/src/client/scripts/esm/game/gui/guigameinfo.js +++ b/src/client/scripts/esm/game/gui/guigameinfo.ts @@ -1,11 +1,18 @@ -// Import Start -import onlinegame from '../misc/onlinegame.js'; -import winconutil from '../../chess/util/winconutil.js'; -import gamefileutility from '../../chess/util/gamefileutility.js'; + +import { MetaData } from '../../chess/util/metadata.js'; + + import gameslot from '../chess/gameslot.js'; import gameloader from '../chess/gameloader.js'; -// Import End +// @ts-ignore +import onlinegame from '../misc/onlinegame/onlinegame.js'; +// @ts-ignore +import winconutil from '../../chess/util/winconutil.js'; +// @ts-ignore +import gamefileutility from '../../chess/util/gamefileutility.js'; + + /** * Type Definitions @@ -21,56 +28,93 @@ import gameloader from '../chess/gameloader.js'; // Variables -// Footer game info -const element_whosturn = document.getElementById('whosturn'); -const element_dot = document.getElementById('dot'); -const element_playerWhite = document.getElementById('playerwhite'); -const element_playerBlack = document.getElementById('playerblack'); +const element_gameInfoBar = document.getElementById('game-info-bar')!; + +const element_whosturn = document.getElementById('whosturn')!; +const element_dot = document.getElementById('dot')!; +const element_playerWhite = document.getElementById('playerwhite')!; +const element_playerBlack = document.getElementById('playerblack')!; + +let isOpen = false; // Functions -function open() { - if (gameslot.getGamefile().gameConclusion) return; +/** + * + * @param metadata - The metadata of the gamefile, with its respective White and Black player names. + */ +function open(metadata: MetaData) { + const { white, black } = getPlayerNamesForGame(metadata); + + element_playerWhite.textContent = white; + element_playerBlack.textContent = black; + updateWhosTurn(); + element_gameInfoBar.classList.remove('hidden'); + isOpen = true; +} + +function close() { + // Restore the player names to original content + element_playerWhite.textContent = ''; + element_playerBlack.textContent = ''; + element_playerWhite.classList.remove('hidden'); + element_playerBlack.classList.remove('hidden'); + // Restore the whosturn marker to original content + element_whosturn.textContent = ''; + element_dot.classList.remove('dotblack'); + element_dot.classList.add('dotwhite'); element_dot.classList.remove('hidden'); + + // Hide the whole bar + element_gameInfoBar.classList.add('hidden'); + + isOpen = false; } -function hidePlayerNames() { - element_playerWhite.classList.add('hidden'); - element_playerBlack.classList.add('hidden'); +function toggle() { + if (isOpen) close(); + else open(gameslot.getGamefile()!.metadata); } -function setAndRevealPlayerNames(gameOptions) { - if (gameOptions) { - const white = gameOptions.metadata.White; - const black = gameOptions.metadata.Black; +function getPlayerNamesForGame(metadata: MetaData): { white: string, black: string } { + if (onlinegame.areInOnlineGame()) { + if (!metadata.White || !metadata.Black) throw Error('White or Black metadata not defined when getting player names for online game.'); + // If you are a guest, then we want your name to be "(You)" instead of "(Guest)" - element_playerWhite.textContent = onlinegame.areWeColor('white') && white === translations.guest_indicator ? translations.you_indicator : white; - element_playerBlack.textContent = onlinegame.areWeColor('black') && black === translations.guest_indicator ? translations.you_indicator : black; + return { + white: onlinegame.areWeColorInOnlineGame('white') && metadata['White'] === translations['guest_indicator'] ? translations['you_indicator'] : metadata['White'], + black: onlinegame.areWeColorInOnlineGame('black') && metadata['Black'] === translations['guest_indicator'] ? translations['you_indicator'] : metadata['Black'] + }; } - element_playerWhite.classList.remove('hidden'); - element_playerBlack.classList.remove('hidden'); + + // Local game + + return { + white: translations['player_name_white_generic'], + black: translations['player_name_black_generic'] + }; } /** * Updates the text at the bottom of the screen displaying who's turn it is now. * Call this after flipping the gamefile's `whosTurn` property. - * @param {gamefile} gamefile - The gamefile */ -function updateWhosTurn(gamefile) { +function updateWhosTurn() { + const gamefile = gameslot.getGamefile()!; + // In the scenario we forward the game to front after the game has adjudicated, // don't modify the game over text saying who won! if (gamefileutility.isGameOver(gamefile)) return; const color = gamefile.whosTurn; - if (color !== 'white' && color !== 'black') - throw new Error(`Cannot set the document element text showing whos turn it is when color is neither white nor black! ${color}`); + if (color !== 'white' && color !== 'black') throw Error(`Cannot set the document element text showing whos turn it is when color is neither white nor black! ${color}`); let textContent = ""; - if (gameloader.areInOnlineGame()) { - const ourTurn = onlinegame.isItOurTurn(gamefile); - textContent = ourTurn ? translations.your_move : translations.their_move; - } else textContent = color === "white" ? translations.white_to_move : translations.black_to_move; + if (onlinegame.areInOnlineGame()) { + const ourTurn = onlinegame.isItOurTurn(); + textContent = ourTurn ? translations['your_move'] : translations['their_move']; + } else textContent = color === "white" ? translations['white_to_move'] : translations['black_to_move']; element_whosturn.textContent = textContent; @@ -85,16 +129,18 @@ function updateWhosTurn(gamefile) { } // Updates the whosTurn text to say who won! -function gameEnd(conclusion) { +function gameEnd(conclusion: string) { // 'white checkmate' / 'black resignation' / 'draw stalemate' time/resignation/stalemate/repetition/checkmate/disconnect/agreement const { victor, condition } = winconutil.getVictorAndConditionFromGameConclusion(conclusion); - const resultTranslations = translations.results; + const resultTranslations = translations['results']; element_dot.classList.add('hidden'); - if (gameloader.areInOnlineGame()) { + const gamefile = gameslot.getGamefile()!; + + if (onlinegame.areInOnlineGame()) { - if (onlinegame.areWeColor(victor)) element_whosturn.textContent = condition === 'checkmate' ? resultTranslations.you_checkmate + if (onlinegame.areWeColorInOnlineGame(victor)) element_whosturn.textContent = condition === 'checkmate' ? resultTranslations.you_checkmate : condition === 'time' ? resultTranslations.you_time : condition === 'resignation' ? resultTranslations.you_resignation : condition === 'disconnect' ? resultTranslations.you_disconnect @@ -106,7 +152,7 @@ function gameEnd(conclusion) { : resultTranslations.you_generic; else if (victor === 'draw') element_whosturn.textContent = condition === 'stalemate' ? resultTranslations.draw_stalemate : condition === 'repetition' ? resultTranslations.draw_repetition - : condition === 'moverule' ? `${resultTranslations.draw_moverule[0]}${(gameslot.getGamefile().gameRules.moveRule / 2)}${resultTranslations.draw_moverule[1]}` + : condition === 'moverule' ? `${resultTranslations.draw_moverule[0]}${(gamefile.gameRules.moveRule! / 2)}${resultTranslations.draw_moverule[1]}` : condition === 'insuffmat' ? resultTranslations.draw_insuffmat : condition === 'agreement' ? resultTranslations.draw_agreement : resultTranslations.draw_generic; @@ -145,7 +191,7 @@ function gameEnd(conclusion) { : resultTranslations.bug_koth; else if (condition === 'stalemate') element_whosturn.textContent = resultTranslations.draw_stalemate; else if (condition === 'repetition') element_whosturn.textContent = resultTranslations.draw_repetition; - else if (condition === 'moverule') element_whosturn.textContent = `${resultTranslations.draw_moverule[0]}${(gameslot.getGamefile().gameRules.moveRule / 2)}${resultTranslations.draw_moverule[1]}`; + else if (condition === 'moverule') element_whosturn.textContent = `${resultTranslations.draw_moverule[0]}${(gamefile.gameRules.moveRule! / 2)}${resultTranslations.draw_moverule[1]}`; else if (condition === 'insuffmat') element_whosturn.textContent = resultTranslations.draw_insuffmat; else { element_whosturn.textContent = resultTranslations.bug_generic; @@ -156,8 +202,8 @@ function gameEnd(conclusion) { export default { open, - hidePlayerNames, - setAndRevealPlayerNames, + close, + toggle, updateWhosTurn, gameEnd }; \ No newline at end of file diff --git a/src/client/scripts/esm/game/gui/guinavigation.ts b/src/client/scripts/esm/game/gui/guinavigation.ts index 72e06fcf9..7e696e67e 100644 --- a/src/client/scripts/esm/game/gui/guinavigation.ts +++ b/src/client/scripts/esm/game/gui/guinavigation.ts @@ -34,6 +34,7 @@ import gameslot from '../chess/gameslot.js'; // eslint-disable-next-line no-unused-vars import type gamefile from '../../chess/logic/gamefile.js'; import gameloader from '../chess/gameloader.js'; +import onlinegame from '../misc/onlinegame/onlinegame.js'; /** * This script handles the navigation bar, in a game, @@ -41,7 +42,7 @@ import gameloader from '../chess/gameloader.js'; * buttons, rewind move, forward move, and pause buttons. */ -const element_Navigation = document.getElementById('navigation')!; +const element_Navigation = document.getElementById('navigation-bar')!; // Navigation const element_Recenter = document.getElementById('recenter')!; @@ -87,30 +88,12 @@ function isOpen() { } /** Called when we push 'N' on the keyboard */ -function toggleNavigationBar() { - // We should only ever do this if we are in a game! - if (!activeGamefile) return; +function toggle() { if (navigationOpen) close(); - else open(activeGamefile); - - navigationOpen = !navigationOpen; - - onToggleNavigationBar(); -} - -function onToggleNavigationBar() { - const gamefile = gameslot.getGamefile(); - if (!gamefile) throw Error("Should not have toggled navigation bar when there's no game. The listener should have been closed."); - if (navigationOpen) { - open(gamefile, { allowEditCoords: !gameloader.areInOnlineGame() }); - guigameinfo.open(); - } - else close(); - - camera.updatePIXEL_HEIGHT_OF_NAVS(); + else open(activeGamefile!, { allowEditCoords: !onlinegame.areInOnlineGame() }); } -function open(gamefile: gamefile, { allowEditCoords = true }: { allowEditCoords?: boolean } = {}) { +function open(gamefile: gamefile, { allowEditCoords = true }: { allowEditCoords?: boolean }) { activeGamefile = gamefile; element_Navigation.classList.remove('hidden'); @@ -491,6 +474,6 @@ export default { update, isCoordinateActive, recenter, - toggleNavigationBar, + toggle, areCoordsAllowedToBeEdited, }; \ No newline at end of file diff --git a/src/client/scripts/esm/game/gui/guipause.js b/src/client/scripts/esm/game/gui/guipause.js index 7dfee8cb9..9669a919e 100644 --- a/src/client/scripts/esm/game/gui/guipause.js +++ b/src/client/scripts/esm/game/gui/guipause.js @@ -1,6 +1,6 @@ // Import Start -import onlinegame from '../misc/onlinegame.js'; +import onlinegame from '../misc/onlinegame/onlinegame.js'; import arrows from '../rendering/arrows.js'; import statustext from './statustext.js'; import copypastegame from '../chess/copypastegame.js'; @@ -60,7 +60,7 @@ function updatePasteButtonTransparency() { const moves = gameslot.getGamefile().moves; const legalInPrivateMatch = onlinegame.getIsPrivate() && moves.length === 0; - if (gameloader.areInOnlineGame() && !legalInPrivateMatch) element_pastegame.classList.add('opacity-0_5'); + if (onlinegame.areInOnlineGame() && !legalInPrivateMatch) element_pastegame.classList.add('opacity-0_5'); else element_pastegame.classList.remove('opacity-0_5'); } @@ -99,7 +99,7 @@ function onReceiveOpponentsMove() { function updateTextOfMainMenuButton({ freezeResignButtonIfNoLongerAbortable } = {}) { if (!isPaused) return; - if (!gameloader.areInOnlineGame() || onlinegame.hasGameConcluded()) return element_mainmenu.textContent = translations.main_menu; + if (!onlinegame.areInOnlineGame() || onlinegame.hasServerConcludedGame()) return element_mainmenu.textContent = translations.main_menu; if (moveutil.isGameResignable(gameslot.getGamefile())) { // If the text currently says "Abort Game", freeze the button for 1 second in case the user clicked it RIGHT after it switched text! They may have tried to abort and actually not want to resign. diff --git a/src/client/scripts/esm/game/input.js b/src/client/scripts/esm/game/input.js index 5ec5b2250..5b8955888 100644 --- a/src/client/scripts/esm/game/input.js +++ b/src/client/scripts/esm/game/input.js @@ -2,7 +2,7 @@ // Import Start import guipause from './gui/guipause.js'; import bufferdata from './rendering/bufferdata.js'; -import onlinegame from './misc/onlinegame.js'; +import onlinegame from './misc/onlinegame/onlinegame.js'; import perspective from './rendering/perspective.js'; import movement from './rendering/movement.js'; import options from './rendering/options.js'; @@ -727,7 +727,7 @@ function moveMouse(touch1, touch2) { // touch2 optional. If provided, will take setTouchesChangeInXYTo0(touch2); } - const oneOrNegOne = gameloader.areInOnlineGame() && onlinegame.areWeColor('black') ? -1 : 1; + const oneOrNegOne = onlinegame.areWeColorInOnlineGame('black') ? -1 : 1; mouseWorldLocation[0] -= touchMovementX * dampeningToMoveMouseInTouchMode * oneOrNegOne; mouseWorldLocation[1] -= touchMovementY * dampeningToMoveMouseInTouchMode * oneOrNegOne; diff --git a/src/client/scripts/esm/game/main.js b/src/client/scripts/esm/game/main.js index 349f87c97..380bd1b7f 100644 --- a/src/client/scripts/esm/game/main.js +++ b/src/client/scripts/esm/game/main.js @@ -10,7 +10,7 @@ import webgl from './rendering/webgl.js'; import loadbalancer from './misc/loadbalancer.js'; import input from './input.js'; -import onlinegame from './misc/onlinegame.js'; +import onlinegame from './misc/onlinegame/onlinegame.js'; import localstorage from '../util/localstorage.js'; import game from './chess/game.js'; import shaders from './rendering/shaders.js'; diff --git a/src/client/scripts/esm/game/misc/drawoffers.js b/src/client/scripts/esm/game/misc/drawoffers.js index 0fc696c8b..eac0f16fb 100644 --- a/src/client/scripts/esm/game/misc/drawoffers.js +++ b/src/client/scripts/esm/game/misc/drawoffers.js @@ -6,7 +6,7 @@ import websocket from '../websocket.js'; import guipause from '../gui/guipause.js'; import sound from './sound.js'; import moveutil from '../../chess/util/moveutil.js'; -import onlinegame from './onlinegame.js'; +import onlinegame from './onlinegame/onlinegame.js'; import gameslot from '../chess/gameslot.js'; import gameloader from '../chess/gameloader.js'; // Import End @@ -43,9 +43,9 @@ let isAcceptingDraw = false; */ function isOfferingDrawLegal() { const gamefile = gameslot.getGamefile(); - if (!gameloader.areInOnlineGame()) return false; // Can't offer draws in local games + if (!onlinegame.areInOnlineGame()) return false; // Can't offer draws in local games if (!moveutil.isGameResignable(gamefile)) return false; // Not atleast 2+ moves - if (onlinegame.hasGameConcluded()) return false; // Can't offer draws after the game has ended + if (onlinegame.hasServerConcludedGame()) return false; // Can't offer draws after the game has ended if (isTooSoonToOfferDraw()) return false; // It's been too soon since our last offer return true; // Is legal to EXTEND } @@ -78,6 +78,11 @@ function onOpponentExtendedOffer() { guipause.updateDrawOfferButton(); } +/** Is called when our opponent declines our draw offer */ +function onOpponentDeclinedOffer() { + statustext.showStatus(`Opponent declined draw offer.`); +} + /** * Extends a draw offer in our current game. * All legality checks have already passed! @@ -152,6 +157,7 @@ export default { callback_AcceptDraw, callback_declineDraw, onOpponentExtendedOffer, + onOpponentDeclinedOffer, extendOffer, set, reset, diff --git a/src/client/scripts/esm/game/misc/loadbalancer.js b/src/client/scripts/esm/game/misc/loadbalancer.js index e96b04067..1dbf8e029 100644 --- a/src/client/scripts/esm/game/misc/loadbalancer.js +++ b/src/client/scripts/esm/game/misc/loadbalancer.js @@ -4,7 +4,7 @@ import websocket from '../websocket.js'; import invites from './invites.js'; import stats from '../gui/stats.js'; import input from '../input.js'; -import onlinegame from './onlinegame.js'; +import onlinegame from './onlinegame/onlinegame.js'; import jsutil from '../../util/jsutil.js'; import config from '../config.js'; // Import End diff --git a/src/client/scripts/esm/game/misc/localgame/localgame.ts b/src/client/scripts/esm/game/misc/localgame/localgame.ts new file mode 100644 index 000000000..9d67525d7 --- /dev/null +++ b/src/client/scripts/esm/game/misc/localgame/localgame.ts @@ -0,0 +1,21 @@ + + +let inLocalGame: boolean = false; + +function areInLocalGame(): boolean { + return inLocalGame; +} + +function initLocalGame() { + inLocalGame = true; +} + +function closeLocalGame() { + +}} + + +export default { + areInLocalGame, + initLocalGame, +}; \ No newline at end of file diff --git a/src/client/scripts/esm/game/misc/onlinegame.js b/src/client/scripts/esm/game/misc/onlinegame.js deleted file mode 100644 index db6aa7cbb..000000000 --- a/src/client/scripts/esm/game/misc/onlinegame.js +++ /dev/null @@ -1,900 +0,0 @@ - -// Import Start -import legalmoves from '../../chess/logic/legalmoves.js'; -import localstorage from '../../util/localstorage.js'; -import gamefileutility from '../../chess/util/gamefileutility.js'; -import drawoffers from './drawoffers.js'; -import guititle from '../gui/guititle.js'; -import clock from '../../chess/logic/clock.js'; -import guiclock from '../gui/guiclock.js'; -import statustext from '../gui/statustext.js'; -import movepiece from '../../chess/logic/movepiece.js'; -import specialdetect from '../../chess/logic/specialdetect.js'; -import selection from '../chess/selection.js'; -import board from '../rendering/board.js'; -import moveutil from '../../chess/util/moveutil.js'; -import websocket from '../websocket.js'; -import perspective from '../rendering/perspective.js'; -import sound from './sound.js'; -import guiplay from '../gui/guiplay.js'; -import input from '../input.js'; -import loadbalancer from './loadbalancer.js'; -import formatconverter from '../../chess/logic/formatconverter.js'; -import guipause from '../gui/guipause.js'; -import guigameinfo from '../gui/guigameinfo.js'; -import colorutil from '../../chess/util/colorutil.js'; -import jsutil from '../../util/jsutil.js'; -import config from '../config.js'; -import pingManager from '../../util/pingManager.js'; -import gameslot from '../chess/gameslot.js'; -import gameloader from '../chess/gameloader.js'; -// Import End - -/** - * Type Definitions - * @typedef {import('../../chess/logic/gamefile.js'} gamefile - * @typedef {import('../../chess/util/moveutil.js').Move} Move - * @typedef {import('../websocket.js').WebsocketMessage} WebsocketMessage -*/ - -"use strict"; - -/** This module keeps trap of the data of the onlinegame we are currently in. */ - -/** The id of the online game we are in, if we are in one. @type {string} */ -let gameID; -/** Whether the game is a private one (joined from an invite code). */ -let isPrivate; -let ourColor; // white/black -/** - * Different from gamefile.gameConclusion, because this is only true if {@link gamefileutility.concludeGame} - * has been called, which IS ONLY called once the SERVER tells us the result of the game, not us! - */ -let gameHasConcluded; - -/** - * Whether we are in sync with the game on the server. - * If false, we do not submit our move. (move auto-submitted upon resyncing) - * Set to false whenever the socket closes, or we unsub from the game. - * Set to true whenever we join game, or successfully resync. - */ -let inSync = false; - -/** Variables regardin the flashing of the tab's name "YOUR MOVE" when you're away. */ -const tabNameFlash = { - originalDocumentTitle: document.title, - timeoutID: undefined, - moveSound_timeoutID: undefined -}; - -/** All variables related to being afk and alerting the server of that */ -const afk = { - timeUntilAFKSecs: 40, // 40 + 20 = 1 minute - timeUntilAFKSecs_Abortable: 20, // 20 + 20 = 40 seconds - timeUntilAFKSecs_Untimed: 100, // 100 + 20 = 2 minutes - /** The amount of time we have, in milliseconds, from the time we alert the - * server we are afk, to the time we lose if we don't return. */ - timerToLossFromAFK: 20000, - /** The ID of the timer to alert the server we are afk. */ - timeoutID: undefined, - timeWeLoseFromAFK: undefined, - /** The timeout ID of the timer to display the next "You are AFK..." message. */ - displayAFKTimeoutID: undefined, - /** The timeout ID of the timer to play the next violin staccato note */ - playStaccatoTimeoutID: undefined, - - timeOpponentLoseFromAFK: undefined, - /** The timeout ID of the timer to display the next "Opponent is AFK..." message. */ - displayOpponentAFKTimeoutID: undefined -}; - -/** All variables related to our opponent having disconnected */ -const disconnect = { - timeOpponentLoseFromDisconnect: undefined, - /** The timeout ID of the timer to display the next "Opponent has disconnected..." message. */ - displayOpponentDisconnectTimeoutID: undefined -}; - -const serverRestart = { - /** The time the server plans on restarting, if it has alerted us it is, otherwise false. */ - time: false, - /** The minute intervals at which to display on screen the server is restarting. */ - keyMinutes: [30, 20, 15, 10, 5, 2, 1, 0], - /** The timeout ID of the timer to display the next "Server restarting..." message. - * This can be used to cancel the timer when the server informs us it's already restarted. */ - timeoutID: undefined -}; - - - -(function init() { - addWarningLeaveGamePopupsToHyperlinks(); -})(); - -/** - * Add an listener for every single hyperlink on the page that will - * confirm to us if we actually want to leave if we are in an online game. - */ -function addWarningLeaveGamePopupsToHyperlinks() { - document.querySelectorAll('a').forEach((link) => { - link.addEventListener('click', confirmNavigationAwayFromGame); - }); -} - -/** - * Confirm that the user DOES actually want to leave the page if they are in an online game. - * - * Sometimes they could leave by accident, or even hit the "Logout" button by accident, - * which just ejects them out of the game - * @param {Event} event - */ -function confirmNavigationAwayFromGame(event) { - // Check if Command (Meta) or Ctrl key is held down - if (event.metaKey || event.ctrlKey) return; // Allow opening in a new tab without confirmation - if (!gameloader.areInOnlineGame() || gamefileutility.isGameOver(gameslot.getGamefile())) return; - - const userConfirmed = confirm('Are you sure you want to leave the game?'); - if (userConfirmed) return; // Follow link like normal. Server starts a 20-second auto-resign timer for disconnecting on purpose. - // Cancel the following of the link. - event.preventDefault(); - - /* - * KEEP IN MIND that if we leave the pop-up open for 10 seconds, - * JavaScript is frozen in that timeframe, which means as - * far as the server can tell we're not communicating anymore, - * so it automatically closes our websocket connection, - * thinking we've disconnected, and starts a 60-second auto-resign timer. - * - * As soon as we hit cancel, we are communicating again. - */ -} - - - -/** - * Returns the game id of the online game we're in. - * @returns {string} - */ -function getGameID() { return gameID; } - -function getIsPrivate() { return isPrivate; } - -function getOurColor() { return ourColor; } - -/** - * Different from {@link gamefileutility.isGameOver}, because this only returns true if {@link gamefileutility.concludeGame} - * has been called, which IS ONLY called once the SERVER tells us the result of the game, not us! - * @returns {boolean} - */ -function hasGameConcluded() { return gameHasConcluded; } - -function setInSyncFalse() { inSync = false; } - -function update() { - if (!gameloader.areInOnlineGame()) return; - - updateAFK(); -} - -function updateAFK() { - if (!input.atleast1InputThisFrame() || gameslot.getGamefile().gameConclusion) return; - // Has been mouse movement, restart the afk auto-resign timer. - if (afk.timeWeLoseFromAFK) tellServerWeBackFromAFK(); - rescheduleAlertServerWeAFK(); -} - -function rescheduleAlertServerWeAFK() { - clearTimeout(afk.timeoutID); - const gamefile = gameslot.getGamefile(); - if (!isItOurTurn() || gamefileutility.isGameOver(gamefile) || isPrivate && clock.isGameUntimed(gamefile) || !clock.isGameUntimed(gamefile) && moveutil.isGameResignable(gamefile)) return; - // Games with less than 2 moves played more-quickly start the AFK auto resign timer - const timeUntilAFKSecs = !moveutil.isGameResignable(gamefile) ? afk.timeUntilAFKSecs_Abortable - : clock.isGameUntimed(gamefile) ? afk.timeUntilAFKSecs_Untimed - : afk.timeUntilAFKSecs; - afk.timeoutID = setTimeout(tellServerWeAFK, timeUntilAFKSecs * 1000); -} - -function cancelAFKTimer() { - clearTimeout(afk.timeoutID); - clearTimeout(afk.displayAFKTimeoutID); - clearTimeout(afk.playStaccatoTimeoutID); - clearTimeout(afk.displayOpponentAFKTimeoutID); -} - -function tellServerWeAFK() { - websocket.sendmessage('game','AFK'); - afk.timeWeLoseFromAFK = Date.now() + afk.timerToLossFromAFK; - - // Play lowtime alert sound - sound.playSound_lowtime(); - - // Display on screen "You are AFK. Auto-resigning in 20..." - displayWeAFK(20); - // The first violin staccato note is played in 10 seconds - afk.playStaccatoTimeoutID = setTimeout(playStaccatoNote, 10000, 'c3', 10); -} - -function tellServerWeBackFromAFK() { - websocket.sendmessage('game','AFK-Return'); - afk.timeWeLoseFromAFK = undefined; - clearTimeout(afk.displayAFKTimeoutID); - clearTimeout(afk.playStaccatoTimeoutID); - afk.displayAFKTimeoutID = undefined; - afk.playStaccatoTimeoutID = undefined; -} - -function displayWeAFK(secsRemaining) { - const resigningOrAborting = moveutil.isGameResignable(gameslot.getGamefile()) ? translations.onlinegame.auto_resigning_in : translations.onlinegame.auto_aborting_in; - statustext.showStatusForDuration(`${translations.onlinegame.afk_warning} ${resigningOrAborting} ${secsRemaining}...`, 1000); - const nextSecsRemaining = secsRemaining - 1; - if (nextSecsRemaining === 0) return; // Stop - const timeRemainUntilAFKLoss = afk.timeWeLoseFromAFK - Date.now(); - const timeToPlayNextDisplayWeAFK = timeRemainUntilAFKLoss - nextSecsRemaining * 1000; - afk.displayAFKTimeoutID = setTimeout(displayWeAFK, timeToPlayNextDisplayWeAFK, nextSecsRemaining); -} - -function playStaccatoNote(note, secsRemaining) { - if (note === 'c3') sound.playSound_viola_c3(); - else if (note === 'c4') sound.playSound_violin_c4(); - else return console.error("Invalid violin note"); - - const nextSecsRemaining = secsRemaining > 5 ? secsRemaining - 1 : secsRemaining - 0.5; - if (nextSecsRemaining === 0) return; // Stop - const nextNote = nextSecsRemaining === Math.floor(nextSecsRemaining) ? 'c3' : 'c4'; - const timeRemainUntilAFKLoss = afk.timeWeLoseFromAFK - Date.now(); - const timeToPlayNextDisplayWeAFK = timeRemainUntilAFKLoss - nextSecsRemaining * 1000; - afk.playStaccatoTimeoutID = setTimeout(playStaccatoNote, timeToPlayNextDisplayWeAFK, nextNote, nextSecsRemaining); -} - -/** - * This is called whenever we lose connection. - * This is NOT when a socket unexpectedly closes, this is when a socket - * unexpectedly closes AND we are unable to establish a new one! - */ -function onLostConnection() { - // Stop saying when the opponent will lose from being afk - clearTimeout(afk.displayOpponentAFKTimeoutID); -} - -/** - * **Universal** function that is called when we receive a server websocket message with subscription marked `game`. - * Joins online games, forwards received opponent's moves. Ends game after receiving resignation. - * @param {WebsocketMessage} data - The incoming server websocket message - */ -function onmessage(data) { // { sub, action, value, id } - // console.log(`Received ${data.action} from server! Message contents:`) - // console.log(data.value) - const message = 5; - switch (data.action) { - case "joingame": - handleJoinGame(data.value); - break; - case "move": - handleOpponentsMove(data.value); - break; - case "clock": { // Contain this case in a block so that it's variables are not hoisted - if (!gameloader.areInOnlineGame()) return; - const message = data.value; // { clockValues: { timerWhite, timerBlack } } - const gamefile = gameslot.getGamefile(); - // Adjust the timer whos turn it is depending on ping. - if (message.clockValues) message.clockValues = clock.adjustClockValuesForPing(message.clockValues); - clock.edit(gamefile, message.clockValues); // Edit the clocks - guiclock.edit(gamefile); - break; - } case "gameupdate": // When the game has ended by time/disconnect/resignation/aborted, OR we are resyncing to the game. - handleServerGameUpdate(data.value); - break; - case "unsub": // The game has been deleted, server no longer sending update - websocket.getSubs().game = false; - inSync = false; - break; - case "login": // Not logged in error - statustext.showStatus(translations.onlinegame.not_logged_in, true, 100); - websocket.getSubs().game = false; - inSync = false; - clock.endGame(gameslot.getGamefile()); - guiclock.stopClocks(gameslot.getGamefile()); - gameslot.getGamefile().gameConclusion = 'limbo'; - selection.unselectPiece(); - board.darkenColor(); - break; - case "nogame": // Game is deleted / no longer exists - statustext.showStatus(translations.onlinegame.game_no_longer_exists, false, 1.5); - websocket.getSubs().game = false; - inSync = false; - gameslot.getGamefile().gameConclusion = 'aborted'; - gameslot.concludeGame(); - requestRemovalFromPlayersInActiveGames(); - break; - case "leavegame": // Another window connected - statustext.showStatus(translations.onlinegame.another_window_connected); - websocket.getSubs().game = false; - inSync = false; - gameloader.unloadGame(); - guititle.open(); - break; - case "opponentafk": - startOpponentAFKCountdown(data.value.millisUntilAutoAFKResign); - break; - case "opponentafkreturn": - stopOpponentAFKCountdown(data.value); - break; - case "opponentdisconnect": - startOpponentDisconnectCountdown(data.value); - break; - case "opponentdisconnectreturn": - stopOpponentDisconnectCountdown(data.value); - break; - case "serverrestart": - initServerRestart(data.value); - break; - case "drawoffer": { - drawoffers.onOpponentExtendedOffer(); - break; - } case "declinedraw": - statustext.showStatus(`Opponent declined draw offer.`); - break; - default: - statustext.showStatus(`${translations.invites.unknown_action_received_1} ${message.action} ${translations.invites.unknown_action_received_2}`, true); - break; - } -} - -function startOpponentAFKCountdown(millisUntilAutoAFKResign) { - if (millisUntilAutoAFKResign === undefined) return console.error("Cannot display opponent is AFK when millisUntilAutoAFKResign not specified"); - // Cancel the previous one if this is overwriting - stopOpponentAFKCountdown(); - - // Ping is round-trip time (RTT), So divided by two to get the approximate - // time that has elapsed since the server sent us the correct clock values - const timeLeftMillis = millisUntilAutoAFKResign - pingManager.getHalfPing(); - - afk.timeOpponentLoseFromAFK = Date.now() + timeLeftMillis; - // How much time is left? Usually starts at 20 seconds - const secsRemaining = Math.ceil(timeLeftMillis / 1000); - displayOpponentAFK(secsRemaining); -} - -function stopOpponentAFKCountdown() { - clearTimeout(afk.displayOpponentAFKTimeoutID); - afk.displayOpponentAFKTimeoutID = undefined; -} - -function displayOpponentAFK(secsRemaining) { - const resigningOrAborting = moveutil.isGameResignable(gameslot.getGamefile()) ? translations.onlinegame.auto_resigning_in : translations.onlinegame.auto_aborting_in; - statustext.showStatusForDuration(`${translations.onlinegame.opponent_afk} ${resigningOrAborting} ${secsRemaining}...`, 1000); - const nextSecsRemaining = secsRemaining - 1; - if (nextSecsRemaining === 0) return; // Stop - const timeRemainUntilAFKLoss = afk.timeOpponentLoseFromAFK - Date.now(); - const timeToPlayNextDisplayWeAFK = timeRemainUntilAFKLoss - nextSecsRemaining * 1000; - afk.displayOpponentAFKTimeoutID = setTimeout(displayOpponentAFK, timeToPlayNextDisplayWeAFK, nextSecsRemaining); -} - -function startOpponentDisconnectCountdown({ millisUntilAutoDisconnectResign, wasByChoice } = {}) { - if (millisUntilAutoDisconnectResign === undefined) return console.error("Cannot display opponent has disconnected when autoResignTime not specified"); - if (wasByChoice === undefined) return console.error("Cannot display opponent has disconnected when wasByChoice not specified"); - // This overwrites the "Opponent is AFK" timer - stopOpponentAFKCountdown(); - // Cancel the previous one if this is overwriting - stopOpponentDisconnectCountdown(); - const timeLeftMillis = millisUntilAutoDisconnectResign - pingManager.getHalfPing(); - disconnect.timeOpponentLoseFromDisconnect = Date.now() + timeLeftMillis; - // How much time is left? Usually starts at 20 / 60 seconds - const secsRemaining = Math.ceil(timeLeftMillis / 1000); - displayOpponentDisconnect(secsRemaining, wasByChoice); -} - -function stopOpponentDisconnectCountdown() { - clearTimeout(disconnect.displayOpponentDisconnectTimeoutID); - disconnect.displayOpponentDisconnectTimeoutID = undefined; -} - -function displayOpponentDisconnect(secsRemaining, wasByChoice) { - const opponent_disconnectedOrLostConnection = wasByChoice ? translations.onlinegame.opponent_disconnected : translations.onlinegame.opponent_lost_connection; - const resigningOrAborting = moveutil.isGameResignable(gameslot.getGamefile()) ? translations.onlinegame.auto_resigning_in : translations.onlinegame.auto_aborting_in; - // The "You are AFK" message should overwrite, be on top of, this message, - // so if that is running, don't display this 1-second disconnect message, but don't cancel it either! - if (!afk.timeWeLoseFromAFK) statustext.showStatusForDuration(`${opponent_disconnectedOrLostConnection} ${resigningOrAborting} ${secsRemaining}...`, 1000); - const nextSecsRemaining = secsRemaining - 1; - if (nextSecsRemaining === 0) return; // Stop - const timeRemainUntilDisconnectLoss = disconnect.timeOpponentLoseFromDisconnect - Date.now(); - const timeToPlayNextDisplayOpponentDisconnect = timeRemainUntilDisconnectLoss - nextSecsRemaining * 1000; - disconnect.displayOpponentDisconnectTimeoutID = setTimeout(displayOpponentDisconnect, timeToPlayNextDisplayOpponentDisconnect, nextSecsRemaining, wasByChoice); -} - -function handleJoinGame(message) { - // The server's message looks like: - // { - // metadata: { Variant, White, Black, TimeControl, UTCDate, UTCTime, Rated }, - // clockValues: ClockValues, - // id, clock, publicity, youAreColor, moves, millisUntilAutoAFKResign, disconnect, gameConclusion, drawOffer, - // } - - // We were auto-unsubbed from the invites list, BUT we want to keep open the socket!! - const subs = websocket.getSubs(); - subs.invites = false; - subs.game = true; - inSync = true; - guititle.close(); - guiplay.close(); - gameloader.startOnlineGame(message); -} - -/** - * Called when we received our opponents move. This verifies they're move - * and claimed game conclusion is legal. If it isn't, it reports them and doesn't forward their move. - * If it is legal, it forwards the game to the front, then forwards their move. - * @param {Object} message - The server's socket message, with the properties `move`, `gameConclusion`, `moveNumber`, `clockValues`. - */ -function handleOpponentsMove(message) { // { move, gameConclusion, moveNumber, clockValues } - if (!gameloader.areInOnlineGame()) return; - const moveAndConclusion = { move: message.move, gameConclusion: message.gameConclusion }; - - // Make sure the move number matches the expected. - // Otherwise, we need to re-sync - const gamefile = gameslot.getGamefile(); - const expectedMoveNumber = gamefile.moves.length + 1; - if (message.moveNumber !== expectedMoveNumber) { - console.log(`We have desynced from the game. Resyncing... Expected opponent's move number: ${expectedMoveNumber}. Actual: ${message.moveNumber}. Opponent's whole move: ${JSON.stringify(moveAndConclusion)}`); - return resyncToGame(); - } - - // Convert the move from compact short format "x,y>x,yN" - // to long format { startCoords, endCoords, promotion } - /** @type {Move} */ - let move; - try { - move = formatconverter.ShortToLong_CompactMove(message.move); // { startCoords, endCoords, promotion } - } catch { - console.error(`Opponent's move is illegal because it isn't in the correct format. Reporting... Move: ${JSON.stringify(message.move)}`); - const reason = 'Incorrectly formatted.'; - return reportOpponentsMove(reason); - } - - // If not legal, this will be a string for why it is illegal. - const moveIsLegal = legalmoves.isOpponentsMoveLegal(gamefile, move, message.gameConclusion); - if (moveIsLegal !== true) console.log(`Buddy made an illegal play: ${JSON.stringify(moveAndConclusion)}`); - if (moveIsLegal !== true && !isPrivate) return reportOpponentsMove(moveIsLegal); // Allow illegal moves in private games - - movepiece.forwardToFront(gamefile, { flipTurn: false, animateLastMove: false, updateProperties: false }); - - // Forward the move... - - const piecemoved = gamefileutility.getPieceAtCoords(gamefile, move.startCoords); - const legalMoves = legalmoves.calculate(gamefile, piecemoved); - const endCoordsToAppendSpecial = jsutil.deepCopyObject(move.endCoords); - legalmoves.checkIfMoveLegal(legalMoves, move.startCoords, endCoordsToAppendSpecial); // Passes on any special moves flags to the endCoords - - move.type = piecemoved.type; - specialdetect.transferSpecialFlags_FromCoordsToMove(endCoordsToAppendSpecial, move); - movepiece.makeMove(gamefile, move); - - selection.reselectPiece(); // Reselect the currently selected piece. Recalc its moves and recolor it if needed. - - // Edit the clocks - - // Adjust the timer whos turn it is depending on ping. - if (message.clockValues) message.clockValues = clock.adjustClockValuesForPing(message.clockValues, gamefile.whosTurn); - clock.edit(gamefile, message.clockValues); - guiclock.edit(gamefile); - - // For online games, we do NOT EVER conclude the game, so do that here if our opponents move concluded the game - if (gamefileutility.isGameOver(gamefile)) { - gameslot.concludeGame(); - requestRemovalFromPlayersInActiveGames(); - } - - rescheduleAlertServerWeAFK(); - stopOpponentAFKCountdown(); // The opponent is no longer AFK if they were - flashTabNameYOUR_MOVE(true); - scheduleMoveSound_timeoutID(); - guipause.onReceiveOpponentsMove(); // Update the pause screen buttons -} - -function flashTabNameYOUR_MOVE(on) { - if (!loadbalancer.isPageHidden()) return document.title = tabNameFlash.originalDocumentTitle; - - document.title = on ? "YOUR MOVE" : tabNameFlash.originalDocumentTitle; - tabNameFlash.timeoutID = setTimeout(flashTabNameYOUR_MOVE, 1500, !on); -} - -function cancelFlashTabTimer() { - document.title = tabNameFlash.originalDocumentTitle; - clearTimeout(tabNameFlash.timeoutID); - tabNameFlash.timeoutID = undefined; -} - -function scheduleMoveSound_timeoutID() { - if (!loadbalancer.isPageHidden()) return; - if (!moveutil.isGameResignable(gameslot.getGamefile())) return; - const timeNextFlashFromNow = (afk.timeUntilAFKSecs * 1000) / 2; - tabNameFlash.moveSound_timeoutID = setTimeout(() => { sound.playSound_move(0); }, timeNextFlashFromNow); -} - -function cancelMoveSound() { - clearTimeout(tabNameFlash.moveSound_timeoutID); - tabNameFlash.moveSound_timeoutID = undefined; -} - -function resyncToGame() { - if (!gameloader.areInOnlineGame()) return; - function onReplyFunc() { inSync = true; } - websocket.sendmessage('game', 'resync', gameID, false, onReplyFunc); -} - -/** - * Called when the server sends us the conclusion of the game when it ends, - * OR we just need to resync! The game may not always be over. - * @param {Object} messageContents - The contents of the server message, with the properties: - * `gameConclusion`, `clockValues`, `moves`, `millisUntilAutoAFKResign`, `offerDraw` - */ -function handleServerGameUpdate(messageContents) { // { gameConclusion, clockValues: ClockValues, moves, millisUntilAutoAFKResign, offerDraw } - if (!gameloader.areInOnlineGame()) return; - const gamefile = gameslot.getGamefile(); - const claimedGameConclusion = messageContents.gameConclusion; - - /** - * Make sure we are in sync with the final move list. - * We need to do this because sometimes the game can end before the - * server sees our move, but on our screen we have still played it. - */ - if (!synchronizeMovesList(gamefile, messageContents.moves, claimedGameConclusion)) { // Cheating detected. Already reported, don't - stopOpponentAFKCountdown(); - return; - } - guigameinfo.updateWhosTurn(gamefile); - - // If Opponent is currently afk, display that countdown - if (messageContents.millisUntilAutoAFKResign !== undefined && !isItOurTurn()) startOpponentAFKCountdown(messageContents.millisUntilAutoAFKResign); - else stopOpponentAFKCountdown(); - - // If opponent is currently disconnected, display that countdown - if (messageContents.disconnect !== undefined) startOpponentDisconnectCountdown(messageContents.disconnect); // { millisUntilAutoDisconnectResign, wasByChoice } - else stopOpponentDisconnectCountdown(); - - // If the server is restarting, start displaying that info. - if (messageContents.serverRestartingAt) initServerRestart(messageContents.serverRestartingAt); - else resetServerRestarting(); - - drawoffers.set(messageContents.drawOffer); - - // Must be set before editing the clocks. - gamefile.gameConclusion = claimedGameConclusion; - - // Adjust the timer whos turn it is depending on ping. - if (messageContents.clockValues) messageContents.clockValues = clock.adjustClockValuesForPing(messageContents.clockValues); - clock.edit(gamefile, messageContents.clockValues); - - if (gamefileutility.isGameOver(gamefile)) { - gameslot.concludeGame(); - requestRemovalFromPlayersInActiveGames(); - } -} - -/** - * Adds or deletes moves in the game until it matches the server's provided moves. - * This can rarely happen when we move after the game is already over, - * or if we're disconnected when our opponent made their move. - * @param {gamefile} gamefile - The gamefile - * @param {string[]} moves - The moves list in the most compact form: `['1,2>3,4','5,6>7,8Q']` - * @param {string} claimedGameConclusion - The supposed game conclusion after synchronizing our opponents move - * @returns {boolean} *false* if it detected an illegal move played by our opponent. - */ -function synchronizeMovesList(gamefile, moves, claimedGameConclusion) { - - // Early exit case. If we have played exactly 1 more move than the server, - // and the rest of the moves list matches, don't modify our moves, - // just re-submit our move! - const hasOneMoreMoveThanServer = gamefile.moves.length === moves.length + 1; - const finalMoveIsOurMove = gamefile.moves.length > 0 && moveutil.getColorThatPlayedMoveIndex(gamefile, gamefile.moves.length - 1) === ourColor; - const previousMoveMatches = (moves.length === 0 && gamefile.moves.length === 1) || gamefile.moves.length > 1 && moves.length > 0 && gamefile.moves[gamefile.moves.length - 2].compact === moves[moves.length - 1]; - if (!claimedGameConclusion && hasOneMoreMoveThanServer && finalMoveIsOurMove && previousMoveMatches) { - console.log("Sending our move again after resyncing.."); - return sendMove(); - } - - const originalMoveIndex = gamefile.moveIndex; - movepiece.forwardToFront(gamefile, { flipTurn: false, animateLastMove: false, updateProperties: false }); - let aChangeWasMade = false; - - while (gamefile.moves.length > moves.length) { // While we have more moves than what the server does.. - movepiece.rewindMove(gamefile, { animate: false }); - console.log("Rewound one move while resyncing to online game."); - aChangeWasMade = true; - } - - let i = moves.length - 1; - while (true) { // Decrement i until we find the latest move at which we're in sync, agreeing with the server about. - if (i === -1) break; // Beginning of game - const thisGamefileMove = gamefile.moves[i]; - if (thisGamefileMove) { // The move is defined - if (thisGamefileMove.compact === moves[i]) break; // The moves MATCH - // The moves don't match... remove this one off our list. - movepiece.rewindMove(gamefile, { animate: false }); - console.log("Rewound one INCORRECT move while resyncing to online game."); - aChangeWasMade = true; - } - i--; - } - - // i is now the index of the latest move that MATCHES in both ours and the server's moves lists. - - const opponentColor = getOpponentColor(ourColor); - while (i < moves.length - 1) { // Increment i, adding the server's correct moves to our moves list - i++; - const thisShortmove = moves[i]; // '1,2>3,4Q' The shortmove from the server's move list to add - const move = movepiece.calculateMoveFromShortmove(gamefile, thisShortmove); - - const colorThatPlayedThisMove = moveutil.getColorThatPlayedMoveIndex(gamefile, i); - const opponentPlayedThisMove = colorThatPlayedThisMove === opponentColor; - - - if (opponentPlayedThisMove) { // Perform legality checks - // If not legal, this will be a string for why it is illegal. - const moveIsLegal = legalmoves.isOpponentsMoveLegal(gamefile, move, claimedGameConclusion); - if (moveIsLegal !== true) console.log(`Buddy made an illegal play: ${thisShortmove} ${claimedGameConclusion}`); - if (moveIsLegal !== true && !isPrivate) { // Allow illegal moves in private games - reportOpponentsMove(moveIsLegal); - return false; - } - - rescheduleAlertServerWeAFK(); - stopOpponentAFKCountdown(); // The opponent is no longer AFK if they were - flashTabNameYOUR_MOVE(); - scheduleMoveSound_timeoutID(); - } else cancelFlashTabTimer(); - - const isLastMove = i === moves.length - 1; - movepiece.makeMove(gamefile, move, { doGameOverChecks: isLastMove, concludeGameIfOver: false, animate: isLastMove }); - console.log("Forwarded one move while resyncing to online game."); - aChangeWasMade = true; - } - - if (!aChangeWasMade) movepiece.rewindGameToIndex(gamefile, originalMoveIndex, { removeMove: false }); - else selection.reselectPiece(); // Reselect the selected piece from before we resynced. Recalc its moves and recolor it if needed. - - return true; // No cheating detected -} - -function reportOpponentsMove(reason) { - // Send the move number of the opponents move so that there's no mixup of which move we claim is illegal. - const opponentsMoveNumber = gameslot.getGamefile().moves.length + 1; - - const message = { - reason, - opponentsMoveNumber - }; - - websocket.sendmessage('game', 'report', message); -} - -/** - * This has to be called before and separate from {@link initOnlineGame} - * because loading the gamefile and the mesh generation requires this script to know our color. - * @param {Object} gameOptions - An object that contains the properties `id`, `publicity`, `youAreColor`, `millisUntilAutoAFKResign`, `disconnect`, `serverRestartingAt` - */ -function setColorAndGameID(gameOptions) { - ourColor = gameOptions.youAreColor; - gameID = gameOptions.id; - isPrivate = gameOptions.publicity === 'private'; - gameHasConcluded = false; -} - -/** - * Inits an online game according to the options provided by the server. - * @param {Object} gameOptions - An object that contains the properties `id`, `publicity`, `youAreColor`, `millisUntilAutoAFKResign`, `disconnect`, `serverRestartingAt` - */ -function initOnlineGame(gameOptions) { - ourColor = gameOptions.youAreColor; - gameID = gameOptions.id; - isPrivate = gameOptions.publicity === 'private'; - gameHasConcluded = false; - - rescheduleAlertServerWeAFK(); - // If Opponent is currently afk, display that countdown - if (gameOptions.millisUntilAutoAFKResign !== undefined) startOpponentAFKCountdown(gameOptions.millisUntilAutoAFKResign); - if (gameOptions.disconnect) startOpponentDisconnectCountdown(gameOptions.disconnect); - if (isItOurTurn()) { - flashTabNameYOUR_MOVE(true); - scheduleMoveSound_timeoutID(); - } - if (gameOptions.serverRestartingAt) initServerRestart(gameOptions.serverRestartingAt); - - // These make sure it will place us in black's perspective - perspective.resetRotations(); -} - -// Call when we leave an online game -function closeOnlineGame() { - gameID = undefined; - isPrivate = undefined; - ourColor = undefined; - inSync = false; - gameHasConcluded = undefined; - resetAFKValues(); - resetServerRestarting(); - cancelFlashTabTimer(); - perspective.resetRotations(); // Without this, leaving an online game of which we were black, won't reset our rotation. - drawoffers.reset(); -} - -function resetAFKValues() { - cancelAFKTimer(); - tabNameFlash.timeoutID = undefined; - afk.timeoutID = undefined, - afk.timeWeLoseFromAFK = undefined; - afk.displayAFKTimeoutID = undefined, - afk.playStaccatoTimeoutID = undefined, - afk.displayOpponentAFKTimeoutID = undefined, - afk.timeOpponentLoseFromAFK = undefined; -} - -/** - * Tests if it's our turn to move - * @returns {boolean} *true* if it's currently our turn to move - */ -function isItOurTurn() { return gameslot.getGamefile().whosTurn === ourColor; } - -/** - * Tests if we are this color in the online game. - * @param {string} color - "white" / "black" - * @returns {boolean} *true* if we are that color. - */ -function areWeColor(color) { return color === ourColor; } - -function sendMove() { - if (!gameloader.areInOnlineGame() || !inSync) return; // Don't do anything if it's a local game - if (config.DEV_BUILD) console.log("Sending our move.."); - - const gamefile = gameslot.getGamefile(); - - const shortmove = moveutil.getLastMove(gamefile.moves).compact; // "x,y>x,yN" - - const data = { - move: shortmove, - moveNumber: gamefile.moves.length, - gameConclusion: gamefile.gameConclusion, - }; - - websocket.sendmessage('game', 'submitmove', data, true); - - // Declines any open draw offer from our opponent. We don't need to inform - // the server because the server auto declines when we submit our move. - drawoffers.callback_declineDraw({ informServer: false }); - - rescheduleAlertServerWeAFK(); -} - -// Aborts / Resigns -function onMainMenuPress() { - if (!gameloader.areInOnlineGame()) return; - const gamefile = gameslot.getGamefile(); - if (gameHasConcluded) { // The server has concluded the game, not us - if (websocket.getSubs().game) { - websocket.sendmessage('general','unsub','game'); - websocket.getSubs().game = false; - } - return; - } - - if (moveutil.isGameResignable(gamefile)) resign(); - else abort(); -} - -function resign() { - websocket.getSubs().game = false; - inSync = false; - websocket.sendmessage('game','resign'); -} - -function abort() { - websocket.getSubs().game = false; - inSync = false; - websocket.sendmessage('game','abort'); -} - -function getOpponentColor() { - return colorutil.getOppositeColor(ourColor); -} - -/** - * Opens a websocket, asks the server if we are in - * a game to connect us to it and send us the game info. - */ -async function askServerIfWeAreInGame() { - // The server only allows sockets if we are either logged in, or have a browser-id cookie. - // browser-id cookies are issued/renewed on every html request. - - const messageContents = undefined; - websocket.sendmessage('game', 'joingame', messageContents, true); -} - -/** - * Lets the server know we have seen the game conclusion, and would - * like to be allowed to join a new game if we leave quickly. - * - * THIS SHOULD ALSO be the point when the server knows we agree - * with the resulting game conclusion (no cheating detected), - * and the server may change the players elos! - */ -function requestRemovalFromPlayersInActiveGames() { - if (!gameloader.areInOnlineGame()) return; - websocket.sendmessage('game', 'removefromplayersinactivegames'); -} - -function initServerRestart(timeToRestart) { - if (serverRestart.time === timeToRestart) return; // We already know the server is restarting. - resetServerRestarting(); // Overwrite the previous one, if it exists. - serverRestart.time = timeToRestart; - const timeRemain = timeToRestart - Date.now(); - const minutesLeft = Math.ceil(timeRemain / (1000 * 60)); - console.log(`Server has informed us it is restarting in ${minutesLeft} minutes!`); - displayServerRestarting(minutesLeft); -} - -/** Displays the next "Server restaring..." message, and schedules the next one. */ -function displayServerRestarting(minutesLeft) { - if (minutesLeft === 0) { - statustext.showStatus(translations.onlinegame.server_restarting, false, 2); - serverRestart.time = false; - return; // Print no more server restarting messages - } - const minutes_plurality = minutesLeft === 1 ? translations.onlinegame.minute : translations.onlinegame.minutes; - statustext.showStatus(`${translations.onlinegame.server_restarting_in} ${minutesLeft} ${minutes_plurality}...`, false, 2); - let nextKeyMinute; - for (const keyMinute of serverRestart.keyMinutes) { - if (keyMinute < minutesLeft) { - nextKeyMinute = keyMinute; - break; - } - } - const timeToDisplayNextServerRestart = serverRestart.time - nextKeyMinute * 60 * 1000; - const timeUntilDisplayNextServerRestart = timeToDisplayNextServerRestart - Date.now(); - serverRestart.timeoutID = setTimeout(displayServerRestarting, timeUntilDisplayNextServerRestart, nextKeyMinute); -} - -/** Cancels the timer to display the next "Server restaring..." message, and resets the values. */ -function resetServerRestarting() { - serverRestart.time = false; - clearTimeout(serverRestart.timeoutID); - serverRestart.timeoutID = undefined; -} - -function deleteCustomVariantOptions() { - // Delete any custom pasted position in a private game. - if (isPrivate) localstorage.deleteItem(gameID); -} - -/** Called when an online game is concluded (termination shown on-screen) */ -function onGameConclude() { - gameHasConcluded = true; // This NEEDS to be above drawoffers.reset(), as that relies on this! - cancelAFKTimer(); - cancelFlashTabTimer(); - cancelMoveSound(); - resetServerRestarting(); - deleteCustomVariantOptions(); - drawoffers.reset(); -} - -export default { - onmessage, - getIsPrivate, - getOurColor, - setInSyncFalse, - setColorAndGameID, - initOnlineGame, - closeOnlineGame, - isItOurTurn, - areWeColor, - sendMove, - onMainMenuPress, - getGameID, - askServerIfWeAreInGame, - requestRemovalFromPlayersInActiveGames, - resyncToGame, - update, - onLostConnection, - cancelMoveSound, - onGameConclude, - hasGameConcluded -}; \ No newline at end of file diff --git a/src/client/scripts/esm/game/misc/onlinegame/afk.ts b/src/client/scripts/esm/game/misc/onlinegame/afk.ts new file mode 100644 index 000000000..883c08d89 --- /dev/null +++ b/src/client/scripts/esm/game/misc/onlinegame/afk.ts @@ -0,0 +1,214 @@ + +/** + * This script keeps track of how long we have been afk in the current online game, + * and if it's for too long, it informs the server that fact, + * then the server starts an auto-resign timer if we don't return. + * + * This will also display a countdown onscreen, and sound effects, + * before we are auto-resigned. + * + * It will also display a countdown until our opponent is auto-resigned, + * if they are the one that is afk. + */ + +// @ts-ignore +import clock from "../../../chess/logic/clock.js"; +import gamefileutility from "../../../chess/util/gamefileutility.js"; +import moveutil from "../../../chess/util/moveutil.js"; +import gameslot from "../../chess/gameslot.js"; +import input from "../../input.js"; +import websocket from "../../websocket.js"; +import onlinegame from "./onlinegame.js"; +import sound from "../sound.js"; +import statustext from "../../gui/statustext.js"; +import pingManager from "../../../util/pingManager.js"; + + + +/** The time, in seconds, we must be AFK for us to alert the server that fact. Afterward the server will start an auto-resign timer. */ +const timeUntilAFKSecs: number = 40; // 40 + 20 = 1 minute + +/** ABORTABLE GAMES ONLY (< 2 moves played): The time, in seconds, we must be AFK for us to alert the server that fact. Afterward the server will start an auto-resign timer. */ +const timeUntilAFKSecs_Abortable: number = 20; // 20 + 20 = 40 seconds + +/** UNTIMED GAMES ONLY: The time, in seconds, we must be AFK for us to alert the server that fact. Afterward the server will start an auto-resign timer. */ +const timeUntilAFKSecs_Untimed: number = 100; // 100 + 20 = 2 minutes + +/** The amount of time we have, in milliseconds, from the time we alert the + * server we are afk, to the time we lose if we don't return. */ +const timerToLossFromAFK: number = 20000; // HAS TO MATCH SERVER-END + +/** The ID of the timeout that can be used to cancel the timer that will alert the server we are afk, if we are not no longer afk by then. */ +let timeoutID: ReturnType | undefined; + +/** The timestamp we will lose from being AFK, if we are not no longer afk by that time. */ +let timeWeLoseFromAFK: number | undefined; + +/** The timeout ID of the timer to display the next "You are AFK..." message. */ +let displayAFKTimeoutID: ReturnType | undefined; + +/** The timeout ID of the timer to play the next staccato violin sound effect of the 10-second countdown to auto-resign from being afk. */ +let playStaccatoTimeoutID: ReturnType | undefined; + +/** The timestamp our opponent will lose from being AFK, if they are not no longer afk by that time. */ +let timeOpponentLoseFromAFK: number | undefined; + +/** The timeout ID of the timer to display the next "Opponent is AFK..." message. */ +let displayOpponentAFKTimeoutID: ReturnType | undefined; + + + +// If we lost connection while displaying status messages of when our opponent +// will disconnect, stop doing that. +document.addEventListener('connection-lost', () => { + // Stop saying when the opponent will lose from being afk + clearTimeout(displayOpponentAFKTimeoutID); +}); + + + +function isOurAFKAutoResignTimerRunning() { + // If the time we will lose from being afk is defined, the timer is running + return timeWeLoseFromAFK !== undefined; +} + +function onGameStart() { + // Start the timer that will inform the server we are afk, the server thenafter starting an auto-resign timer. + rescheduleAlertServerWeAFK(); +} + +function onGameClose() { + // Reset everything + cancelAFKTimer(); + timeoutID = undefined, + timeWeLoseFromAFK = undefined; + displayAFKTimeoutID = undefined, + playStaccatoTimeoutID = undefined, + displayOpponentAFKTimeoutID = undefined, + timeOpponentLoseFromAFK = undefined; +} + +function onMovePlayed({ isOpponents }: { isOpponents: boolean }) { + // Restart the timer that will inform the server we are afk, the server thenafter starting an auto-resign timer. + rescheduleAlertServerWeAFK(); + if (isOpponents) stopOpponentAFKCountdown(); // The opponent is no longer AFK if they were) +} + +function updateAFK() { + if (!input.atleast1InputThisFrame() || gamefileutility.isGameOver(gameslot.getGamefile()!)) return; // No input this frame, don't reset the timer to tell the server we are afk. + // There has been mouse movement, restart the afk auto-resign timer. + if (isOurAFKAutoResignTimerRunning()) tellServerWeBackFromAFK(); // Also tell the server we are back, IF it had started an auto-resign timer! + rescheduleAlertServerWeAFK(); +} + +/** + * Restarts the timer that will inform the server we are afk, + * the server thenafter starting an auto-resign timer. + */ +function rescheduleAlertServerWeAFK() { + clearTimeout(timeoutID); + const gamefile = gameslot.getGamefile()!; + if (!onlinegame.isItOurTurn() || gamefileutility.isGameOver(gamefile) || onlinegame.getIsPrivate() && clock.isGameUntimed(gamefile) || !clock.isGameUntimed(gamefile) && moveutil.isGameResignable(gamefile)) return; + // Games with less than 2 moves played more-quickly start the AFK auto resign timer + const timeUntilAlertServerWeAFKSecs = !moveutil.isGameResignable(gamefile) ? timeUntilAFKSecs_Abortable + : clock.isGameUntimed(gamefile) ? timeUntilAFKSecs_Untimed + : timeUntilAFKSecs; + timeoutID = setTimeout(tellServerWeAFK, timeUntilAlertServerWeAFKSecs * 1000); +} + +function cancelAFKTimer() { + clearTimeout(timeoutID); + clearTimeout(displayAFKTimeoutID); + clearTimeout(playStaccatoTimeoutID); + clearTimeout(displayOpponentAFKTimeoutID); +} + +function tellServerWeAFK() { + websocket.sendmessage('game','AFK'); + timeWeLoseFromAFK = Date.now() + timerToLossFromAFK; + + // Play lowtime alert sound + sound.playSound_lowtime(); + + // Display on screen "You are AFK. Auto-resigning in 20..." + displayWeAFK(20); + // The first violin staccato note is played in 10 seconds + playStaccatoTimeoutID = setTimeout(playStaccatoNote, 10000, 'c3', 10); +} + +function tellServerWeBackFromAFK() { + websocket.sendmessage('game','AFK-Return'); + timeWeLoseFromAFK = undefined; + clearTimeout(displayAFKTimeoutID); + clearTimeout(playStaccatoTimeoutID); + displayAFKTimeoutID = undefined; + playStaccatoTimeoutID = undefined; +} + +function displayWeAFK(secsRemaining: number) { + const resigningOrAborting = moveutil.isGameResignable(gameslot.getGamefile()!) ? translations.onlinegame.auto_resigning_in : translations.onlinegame.auto_aborting_in; + statustext.showStatusForDuration(`${translations.onlinegame.afk_warning} ${resigningOrAborting} ${secsRemaining}...`, 1000); + const nextSecsRemaining = secsRemaining - 1; + if (nextSecsRemaining === 0) return; // Stop + const timeRemainUntilAFKLoss = timeWeLoseFromAFK! - Date.now(); + const timeToPlayNextDisplayWeAFK = timeRemainUntilAFKLoss - nextSecsRemaining * 1000; + displayAFKTimeoutID = setTimeout(displayWeAFK, timeToPlayNextDisplayWeAFK, nextSecsRemaining); +} + +function playStaccatoNote(note: 'c3' | 'c4', secsRemaining: number) { + if (note === 'c3') sound.playSound_viola_c3(); + else if (note === 'c4') sound.playSound_violin_c4(); + else return console.error("Invalid violin note"); + + const nextSecsRemaining = secsRemaining > 5 ? secsRemaining - 1 : secsRemaining - 0.5; + if (nextSecsRemaining === 0) return; // Stop + const nextNote = nextSecsRemaining === Math.floor(nextSecsRemaining) ? 'c3' : 'c4'; + const timeRemainUntilAFKLoss = timeWeLoseFromAFK! - Date.now(); + const timeToPlayNextDisplayWeAFK = timeRemainUntilAFKLoss - nextSecsRemaining * 1000; + playStaccatoTimeoutID = setTimeout(playStaccatoNote, timeToPlayNextDisplayWeAFK, nextNote, nextSecsRemaining); +} + + + + +function startOpponentAFKCountdown(millisUntilAutoAFKResign: number) { + // Cancel the previous one if this is overwriting + stopOpponentAFKCountdown(); + + // Ping is round-trip time (RTT), So divided by two to get the approximate + // time that has elapsed since the server sent us the correct clock values + const timeLeftMillis = millisUntilAutoAFKResign - pingManager.getHalfPing(); + + timeOpponentLoseFromAFK = Date.now() + timeLeftMillis; + // How much time is left? Usually starts at 20 seconds + const secsRemaining = Math.ceil(timeLeftMillis / 1000); + displayOpponentAFK(secsRemaining); +} + +function stopOpponentAFKCountdown() { + clearTimeout(displayOpponentAFKTimeoutID); + displayOpponentAFKTimeoutID = undefined; +} + +function displayOpponentAFK(secsRemaining: number) { + const resigningOrAborting = moveutil.isGameResignable(gameslot.getGamefile()!) ? translations.onlinegame.auto_resigning_in : translations.onlinegame.auto_aborting_in; + statustext.showStatusForDuration(`${translations.onlinegame.opponent_afk} ${resigningOrAborting} ${secsRemaining}...`, 1000); + const nextSecsRemaining = secsRemaining - 1; + if (nextSecsRemaining === 0) return; // Stop + const timeRemainUntilAFKLoss = timeOpponentLoseFromAFK! - Date.now(); + const timeToPlayNextDisplayWeAFK = timeRemainUntilAFKLoss - nextSecsRemaining * 1000; + displayOpponentAFKTimeoutID = setTimeout(displayOpponentAFK, timeToPlayNextDisplayWeAFK, nextSecsRemaining); +} + + + +export default { + onGameStart, + isOurAFKAutoResignTimerRunning, + onMovePlayed, + updateAFK, + timeUntilAFKSecs, + onGameClose, + startOpponentAFKCountdown, + stopOpponentAFKCountdown, +}; \ No newline at end of file diff --git a/src/client/scripts/esm/game/misc/onlinegame/disconnect.ts b/src/client/scripts/esm/game/misc/onlinegame/disconnect.ts new file mode 100644 index 000000000..e846e0033 --- /dev/null +++ b/src/client/scripts/esm/game/misc/onlinegame/disconnect.ts @@ -0,0 +1,71 @@ + + +/** + * This script displays a countdown on screen, when our opponent disconnects, + * how much longer they have remaining until they are auto-resigned. + * + * If they disconnect not by choice (bad network), the server they are gives them a little + * extra time to reconnect. + */ + +import moveutil from "../../../chess/util/moveutil"; +import pingManager from "../../../util/pingManager"; +import gameslot from "../../chess/gameslot"; +import statustext from "../../gui/statustext"; +import afk from "./afk"; + + +/** The timestamp our opponent will lose from disconnection, if they don't reconnect before then. */ +let timeOpponentLoseFromDisconnect: number | undefined; + +/** The timeout ID of the timer to display the next "Opponent has disconnected..." message. */ +let displayOpponentDisconnectTimeoutID: ReturnType | undefined; + + + +/** + * Starts the countdown for when the opponent will be auto-resigned due to disconnection. + * This will overwrite any existing "Opponent is AFK" or disconnection countdowns. + * @param params - Parameters for the countdown. + * @param params.millisUntilAutoDisconnectResign - The number of milliseconds remaining until the opponent is auto-resigned for disconnecting. + * @param params.wasByChoice - Indicates whether the opponent disconnected intentionally (true) or unintentionally (false). + */ +function startOpponentDisconnectCountdown({ millisUntilAutoDisconnectResign, wasByChoice }: { + millisUntilAutoDisconnectResign: number, + wasByChoice: boolean +}) { + // This overwrites the "Opponent is AFK" timer + afk.stopOpponentAFKCountdown(); + // Cancel the previous one if this is overwriting + stopOpponentDisconnectCountdown(); + const timeLeftMillis = millisUntilAutoDisconnectResign - pingManager.getHalfPing(); + timeOpponentLoseFromDisconnect = Date.now() + timeLeftMillis; + // How much time is left? Usually starts at 20 | 60 seconds + const secsRemaining = Math.ceil(timeLeftMillis / 1000); + displayOpponentDisconnect(secsRemaining, wasByChoice); +} + +function stopOpponentDisconnectCountdown() { + clearTimeout(displayOpponentDisconnectTimeoutID); + displayOpponentDisconnectTimeoutID = undefined; +} + +function displayOpponentDisconnect(secsRemaining: number, wasByChoice: boolean) { + const opponent_disconnectedOrLostConnection = wasByChoice ? translations.onlinegame.opponent_disconnected : translations.onlinegame.opponent_lost_connection; + const resigningOrAborting = moveutil.isGameResignable(gameslot.getGamefile()!) ? translations.onlinegame.auto_resigning_in : translations.onlinegame.auto_aborting_in; + // The "You are AFK" message should overwrite, be on top of, this message, + // so if that is running, don't display this 1-second disconnect message, but don't cancel it either! + if (!afk.isOurAFKAutoResignTimerRunning()) statustext.showStatusForDuration(`${opponent_disconnectedOrLostConnection} ${resigningOrAborting} ${secsRemaining}...`, 1000); + const nextSecsRemaining = secsRemaining - 1; + if (nextSecsRemaining === 0) return; // Stop + const timeRemainUntilDisconnectLoss = timeOpponentLoseFromDisconnect! - Date.now(); + const timeToPlayNextDisplayOpponentDisconnect = timeRemainUntilDisconnectLoss - nextSecsRemaining * 1000; + displayOpponentDisconnectTimeoutID = setTimeout(displayOpponentDisconnect, timeToPlayNextDisplayOpponentDisconnect, nextSecsRemaining, wasByChoice); +} + + + +export default { + startOpponentDisconnectCountdown, + stopOpponentDisconnectCountdown, +}; \ No newline at end of file diff --git a/src/client/scripts/esm/game/misc/onlinegame/onlinegame.ts b/src/client/scripts/esm/game/misc/onlinegame/onlinegame.ts new file mode 100644 index 000000000..80e7e9cf5 --- /dev/null +++ b/src/client/scripts/esm/game/misc/onlinegame/onlinegame.ts @@ -0,0 +1,475 @@ + +/** + * This module keeps trap of the data of the onlinegame we are currently in. + * */ + +import type gamefile from '../../../chess/logic/gamefile.js'; +import type { Move } from '../../../chess/util/moveutil.js'; +import type { WebsocketMessage } from '../../websocket.js'; + + +import legalmoves from '../../../chess/logic/legalmoves.js'; +import localstorage from '../../../util/localstorage.js'; +import gamefileutility from '../../../chess/util/gamefileutility.js'; +import drawoffers from '../drawoffers.js'; +import guititle from '../../gui/guititle.js'; +import clock from '../../../chess/logic/clock.js'; +import guiclock from '../../gui/guiclock.js'; +import statustext from '../../gui/statustext.js'; +import movepiece from '../../../chess/logic/movepiece.js'; +import specialdetect from '../../../chess/logic/specialdetect.js'; +import selection from '../../chess/selection.js'; +import board from '../../rendering/board.js'; +import moveutil from '../../../chess/util/moveutil.js'; +import websocket from '../../websocket.js'; +import perspective from '../../rendering/perspective.js'; +import sound from '../sound.js'; +import guiplay from '../../gui/guiplay.js'; +import input from '../../input.js'; +import loadbalancer from '../loadbalancer.js'; +import formatconverter from '../../../chess/logic/formatconverter.js'; +import guipause from '../../gui/guipause.js'; +import guigameinfo from '../../gui/guigameinfo.js'; +import colorutil from '../../../chess/util/colorutil.js'; +import jsutil from '../../../util/jsutil.js'; +import config from '../../config.js'; +import pingManager from '../../../util/pingManager.js'; +import gameslot from '../../chess/gameslot.js'; +import gameloader from '../../chess/gameloader.js'; +import afk from './afk.js'; +import { DisconnectInfo, DrawOfferInfo } from '../onlinegamerouter.js'; +import tabnameflash from './tabnameflash.js'; +import disconnect from './disconnect.js'; +import serverrestart from './serverrestart.js'; + + +// Variables ------------------------------------------------------------------------------------------------------ + + +/** Whether or not we are currently in an online game. */ +const inOnlineGame: boolean = false; + +/** + * The id of the online game we are in, if we are in one. @type {string} + */ +let id: string | undefined; + +/** + * Whether the game is a private one (joined from an invite code). + */ +let isPrivate: boolean | undefined; + +/** + * The color we are in the online game. + */ +let ourColor: 'white' | 'black' | undefined; + +/** + * Different from gamefile.gameConclusion, because this is only true if {@link gamefileutility.concludeGame} + * has been called, which IS ONLY called once the SERVER tells us the result of the game, not us! + */ +let serverHasConcludedGame: boolean | undefined; + +/** + * Whether we are in sync with the game on the server. + * If false, we do not submit our move. (move auto-submitted upon resyncing) + * Set to false whenever the socket closes, or we unsub from the game. + * Set to true whenever we join game, or successfully resync. + */ +let inSync: boolean | undefined; + + + +// Functions ------------------------------------------------------------------------------------------------------ + + +(function init() { + addWarningLeaveGamePopupsToHyperlinks(); +})(); + +/** + * Add an listener for every single hyperlink on the page that will + * confirm to us if we actually want to leave if we are in an online game. + */ +function addWarningLeaveGamePopupsToHyperlinks() { + document.querySelectorAll('a').forEach((link) => { + link.addEventListener('click', confirmNavigationAwayFromGame); + }); +} + +/** + * Confirm that the user DOES actually want to leave the page if they are in an online game. + * + * Sometimes they could leave by accident, or even hit the "Logout" button by accident, + * which just ejects them out of the game + * @param {Event} event + */ +function confirmNavigationAwayFromGame(event) { + // Check if Command (Meta) or Ctrl key is held down + if (event.metaKey || event.ctrlKey) return; // Allow opening in a new tab without confirmation + if (!areInOnlineGame() || gamefileutility.isGameOver(gameslot.getGamefile()!)) return; + + const userConfirmed = confirm('Are you sure you want to leave the game?'); + if (userConfirmed) return; // Follow link like normal. Server then starts a 20-second auto-resign timer for disconnecting on purpose. + // Cancel the following of the link. + event.preventDefault(); + + /* + * KEEP IN MIND that if we leave the pop-up open for 10 seconds, + * JavaScript is frozen in that timeframe, which means as + * far as the server can tell we're not communicating anymore, + * so it automatically closes our websocket connection, + * thinking we've disconnected, and starts a 60-second auto-resign timer. + * + * As soon as we hit cancel, we are communicating again. + */ +} + + +// Getters -------------------------------------------------------------------------------------------------------------- + + +function areInOnlineGame(): boolean { + return inOnlineGame; +} + +/** + * Returns the game id of the online game we're in. + */ +function getGameID(): string { + if (!inOnlineGame) throw Error("Cannot get id of online game when we're not in an online game."); + return id!; +} + +function getIsPrivate(): boolean { + if (!inOnlineGame) throw Error("Cannot get isPrivate of online game when we're not in an online game."); + return isPrivate!; +} + +function getOurColor(): 'white' | 'black' { + if (!inOnlineGame) throw Error("Cannot get color we are in online game when we're not in an online game."); + return ourColor!; +} + +function getOpponentColor(): 'white' | 'black' { + return colorutil.getOppositeColor(ourColor!); +} + +function areWeColorInOnlineGame(color: string): boolean { + if (!inOnlineGame) return false; // Can't be that color, because we aren't even in a game. + return ourColor === color; +} + +function isItOurTurn(): boolean { + if (!inOnlineGame) throw Error("Cannot get isItOurTurn of online game when we're not in an online game."); + return gameslot.getGamefile()!.whosTurn === ourColor; +} + +/** + * Different from {@link gamefileutility.isGameOver}, because this only returns true if {@link gamefileutility.concludeGame} + * has been called, which IS ONLY called once the SERVER tells us the result of the game, not us! + */ +function hasServerConcludedGame(): boolean { + if (!inOnlineGame) throw Error("Cannot get serverHasConcludedGame of online game when we're not in an online game."); + return serverHasConcludedGame!; +} + + + + +function setInSyncFalse() { inSync = false; } +function setInSyncTrue() { inSync = true; } + + + + + + + +function update() { + afk.updateAFK(); +} + +/** + * Requests a game update from the server, since we are out of sync. + */ +function resyncToGame() { + if (!areInOnlineGame()) return; + function onReplyFunc() { inSync = true; } + websocket.sendmessage('game', 'resync', id, false, onReplyFunc); +} + +/** + * Adds or deletes moves in the game until it matches the server's provided moves. + * This can rarely happen when we move after the game is already over, + * or if we're disconnected when our opponent made their move. + * @param gamefile - The gamefile + * @param moves - The moves list in the most compact form: `['1,2>3,4','5,6>7,8Q']` + * @param claimedGameConclusion - The supposed game conclusion after synchronizing our opponents move + * @returns A result object containg the property `opponentPlayedIllegalMove`. If that's true, we'll report it to the server. + */ +function synchronizeMovesList(gamefile: gamefile, moves: string[], claimedGameConclusion: string | false): { opponentPlayedIllegalMove: boolean } { + + // Early exit case. If we have played exactly 1 more move than the server, + // and the rest of the moves list matches, don't modify our moves, + // just re-submit our move! + const hasOneMoreMoveThanServer = gamefile.moves.length === moves.length + 1; + const finalMoveIsOurMove = gamefile.moves.length > 0 && moveutil.getColorThatPlayedMoveIndex(gamefile, gamefile.moves.length - 1) === ourColor; + const previousMoveMatches = (moves.length === 0 && gamefile.moves.length === 1) || gamefile.moves.length > 1 && moves.length > 0 && gamefile.moves[gamefile.moves.length - 2].compact === moves[moves.length - 1]; + if (!claimedGameConclusion && hasOneMoreMoveThanServer && finalMoveIsOurMove && previousMoveMatches) { + console.log("Sending our move again after resyncing.."); + sendMove(); + return { opponentPlayedIllegalMove: false }; + } + + const originalMoveIndex = gamefile.moveIndex; + movepiece.forwardToFront(gamefile, { flipTurn: false, animateLastMove: false, updateProperties: false }); + let aChangeWasMade = false; + + while (gamefile.moves.length > moves.length) { // While we have more moves than what the server does.. + movepiece.rewindMove(gamefile, { animate: false }); + console.log("Rewound one move while resyncing to online game."); + aChangeWasMade = true; + } + + let i = moves.length - 1; + while (true) { // Decrement i until we find the latest move at which we're in sync, agreeing with the server about. + if (i === -1) break; // Beginning of game + const thisGamefileMove = gamefile.moves[i]; + if (thisGamefileMove) { // The move is defined + if (thisGamefileMove.compact === moves[i]) break; // The moves MATCH + // The moves don't match... remove this one off our list. + movepiece.rewindMove(gamefile, { animate: false }); + console.log("Rewound one INCORRECT move while resyncing to online game."); + aChangeWasMade = true; + } + i--; + } + + // i is now the index of the latest move that MATCHES in both ours and the server's moves lists. + + const opponentColor = getOpponentColor(); + while (i < moves.length - 1) { // Increment i, adding the server's correct moves to our moves list + i++; + const thisShortmove = moves[i]; // '1,2>3,4Q' The shortmove from the server's move list to add + const move = movepiece.calculateMoveFromShortmove(gamefile, thisShortmove); + + const colorThatPlayedThisMove = moveutil.getColorThatPlayedMoveIndex(gamefile, i); + const opponentPlayedThisMove = colorThatPlayedThisMove === opponentColor; + + + if (opponentPlayedThisMove) { // Perform legality checks + // If not legal, this will be a string for why it is illegal. + const moveIsLegal = legalmoves.isOpponentsMoveLegal(gamefile, move, claimedGameConclusion); + if (moveIsLegal !== true) console.log(`Buddy made an illegal play: ${thisShortmove} ${claimedGameConclusion}`); + if (moveIsLegal !== true && !isPrivate) { // Allow illegal moves in private games + reportOpponentsMove(moveIsLegal); + return { opponentPlayedIllegalMove: true }; + } + + afk.onMovePlayed({ isOpponents: true }); + tabnameflash.onMovePlayed({ isOpponents: true }); + } else cancelFlashTabTimer(); + + const isLastMove = i === moves.length - 1; + movepiece.makeMove(gamefile, move, { doGameOverChecks: isLastMove, concludeGameIfOver: false, animate: isLastMove }); + console.log("Forwarded one move while resyncing to online game."); + aChangeWasMade = true; + } + + if (!aChangeWasMade) movepiece.rewindGameToIndex(gamefile, originalMoveIndex, { removeMove: false }); + else selection.reselectPiece(); // Reselect the selected piece from before we resynced. Recalc its moves and recolor it if needed. + + return true; // No cheating detected +} + +function reportOpponentsMove(reason) { + // Send the move number of the opponents move so that there's no mixup of which move we claim is illegal. + const opponentsMoveNumber = gameslot.getGamefile().moves.length + 1; + + const message = { + reason, + opponentsMoveNumber + }; + + websocket.sendmessage('game', 'report', message); +} + + +function initOnlineGame(options: { + /** The id of the online game */ + id: string, + youAreColor: 'white' | 'black', + publicity: 'public' | 'private', + drawOffer: DrawOfferInfo, + /** If our opponent has disconnected, this will be present. */ + disconnect?: DisconnectInfo, + /** + * If our opponent is afk, this is how many millseconds left until they will be auto-resigned, + * at the time the server sent the message. Subtract half our ping to get the correct estimated value! + */ + millisUntilAutoAFKResign?: number, + /** If the server us restarting soon for maintenance, this is the time (on the server's machine) that it will be restarting. */ + serverRestartingAt?: number, +}) { + + id = options.id; + ourColor = options.youAreColor; + isPrivate = options.publicity === 'private'; + + drawoffers.set(options.drawOffer); + + + if (options.disconnect) disconnect.startOpponentDisconnectCountdown(options.disconnect); + afk.onGameStart(); + // If Opponent is currently afk, display that countdown + if (options.millisUntilAutoAFKResign !== undefined) afk.startOpponentAFKCountdown(options.millisUntilAutoAFKResign); + if (options.serverRestartingAt) serverrestart.initServerRestart(options.serverRestartingAt); + + tabnameflash.onGameStart({ isOurMove: isItOurTurn() }); + + // These make sure it will place us in black's perspective + // perspective.resetRotations(); + + serverHasConcludedGame = false; + +} + +// Call when we leave an online game +function closeOnlineGame() { + id = undefined; + isPrivate = undefined; + ourColor = undefined; + inSync = false; + serverHasConcludedGame = undefined; + afk.onGameClose(); + tabnameflash.onGameClose(); + resetServerRestarting(); + cancelFlashTabTimer(); + perspective.resetRotations(); // Without this, leaving an online game of which we were black, won't reset our rotation. + drawoffers.reset(); +} + + +function sendMove() { + if (!areInOnlineGame() || !inSync) return; // Don't do anything if it's a local game + if (config.DEV_BUILD) console.log("Sending our move.."); + + const gamefile = gameslot.getGamefile()!; + + const shortmove = moveutil.getLastMove(gamefile.moves).compact; // "x,y>x,yN" + + const data = { + move: shortmove, + moveNumber: gamefile.moves.length, + gameConclusion: gamefile.gameConclusion, + }; + + websocket.sendmessage('game', 'submitmove', data, true); + + // Declines any open draw offer from our opponent. We don't need to inform + // the server because the server auto declines when we submit our move. + drawoffers.callback_declineDraw({ informServer: false }); + + afk.onMovePlayed({ isOpponents: false }); +} + +// Aborts / Resigns +function onMainMenuPress() { + if (!areInOnlineGame()) return; + const gamefile = gameslot.getGamefile(); + if (serverHasConcludedGame) { // The server has concluded the game, not us + if (websocket.getSubs().game) { + websocket.sendmessage('general','unsub','game'); + websocket.getSubs().game = false; + } + return; + } + + if (moveutil.isGameResignable(gamefile)) resign(); + else abort(); +} + +function resign() { + websocket.getSubs().game = false; + inSync = false; + websocket.sendmessage('game','resign'); +} + +function abort() { + websocket.getSubs().game = false; + inSync = false; + websocket.sendmessage('game','abort'); +} + +/** + * Opens a websocket, asks the server if we are in + * a game to connect us to it and send us the game info. + */ +async function askServerIfWeAreInGame() { + // The server only allows sockets if we are either logged in, or have a browser-id cookie. + // browser-id cookies are issued/renewed on every html request. + + const messageContents = undefined; + websocket.sendmessage('game', 'joingame', messageContents, true); +} + +/** + * Lets the server know we have seen the game conclusion, and would + * like to be allowed to join a new game if we leave quickly. + * + * THIS SHOULD ALSO be the point when the server knows we agree + * with the resulting game conclusion (no cheating detected), + * and the server may change the players elos! + */ +function requestRemovalFromPlayersInActiveGames() { + if (!areInOnlineGame()) return; + websocket.sendmessage('game', 'removefromplayersinactivegames'); +} + + +function deleteCustomVariantOptions() { + // Delete any custom pasted position in a private game. + if (isPrivate) localstorage.deleteItem(id); +} + +/** Called when an online game is concluded (termination shown on-screen) */ +function onGameConclude() { + serverHasConcludedGame = true; // This NEEDS to be above drawoffers.reset(), as that relies on this! + cancelAFKTimer(); + cancelFlashTabTimer(); + cancelMoveSound(); + resetServerRestarting(); + deleteCustomVariantOptions(); + drawoffers.reset(); +} + +function onReceivedOpponentsMove() { + afk.onMovePlayed({ isOpponents: true }); + tabnameflash.onMovePlayed({ isOpponents: true }); +} + +export default { + onmessage, + getGameID, + getIsPrivate, + getOurColor, + setInSyncFalse, + setInSyncTrue, + initOnlineGame, + closeOnlineGame, + isItOurTurn, + sendMove, + onMainMenuPress, + askServerIfWeAreInGame, + requestRemovalFromPlayersInActiveGames, + resyncToGame, + update, + onGameConclude, + hasServerConcludedGame, + reportOpponentsMove, + onReceivedOpponentsMove, + synchronizeMovesList, + areInOnlineGame, + areWeColorInOnlineGame, +}; \ No newline at end of file diff --git a/src/client/scripts/esm/game/misc/onlinegame/serverrestart.ts b/src/client/scripts/esm/game/misc/onlinegame/serverrestart.ts new file mode 100644 index 000000000..5992ff042 --- /dev/null +++ b/src/client/scripts/esm/game/misc/onlinegame/serverrestart.ts @@ -0,0 +1,70 @@ + + +/** + * This script manages the periodic messages that display on-screen when you're in a game, + * stating the server will restart in N minutes. + */ + +import statustext from "../../gui/statustext.js"; + + +/** The minute intervals at which to display on scree, reminding the user the server is restarting. */ +const keyMinutes: number[] = [30, 20, 15, 10, 5, 2, 1, 0]; + +/** The time the server plans on restarting, if it has alerted us it is, otherwise false. */ +let time: number | undefined; + +/** The timeout ID of the timer to display the next "Server restarting..." message. + * This can be used to cancel the timer when the server informs us it's already restarted. */ +let timeoutID: ReturnType | undefined = undefined; + + + +/** + * Called when the server informs us they will be restarting shortly. + * This periodically reminds the user, in a game, of that fact. + * @param timeToRestart - The timestamp the server informed us it will be restarting. + */ +function initServerRestart(timeToRestart: number) { + if (time === timeToRestart) return; // We already know the server is restarting. + resetServerRestarting(); // Overwrite the previous one, if it exists. + time = timeToRestart; + const timeRemain = timeToRestart - Date.now(); + const minutesLeft = Math.ceil(timeRemain / (1000 * 60)); + console.log(`Server has informed us it is restarting in ${minutesLeft} minutes!`); + displayServerRestarting(minutesLeft); +} + +/** Displays the next "Server restaring..." message, and schedules the next one. */ +function displayServerRestarting(minutesLeft: number) { + if (minutesLeft === 0) { + statustext.showStatus(translations.onlinegame.server_restarting, false, 2); + time = undefined; + return; // Print no more server restarting messages + } + const minutes_plurality = minutesLeft === 1 ? translations.onlinegame.minute : translations.onlinegame.minutes; + statustext.showStatus(`${translations.onlinegame.server_restarting_in} ${minutesLeft} ${minutes_plurality}...`, false, 2); + let nextKeyMinute: number; + for (const keyMinute of keyMinutes) { + if (keyMinute < minutesLeft) { + nextKeyMinute = keyMinute; + break; + } + } + const timeToDisplayNextServerRestart = time! - nextKeyMinute! * 60 * 1000; + const timeUntilDisplayNextServerRestart = timeToDisplayNextServerRestart - Date.now(); + timeoutID = setTimeout(displayServerRestarting, timeUntilDisplayNextServerRestart, nextKeyMinute!); +} + +/** Cancels the timer to display the next "Server restaring..." message, and resets the values. */ +function resetServerRestarting() { + time = undefined; + clearTimeout(timeoutID); + timeoutID = undefined; +} + + +export default { + initServerRestart, + resetServerRestarting, +}; \ No newline at end of file diff --git a/src/client/scripts/esm/game/misc/onlinegame/tabnameflash.ts b/src/client/scripts/esm/game/misc/onlinegame/tabnameflash.ts new file mode 100644 index 000000000..2b10a111e --- /dev/null +++ b/src/client/scripts/esm/game/misc/onlinegame/tabnameflash.ts @@ -0,0 +1,85 @@ + +/** + * This script controls the flashing of the tab name "YOUR MOVE" + * when it is your turn and your in another tab. + */ + +import moveutil from "../../../chess/util/moveutil.js"; +import gameslot from "../../chess/gameslot.js"; +import loadbalancer from "../loadbalancer.js"; +import afk from "./afk.js"; +import sound from "../sound.js"; + + +/** The original tab title. We will always revert to this after temporarily changing the name name to alert player's it's their move. */ +const originalDocumentTitle: string = document.title; + +/** How rapidly the tab title should flash "YOUR MOVE" */ +const periodicityMillis = 1500; + +/** The ID of the timeout that can be used to cancel the timer that flips the tab title between "YOUR MOVE" and the default title. */ +let timeoutID: ReturnType | undefined; + +/** The ID of the timeout that can be used to cancel the timer that will play a move sound effect to help you realize it's your move. Typically about 20 seconds. */ +let moveSound_timeoutID: ReturnType | undefined; + + + +function onGameStart({ isOurMove }: { isOurMove: boolean }) { + // This will already flash the tab name + onMovePlayed({ isOpponents: isOurMove }); +} + +function onGameClose() { + timeoutID = undefined; +} + +function onMovePlayed({ isOpponents }: { isOpponents: boolean }) { + if (!isOpponents) return; + // Flash the tab name + flashTabNameYOUR_MOVE(true); + scheduleMoveSound_timeoutID(); +} + +/** + * Toggles the document title showing "YOUR MOVE", + * and sets a timer for the next toggle. + * @param parity - If true, the tab name becomes "YOUR MOVE", otherwise it reverts to the original title + */ +function flashTabNameYOUR_MOVE(parity: boolean) { + if (!loadbalancer.isPageHidden()) { + // The page is no longer hidden, restore the tab's original title, + // and stop flashing "YOUR MOVE" + document.title = originalDocumentTitle; + } + + document.title = parity ? "YOUR MOVE" : originalDocumentTitle; + // Set a timer for the next toggle + timeoutID = setTimeout(flashTabNameYOUR_MOVE, periodicityMillis, !parity); +} + +function cancelFlashTabTimer() { + document.title = originalDocumentTitle; + clearTimeout(timeoutID); + timeoutID = undefined; +} + +function scheduleMoveSound_timeoutID() { + if (!loadbalancer.isPageHidden()) return; // Don't schedule it if the page is already visible + if (!moveutil.isGameResignable(gameslot.getGamefile()!)) return; + const timeNextSoundFromNow = (afk.timeUntilAFKSecs * 1000) / 2; + moveSound_timeoutID = setTimeout(sound.playSound_move, timeNextSoundFromNow, 0); +} + +function cancelMoveSound() { + clearTimeout(moveSound_timeoutID); + moveSound_timeoutID = undefined; +} + + + +export default { + onGameStart, + onGameClose, + onMovePlayed, +}; \ No newline at end of file diff --git a/src/client/scripts/esm/game/misc/onlinegamerouter.ts b/src/client/scripts/esm/game/misc/onlinegamerouter.ts new file mode 100644 index 000000000..06e360e29 --- /dev/null +++ b/src/client/scripts/esm/game/misc/onlinegamerouter.ts @@ -0,0 +1,405 @@ + + +import type { ClockValues } from "../../chess/logic/clock.js"; +import type { MetaData } from "../../chess/util/metadata.js"; +// @ts-ignore +import type { WebsocketMessage } from "../websocket.js"; +// @ts-ignore +import type { Move } from "../../chess/util/moveutil.js"; +// @ts-ignore +import type gamefile from "../../chess/logic/gamefile.js"; + + +import gameloader from "../chess/gameloader.js"; +import gameslot from "../chess/gameslot.js"; +import guititle from "../gui/guititle.js"; +import jsutil from "../../util/jsutil.js"; +import clock from "../../chess/logic/clock.js"; +import guigameinfo from "../gui/guigameinfo.js"; +// @ts-ignore +import guiplay from "../gui/guiplay.js"; +// @ts-ignore +import websocket from "../websocket.js"; +// @ts-ignore +import onlinegame from "./onlinegame/onlinegame.js"; +// @ts-ignore +import statustext from "../gui/statustext.js"; +// @ts-ignore +import formatconverter from "../../chess/logic/formatconverter.js"; +// @ts-ignore +import legalmoves from "../../chess/logic/legalmoves.js"; +// @ts-ignore +import movepiece from "../../chess/logic/movepiece.js"; +// @ts-ignore +import gamefileutility from "../../chess/util/gamefileutility.js"; +// @ts-ignore +import specialdetect from "../../chess/logic/specialdetect.js"; +// @ts-ignore +import selection from "../chess/selection.js"; +// @ts-ignore +import guiclock from "../gui/guiclock.js"; +// @ts-ignore +import guipause from "../gui/guipause.js"; +// @ts-ignore +import drawoffers from "./drawoffers.js"; +// @ts-ignore +import board from "../rendering/board.js"; +import disconnect from "./onlinegame/disconnect.js"; +import afk from "./onlinegame/afk.js"; +import serverrestart from "./onlinegame/serverrestart.js"; + + +// Type Definitions -------------------------------------------------------------------------------------- + + +/** + * The message contents expected when we receive a server websocket 'joingame' message. + * This contains everything a {@link GameUpdateMessage} message would have, and more!! + * + * The stuff included here does not need to be specified when we're resyncing to + * a game, or receiving a game update, as we already know this stuff. + */ +interface JoinGameMessage extends GameUpdateMessage { + /** The id of the online game */ + id: string, + /** The metadata of the game, including the TimeControl, player names, date, etc.. */ + metadata: MetaData, + publicity: 'public' | 'private', + youAreColor: 'white' | 'black', +}; + +/** The message contents expected when we receive a server websocket 'move' message. */ +interface GameUpdateMessage { + gameConclusion: string | false, + /** Existing moves, if any, to forward to the front of the game. Should be specified if reconnecting to an online. Each move should be in the most compact notation, e.g., `['1,2>3,4','10,7>10,8Q']`. */ + moves: string[], + drawOffer: DrawOfferInfo, + clockValues?: ClockValues, + /** If our opponent has disconnected, this will be present. */ + disconnect?: DisconnectInfo, + /** + * If our opponent is afk, this is how many millseconds left until they will be auto-resigned, + * at the time the server sent the message. Subtract half our ping to get the correct estimated value! + */ + millisUntilAutoAFKResign?: number, + /** If the server us restarting soon for maintenance, this is the time (on the server's machine) that it will be restarting. */ + serverRestartingAt?: number, +} + +/** The message contents expected when we receive a server websocket 'move' message. */ +interface OpponentsMoveMessage { + /** The move our opponent played. In the most compact notation: `"5,2>5,4"` */ + move: string, + gameConclusion: string | false, + /** Our opponent's move number, 1-based. */ + moveNumber: number, + /** If the game is timed, this will be the current clock values. */ + clockValues?: ClockValues, +} + + + + + +interface DisconnectInfo { + /** + * How many milliseconds left until our opponent will be auto-resigned from disconnection, + * at the time the server sent the message. Subtract half our ping to get the correct estimated value! + */ + millisUntilAutoDisconnectResign: number, + /** Whether the opponent disconnected by choice, or if it was non-intentional (lost network). */ + wasByChoice: boolean +} + +interface DrawOfferInfo { + /** True if our opponent has extended a draw offer we haven't yet confirmed/denied */ + unconfirmed: boolean, + /** The move ply WE HAVE last offered a draw, if we have, otherwise undefined. */ + lastOfferPly?: number, +} + + +// Routers -------------------------------------------------------------------------------------- + + +/** + * Routes a server websocket message with subscription marked `game`. + * This handles all messages related to the active game we're in. + * @param {WebsocketMessage} data - The incoming server websocket message + */ +function routeMessage(data: WebsocketMessage) { // { sub, action, value, id } + // console.log(`Received ${data.action} from server! Message contents:`) + // console.log(data.value) + + // This action is listened to, even when we're not in a game. + + if (data.action === 'joingame') handleJoinGame(data.value); + + // All other actions should be ignored if we're not in a game... + + if (!onlinegame.areInOnlineGame()) { + console.log(`Received server 'game' message when we're not in an online game. Ignoring. Message: ${JSON.stringify(data.value)}`); + return; + } + + const gamefile = gameslot.getGamefile()!; + + switch (data.action) { + case "move": + handleOpponentsMove(gamefile, data.value); + break; + case "clock": + handleUpdatedClock(gamefile, data.value); + break; + case "gameupdate": + handleServerGameUpdate(gamefile, data.value); + break; + case "unsub": + handleUnsubbing(); + break; + case "login": + handleLogin(gamefile); + break; + case "nogame": // Game is deleted / no longer exists + handleNoGame(gamefile); + break; + case "leavegame": + handleLeaveGame(); + break; + case "opponentafk": + afk.startOpponentAFKCountdown(data.value.millisUntilAutoAFKResign); + break; + case "opponentafkreturn": + afk.stopOpponentAFKCountdown(); + break; + case "opponentdisconnect": + disconnect.startOpponentDisconnectCountdown(data.value); + break; + case "opponentdisconnectreturn": + disconnect.stopOpponentDisconnectCountdown(); + break; + case "serverrestart": + serverrestart.initServerRestart(data.value); + break; + case "drawoffer": + drawoffers.onOpponentExtendedOffer(); + break; + case "declinedraw": + drawoffers.onOpponentDeclinedOffer(); + break; + default: + statustext.showStatus(`Unknown action "${data.action}" received from server in 'game' route.`, true); + break; + } +} + + + +/** + * Joins a game when the server tells us we are now in one. + * + * This happens when we click an invite, or our invite is accepted. + * + * This type of message contains the MOST information about the game. + * Less then "gameupdate"s, or resyncing. + */ +function handleJoinGame(message: JoinGameMessage) { + // We were auto-unsubbed from the invites list, BUT we want to keep open the socket!! + const subs = websocket.getSubs(); + subs.invites = false; + subs.game = true; + onlinegame.setInSyncTrue(); + guititle.close(); + guiplay.close(); + gameloader.startOnlineGame(message); +} + +/** + * Called when we received our opponents move. This verifies they're move + * and claimed game conclusion is legal. If it isn't, it reports them and doesn't forward their move. + * If it is legal, it forwards the game to the front, then forwards their move. + */ +function handleOpponentsMove(gamefile: gamefile, message: OpponentsMoveMessage) { + // Make sure the move number matches the expected. + // Otherwise, we need to re-sync + const expectedMoveNumber = gamefile.moves.length + 1; + if (message.moveNumber !== expectedMoveNumber) { + console.log(`We have desynced from the game. Resyncing... Expected opponent's move number: ${expectedMoveNumber}. Actual: ${message.moveNumber}. Opponent's move: ${JSON.stringify(message.move)}. Move number: ${message.moveNumber}`); + return onlinegame.resyncToGame(); + } + + // Convert the move from compact short format "x,y>x,yN" + let move: Move; // { startCoords, endCoords, promotion } + try { + move = formatconverter.ShortToLong_CompactMove(message.move); // { startCoords, endCoords, promotion } + } catch { + console.error(`Opponent's move is illegal because it isn't in the correct format. Reporting... Move: ${JSON.stringify(message.move)}`); + const reason = 'Incorrectly formatted.'; + return onlinegame.reportOpponentsMove(reason); + } + + // If not legal, this will be a string for why it is illegal. + const moveIsLegal = legalmoves.isOpponentsMoveLegal(gamefile, move as Move, message.gameConclusion); + if (moveIsLegal !== true) console.log(`Buddy made an illegal play: ${JSON.stringify(message.move)}. Move number: ${message.moveNumber}`); + if (moveIsLegal !== true && !onlinegame.getIsPrivate()) return onlinegame.reportOpponentsMove(moveIsLegal); // Allow illegal moves in private games + + movepiece.forwardToFront(gamefile, { flipTurn: false, animateLastMove: false, updateProperties: false }); + + // Forward the move... + + const piecemoved = gamefileutility.getPieceAtCoords(gamefile, move.startCoords)!; + const legalMoves = legalmoves.calculate(gamefile, piecemoved); + const endCoordsToAppendSpecial = jsutil.deepCopyObject(move.endCoords); + legalmoves.checkIfMoveLegal(legalMoves, move.startCoords, endCoordsToAppendSpecial); // Passes on any special moves flags to the endCoords + + move.type = piecemoved.type; + specialdetect.transferSpecialFlags_FromCoordsToMove(endCoordsToAppendSpecial, move); + movepiece.makeMove(gamefile, move); + + selection.reselectPiece(); // Reselect the currently selected piece. Recalc its moves and recolor it if needed. + + // Edit the clocks + + // Adjust the timer whos turn it is depending on ping. + if (message.clockValues) message.clockValues = clock.adjustClockValuesForPing(message.clockValues); + clock.edit(gamefile, message.clockValues); + guiclock.edit(gamefile); + + // For online games, we do NOT EVER conclude the game, so do that here if our opponents move concluded the game + if (gamefileutility.isGameOver(gamefile)) { + gameslot.concludeGame(); + onlinegame.requestRemovalFromPlayersInActiveGames(); + } + + onlinegame.onReceivedOpponentsMove(); + guipause.onReceiveOpponentsMove(); // Update the pause screen buttons +} + +/** + * Called when we received the updated clock values from the server after submitting our move. + */ +function handleUpdatedClock(gamefile: gamefile, clockValues: ClockValues) { + // Adjust the timer whos turn it is depending on ping. + if (clockValues) clockValues = clock.adjustClockValuesForPing(clockValues); + clock.edit(gamefile, clockValues); // Edit the clocks + guiclock.edit(gamefile); +} + +/** + * Called when the server sends us the conclusion of the game when it ends, + * OR we just need to resync! The game may not always be over. + */ +function handleServerGameUpdate(gamefile: gamefile, message: GameUpdateMessage) { + const claimedGameConclusion = message.gameConclusion; + + /** + * Make sure we are in sync with the final move list. + * We need to do this because sometimes the game can end before the + * server sees our move, but on our screen we have still played it. + */ + if (!onlinegame.synchronizeMovesList(gamefile, message.moves, claimedGameConclusion)) { // Cheating detected. Already reported, don't + afk.stopOpponentAFKCountdown(); + return; + } + guigameinfo.updateWhosTurn(); + + // If Opponent is currently afk, display that countdown + if (message.millisUntilAutoAFKResign !== undefined && !onlinegame.isItOurTurn()) afk.startOpponentAFKCountdown(message.millisUntilAutoAFKResign); + else afk.stopOpponentAFKCountdown(); + + // If opponent is currently disconnected, display that countdown + if (message.disconnect !== undefined) disconnect.startOpponentDisconnectCountdown(message.disconnect); // { millisUntilAutoDisconnectResign, wasByChoice } + else disconnect.stopOpponentDisconnectCountdown(); + + // If the server is restarting, start displaying that info. + if (message.serverRestartingAt) serverrestart.initServerRestart(message.serverRestartingAt); + else serverrestart.resetServerRestarting(); + + drawoffers.set(message.drawOffer); + + // Must be set before editing the clocks. + gamefile.gameConclusion = claimedGameConclusion; + + // Adjust the timer whos turn it is depending on ping. + if (message.clockValues) message.clockValues = clock.adjustClockValuesForPing(message.clockValues); + clock.edit(gamefile, message.clockValues); + + if (gamefileutility.isGameOver(gamefile)) { + gameslot.concludeGame(); + onlinegame.requestRemovalFromPlayersInActiveGames(); + } +} + +/** + * Called after the server deletes the game after it has ended. + * It basically tells us the server will no longer be sending updates related to the game, + * so we should just unsub. + * + * Called when the server informs us they have unsubbed us from receiving updates from the game. + * At this point we should leave the game. + */ +function handleUnsubbing() { + websocket.getSubs().game = false; + onlinegame.setInSyncFalse(); +} + +/** + * The server has unsubscribed us from receiving updates from the game + * and from submitting actions as ourselves, + * due to the reason we are no longer logged in. + */ +function handleLogin(gamefile: gamefile) { + statustext.showStatus(translations['onlinegame'].not_logged_in, true, 100); + websocket.deleteSub('game'); + onlinegame.setInSyncFalse(); + clock.endGame(gamefile); + guiclock.stopClocks(gamefile); + selection.unselectPiece(); + board.darkenColor(); +} + +/** + * The server has reported the game no longer exists, + * there will be nore more updates for it. + * + * Visually, abort the game. + * + * This can happen when either: + * * Your page tries to resync to the game after it's long over. + * * The server restarts mid-game. + */ +function handleNoGame(gamefile: gamefile) { + statustext.showStatus(translations['onlinegame'].game_no_longer_exists, false, 1.5); + websocket.getSubs().game = false; + onlinegame.setInSyncFalse(); + gamefile.gameConclusion = 'aborted'; + gameslot.concludeGame(); + onlinegame.requestRemovalFromPlayersInActiveGames(); +} + +/** + * You have connected to the same game from another window/device. + * Leave the game on this page. + * + * This allows you to return to the invite creation screen, + * but you won't be allowed to create an invite if you're still in a game. + * However you can start a local game. + */ +function handleLeaveGame() { + statustext.showStatus(translations['onlinegame'].another_window_connected); + websocket.deleteSub('game'); + onlinegame.setInSyncFalse(); + gameloader.unloadGame(); + guititle.open(); +} + + + +export default { + routeMessage, +}; + +export type { + DisconnectInfo, + DrawOfferInfo, +}; \ No newline at end of file diff --git a/src/client/scripts/esm/game/rendering/arrows.js b/src/client/scripts/esm/game/rendering/arrows.js index d35bc6f0d..2a1b12b4a 100644 --- a/src/client/scripts/esm/game/rendering/arrows.js +++ b/src/client/scripts/esm/game/rendering/arrows.js @@ -3,7 +3,7 @@ import legalmoves from '../../chess/logic/legalmoves.js'; import input from '../input.js'; import legalmovehighlights from './highlights/legalmovehighlights.js'; -import onlinegame from '../misc/onlinegame.js'; +import onlinegame from '../misc/onlinegame/onlinegame.js'; import bufferdata from './bufferdata.js'; import perspective from './perspective.js'; import gamefileutility from '../../chess/util/gamefileutility.js'; @@ -438,7 +438,7 @@ function onPieceIndicatorHover(type, pieceCoords, direction) { // Determine what color the legal move highlights should be... const pieceColor = colorutil.getPieceColorFromType(type); - const opponentColor = gameloader.areInOnlineGame() ? colorutil.getOppositeColor(onlinegame.getOurColor()) : colorutil.getOppositeColor(gamefile.whosTurn); + const opponentColor = onlinegame.areInOnlineGame() ? colorutil.getOppositeColor(onlinegame.getOurColor()) : colorutil.getOppositeColor(gamefile.whosTurn); const isOpponentPiece = pieceColor === opponentColor; const isOurTurn = gamefile.whosTurn === pieceColor; const color = options.getLegalMoveHighlightColor({ isOpponentPiece, isPremove: !isOurTurn }); diff --git a/src/client/scripts/esm/game/rendering/options.js b/src/client/scripts/esm/game/rendering/options.js index 81c907fee..5d1302a6b 100644 --- a/src/client/scripts/esm/game/rendering/options.js +++ b/src/client/scripts/esm/game/rendering/options.js @@ -1,7 +1,7 @@ // Import Start import input from '../input.js'; -import onlinegame from '../misc/onlinegame.js'; +import onlinegame from '../misc/onlinegame/onlinegame.js'; import stats from '../gui/stats.js'; import perspective from './perspective.js'; import selection from '../chess/selection.js'; @@ -86,8 +86,8 @@ function isFPSOn() { function toggleEM() { // Make sure it's legal - const legalInPrivate = gameloader.areInOnlineGame() && onlinegame.getIsPrivate() && input.isKeyHeld('0'); - if (gameloader.areInOnlineGame() && !legalInPrivate) return; // Don't toggle if in an online game + const legalInPrivate = onlinegame.areInOnlineGame() && onlinegame.getIsPrivate() && input.isKeyHeld('0'); + if (onlinegame.areInOnlineGame() && !legalInPrivate) return; // Don't toggle if in an online game frametracker.onVisualChange(); // Visual change, render the screen this frame em = !em; diff --git a/src/client/scripts/esm/game/rendering/perspective.js b/src/client/scripts/esm/game/rendering/perspective.js index c7e76911d..611405a23 100644 --- a/src/client/scripts/esm/game/rendering/perspective.js +++ b/src/client/scripts/esm/game/rendering/perspective.js @@ -6,7 +6,7 @@ import piecesmodel from './piecesmodel.js'; import camera from './camera.js'; import statustext from '../gui/statustext.js'; import { createModel } from './buffermodel.js'; -import onlinegame from '../misc/onlinegame.js'; +import onlinegame from '../misc/onlinegame/onlinegame.js'; import mat4 from './gl-matrix.js'; import input from '../input.js'; import selection from '../chess/selection.js'; diff --git a/src/client/scripts/esm/game/rendering/pieces.js b/src/client/scripts/esm/game/rendering/pieces.js index 2d8c381f0..03b1818f6 100644 --- a/src/client/scripts/esm/game/rendering/pieces.js +++ b/src/client/scripts/esm/game/rendering/pieces.js @@ -6,7 +6,7 @@ import movement from './movement.js'; import piecesmodel from './piecesmodel.js'; import voids from './voids.js'; import board from './board.js'; -import onlinegame from '../misc/onlinegame.js'; +import onlinegame from '../misc/onlinegame/onlinegame.js'; import options from './options.js'; import { createModel } from './buffermodel.js'; import shapes from './shapes.js'; @@ -73,7 +73,7 @@ function renderPieces(gamefile) { const scale = [boardScale, boardScale, 1]; let modelToUse; - if (onlinegame.areWeColor('black')) modelToUse = perspective.getEnabled() && !perspective.getIsViewingBlackPerspective() && gamefile.mesh.rotatedModel !== undefined ? gamefile.mesh.rotatedModel : gamefile.mesh.model; + if (onlinegame.areWeColorInOnlineGame('black')) modelToUse = perspective.getEnabled() && !perspective.getIsViewingBlackPerspective() && gamefile.mesh.rotatedModel !== undefined ? gamefile.mesh.rotatedModel : gamefile.mesh.model; else modelToUse = perspective.getEnabled() && perspective.getIsViewingBlackPerspective() && gamefile.mesh.rotatedModel !== undefined ? gamefile.mesh.rotatedModel : gamefile.mesh.model; modelToUse.render(position, scale); diff --git a/src/client/scripts/esm/game/websocket.js b/src/client/scripts/esm/game/websocket.js index beb9fc510..6d7884dad 100644 --- a/src/client/scripts/esm/game/websocket.js +++ b/src/client/scripts/esm/game/websocket.js @@ -3,7 +3,7 @@ import statustext from './gui/statustext.js'; import invites from './misc/invites.js'; import guiplay from './gui/guiplay.js'; -import onlinegame from './misc/onlinegame.js'; +import onlinegame from './misc/onlinegame/onlinegame.js'; import localstorage from '../util/localstorage.js'; import timeutil from '../util/timeutil.js'; import uuid from '../util/uuid.js'; @@ -12,6 +12,7 @@ import thread from '../util/thread.js'; import validatorama from '../util/validatorama.js'; import wsutil from '../util/wsutil.js'; import options from './rendering/options.js'; +import onlinegamerouter from './misc/onlinegamerouter.js'; // Import End "use strict"; @@ -139,7 +140,6 @@ async function establishSocket() { // Request came back with an error noConnection = true; statustext.showStatusForDuration(translations.websocket.no_connection, timeToResubAfterNetworkLossMillis); - onlinegame.onLostConnection(); invites.clearIfOnPlayPage(); // Erase on-screen invites. await thread.sleep(timeToResubAfterNetworkLossMillis); success = await openSocket(); @@ -290,7 +290,7 @@ function onmessage(serverMessage) { // data: { sub, action, value, id, replyto } invites.onmessage(message); break; case "game": - onlinegame.onmessage(message); + onlinegamerouter.routeMessage(message); break; default: console.error("Unknown socket subscription received from the server! Message:"); @@ -505,8 +505,8 @@ function leaveTimeout() { * Sends a message to the server with the provided route, action, and values * @param {string} route - Where the server needs to forward this to. general/invites/game * @param {string} action - What action to take within the route. - * @param {*} value - The contents of the message - * @param {boolean} isUserAction - Whether this message is a direct result of a user action. If so, and we happen to receive the "Too many requests" error, then that will be displayed on screen. Default: false + * @param {*} [value] - The contents of the message + * @param {boolean} [isUserAction] - Whether this message is a direct result of a user action. If so, and we happen to receive the "Too many requests" error, then that will be displayed on screen. Default: false * @param {Function} [onreplyFunc] An optional function to execute when we receive the server's response to this message, or to execute immediately if we can't establish a socket, or after 5 seconds if we don't hear anything back. * @returns {boolean} *true* if the message was able to send. */ @@ -671,7 +671,7 @@ async function resubAll() { function unsubFromInvites() { invites.clear({ recentUsersInLastList: true }); if (subs.invites === false) return; // Already unsubbed - subs.invites = false; + deleteSub('invites'); sendmessage("general", "unsub", "invites"); } @@ -722,10 +722,22 @@ async function onAuthenticationNeeded() { resubAll(); } +/** + * Marks ourself as no longer subscribed to a subscription list. + * + * If our websocket happens to close unexpectedly, we won't re-subscribe to it. + * @param {'invites' | 'game'} sub - The name of the sub to delete + */ +function deleteSub(sub) { + if (!validSubs.includes(sub)) throw Error(`Can't delete invalid sub "${sub}".`); + subs[sub] = false; +} + export default { closeSocket, sendmessage, unsubFromInvites, getSubs, - addTimerIDToCancelOnNewSocket + deleteSub, + addTimerIDToCancelOnNewSocket, }; \ No newline at end of file diff --git a/src/client/scripts/esm/util/jsutil.ts b/src/client/scripts/esm/util/jsutil.ts index d61c09896..23c23b952 100644 --- a/src/client/scripts/esm/util/jsutil.ts +++ b/src/client/scripts/esm/util/jsutil.ts @@ -192,6 +192,62 @@ function getMissingStringsFromArray(array1: string[], array2: string[]): string[ } +/** + * Estimates the size, in memory, of ANY object, no matter how deep it's nested, + * and returns that number in a human-readable string. + * + * This takes into account added overhead from each object/array created, + * as those have extra prototype methods, etc, adding more memory. + * + * For that reason, it'd be good to avoid the number of objects we create being + * linear with the number of pieces in our game. + */ +function estimateMemorySizeOf(obj: any): string { + // Credit: Liangliang Zheng https://stackoverflow.com/a/6367736 + function roughSizeOfObject(value: any, level?: number ) { + if (level === undefined) level = 0; + let bytes = 0; + + if (typeof value === 'boolean') bytes = 4; + else if (typeof value === 'string' ) bytes = value.length * 2; + else if (typeof value === 'number') bytes = 8; + else if (value === null) bytes = 1; + else if (typeof value === 'object') { + if (value['__visited__']) return 0; + value['__visited__'] = 1; + for (const i in value) { + bytes += i.length * 2; + bytes += 8; // an assumed existence overhead + bytes += roughSizeOfObject(value[i], 1); + } + } + + if (level === 0) clear__visited__(value); + return bytes; + } + + function clear__visited__(value: any) { + if (typeof value === 'object' && value !== null) { + delete value['__visited__']; + for (const i in value) { + clear__visited__(value[i]); + } + } + } + + // Turns the number into a human-readable string + function formatByteSize(bytes: number): string { + if (bytes < 1000) return bytes + " bytes"; + else if (bytes < 1000000) return (bytes / 1000).toFixed(3) + " KB"; + else if (bytes < 1000000000) return (bytes / 1000000).toFixed(3) + " MB"; + else return (bytes / 1000000000).toFixed(3) + " GB"; + }; + + return formatByteSize(roughSizeOfObject(obj)); +}; + + + export default { deepCopyObject, @@ -206,4 +262,5 @@ export default { invertObj, removeObjectFromArray, getMissingStringsFromArray, + estimateMemorySizeOf, }; \ No newline at end of file diff --git a/src/client/views/play.ejs b/src/client/views/play.ejs index 942e48ab2..43b173ac5 100644 --- a/src/client/views/play.ejs +++ b/src/client/views/play.ejs @@ -375,85 +375,82 @@ -