diff --git a/ZATACKA.html b/ZATACKA.html
index dc55ddf..9edb4af 100644
--- a/ZATACKA.html
+++ b/ZATACKA.html
@@ -100,6 +100,7 @@
+
@@ -107,6 +108,12 @@
+
+
+
+
+
+
diff --git a/Zatacka.css b/Zatacka.css
index 38b5aa0..6e8caa3 100644
--- a/Zatacka.css
+++ b/Zatacka.css
@@ -60,6 +60,10 @@ input[type="checkbox"]:checked + label::before {
background-repeat: no-repeat;
}
+.nocursor {
+ cursor: none;
+}
+
#debug {
background-color: black;
border: 1px white solid;
@@ -139,7 +143,7 @@ input[type="checkbox"]:checked + label::before {
#lobby #controls {
list-style-type: none;
- margin-left: 81px;
+ margin-left: 80px;
margin-top: 50px;
}
@@ -154,7 +158,7 @@ input[type="checkbox"]:checked + label::before {
#lobby #controls .controls {
background-image: url("resources/kurve-lobby-controls-ready.png");
- width: 159px;
+ width: 160px;
}
#lobby #controls .ready {
@@ -168,17 +172,17 @@ input[type="checkbox"]:checked + label::before {
}
#lobby #controls .red.controls { background-position: 0px 0px; }
-#lobby #controls .red.ready { background-position: -159px 0px; }
+#lobby #controls .red.ready { background-position: -160px 0px; }
#lobby #controls .yellow.controls { background-position: 0px -50px; }
-#lobby #controls .yellow.ready { background-position: -159px -50px; }
+#lobby #controls .yellow.ready { background-position: -160px -50px; }
#lobby #controls .orange.controls { background-position: 0px -100px; }
-#lobby #controls .orange.ready { background-position: -159px -100px; }
+#lobby #controls .orange.ready { background-position: -160px -100px; }
#lobby #controls .green.controls { background-position: 0px -150px; }
-#lobby #controls .green.ready { background-position: -159px -150px; }
+#lobby #controls .green.ready { background-position: -160px -150px; }
#lobby #controls .pink.controls { background-position: 0px -200px; }
-#lobby #controls .pink.ready { background-position: -159px -200px; }
+#lobby #controls .pink.ready { background-position: -160px -200px; }
#lobby #controls .blue.controls { background-position: 0px -250px; }
-#lobby #controls .blue.ready { background-position: -159px -250px; }
+#lobby #controls .blue.ready { background-position: -160px -250px; }
#lobby footer {
padding: 0 80px;
@@ -191,7 +195,8 @@ input[type="checkbox"]:checked + label::before {
.message {
font-size: 8px;
padding: 2px 80px;
- background-color: rgba(0, 0, 0, 0.5);
+ text-align: center;
+ text-shadow: 0 0 2px black, 0 0 2px black, 0 0 4px black, 0 0 4px black, 0 0 4px black, 0 0 4px black, 0 0 8px black, 0 0 8px black, 0 0 8px black, 0 0 8px black;
}
.info.message {
diff --git a/js/GUIController.js b/js/GUIController.js
index 710967c..6a904fb 100644
--- a/js/GUIController.js
+++ b/js/GUIController.js
@@ -2,8 +2,9 @@
function GUIController(cfg) {
- const CLASS_ACTIVE = "active";
- const CLASS_HIDDEN = "hidden";
+ const CURSOR_VISIBLE = "visible";
+ const CURSOR_HIDDEN_ON_CANVAS = "hidden_on_canvas";
+ const CURSOR_HIDDEN = "hidden";
const config = cfg;
const lobby = byID("lobby");
@@ -25,7 +26,12 @@ function GUIController(cfg) {
function hideLobby() {
log("Hiding lobby.");
- lobby.classList.add(CLASS_HIDDEN);
+ lobby.classList.add(STRINGS.class_hidden);
+ }
+
+ function showLobby() {
+ log("Showing lobby.");
+ lobby.classList.remove(STRINGS.class_hidden);
}
function isLobbyEntry(element) {
@@ -40,6 +46,31 @@ function GUIController(cfg) {
Array.from(scoreboard.children).forEach(resetScoreboardEntry);
}
+ function resetResults() {
+ Array.from(results.children).forEach(resetScoreboardEntry);
+ }
+
+ function setCursorBehavior(behavior) {
+ switch (behavior) {
+ case CURSOR_VISIBLE:
+ document.body.classList.remove(STRINGS.class_nocursor);
+ break;
+ case CURSOR_HIDDEN_ON_CANVAS:
+ canvas_main.classList.add(STRINGS.class_nocursor);
+ canvas_overlay.classList.add(STRINGS.class_nocursor);
+ break;
+ case CURSOR_HIDDEN:
+ document.body.classList.add(STRINGS.class_nocursor);
+ break;
+ default:
+ logError(`Cannot set cursor behavior to '${behavior}'.`);
+ }
+ }
+
+ function resetCursorBehavior() {
+ setCursorBehavior(CURSOR_VISIBLE);
+ }
+
// PUBLIC API
@@ -53,7 +84,7 @@ function GUIController(cfg) {
if (!isLobbyEntry(entry)) {
logWarning(`Cannot mark player ${id} as ready because controls.children[${index}] (${controls.children[index]}) is not a valid lobby entry.`);
} else {
- entry.children[1].classList.add(CLASS_ACTIVE);
+ entry.children[1].classList.add(STRINGS.class_active);
}
}
@@ -63,7 +94,13 @@ function GUIController(cfg) {
if (!isLobbyEntry(entry)) {
logWarning(`Cannot mark player ${id} as unready because controls.children[${index}] (${controls.children[index]}) is not a valid lobby entry.`);
} else {
- entry.children[1].classList.remove(CLASS_ACTIVE);
+ entry.children[1].classList.remove(STRINGS.class_active);
+ }
+ }
+
+ function allPlayersUnready() {
+ for (let id = 1; id <= controls.children.length; id++) {
+ playerUnready(id);
}
}
@@ -71,11 +108,29 @@ function GUIController(cfg) {
hideLobby();
}
+ function gameQuit() {
+ hideKonecHry();
+ showLobby();
+ clearMessages();
+ resetScoreboard();
+ resetResults();
+ allPlayersUnready();
+ resetCursorBehavior();
+ }
+
function konecHry() {
- KONEC_HRY.classList.remove("hidden");
+ showKonecHry();
resetScoreboard();
}
+ function showKonecHry() {
+ KONEC_HRY.classList.remove(STRINGS.class_hidden);
+ }
+
+ function hideKonecHry() {
+ KONEC_HRY.classList.add(STRINGS.class_hidden);
+ }
+
function showMessage(message) {
if (!currentMessages.includes(message)) {
currentMessages.push(message);
@@ -135,15 +190,20 @@ function GUIController(cfg) {
}
return {
+ CURSOR_VISIBLE,
+ CURSOR_HIDDEN_ON_CANVAS,
+ CURSOR_HIDDEN,
playerReady,
playerUnready,
gameStarted,
+ gameQuit,
konecHry,
updateScoreOfPlayer,
updateMessages,
showMessage,
hideMessage,
clearMessages,
+ setCursorBehavior,
setEdgePadding
};
diff --git a/js/Game.js b/js/Game.js
index 645bf94..0d8023c 100644
--- a/js/Game.js
+++ b/js/Game.js
@@ -343,11 +343,6 @@ class Game {
this.beginNewRound();
}
- /** Quits the game. */
- quit() {
- document.location.reload();
- }
-
/** Announce KONEC HRY, show results etc. */
konecHry() {
log(this.constructor.KONEC_HRY);
@@ -356,6 +351,11 @@ class Game {
this.quitHintTimer = setTimeout(this.showQuitHint.bind(this), this.config.hintDelay);
}
+ quit() {
+ clearTimeout(this.quitHintTimer);
+ clearTimeout(this.proceedHintTimer);
+ }
+
clearField() {
this.pixels.fill(0);
this.Render_clearField();
@@ -562,10 +562,7 @@ class Game {
proceedKeyPressed() {
this.hideProceedHint();
this.hideQuitHint();
- if (this.isEnded()) {
- // The game is ended, so a proceed key press should quit:
- this.quit();
- } else if (this.isGameOver()) {
+ if (this.isGameOver()) {
// The game is over, so we should show KONEC HRY:
this.konecHry();
} else if (this.isPostRound()) {
@@ -574,10 +571,12 @@ class Game {
}
}
- quitKeyPressed() {
- if (this.isPostRound() && !this.isGameOver()) {
- this.quit();
- }
+ shouldQuitOnQuitKey() {
+ return this.isPostRound() && !this.isGameOver();
+ }
+
+ shouldQuitOnProceedKey() {
+ return this.isEnded();
}
diff --git a/js/Player.js b/js/Player.js
index 1593c4e..7d81be0 100644
--- a/js/Player.js
+++ b/js/Player.js
@@ -90,6 +90,10 @@ class Player {
|| this.R_keys.includes(button));
}
+ usesAnyMouseButton() {
+ return MOUSE_BUTTONS.some((button) => this.hasMouseButton(button));
+ }
+
hasKey(key) {
return this.L_keys.includes(key)
|| this.R_keys.includes(key);
diff --git a/js/Zatacka.js b/js/Zatacka.js
index af3e317..9793abf 100644
--- a/js/Zatacka.js
+++ b/js/Zatacka.js
@@ -39,15 +39,40 @@ const Zatacka = ((window, document) => {
mouse: new WarningMessage(text.hint_mouse),
}),
defaultPlayers: Object.freeze([
- { id: 1, name: "Red" , color: "#FF2800", keyL: KEY["1"] , keyR: KEY.Q },
- { id: 2, name: "Yellow", color: "#C3C300", keyL: KEY.CTRL , keyR: KEY.ALT },
- { id: 3, name: "Orange", color: "#FF7900", keyL: KEY.M , keyR: KEY.COMMA },
- { id: 4, name: "Green" , color: "#00CB00", keyL: KEY.LEFT_ARROW, keyR: KEY.DOWN_ARROW },
- { id: 5, name: "Pink" , color: "#DF51B6", keyL: KEY.DIVIDE , keyR: KEY.MULTIPLY },
- { id: 6, name: "Blue" , color: "#00A2CB", keyL: MOUSE.LEFT , keyR: MOUSE.RIGHT }
+ { id: 1, name: "Red" , color: "#FF2800", keyL: KEY["1"] , keyR: KEY.Q },
+ { id: 2, name: "Yellow", color: "#C3C300", keyL: KEY.CTRL , keyR: KEY.ALT },
+ { id: 3, name: "Orange", color: "#FF7900", keyL: KEY.M , keyR: KEY.COMMA },
+ { id: 4, name: "Green" , color: "#00CB00", keyL: KEY.LEFT_ARROW , keyR: KEY.DOWN_ARROW },
+ { id: 5, name: "Pink" , color: "#DF51B6", keyL: [ KEY.DIVIDE, KEY.END, KEY.PAGE_DOWN ], keyR: [ KEY.MULTIPLY, KEY.PAGE_UP ] },
+ { id: 6, name: "Blue" , color: "#00A2CB", keyL: MOUSE.LEFT , keyR: MOUSE.RIGHT }
])
});
+ const PREFERENCES = Object.freeze([
+ {
+ type: MultichoicePreference,
+ key: STRINGS.pref_key_cursor,
+ values: [
+ STRINGS.pref_value_cursor_always_visible,
+ STRINGS.pref_value_cursor_hidden_when_mouse_used_by_player,
+ STRINGS.pref_value_cursor_always_hidden
+ ],
+ default: STRINGS.pref_value_cursor_hidden_when_mouse_used_by_player
+ },
+ {
+ type: MultichoicePreference,
+ key: STRINGS.pref_key_hints,
+ values: [
+ STRINGS.pref_value_hints_all,
+ STRINGS.pref_value_hints_warnings_only,
+ STRINGS.pref_value_hints_none
+ ],
+ default: STRINGS.pref_value_hints_all
+ }
+ ]);
+
+ const preferenceManager = new PreferenceManager(PREFERENCES);
+
function isProceedKey(key) {
return config.keys.proceed.includes(key);
}
@@ -122,6 +147,23 @@ const Zatacka = ((window, document) => {
getPaddedHoleConfig());
}
+ function applyCursorBehavior() {
+ const mouseIsBeingUsed = game.getPlayers().some(hasMouseButton);
+ let behavior;
+ switch (preferenceManager.get(STRINGS.pref_key_cursor)) {
+ case STRINGS.pref_value_cursor_hidden_when_mouse_used_by_player:
+ behavior = mouseIsBeingUsed ? guiController.CURSOR_HIDDEN : guiController.CURSOR_VISIBLE;
+ break;
+ case STRINGS.pref_value_cursor_always_hidden:
+ behavior = guiController.CURSOR_HIDDEN;
+ break;
+ default:
+ behavior = guiController.CURSOR_VISIBLE;
+ }
+ log(`Setting cursor behavior to ${behavior}.`);
+ guiController.setCursorBehavior(behavior);
+ }
+
function proceedKeyPressedInLobby() {
const numberOfReadyPlayers = game.getNumberOfPlayers();
if (numberOfReadyPlayers > 0) {
@@ -130,13 +172,14 @@ const Zatacka = ((window, document) => {
guiController.clearMessages();
removeLobbyEventListeners();
addGameEventListeners();
+ applyCursorBehavior();
game.setMode(numberOfReadyPlayers === 1 ? Game.PRACTICE : Game.COMPETITIVE);
game.start();
}
}
function hasMouseButton(player) {
- return Object.keys(MOUSE).some((buttonName) => player.hasMouseButton(MOUSE[buttonName]));
+ return player.usesAnyMouseButton();
}
function checkForDangerousInput() {
@@ -183,10 +226,18 @@ const Zatacka = ((window, document) => {
}
}
+ function defaultPlayerHasLeftKey(playerData, pressedKey) {
+ return pressedKey === playerData.keyL || (playerData.keyL instanceof Array && playerData.keyL.includes(pressedKey));
+ }
+
+ function defaultPlayerHasRightKey(playerData, pressedKey) {
+ return pressedKey === playerData.keyR || (playerData.keyR instanceof Array && playerData.keyR.includes(pressedKey));
+ }
+
function addOrRemovePlayer(playerData, pressedKey) {
- if (pressedKey === playerData.keyL) {
+ if (defaultPlayerHasLeftKey(playerData, pressedKey)) {
addPlayer(playerData.id);
- } else if (pressedKey === playerData.keyR) {
+ } else if (defaultPlayerHasRightKey(playerData, pressedKey)) {
removePlayer(playerData.id);
}
}
@@ -220,15 +271,27 @@ const Zatacka = ((window, document) => {
mouseClickedInLobby(event.button);
}
+ function quitGame() {
+ removeGameEventListeners();
+ addLobbyEventListeners();
+ game.quit();
+ guiController.gameQuit();
+ game = newGame();
+ }
+
function gameKeyHandler(event) {
const pressedKey = event.keyCode;
if (shouldPreventDefault(pressedKey)) {
event.preventDefault();
}
if (isProceedKey(pressedKey)) {
- game.proceedKeyPressed();
- } else if (isQuitKey(pressedKey)) {
- game.quitKeyPressed();
+ if (game.shouldQuitOnProceedKey()) {
+ quitGame();
+ } else {
+ game.proceedKeyPressed();
+ }
+ } else if (isQuitKey(pressedKey) && game.shouldQuitOnQuitKey()) {
+ quitGame();
}
}
@@ -273,8 +336,12 @@ const Zatacka = ((window, document) => {
addLobbyEventListeners();
+ function newGame() {
+ return new Game(config, Renderer(canvas_main, canvas_overlay), guiController);
+ }
+
const guiController = GUIController(config);
- const game = new Game(config, Renderer(canvas_main, canvas_overlay), guiController);
+ let game = newGame();
let hintProceedTimer;
let hintPickTimer = setTimeout(() => {
diff --git a/js/lib/Utilities.js b/js/lib/Utilities.js
index f36b9ef..bd0161f 100644
--- a/js/lib/Utilities.js
+++ b/js/lib/Utilities.js
@@ -28,6 +28,10 @@ const F_KEYS = Object.freeze([
KEY.F1, KEY.F2, KEY.F3, KEY.F4, KEY.F5, KEY.F6, KEY.F7, KEY.F8, KEY.F9, KEY.F10, KEY.F11, KEY.F12
]);
+const MOUSE_BUTTONS = Object.freeze([
+ MOUSE.LEFT, MOUSE.RIGHT, MOUSE.MIDDLE, MOUSE.MOUSE4, MOUSE.MOUSE5
+]);
+
function isObject(obj) {
return typeOf(obj) === "object";
}
diff --git a/js/lib/preferences/BooleanPreference.js b/js/lib/preferences/BooleanPreference.js
new file mode 100644
index 0000000..f0ec9a4
--- /dev/null
+++ b/js/lib/preferences/BooleanPreference.js
@@ -0,0 +1,19 @@
+"use strict";
+
+class BooleanPreference extends MultichoicePreference {
+ constructor(data) {
+ super({
+ key: data.key,
+ values: ["true", "false"],
+ default: data.default
+ });
+ }
+
+ static stringify(value) {
+ return value.toString();
+ }
+
+ static parse(stringifiedValue) {
+ return stringifiedValue === "true";
+ }
+}
diff --git a/js/lib/preferences/IntegerRangePreference.js b/js/lib/preferences/IntegerRangePreference.js
new file mode 100644
index 0000000..92459d4
--- /dev/null
+++ b/js/lib/preferences/IntegerRangePreference.js
@@ -0,0 +1,27 @@
+"use strict";
+
+class IntegerRangePreference extends RangePreference {
+ constructor(data) {
+ if (!isInt(data.min) || !isInt(data.max)) {
+ throw new TypeError(`min and max must be integers (found ${data.min} and ${data.max} for preference '${data.key}').`);
+ }
+ super(data);
+ this.min = data.min;
+ this.max = data.max;
+ if (!this.isValidValue(data.default)) {
+ super.invalidValue(data.default);
+ }
+ }
+
+ isValidValue(value) {
+ return isInt(value) && value >= this.min && value <= this.max;
+ }
+
+ static stringify(value) {
+ return value.toString();
+ }
+
+ static parse(stringifiedValue) {
+ return parseInt(stringifiedValue);
+ }
+}
diff --git a/js/lib/preferences/MultichoicePreference.js b/js/lib/preferences/MultichoicePreference.js
new file mode 100644
index 0000000..73ca245
--- /dev/null
+++ b/js/lib/preferences/MultichoicePreference.js
@@ -0,0 +1,30 @@
+"use strict";
+
+class MultichoicePreference extends Preference {
+ constructor(data) {
+ if (!isNonEmptyStringArray(data.values)) {
+ throw new TypeError(`values must be a non-empty string array (found ${data.values} for preference '${data.key}').`);
+ }
+ super(data);
+ this.values = data.values;
+ if (!this.isValidValue(data.default)) {
+ super.invalidValue(data.default);
+ }
+
+ function isNonEmptyStringArray(strings) {
+ return strings instanceof Array && strings.length > 0 && strings.every(isString);
+ }
+ }
+
+ isValidValue(value) {
+ return this.values.indexOf(value) > -1;
+ }
+
+ static stringify(value) {
+ return value;
+ }
+
+ static parse(stringifiedValue) {
+ return stringifiedValue;
+ }
+}
diff --git a/js/lib/preferences/Preference.js b/js/lib/preferences/Preference.js
new file mode 100644
index 0000000..d82f17e
--- /dev/null
+++ b/js/lib/preferences/Preference.js
@@ -0,0 +1,33 @@
+"use strict";
+
+class Preference {
+ constructor(data) {
+ if (!isString(data.key)) {
+ throw new TypeError(`key must be a string (found ${data.key}). More info: ${data}`);
+ } else if (data.default === undefined) {
+ throw new TypeError(`Preference '${data.key}' must have a default value.`);
+ }
+ this.key = data.key;
+ this.default = data.default;
+ }
+
+ isValidValue(value) {
+ return isString(value);
+ }
+
+ invalidValue(value) {
+ throw new TypeError(`${value} is not a valid value for preference '${this.key}'.`);
+ }
+
+ static stringify(value) {
+ return value.toString();
+ }
+
+ static parse(stringifiedValue) {
+ return stringifiedValue;
+ }
+
+ getDefaultValue() {
+ return this.default;
+ }
+}
diff --git a/js/lib/preferences/PreferenceManager.js b/js/lib/preferences/PreferenceManager.js
new file mode 100644
index 0000000..3bf58b0
--- /dev/null
+++ b/js/lib/preferences/PreferenceManager.js
@@ -0,0 +1,93 @@
+"use strict";
+
+function PreferenceManager(preferencesData) {
+ const LOCALSTORAGE_PREFIX = "pref_key_";
+
+ // Parse and validate preferences:
+ log("Validating preferences ...");
+ const PREFERENCES = parsePreferences(preferencesData);
+ log("Done.");
+
+ function parsePreferences(preferencesData) {
+ return preferencesData.map(parsePreference);
+ }
+
+ function parsePreference(pref, index) {
+ if (!isString(pref.key)) {
+ throw new TypeError(`'The preference at index ${index} does not have a valid key (found ${pref.key}).`);
+ } else if (pref.type === undefined || !(pref.type.prototype instanceof Preference)) {
+ throw new TypeError(`Preference '${pref.key}' does not use a valid preference type (found ${pref.type}).`);
+ } else if (pref.default === undefined) {
+ throw new TypeError(`Preference '${pref.key}' has no default value.`);
+ }
+ return new (pref.type)(pref);
+ }
+
+ function preferenceExists(key) {
+ return getPreference(key) !== undefined;
+ }
+
+ function getPreference(key) {
+ return PREFERENCES.find((pref) => pref.key === key);
+ }
+
+ function getKey(pref) {
+ return pref.key;
+ }
+
+ function isValidPreferenceValue(key, value) {
+ return getPreference(key).isValidValue(value);
+ }
+
+ function setToDefaultValue(key) {
+ set(key, getDefaultValue(key));
+ }
+
+ function getDefaultValue(key) {
+ if (!preferenceExists(key)) {
+ throw new Error(`Preference ${key} does not exist.`);
+ }
+ return getPreference(key).getDefaultValue();
+ }
+
+ function LS_prefix(key) {
+ return LOCALSTORAGE_PREFIX + key;
+ }
+
+ function set(key, value) {
+ if (!preferenceExists(key)) {
+ throw new Error(`There is no preference with key '${key}'.`);
+ }
+ const pref = getPreference(key);
+ if (!isValidPreferenceValue(key, value)) {
+ pref.invalidValue(value);
+ } else {
+ log(`Setting preference ${key} to ${value}.`);
+ localStorage.setItem(LS_prefix(key), pref.stringify(value));
+ }
+ }
+
+ function get(key) {
+ if (!preferenceExists(key)) {
+ throw new Error(`There is no preference with key '${key}'.`);
+ }
+ const pref = getPreference(key);
+ const savedValue = localStorage.getItem(LS_prefix(key));
+ return isValidPreferenceValue(key, savedValue) ? pref.parse(savedValue) : getDefaultValue(key);
+ }
+
+ function setAllToDefault() {
+ log("Resetting all preferences ...");
+ PREFERENCES.map(getKey).forEach(setToDefaultValue);
+ log("Done.");
+ }
+
+ return {
+ isValidPreferenceValue,
+ set,
+ get,
+ setToDefaultValue,
+ getDefaultValue,
+ setAllToDefault
+ }
+}
\ No newline at end of file
diff --git a/js/lib/preferences/RangePreference.js b/js/lib/preferences/RangePreference.js
new file mode 100644
index 0000000..38826ee
--- /dev/null
+++ b/js/lib/preferences/RangePreference.js
@@ -0,0 +1,29 @@
+"use strict";
+
+class RangePreference extends Preference {
+ constructor(data) {
+ if (!isNumber(data.min) || !isNumber(data.max)) {
+ throw new TypeError(`min and max must be numbers (found ${data.min} and ${data.max} for preference '${data.key}').`);
+ } else if (data.min > data.max) {
+ throw new TypeError(`min cannot be greater than max (found ${data.min} and ${data.max} for preference '${data.key}', respectively).`);
+ }
+ super(data);
+ this.min = data.min;
+ this.max = data.max;
+ if (!this.isValidValue(data.default)) {
+ super.invalidValue(data.default)
+ }
+ }
+
+ isValidValue(value) {
+ return isNumber(value) && value >= this.min && value <= this.max;
+ }
+
+ static stringify(value) {
+ return value.toString();
+ }
+
+ static parse(stringifiedValue) {
+ return parseFloat(stringifiedValue);
+ }
+}
diff --git a/js/strings.js b/js/strings.js
new file mode 100644
index 0000000..8bb4816
--- /dev/null
+++ b/js/strings.js
@@ -0,0 +1,17 @@
+"use strict";
+
+const STRINGS = Object.freeze({
+ class_hidden: "hidden",
+ class_active: "active",
+ class_nocursor: "nocursor",
+
+ pref_key_cursor: "cursor",
+ pref_value_cursor_always_visible: "always_visible",
+ pref_value_cursor_hidden_when_mouse_used_by_player: "hidden_when_mouse_used_by_player",
+ pref_value_cursor_always_hidden: "always_hidden",
+
+ pref_key_hints: "hints",
+ pref_value_hints_all: "all",
+ pref_value_hints_warnings_only: "warnings",
+ pref_value_hints_none: "none",
+});
diff --git a/resources/kurve-lobby-controls-ready.png b/resources/kurve-lobby-controls-ready.png
index c7b91d3..a231a00 100644
Binary files a/resources/kurve-lobby-controls-ready.png and b/resources/kurve-lobby-controls-ready.png differ