trivabble/public/trivabble.js

1724 lines
56 KiB
JavaScript

/**
* Copyright (C) 2016-2020 Raphaël Jakse <raphael.trivabble@jakse.fr>
*
* @licstart
* This file is part of Trivabble.
*
* Trivabble is free software: you can redistribute it and/or modify it
* under the terms of the GNU Affero General Public License (GNU AGPL)
* as published by the Free Software Foundation, either version 3 of
* the License, or (at your option) any later version.
*
* Trivabble is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with Trivabble. If not, see <http://www.gnu.org/licenses/>.
* @licend
*
* @source: https://trivabble.1s.fr/
* @source: https://gitlab.com/raphj/trivabble/
*/
/*global libD, myConfirm, myAlert, myPrompt*/
(function () {
"use strict";
const VERSION = 202005070130;
const Conf = window.TrivabbleConf || {};
function setConf(parameterName, defaultValue) {
if (typeof Conf[parameterName] === "undefined") {
Conf[parameterName] = defaultValue;
}
}
setConf("POLLING_DELAY", 2000);
setConf("ENABLE_WEBSOCKETS", true);
setConf("ENABLE_EVENT_SOURCE", true);
setConf("MAX_WEBSOCKET_ERRORS", 1);
setConf("APP_PATH", "");
const _ = (window.libD && libD.l10n) ? libD.l10n() : function (s) {
return s;
};
const trivabble = window.trivabble = {l10n: _};
function format(s, v) {
return s.replace("{0}", v);
}
let board;
let rack;
const boardCells = [];
let scoreOf;
let bag;
let boardLangSelect;
const playerLetters = [];
let currentPlayer = "";
let audioNotification;
let audioChat;
let chatMessages;
let chatTextarea;
let helpBag;
let helpClear;
let tablePlayers = {};
let participantPlaceholder;
let participants;
let name;
let blockMove = 0;
let remainingLetters = 0;
let needsRestart = false;
const gameInProgress = false;
let eventSource = null;
let webSocket = null;
let connectionLostMessage = null;
let boundEventShowConnectionLost = null;
let retryPollingTimeout = 0;
let pollingServer = false;
let connectionStopped = false;
let currentMessageId = 1;
const waitingMsgs = [];
let serverVersion = 0;
// HTTP path url prefix for any socket (xhr,ess or ws) from browser to server access
function getApiEntryPoint() {
return Conf.APP_PATH + "/:trivabble";
}
function mouseDown(ele, fun, stop) {
const meth = stop ? "removeEventListener" : "addEventListener";
ele[meth]("mousedown", fun, false);
}
function mouseUp(ele, fun, stop) {
const meth = stop ? "removeEventListener" : "addEventListener";
ele[meth]("mouseup", fun, false);
}
function mouseMove(ele, fun, stop) {
const meth = stop ? "removeEventListener" : "addEventListener";
ele[meth]("mousemove", fun, false);
}
function setRack(rack) {
for (let i = 0; i < 7; i++) {
setTileParent(playerLetters[i], rack[i] || "");
}
}
function getFreeRackSpaceIndex() {
for (let i = 0; i < 7; i++) {
if (!playerLetters[i].getElementsByClassName("tile")[0]) {
return i;
}
}
return -1;
}
function showConnectionLost(msg) {
unbindEventsShowConnectionLost();
if (!connectionLostMessage) {
connectionLostMessage = chatMessage("", msg || _("Whoops, there is a problem. We are trying to fix it as soon as possible. Please wait a few seconds. If it persists, please contact the person who is able to fix the problem."));
connectionLostMessage.classList.add("error");
}
}
function unbindEventsShowConnectionLost() {
if (boundEventShowConnectionLost) {
document.body.removeEventListener("mousemove", boundEventShowConnectionLost);
document.body.removeEventListener("mousedown", boundEventShowConnectionLost);
document.body.removeEventListener("touchstart", boundEventShowConnectionLost);
boundEventShowConnectionLost = null;
}
}
function removeElem(elem) {
elem.parentNode.removeChild(elem);
}
function connectionReady() {
pollingReady = true;
retriedImmediately = false;
unbindEventsShowConnectionLost();
if (connectionLostMessage) {
connectionLostMessage.querySelector(".msg-content").textContent = _("The problem is solved, sorry for the inconvenience!");
connectionLostMessage.classList.remove("error");
connectionLostMessage.classList.add("ok");
const removeMessage = removeElem.bind(null, connectionLostMessage);
connectionLostMessage.addEventListener("click", removeMessage);
connectionLostMessage.addEventListener("touch", removeMessage);
connectionLostMessage = null;
}
}
let tileInitCoords = null;
let tileInitMouseCoords = null;
let tileDest = null;
let tileInitDest = null;
let movingTile = null;
let rackBCR = null;
let boardBCR = null;
let bagBCR = null;
let moveCMD = null;
let rackChanged = false;
function getLetter(l) {
const tile = l.getElementsByClassName("tile")[0];
if (!tile) {
return "";
}
return tile.getElementsByClassName("tile-letter")[0].textContent;
}
function isParentOf(p, elem) {
while (elem) {
if (elem === p) {
return true;
}
elem = elem.parentNode;
}
return false;
}
function dragTileEnd() {
movingTile.style.left = "";
movingTile.style.top = "";
movingTile.style.width = "";
movingTile.style.height = "";
mouseUp(document, dragTileEnd, true);
mouseMove(document, dragTileMove, true);
if (tileDest === bag) {
moveCMD.to = "bag";
moveCMD.indexTo = -1;
movingTile.parentNode.removeChild(movingTile);
} else if (tileDest) {
tileDest.appendChild(movingTile);
if (isParentOf(board, tileDest)) {
for (const tile of [].slice.call(board.getElementsByClassName("tile-highlight"))) {
tile.classList.remove("tile-highlight");
}
}
} else if (tileInitDest.getElementsByClassName("tile")[0]) {
for (let i = 0; i < 7; i++) {
if (!playerLetters[i].getElementsByClassName("tile")[0]) {
playerLetters[i].appendChild(movingTile);
break;
}
}
} else {
tileInitDest.appendChild(movingTile);
}
if (tileDest) {
tileDest.classList.remove("tile-target");
const moveRack = {
cmd: "setRack",
rack: playerLetters.map(getLetter)
};
if (moveCMD.to === moveCMD.from) {
if (moveCMD.indexTo === moveCMD.indexFrom) {
if (rackChanged) {
sendCmds([moveRack]);
}
} else if (moveCMD.from === "rack") {
sendCmds([moveRack]);
} else if (rackChanged) {
sendCmds([moveCMD, moveRack]);
} else {
sendCmds([moveCMD]);
}
} else if (moveCMD.to === "rack") {
moveRack.rack[moveCMD.indexTo] = "";
sendCmds([moveRack, moveCMD]);
} else if (rackChanged) {
sendCmds([moveCMD, moveRack]);
} else {
sendCmds([moveCMD]);
}
}
tileInitMouseCoords = null;
tileInitCoords = null;
tileInitDest = null;
tileDest = null;
movingTile = null;
boardBCR = null;
bagBCR = null;
rackBCR = null;
moveCMD = null;
}
function dragTileMove(e) {
let newLeft = (tileInitCoords.left + (e.clientX - tileInitMouseCoords.clientX));
let newTop = (tileInitCoords.top + (e.clientY - tileInitMouseCoords.clientY));
movingTile.style.left = (newLeft + window.scrollX) + "px";
movingTile.style.top = (newTop + window.scrollY) + "px";
let newDest = null;
newTop += tileInitCoords.height / 2;
newLeft += tileInitCoords.width / 2;
if (
(newTop > boardBCR.top && newTop < (boardBCR.top + boardBCR.height)) &&
(newLeft > boardBCR.left && newLeft < (boardBCR.left + boardBCR.width))
) {
const rowIndex = Math.floor(
(
(newTop - boardBCR.top) / boardBCR.height
) * board.rows.length
);
if (rowIndex > 0 && rowIndex < board.rows.length - 1) {
const row = board.rows[rowIndex];
const colIndex = Math.floor(
(
(newLeft - boardBCR.left) / boardBCR.width
) * row.cells.length
);
if (colIndex > 0 && colIndex < row.cells.length - 1) {
newDest = row.cells[colIndex].firstChild;
}
}
if (newDest && newDest.getElementsByClassName("tile")[0]) {
newDest = null;
}
if (newDest) {
moveCMD.to = "board";
moveCMD.indexTo = boardCells.indexOf(newDest.parentNode);
}
} else if (
(newTop > rackBCR.top && newTop < (rackBCR.top + rackBCR.height)) &&
(newLeft > rackBCR.left && newLeft < (rackBCR.left + rackBCR.width))
) {
const index = Math.floor(
(
(newLeft - rackBCR.left) / rackBCR.width
) * playerLetters.length
);
newDest = playerLetters[index];
if (newDest && newDest.getElementsByClassName("tile")[0]) {
let i = index + 1;
let tile;
while (playerLetters[i]) {
tile = playerLetters[i].getElementsByClassName("tile")[0];
if (!tile) {
let j = i;
while (j > index) {
rackChanged = true;
playerLetters[j].appendChild(
playerLetters[j - 1].getElementsByClassName("tile")[0]
);
j--;
}
break;
}
i++;
}
if (newDest.getElementsByClassName("tile")[0]) {
i = index - 1;
while (playerLetters[i]) {
tile = playerLetters[i].getElementsByClassName("tile")[0];
if (!tile) {
let j = i;
while (j < index) {
playerLetters[j].appendChild(
playerLetters[j + 1].getElementsByClassName("tile")[0]
);
j++;
}
break;
}
i--;
}
}
}
if (newDest.getElementsByClassName("tile")[0]) {
newDest = null;
} else {
moveCMD.to = "rack";
moveCMD.indexTo = index;
}
} else if (
(newTop > bagBCR.top && newTop < (bagBCR.top + bagBCR.height)) &&
(newLeft > bagBCR.left && newLeft < (bagBCR.left + bagBCR.width))
) {
newDest = bag;
}
if (newDest !== tileDest) {
if (tileDest) {
tileDest.classList.remove("tile-target");
}
if (newDest) {
newDest.classList.add("tile-target");
}
}
tileDest = newDest;
}
function dragTileBegin(e) {
preventDefault(e);
if (blockMove || waitingMsgs.length) {
return;
}
if (!pollingReady) {
showConnectionLost();
return;
}
loadAudio();
rackChanged = false;
movingTile = e.currentTarget;
tileInitMouseCoords = e;
tileInitCoords = movingTile.getBoundingClientRect();
tileInitDest = movingTile.parentNode;
boardBCR = board.getBoundingClientRect();
rackBCR = rack.getBoundingClientRect();
bagBCR = bag.getBoundingClientRect();
let from;
let index;
let p = movingTile.parentNode;
let oldP = movingTile;
let oldOldP = null;
while (p) {
if (p === board) {
from = "board";
index = boardCells.indexOf(oldOldP);
break;
}
if (p === rack) {
from = "rack";
index = playerLetters.indexOf(oldP);
break;
}
oldOldP = oldP;
oldP = p;
p = p.parentNode;
}
moveCMD = {
cmd: "moveLetter",
from: from,
indexFrom: index
};
mouseMove(document, dragTileMove);
mouseUp(document, dragTileEnd);
movingTile.style.left = tileInitCoords.left + window.scrollX + "px";
movingTile.style.top = tileInitCoords.top + window.scrollY + "px";
movingTile.style.width = tileInitCoords.width + "px";
movingTile.style.height = tileInitCoords.height + "px";
document.body.appendChild(movingTile);
}
function setLetter(tile, letter, highlight) {
tile.firstChild.textContent = letter;
tile.lastChild.textContent = scoreOf[letter] || "";
if (highlight) {
tile.classList.add("tile-highlight");
if (tilesSound.checked) {
audioNotification.play();
}
} else {
tile.classList.remove("tile-highlight");
}
}
function preventDefault(e) {
e.preventDefault();
e.stopPropagation();
return false;
}
function makeLetter(letter, highlight, noEvents) {
const tile = document.createElement("span");
tile.translate = false;
tile.className = "tile";
tile.appendChild(document.createElement("span"));
tile.lastChild.className = "tile-letter";
tile.appendChild(document.createElement("span"));
tile.lastChild.className = "tile-score";
if (!noEvents) {
mouseDown(tile, dragTileBegin);
tile.addEventListener("contextmenu", preventDefault);
tile.addEventListener("touchstart", preventDefault);
tile.addEventListener("touchmove", preventDefault);
}
setLetter(tile, letter, highlight);
return tile;
}
function setTileParent(p, letter, highlight) {
let tile = p.getElementsByClassName("tile")[0];
if (tile) {
if (letter) {
setLetter(tile, letter, highlight);
} else {
p.removeChild(tile);
}
} else if (letter) {
tile = makeLetter(letter, highlight);
p.appendChild(tile);
}
}
function setCell(index, letter, highlight) {
setTileParent(boardCells[index].getElementsByClassName("tile-placeholder")[0], letter, highlight);
}
function setBoard(board) {
for (let i = 0; i < 15 * 15; i++) {
setCell(i, board[i]);
}
}
function set(key, value) {
switch (key) {
case "playerName":
name.textContent = localStorage.trivabblePlayerName = value;
break;
case "gameNumber":
document.getElementById("number").textContent = localStorage.trivabbleGameNumber = value;
break;
case "boardLang":
localStorage.trivabbleBoardLang = value;
document.getElementById("board-lang").value = value;
break;
}
}
function checkGameInProgress(f) {
if (!gameInProgress) {
return f();
}
myConfirm(
_("Your game is not over. Are you sure you want to leave now?"),
function () {
myAlert(
format(
_("You are about to leave the current game. To recover it, please note its number: {0}"),
localStorage.trivabbleGameNumber
),
f
);
}
);
}
let pollingReady = false;
let retriedImmediately = false;
function forceReload(msg) {
needsRestart = true;
myConfirm(
msg,
function () {
location.reload();
}, function () {
blockMove = 1000;
document.getElementById("panel").appendChild(document.createElement("a"));
document.getElementById("panel").lastChild.href = "#";
document.getElementById("panel").lastChild.onclick = location.reload.bind(location);
document.getElementById("panel").lastChild.textContent = _("To continue playing, click here");
}
);
}
function fatalError(e) {
if (!needsRestart) {
// No need to show the user that a problem just happened if a message asking to restart has already been shown.
forceReload(_("Sorry, a problem just happened. The page must be reloaded. If the problem is not too serious, you should be able to keep playing normally. Otherwise, contact the person who is able to fix the problem. Click on “Yes” to reload the page."));
}
throw (e || new Error("interrupting the game because of a fatal error"));
}
function jsonError(json, e) {
console.error("An error ocurred while parsing this JSON message:", json);
fatalError(e);
}
function setRackCell(index, letter) {
setTileParent(playerLetters[index], letter);
}
function blinkTransitionEnd(element) {
element.removeEventListener("animationend", element._te, false);
element.classList.remove("blink");
}
function blink(element) {
element._te = blinkTransitionEnd.bind(null, element);
element.addEventListener("animationend", element._te, false);
setTimeout(element._te, 5000);
element.classList.add("blink");
}
function chatMessage(sender, content) {
const msgDom = document.createElement("div");
msgDom.className = "msg";
if (sender) {
msgDom.appendChild(document.createElement("span"));
msgDom.lastChild.className = "msg-sender";
msgDom.lastChild.textContent = _("{0}: ").replace("{0}", sender);
}
if (content instanceof Element) {
msgDom.appendChild(content);
} else {
msgDom.appendChild(document.createElement("span"));
msgDom.lastChild.className = "msg-content";
msgDom.lastChild.textContent = content;
}
chatMessages.appendChild(msgDom);
chatMessages.scrollTop = chatMessages.scrollHeight;
if (sender && sender !== localStorage.trivabblePlayerName) {
if (msgSound.checked) {
audioChat.play();
}
blink(chatMessages);
}
return msgDom;
}
function handleChatMessage(msg) {
if (msg.specialMsg) {
if (msg.specialMsg.type === "rack") {
const content = document.createElement("div");
content.appendChild(document.createElement("span"));
content.lastChild.className = "info";
content.lastChild.textContent = format(_("{0} shows their rack:"), msg.sender);
const letters = document.createElement("div");
letters.className = "tile-list";
for (let i = 0; i < msg.specialMsg.rack.length; i++) {
letters.appendChild(makeLetter(msg.specialMsg.rack[i], false, true));
}
content.appendChild(letters);
chatMessage("", content);
return;
}
}
chatMessage(msg.sender, msg.content);
}
function refreshCurrentPlayer() {
const row = tablePlayers[currentPlayer];
if (currentPlayer && row && !row.classList.contains("current-player")) {
row.classList.add("current-player");
blink(row.firstChild);
}
}
function setCurrentPlayer(player) {
if (currentPlayer && tablePlayers[currentPlayer]) {
tablePlayers[currentPlayer].classList.remove("current-player");
}
currentPlayer = player;
refreshCurrentPlayer();
}
function setPlayers(players) {
if (participantPlaceholder) {
participantPlaceholder.parentNode.removeChild(participantPlaceholder);
participantPlaceholder = null;
}
for (let i = 0; i < players.length; i++) {
const player = players[i];
const playerName = players[i].player;
if (!tablePlayers[playerName]) {
let before = null;
for (let j = 1; j < participants.rows.length; j++) {
if (playerName < participants.rows[j].cells[0].textContent) {
before = participants.rows[j];
break;
}
}
const row = document.createElement("tr");
participants.insertBefore(row, before);
row.appendChild(document.createElement("td"));
row.lastChild.textContent = playerName;
row.appendChild(document.createElement("td"));
row.appendChild(document.createElement("td"));
row.lastChild.className = "score-cell";
row.lastChild.appendChild(document.createElement("span"));
row.lastChild.lastChild.onclick = (
function (row) {
return function () {
if (!pollingReady) {
showConnectionLost();
return;
}
myPrompt(
format(
_("Enter the new score of {0}:"),
row.firstChild.textContent
),
function (res) {
res = parseInt(res);
if (!isNaN(res)) {
sendCmds([{
cmd: "score",
player: row.firstChild.textContent,
score: res
}]);
}
},
row.lastChild.firstChild.textContent
);
};
}
)(row);
row.lastChild.appendChild(document.createTextNode(" "));
row.lastChild.appendChild(document.createElement("button"));
row.lastChild.lastChild.textContent = "+";
row.lastChild.lastChild.onclick = (
function (row) {
return function () {
if (!pollingReady) {
showConnectionLost();
return;
}
myPrompt(
format(
_("Enter the score to add to {0}:"),
row.firstChild.textContent
),
function (res) {
res = parseInt(res);
if (!isNaN(res)) {
sendCmds([{
cmd: "score",
player: row.firstChild.textContent,
score: parseInt(row.childNodes[2].childNodes[0].textContent) + res
}]);
}
},
row.lastChild.firstChild.textContent
);
};
}
)(row);
row.appendChild(document.createElement("td"));
row.lastChild.className = "turn-cell";
row.lastChild.appendChild(document.createElement("button"));
const img = new Image();
img.src = "baton.svg";
row.lastChild.lastChild.appendChild(img);
row.lastChild.lastChild.onclick = (function (playerName) {
sendCmds([{
cmd: "currentPlayer",
player: playerName === currentPlayer ? "" : playerName
}]);
}).bind(null, playerName);
tablePlayers[playerName] = row;
}
if (Object.prototype.hasOwnProperty.call(player, "score")) {
const scoreCell = tablePlayers[playerName].childNodes[2].childNodes[0];
scoreCell.textContent = player.score;
blink(scoreCell);
}
if (Object.prototype.hasOwnProperty.call(player, "rackCount")) {
const countCell = tablePlayers[playerName].childNodes[1];
countCell.textContent = player.rackCount;
blink(countCell);
}
}
refreshCurrentPlayer();
}
function applyAction(data) {
switch (data.action) {
case "pushBag": //TODO
break;
case "popBag": //TODO
break;
case "reset": //TODO
tablePlayers = {};
while (participants.rows[1]) {
participants.removeChild(participants.rows[1]);
}
sendCmds([{cmd: "hello"}]);
break;
case "moveLetter":
if (data.from === "board") {
setCell(data.indexFrom, "");
} else if (data.from === "rack") {
setRackCell(data.indexFrom, "");
}
if (data.to === "board") {
setCell(data.indexTo, data.letter, Object.prototype.hasOwnProperty.call(data, "player") && data.player !== localStorage.trivabblePlayerName);
} else if (data.to === "rack") {
setRackCell(data.indexTo, data.letter);
}
break;
case "setCell":
setCell(data.indexTo, data.letter, Object.prototype.hasOwnProperty.call(data, "player") && data.player !== localStorage.trivabblePlayerName);
break;
case "setRackCell":
setRackCell(data.indexTo, data.letter);
}
}
function handleReceivedData(data) {
if (Array.isArray(data)) {
data.forEach(handleReceivedData);
return;
}
if (data.id) {
const pos = waitingMsgs.indexOf(data.id);
if (pos !== -1) {
waitingMsgs.splice(pos, 1);
}
}
if (data.version) {
serverVersion = data.version;
}
if (data.pleaseRestart) {
forceReload(_("Sorry to disturb you, but we need to reload the game so you can continue playing. You will not lose anything except the messages in the chat. Are you ready?"));
}
if (data.error) {
fatalError(new Error("Error from the server: " + data.error + " - " + data.reason));
}
if (data.stopping) {
stopConnection();
retryPolling(
(typeof data.stopping === "number" && data.stopping > 0)
? data.stopping
: 0
);
}
if (data.msg) {
handleChatMessage(data.msg);
}
if (typeof data.currentPlayer === "string") {
setCurrentPlayer(data.currentPlayer);
}
if (data.players) {
setPlayers(data.players);
}
if (data.playerName) {
set("playerName", data.playerName);
}
if (data.gameNumber) {
set("gameNumber", data.gameNumber);
}
if (data.availableBoardLangs) {
setAvailableBoardLangs(data.availableBoardLangs);
}
if (data.boardLang) {
set("boardLang", data.boardLang);
(boardLangSelect || {}).value = data.boardLang;
}
if (data.letterValues) {
scoreOf = data.letterValues;
}
if (data.board) {
setBoard(data.board);
}
if (typeof data.remainingLetters === "number") {
remainingLetters = data.remainingLetters;
document.getElementById("remaining-letters").textContent = data.remainingLetters;
if (data.remainingLetters === 0) {
helpBag.style.display = "none";
helpClear.style.display = "";
} else {
helpBag.style.display = "";
helpClear.style.display = "none";
}
}
if (data.rack) {
setRack(data.rack);
}
if (data.action) {
applyAction(data);
}
}
function retryPolling(delay) {
if (needsRestart) {
return;
}
pollingReady = false;
if (!boundEventShowConnectionLost) {
boundEventShowConnectionLost = showConnectionLost.bind(null, null);
document.body.addEventListener("mousemove", boundEventShowConnectionLost);
document.body.addEventListener("mousedown", boundEventShowConnectionLost);
document.body.addEventListener("touchstart", boundEventShowConnectionLost);
}
if (retryPollingTimeout) {
return;
}
if (delay || retriedImmediately) {
retryPollingTimeout = setTimeout(
function () {
connectionStopped = false;
pollingServer = false;
startConnection();
retryPollingTimeout = 0;
},
delay || Conf.POLLING_DELAY
);
} else {
retriedImmediately = true;
connectionStopped = false;
pollingServer = false;
startConnection();
}
}
function parseAndHandleReceivedData(r) {
let m;
try {
m = JSON.parse(r);
} catch (e) {
jsonError(r, e);
}
try {
handleReceivedData(m);
} catch (e) {
fatalError(e);
}
}
function closeConnections() {
if (eventSource) {
eventSource.onerror = null;
eventSource.close();
eventSource = null;
}
if (webSocket) {
webSocket.onclose = null;
webSocket.onerror = null;
webSocket.close();
webSocket = null;
}
}
function stopConnection() {
closeConnections();
if (retryPollingTimeout) {
clearTimeout(retryPollingTimeout);
retryPollingTimeout = 0;
}
connectionStopped = true;
pollingServer = false;
pollingReady = false;
}
let blacklistWebsockets = false;
let webSocketErrors = 0;
function bindConnectionEvents(connection) {
connection.onopen = connectionReady;
connection.onmessage = function (e) {
if (connection === webSocket) {
webSocketErrors = 0;
}
connectionReady();
parseAndHandleReceivedData(e.data);
};
connection.onerror = function (e) {
console.error("Connection error:", e);
if (connection === eventSource) {
retryPolling();
} else {
webSocketErrors++;
if (webSocketErrors > Conf.MAX_WEBSOCKET_ERRORS) {
blacklistWebsockets = true;
}
}
};
if (connection === webSocket) {
connection.onclose = function (e) {
webSocket = null;
if (e.code === 1002 || e.code === 1003) {
// CLOSE_PROTOCOL_ERROR or CLOSE_UNSUPPORTED
blacklistWebsockets = true;
pollServerWithEventSource();
return;
}
retryPolling();
};
}
}
function canConnect() {
return !pollingServer && !needsRestart && !connectionStopped && navigator.onLine !== false;
// DO NOT change navigator.onLine !== false by navigator.onLine === true
// This would block connection on a browser not supporting this property
}
function pollServerWithEventSource() {
if (canConnect() && Conf.ENABLE_EVENT_SOURCE && window.EventSource) {
closeConnections();
pollingServer = true;
eventSource = new EventSource(getApiEntryPoint() + "/sse/" + JSON.stringify(cmdsWithContext()));
bindConnectionEvents(eventSource);
return;
}
pollServerWithXHR();
}
function pollServerWithWebSocket() {
if (canConnect() && Conf.ENABLE_WEBSOCKETS && !blacklistWebsockets && window.WebSocket) {
closeConnections();
pollingServer = true;
webSocket = new WebSocket(
(window.location.protocol === "http:" ? "ws://" : "wss://") +
window.location.host +
getApiEntryPoint() + "/ws/" +
JSON.stringify(cmdsWithContext())
);
bindConnectionEvents(webSocket);
return;
}
pollServerWithEventSource();
}
function xhrRequest(data, onreadystatechange) {
const xhr = new XMLHttpRequest();
xhr.open("POST", getApiEntryPoint(), true);
xhr.setRequestHeader("Content-Type", "text/plain");
xhr.send(JSON.stringify(data));
xhr.onreadystatechange = onreadystatechange.bind(null, xhr);
}
function pollServerWithXHR() {
if (!canConnect()) {
return;
}
pollingServer = true;
let currentIndex = 0;
let expectedLength = 0;
xhrRequest(cmdsWithContext(), function (xhr) {
if (xhr.readyState === 2) {
connectionReady();
} else if (xhr.readyState === 3) {
connectionReady();
while (true) {
if (!expectedLength) {
let i = currentIndex;
while (i < xhr.responseText.length) {
if ("0123456789".indexOf(xhr.responseText.charAt(i)) === -1) {
expectedLength = parseInt(xhr.responseText.substring(currentIndex, i));
currentIndex = i;
break;
}
++i;
}
}
if (expectedLength && (xhr.responseText.length >= currentIndex + expectedLength)) {
const end = currentIndex + expectedLength;
let msgs;
try {
msgs = JSON.parse(
xhr.responseText.substring(
currentIndex,
end
)
);
currentIndex = end;
expectedLength = 0;
} catch (e) {
jsonError(xhr.responseText.substring(
currentIndex,
end
), e);
}
handleReceivedData(msgs);
} else {
break;
}
}
} else if (xhr.readyState === 4) {
startConnection();
}
});
}
function startConnection() {
pollServerWithWebSocket();
}
function sendXHR(cmds) {
if (needsRestart) {
return;
}
blockMove++;
xhrRequest(cmdsWithContext(cmds), function (xhr) {
if (xhr.readyState === 4) {
if (xhr.status === 0 || xhr.status >= 300) {
setTimeout(sendCmds.bind(null, cmds), Conf.POLLING_DELAY);
return;
}
parseAndHandleReceivedData(xhr.responseText);
blockMove--;
if (!pollingReady) {
startConnection();
}
}
});
}
function cmdsWithContext(cmds) {
return {
gameNumber: localStorage.trivabbleGameNumber || "",
playerName: localStorage.trivabblePlayerName,
boardLang: localStorage.trivabbleBoardLang,
version: VERSION,
cmds: cmds
};
}
function sendCmds(cmds) {
if (webSocket && webSocket.readyState === 1) {
if (serverVersion >= 202004281800) {
for (const cmd of cmds) {
waitingMsgs.push(currentMessageId);
cmd.id = currentMessageId++;
}
}
webSocket.send(JSON.stringify(cmds));
return;
}
if (!pollingReady) {
startConnection();
setTimeout(sendCmds, Conf.POLLING_DELAY, cmds);
return;
}
sendXHR(cmds);
}
function joinGame() {
checkGameInProgress(
function () {
myPrompt(
_("To join a game, please give the number which is displayed on your adversary(ies)' screen.\nIf you do not know it, ask them.\n\nWarning: your adversary must not take your number, (s)he must keep his/her own. If you whish to recover your current game, please not the following number: {0}.").replace("{0}", localStorage.trivabbleGameNumber),
function (n) {
n = parseInt(n);
if (isNaN(n)) {
myAlert(_("It seems your did not give a correct number, or you clicked on “Cancel”. As a result, the current game continues, if any. To join a game, click on “Join a game” again."));
} else {
localStorage.trivabbleGameNumber = n;
location.reload();
}
}
);
}
);
}
const specialTypesText = {
doubleLetter: _("Double\nLetter"),
doubleWord: _("Double\nWord"),
tripleLetter: _("Triple\nLetter"),
tripleWord: _("Triple\nWord")
};
function specialCell(type, cell) {
cell.firstChild.appendChild(document.createElement("span"));
cell.classList.add("special-cell");
cell.classList.add("special-cell-" + type);
cell.lastChild.lastChild.textContent = _(specialTypesText[type]);
cell.lastChild.lastChild.className = "special-cell-label";
}
function changeName() {
myPrompt(
_("To change your name, enter a new one. You can keep using your current name by cancelling. Please note that if you change your name and you have games in progress, you will not be able to keep playing them anymore unless you get back to your current name."),
function (newName) {
if (newName && newName.trim()) {
localStorage.trivabblePlayerName = newName.trim();
name.textContent = localStorage.trivabblePlayerName;
}
},
localStorage.trivabblePlayerName
);
}
function startGame(number) {
if (number) {
localStorage.trivabbleGameNumber = number;
}
startConnection();
}
let audioTileLoaded = false;
let audioMsgLoaded = false;
let tilesSound;
let msgSound;
function loadAudio() {
if (!audioTileLoaded && tilesSound.checked) {
audioTileLoaded = true;
audioNotification.load();
}
if (!audioMsgLoaded && msgSound.checked) {
audioMsgLoaded = true;
audioChat.load();
}
}
function bagClicked(e) {
preventDefault(e);
if (blockMove || waitingMsgs.length) {
return;
}
if (!remainingLetters) {
myAlert(_("You cannot take another tile: the bag is empty."));
return;
}
loadAudio();
if (!pollingReady) {
showConnectionLost();
return;
}
const index = getFreeRackSpaceIndex();
if (index === -1) {
myAlert(_("You cannot take another tile: your rack is full."));
} else {
sendCmds(
[{cmd: "moveLetter", from: "bag", to: "rack", indexTo: index}]
);
}
}
function clearGame() {
myConfirm(
_("Are you sure you want to put all the tiles back in the bag (in order to play another game)?"),
function () {
sendCmds([{cmd: "resetGame"}]);
}
);
}
function onChangeBoardLang() {
const code = document.getElementById("board-lang").value;
const lang = document.getElementById("board-lang").textContent;
myConfirm(
format(_("Are you sure you want to change board to '{0}'? This will put all the tiles back in the bag and start another game."), _(lang)),
function () {
sendCmds([{cmd: "changeBoard", lang: code}]);
},
function () {
document.getElementById("board-lang").value = localStorage.trivabbleBoardLang;
}
);
}
function clearRack() {
myConfirm(
_("Are you sure you want to put all your tiles back in the bag?"),
function () {
sendCmds([
{cmd: "moveLetter", from: "rack", indexFrom: 0, to: "bag", indexTo: -1},
{cmd: "moveLetter", from: "rack", indexFrom: 1, to: "bag", indexTo: -1},
{cmd: "moveLetter", from: "rack", indexFrom: 2, to: "bag", indexTo: -1},
{cmd: "moveLetter", from: "rack", indexFrom: 3, to: "bag", indexTo: -1},
{cmd: "moveLetter", from: "rack", indexFrom: 4, to: "bag", indexTo: -1},
{cmd: "moveLetter", from: "rack", indexFrom: 5, to: "bag", indexTo: -1},
{cmd: "moveLetter", from: "rack", indexFrom: 6, to: "bag", indexTo: -1}
]);
setRack("");
}
);
}
function showRack() {
myConfirm(
_("Are you sure you want to show your rack to everybody?"),
function () {
const letters = playerLetters.map(
function (tilePlaceholer) {
return tilePlaceholer.querySelector(".tile");
}
).filter(function (tile) {
return tile !== null;
});
sendCmds([{
cmd: "msg",
msg: _("Here is my rack:") + " " + letters.map(
function (tile) {
return (
tile.textContent === ""
? _("joker")
: tile.querySelector(".tile-letter").textContent
);
}
).join(", "),
specialMsg: {
type: "rack",
rack: letters.map(
function (tile) {
return tile.querySelector(".tile-letter").textContent;
}
)
}
}]);
}
);
}
function initChat() {
chatMessages.style.width = chatMessages.offsetWidth + "px";
const btn = document.getElementById("chat-btn");
chatTextarea.onmouseup = function () {
chatMessages.style.width = chatTextarea.offsetWidth + "px";
};
btn.onclick = function () {
if (!pollingReady) {
return;
}
loadAudio();
sendCmds([{
cmd: "msg",
msg: chatTextarea.value
}]);
chatTextarea.value = "";
};
chatTextarea.onkeydown = function (e) {
if (e.keyCode === 13) {
preventDefault(e);
chatTextarea.focus();
btn.onclick();
}
};
}
function initSound() {
audioNotification = new Audio();
audioNotification.preload = "auto";
audioNotification.volume = 1;
let audioSourceOGG = document.createElement("source");
audioSourceOGG.src = "notification.ogg";
let audioSourceMP3 = document.createElement("source");
audioSourceMP3.src = "notification.mp3";
audioNotification.appendChild(audioSourceOGG);
audioNotification.appendChild(audioSourceMP3);
audioChat = new Audio();
audioChat.preload = "auto";
audioChat.volume = 1;
audioSourceOGG = document.createElement("source");
audioSourceOGG.src = "receive.ogg";
audioSourceMP3 = document.createElement("source");
audioSourceMP3.src = "receive.mp3";
audioChat.appendChild(audioSourceOGG);
audioChat.appendChild(audioSourceMP3);
tilesSound = document.getElementById("tiles-sound");
msgSound = document.getElementById("msg-sound");
tilesSound.onclick = function () {
localStorage.trivabbleTileSound = tilesSound.checked;
};
msgSound.onclick = function () {
localStorage.trivabbleMsgSound = msgSound.checked;
};
if (Object.prototype.hasOwnProperty.call(localStorage, "trivabbleMsgSound")) {
msgSound.checked = localStorage.trivabbleMsgSound === "true";
} else {
localStorage.trivabbleMsgSound = msgSound.checked;
}
if (Object.prototype.hasOwnProperty.call(localStorage, "trivabbleTileSound")) {
tilesSound.checked = localStorage.trivabbleTileSound === "true";
} else {
localStorage.trivabbleTilesSound = tilesSound.checked;
}
}
function repromptName(f) {
if (localStorage.trivabblePlayerName && localStorage.trivabblePlayerName.trim()) {
f();
} else {
myPrompt(
_("It seems your did not give your name. You need to do it for the game to run properly."),
function (name) {
if (name && name.trim()) {
localStorage.trivabblePlayerName = name.trim();
}
repromptName(f);
}
);
}
}
trivabble.applyL10n = function () {
document.documentElement.lang = libD.lang;
document.documentElement.setAttribute("xml:lang", libD.lang);
for (const node of [].slice.call(document.querySelectorAll("[data-l10n]"))) {
if (node.dataset.l10n === "text-content") {
node.textContent = _(node.textContent.trim());
} else {
node.setAttribute(node.dataset.l10n, _(node.getAttribute(node.dataset.l10n)));
}
}
trivabble.run();
};
function setAvailableBoardLangs(availableBoardLangs) {
if (!boardLangSelect) {
const boardLangSelection = document.getElementById("board-lang-selection");
boardLangSelect = boardLangSelection.querySelector("select");
boardLangSelection.style.display = "";
}
boardLangSelect.textContent = "";
for (const key of Object.keys(availableBoardLangs)) {
boardLangSelect.add(new Option(_(availableBoardLangs[key]), key));
}
Array.prototype.sort.call(boardLangSelect.options, function (a, b) {
return (
(a.textContent > b.textContent)
? 1
: (
(b.textContent > a.textContent)
? -1
: 0
)
);
});
boardLangSelect.value = localStorage.trivabbleBoardLang;
}
function langSelectionChange(e) {
localStorage.trivabbleLang = e.target.value;
location.reload();
}
function initGlobals() {
board = document.getElementById("board");
rack = document.getElementById("rack");
participants = document.getElementById("participants");
name = document.getElementById("name");
bag = document.getElementById("bag");
chatMessages = document.getElementById("chat-messages");
chatTextarea = document.getElementById("chat-ta");
helpBag = document.getElementById("help-bag");
helpClear = document.getElementById("help-clear");
participantPlaceholder = document.getElementById("participants-placeholder");
}
function initEvents() {
mouseDown(bag, bagClicked);
document.getElementById("clear-game").onclick = clearGame;
document.getElementById("board-lang").onchange = onChangeBoardLang;
document.getElementById("change-name").onclick = changeName;
document.getElementById("join-game").onclick = joinGame;
document.getElementById("clear-rack").onclick = clearRack;
document.getElementById("show-rack").onclick = showRack;
helpClear.onclick = clearGame;
}
function initGame() {
if (!localStorage.trivabblePlayerName) {
myPrompt(
_("Hello! To begin, enter your name. Your adversaries will see this name when you play with them."),
function (name) {
if (name && name.trim()) {
localStorage.trivabblePlayerName = name;
}
repromptName(initGame);
}
);
return;
}
name.textContent = localStorage.trivabblePlayerName;
const letters = "ABCDEFGHIJKLMNO";
const doubleLetter = {
"0,3": true,
"0,11": true,
"2,6": true,
"2,8": true,
"3,0": true,
"3,7": true,
"3,14": true,
"6,2": true,
"6,6": true,
"6,8": true,
"6,12": true
};
let cell;
for (let i = 0; i < 7; i++) {
const span = document.createElement("span");
span.className = "tile-placeholder";
rack.appendChild(span);
playerLetters.push(span);
}
for (let i = 0; i < 15; i++) {
board.rows[0].appendChild(document.createElement("th"));
board.rows[0].lastChild.textContent = i + 1;
board.appendChild(document.createElement("tr"));
board.lastChild.appendChild(document.createElement("th"));
board.lastChild.lastChild.textContent = letters[i];
for (let j = 0; j < 15; j++) {
cell = document.createElement("td");
boardCells.push(cell);
board.lastChild.appendChild(cell);
cell.appendChild(document.createElement("div"));
cell.lastChild.className = "tile-placeholder";
if (i === j && i === 7) {
specialCell("doubleWord", board.lastChild.lastChild);
cell = board.lastChild.lastChild.getElementsByClassName("special-cell-label")[0];
cell.textContent = "★";
board.lastChild.lastChild.id = "center-cell";
} else if (i % 7 === 0 && j % 7 === 0) {
specialCell("tripleWord", board.lastChild.lastChild);
} else if ((i === j || i + j === 14) && (i < 5 || i > 9)) {
specialCell("doubleWord", board.lastChild.lastChild);
} else if ((i % 4 === 1) && (j % 4 === 1)) {
specialCell("tripleLetter", board.lastChild.lastChild);
} else if ((i < 8 && doubleLetter[i + "," + j]) || (i > 7 && doubleLetter[(14 - i) + "," + j]) || (i === 7 && (j === 3 || j === 11))) {
specialCell("doubleLetter", board.lastChild.lastChild);
}
}
board.lastChild.appendChild(document.createElement("th"));
board.lastChild.lastChild.textContent = letters[i];
}
board.rows[0].appendChild(board.rows[0].cells[0].cloneNode(false));
board.appendChild(document.createElement("tr"));
board.lastChild.appendChild(board.rows[0].cells[0].cloneNode(false));
for (let i = 0; i < 15; i++) {
board.lastChild.appendChild(document.createElement("th"));
board.lastChild.lastChild.textContent = i + 1;
}
board.lastChild.appendChild(board.rows[0].cells[0].cloneNode(false));
if (localStorage.trivabbleGameNumber) {
document.getElementById("number").textContent = localStorage.trivabbleGameNumber;
}
if (localStorage.trivabbleBoardLang) {
document.getElementById("board-lang").value = localStorage.trivabbleBoardLang;
}
startGame(localStorage.trivabbleGameNumber, localStorage.trivabbleBoardLang);
}
function initLang() {
const lang = libD.lang = localStorage.trivabbleLang || libD.lang;
const langSel = document.getElementById("select-lang");
langSel.value = lang;
langSel.onchange = langSelectionChange;
const script = document.createElement("script");
script.src = "l10n/js/" + lang + ".js";
script.onerror = trivabble.l10nError;
document.getElementsByTagName("head")[0].appendChild(script);
}
trivabble.run = function () {
initGlobals();
initEvents();
initChat();
initGame();
initSound();
};
trivabble.l10nError = trivabble.run;
window.addEventListener("DOMContentLoaded", initLang);
window.addEventListener("beforeunload", stopConnection);
window.addEventListener("offline", function () {
stopConnection();
showConnectionLost(_("It seems your browser went offline. Please reconnect to continue playing."));
});
window.addEventListener("online", startConnection);
}());