|
46 | 46 | <img src="{{.RootPath}}/resources/fullscreen.svg?cache_bust={{.CacheBust}}"
|
47 | 47 | class="header-button-image" />
|
48 | 48 | </button>
|
| 49 | + <button onclick="toggleSpectate()" class="dialog-button header-button" |
| 50 | + alt="{{.Translation.Get "toggle-spectate"}}" title="{{.Translation.Get "toggle-spectate"}}"> |
| 51 | + <img src="{{.RootPath}}/resources/spectate.svg?cache_bust={{.CacheBust}}" |
| 52 | + class="header-button-image" /> |
| 53 | + </button> |
49 | 54 | </div>
|
50 | 55 | <div id="word-container"></div>
|
51 | 56 | <div>
|
|
541 | 546 | function showDialog(id, title, contentNode, buttonBar) {
|
542 | 547 | const newDialog = document.createElement("div");
|
543 | 548 | newDialog.classList.add("center-dialog");
|
544 |
| - if (id !== null && id !== "") { |
| 549 | + if (id && id !== "") { |
545 | 550 | newDialog.id = id;
|
546 | 551 | }
|
547 | 552 |
|
|
563 | 568 | centerDialogs.appendChild(newDialog);
|
564 | 569 | }
|
565 | 570 |
|
| 571 | + // Shows an information dialog with a button that closes the dialog and |
| 572 | + // removes it from the DOM. |
| 573 | + function showInfoDialog(title, message, buttonText) { |
| 574 | + const dialogId = "info_dialog"; |
| 575 | + closeDialog(dialogId); |
| 576 | + |
| 577 | + const closeButton = createDialogButton(buttonText); |
| 578 | + closeButton.addEventListener("click", event => { |
| 579 | + closeDialog(dialogId) |
| 580 | + }) |
| 581 | + |
| 582 | + const messageNode = document.createElement("span"); |
| 583 | + messageNode.innerText = message; |
| 584 | + |
| 585 | + showDialog(dialogId, title, messageNode, createDialogButtonBar(closeButton)); |
| 586 | + } |
| 587 | + |
| 588 | + function createDialogButton(text) { |
| 589 | + const button = document.createElement("button"); |
| 590 | + button.innerText = text; |
| 591 | + button.classList.add("dialog-button"); |
| 592 | + return button; |
| 593 | + } |
| 594 | + |
| 595 | + function createDialogButtonBar(...buttons) { |
| 596 | + const buttonBar = document.createElement("div"); |
| 597 | + buttonBar.classList.add("button-center-wrapper"); |
| 598 | + buttons.forEach(button => buttonBar.appendChild(button)); |
| 599 | + return buttonBar; |
| 600 | + } |
| 601 | + |
566 | 602 | function closeDialog(id) {
|
567 | 603 | const dialog = document.getElementById(id);
|
568 | 604 | if (dialog !== undefined && dialog !== null) {
|
|
590 | 626 | const controlsTextThree = document.createElement("p");
|
591 | 627 | controlsTextThree.innerHTML = '{{printf (.Translation.Get "switch-pencil-sizes") "<kbd>1</kbd>" "<kbd>4</kbd>"}}';
|
592 | 628 |
|
593 |
| - const closeButton = document.createElement("button"); |
594 |
| - closeButton.innerText = '{{.Translation.Get "close"}}'; |
595 |
| - closeButton.classList.add("dialog-button"); |
| 629 | + const closeButton = createDialogButton('{{.Translation.Get "close"}}'); |
596 | 630 | closeButton.addEventListener("click", event => {
|
597 | 631 | closeDialog(helpDialogId)
|
598 | 632 | })
|
599 | 633 |
|
600 |
| - const buttonBar = document.createElement("div"); |
601 |
| - buttonBar.classList.add("button-center-wrapper"); |
602 |
| - buttonBar.appendChild(closeButton); |
| 634 | + const buttonBar = createDialogButtonBar(closeButton); |
603 | 635 |
|
604 | 636 | const dialogContent = document.createElement("div");
|
605 | 637 | dialogContent.appendChild(controlsLabel)
|
|
748 | 780 | const fillBucket = 2;
|
749 | 781 |
|
750 | 782 | let allowDrawing = false;
|
| 783 | + let spectating = false; |
| 784 | + let spectateRequested = false; |
751 | 785 |
|
752 | 786 | //Initially, we require some values to avoid running into nullpointers
|
753 | 787 | //or undefined errors. The specific values don't really matter.
|
|
916 | 950 | return `<circle cx="` + circleRadius + `" cy="` + circleRadius + `" r="` + circleRadius + `" style="fill: ` + innerColorCSS + `; stroke: ` + outerColorCSS + `;"/>`;
|
917 | 951 | }
|
918 | 952 |
|
| 953 | + function toggleSpectate() { |
| 954 | + socket.send(JSON.stringify({ |
| 955 | + type: "toggle-spectate", |
| 956 | + })); |
| 957 | + if (gameState === "ongoing") { |
| 958 | + } |
| 959 | + } |
| 960 | + |
| 961 | + function setSpectateMode(requestedValue, spectatingValue) { |
| 962 | + const modeUnchanged = spectatingValue === spectating; |
| 963 | + const requestUnchanged = requestedValue === spectateRequested; |
| 964 | + if (modeUnchanged && requestUnchanged) { |
| 965 | + return; |
| 966 | + } |
| 967 | + |
| 968 | + if (spectateRequested && !requestedValue && modeUnchanged) { |
| 969 | + showInfoDialog( |
| 970 | + '{{.Translation.Get "spectation-request-cancelled-title"}}', |
| 971 | + '{{.Translation.Get "spectation-request-cancelled-text"}}', |
| 972 | + "Okay"); |
| 973 | + } else if (spectateRequested && !requestedValue && modeUnchanged) { |
| 974 | + showInfoDialog( |
| 975 | + '{{.Translation.Get "participation-request-cancelled-title"}}', |
| 976 | + '{{.Translation.Get "participation-request-cancelled-text"}}', |
| 977 | + "Okay"); |
| 978 | + } else if (!spectateRequested && requestedValue && !spectatingValue) { |
| 979 | + showInfoDialog( |
| 980 | + '{{.Translation.Get "spectation-requested-title"}}', |
| 981 | + '{{.Translation.Get "spectation-requested-text"}}', |
| 982 | + "Okay"); |
| 983 | + } else if (!spectateRequested && requestedValue && spectatingValue) { |
| 984 | + showInfoDialog( |
| 985 | + '{{.Translation.Get "participation-requested-title"}}', |
| 986 | + '{{.Translation.Get "participation-requested-text"}}', |
| 987 | + "Okay"); |
| 988 | + } else if (spectatingValue && !spectating) { |
| 989 | + showInfoDialog( |
| 990 | + '{{.Translation.Get "now-spectating-title"}}', |
| 991 | + '{{.Translation.Get "now-spectating-text"}}', |
| 992 | + "Okay"); |
| 993 | + } else if (!spectatingValue && spectating) { |
| 994 | + showInfoDialog( |
| 995 | + '{{.Translation.Get "now-participating-title"}}', |
| 996 | + '{{.Translation.Get "now-participating-text"}}', |
| 997 | + "Okay"); |
| 998 | + } |
| 999 | + |
| 1000 | + spectateRequested = requestedValue; |
| 1001 | + spectating = spectatingValue; |
| 1002 | + } |
| 1003 | + |
919 | 1004 | function toggleReadiness() {
|
920 | 1005 | socket.send(JSON.stringify({
|
921 | 1006 | type: "toggle-readiness",
|
|
975 | 1060 | return false;
|
976 | 1061 | }
|
977 | 1062 |
|
| 1063 | + function setAllowDrawing(value) { |
| 1064 | + allowDrawing = value; |
| 1065 | + updateCursor(); |
| 1066 | + |
| 1067 | + if (allowDrawing) { |
| 1068 | + document.getElementById("toolbox").style.display = "flex"; |
| 1069 | + } else { |
| 1070 | + document.getElementById("toolbox").style.display = "none"; |
| 1071 | + } |
| 1072 | + } |
| 1073 | + |
978 | 1074 | function chooseWord(index) {
|
979 | 1075 | socket.send(JSON.stringify({
|
980 | 1076 | type: "choose-word",
|
981 | 1077 | data: index
|
982 | 1078 | }));
|
983 |
| - allowDrawing = true; |
984 |
| - updateCursor(); |
| 1079 | + setAllowDrawing(true); |
985 | 1080 | wordDialog.style.visibility = "hidden";
|
986 | 1081 | }
|
987 | 1082 |
|
|
1026 | 1121 | } else if (parsed.type === "update-players") {
|
1027 | 1122 | applyPlayers(parsed.data);
|
1028 | 1123 | } else if (parsed.type === "name-change") {
|
1029 |
| - const player = getPlayer(parsed.data.playerId); |
| 1124 | + const player = getCachedPlayer(parsed.data.playerId); |
1030 | 1125 | if (player !== null) {
|
1031 | 1126 | player.name = parsed.data.playerName;
|
1032 | 1127 | }
|
|
1047 | 1142 | if (parsed.data === ownID) {
|
1048 | 1143 | appendMessage("correct-guess-message", null, '{{.Translation.Get "correct-guess"}}');
|
1049 | 1144 | } else {
|
1050 |
| - const player = getPlayer(parsed.data) |
| 1145 | + const player = getCachedPlayer(parsed.data) |
1051 | 1146 | if (player !== null) {
|
1052 | 1147 | appendMessage("correct-guess-message-other-player", null, '{{.Translation.Get "correct-guess-other-player"}}'.format(player.name));
|
1053 | 1148 | }
|
|
1109 | 1204 | //We clear this, since there's no word chosen right now.
|
1110 | 1205 | wordContainer.innerHTML = "";
|
1111 | 1206 |
|
1112 |
| - allowDrawing = false; |
1113 |
| - updateCursor(); |
| 1207 | + setAllowDrawing(false); |
1114 | 1208 | } else if (parsed.type === "your-turn") {
|
1115 | 1209 | playWav('{{.RootPath}}/resources/your-turn.wav?cache_bust={{.CacheBust}}');
|
1116 | 1210 | //This dialog could potentially stay visible from last
|
|
1164 | 1258 | }
|
1165 | 1259 | }
|
1166 | 1260 |
|
1167 |
| - function getPlayer(playerID) { |
| 1261 | + function getCachedPlayer(playerID) { |
| 1262 | + if (!cachedPlayers) { |
| 1263 | + return null; |
| 1264 | + } |
| 1265 | + |
1168 | 1266 | for (let i = 0; i < cachedPlayers.length; i++) {
|
1169 | 1267 | let player = cachedPlayers[i];
|
1170 | 1268 | if (player.id === playerID) {
|
|
1197 | 1295 |
|
1198 | 1296 | setRoundEndTime(ready.roundEndTime);
|
1199 | 1297 | setUsernameLocally(ready.playerName);
|
1200 |
| - allowDrawing = ready.allowDrawing; |
| 1298 | + setAllowDrawing(ready.allowDrawing); |
1201 | 1299 | round = ready.round;
|
1202 | 1300 | rounds = ready.rounds;
|
1203 | 1301 | gameState = ready.gameState;
|
|
1214 | 1312 | if (ready.wordHints && ready.wordHints.length) {
|
1215 | 1313 | applyWordHints(ready.wordHints);
|
1216 | 1314 | }
|
1217 |
| - updateCursor(); |
1218 | 1315 |
|
1219 | 1316 | if (ready.gameState === "unstarted") {
|
1220 | 1317 | startDialog.style.visibility = "visible";
|
|
1372 | 1469 | let readyPlayersRequired = 0;
|
1373 | 1470 |
|
1374 | 1471 | players.forEach(player => {
|
1375 |
| - if (!player.connected) { |
| 1472 | + if (!player.connected || player.state === "spectating") { |
1376 | 1473 | return;
|
1377 | 1474 | }
|
1378 | 1475 |
|
|
1399 | 1496 | }
|
1400 | 1497 |
|
1401 | 1498 | playerContainer.innerHTML = "";
|
1402 |
| - cachedPlayers = players; |
1403 | 1499 | players.forEach(player => {
|
1404 | 1500 | //We don't wanna show the disconnected players.
|
1405 | 1501 | if (!player.connected) {
|
1406 | 1502 | return;
|
1407 | 1503 | }
|
1408 | 1504 |
|
| 1505 | + if (player.id === ownID) { |
| 1506 | + setSpectateMode(player.spectateToggleRequested, player.state === "spectating"); |
| 1507 | + } |
| 1508 | + |
| 1509 | + const oldPlayer = getCachedPlayer(player.id); |
| 1510 | + if (oldPlayer && oldPlayer.state === "spectating" && player.state !== "spectating") { |
| 1511 | + appendMessage( |
| 1512 | + "system-message", |
| 1513 | + '{{.Translation.Get "system"}}', |
| 1514 | + `${player.name} is now participating`); |
| 1515 | + } else if (oldPlayer && oldPlayer.state !== "spectating" && player.state === "spectating") { |
| 1516 | + appendMessage( |
| 1517 | + "system-message", |
| 1518 | + '{{.Translation.Get "system"}}', |
| 1519 | + `${player.name} is now spectating`); |
| 1520 | + } |
| 1521 | + |
| 1522 | + if (player.state === "spectating") { |
| 1523 | + return; |
| 1524 | + } |
| 1525 | + |
1409 | 1526 | const playerDiv = document.createElement("div");
|
1410 | 1527 |
|
1411 | 1528 | playerDiv.classList.add("player");
|
|
1464 | 1581 |
|
1465 | 1582 | playerContainer.appendChild(playerDiv);
|
1466 | 1583 | });
|
| 1584 | + |
| 1585 | + // We do this at the end, so we can access the old values while |
| 1586 | + // iterating over the new ones |
| 1587 | + cachedPlayers = players; |
1467 | 1588 | }
|
1468 | 1589 |
|
1469 | 1590 | function createPlayerStateImageNode(path) {
|
|
0 commit comments