From e2a5d2ce43c8aea4446f5af7bec9b5418e990261 Mon Sep 17 00:00:00 2001 From: Simon Alling Date: Sun, 12 Mar 2017 16:34:31 +0100 Subject: [PATCH 01/14] Rephrase description for hints setting --- js/locales/Zatacka.en_US.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/js/locales/Zatacka.en_US.js b/js/locales/Zatacka.en_US.js index b6d706f..f7f85e4 100644 --- a/js/locales/Zatacka.en_US.js +++ b/js/locales/Zatacka.en_US.js @@ -36,7 +36,7 @@ const TEXT = (() => { pref_label_edge_fix_off: `Off`, pref_label_hints: `Hints`, - pref_label_description_hints: `Hints, except for warnings, are redundant for experienced Kurve players.`, + pref_label_description_hints: `Hints may be redundant for experienced Kurve players.`, pref_label_hints_all: `All`, pref_label_hints_warnings_only: `Warnings only`, pref_label_hints_none: `None`, From 2a7af118fd5c11b0e2e2db5c801b119a8d792ac3 Mon Sep 17 00:00:00 2001 From: Simon Alling Date: Mon, 13 Mar 2017 00:54:05 +0100 Subject: [PATCH 02/14] Add spawnkill prevention It sometimes happens in Kurve that two players spawn close enough to each other that it is not even theoretically possible for (at least) one of them to avoid a collision at round start. The spawnkill prevention setting added by this commit greatly reduces the risk of such situations by enforcing a minimum distance between players' spawn points. The minimum distance is normally based on the turning radius and Kurve thickness, but it is capped to a value such that the circular areas "reserved" by all Kurves together cannot cover more than half the available spawn area, so as to avoid sampling for a valid spawn point for an absurdly long (or infinite) time. All constants related to the minimum distance can be configured in code. --- js/Game.js | 50 +++++++++++++++++++++++++++++++++++++++++++++++++- js/Zatacka.js | 26 +++++++++++++++++++------- 2 files changed, 68 insertions(+), 8 deletions(-) diff --git a/js/Game.js b/js/Game.js index 0d8023c..f8de2c4 100644 --- a/js/Game.js +++ b/js/Game.js @@ -9,6 +9,8 @@ class Game { this.constructor.DEFAULT_TARGET_SCORE = 10; this.constructor.MAX_TARGET_SCORE = 1000; this.constructor.MAX_PLAYERS = 255; // since we use a Uint8Array + this.constructor.MAX_QUOTA_THAT_SPAWN_CIRCLES_MAY_FILL = 0.5; // out of available spawn area + this.constructor.DESIRED_MINIMUM_SPAWN_DISTANCE_TURNING_RADIUS_FACTOR = 1; this.constructor.KONEC_HRY = "KONEC HRY!"; if (renderer === undefined) { @@ -33,6 +35,7 @@ class Game { this.renderer = renderer; this.guiController = guiController; this.mode = this.constructor.DEFAULT_MODE; + this.preventSpawnkill = config.preventSpawnkill; this.totalNumberOfTicks = 0; this.targetScore = null; this.initMainLoop(); @@ -119,6 +122,43 @@ class Game { return hitboxPixels; } + desiredMinimumSpawnDistance() { // to closest opponent + // This is calculated by multiplying the turning radius with a constant factor and then adding the Kurve thickness. + const turningRadiusPart = this.config.turningRadius * this.constructor.DESIRED_MINIMUM_SPAWN_DISTANCE_TURNING_RADIUS_FACTOR; + return round(this.config.thickness + turningRadiusPart, 2); + } + + safeMinimumSpawnDistance() { // to closest opponent, without risking infinite or too much sampling + const spawnAreaCoordinates = this.computeSpawnArea(); + const availableSpawnArea = (spawnAreaCoordinates.x_max - spawnAreaCoordinates.x_min) * (spawnAreaCoordinates.y_max - spawnAreaCoordinates.y_min); + const maximumSafeDistance = Math.sqrt( this.constructor.MAX_QUOTA_THAT_SPAWN_CIRCLES_MAY_FILL * availableSpawnArea / (this.getNumberOfPlayers() * Math.PI) ); + return Math.min( + this.desiredMinimumSpawnDistance(), + round(maximumSafeDistance, 2) + ); + } + + isSafeSpawnPosition(pos) { + function distanceBetween(pos1, pos2) { + return Math.sqrt(Math.pow(pos2.x - pos1.x, 2) + Math.pow(pos2.y - pos1.y, 2)); + } + for (let i = 0; i < this.players.length; i++) { + const playerPos = { x: this.players[i].x, y: this.players[i].y }; + if (distanceBetween(playerPos, pos) < this.safeMinimumSpawnDistance()) { + return false; + } + } + return true; + } + + safeSpawnPosition() { + let safePos; + do { + safePos = this.randomSpawnPosition(); + } while (!this.isSafeSpawnPosition(safePos)); + return safePos; + } + randomSpawnPosition() { let spawnArea = this.computeSpawnArea(); return { @@ -222,6 +262,10 @@ class Game { this.renderer.setSize(width, height); } + setPreventSpawnkill(mode) { + this.preventSpawnkill = mode; + } + // CHECKERS @@ -473,10 +517,14 @@ class Game { /** Spawns and then starts all players. */ spawnAndStartPlayers() { const self = this; + log(`Spawnkill prevention is ` + (this.preventSpawnkill + ? `enabled. No two players will spawn within ${self.safeMinimumSpawnDistance()} Kuxels of each other.` + : `disabled. Players may spawn arbitrarily close to each other.`)); // Spawn each player, then wait for it to finish flickering before spawning the next one: (function spawnPlayer(i) { if (i < self.players.length) { - self.spawn(self.players[i], self.randomSpawnPosition(), self.randomSpawnAngle()); + const spawnPosition = self.preventSpawnkill ? self.safeSpawnPosition() : self.randomSpawnPosition(); + self.spawn(self.players[i], spawnPosition, self.randomSpawnAngle()); setTimeout(() => spawnPlayer(++i), self.config.flickerDuration); } else { // All players have spawned. Start them! diff --git a/js/Zatacka.js b/js/Zatacka.js index 92ab933..8207631 100644 --- a/js/Zatacka.js +++ b/js/Zatacka.js @@ -18,6 +18,7 @@ const Zatacka = ((window, document) => { minSpawnAngle: -Math.PI/2, // radians maxSpawnAngle: Math.PI/2, // radians spawnMargin: 100, // Kuxels + preventSpawnkill: false, flickerFrequency: 20, // Hz, when spawning flickerDuration: 830, // ms, when spawning minHoleInterval: 90, // Kuxels @@ -51,13 +52,13 @@ const Zatacka = ((window, document) => { }); const PREFERENCES = Object.freeze([ - // { - // type: BooleanPreference, - // key: STRINGS.pref_key_prevent_spawnkill, - // label: TEXT.pref_label_prevent_spawnkill, - // description: TEXT.pref_label_description_prevent_spawnkill, - // default: true, - // }, + { + type: BooleanPreference, + key: STRINGS.pref_key_prevent_spawnkill, + label: TEXT.pref_label_prevent_spawnkill, + description: TEXT.pref_label_description_prevent_spawnkill, + default: false, + }, { type: MultichoicePreference, key: STRINGS.pref_key_cursor, @@ -150,6 +151,14 @@ const Zatacka = ((window, document) => { } } + function setPreventSpawnkill(mode) { + if (game.isStarted()) { + throw new Error("Cannot change edge padding when the game is running."); + } else { + game.setPreventSpawnkill(mode); + } + } + function getHoleConfig() { return { minHoleSize: config.minHoleSize, @@ -392,10 +401,13 @@ const Zatacka = ((window, document) => { setEdgeMode(preferenceManager.getSaved(STRINGS.pref_key_edge_fix)); // Hints: guiController.setMessageMode(preferenceManager.getSaved(STRINGS.pref_key_hints)); + // Prevent spawnkill: + game.setPreventSpawnkill(preferenceManager.getSaved(STRINGS.pref_key_prevent_spawnkill)); } catch(e) { logWarning("Could not load settings from localStorage. Using cached settings instead."); setEdgeMode(preferenceManager.getCached(STRINGS.pref_key_edge_fix)); guiController.setMessageMode(preferenceManager.getCached(STRINGS.pref_key_hints)); + game.setPreventSpawnkill(preferenceManager.getCached(STRINGS.pref_key_prevent_spawnkill)); handleSettingsAccessError(e); } } From 3d8fa17fc1396620d0804a82142ff00e405d25de Mon Sep 17 00:00:00 2001 From: Simon Alling Date: Mon, 13 Mar 2017 09:43:09 +0100 Subject: [PATCH 03/14] Add basic GUI support for RangePreference --- js/GUIController.js | 13 ++++++++++++- js/strings.js | 3 +++ 2 files changed, 15 insertions(+), 1 deletion(-) diff --git a/js/GUIController.js b/js/GUIController.js index ac8130e..5dd7571 100644 --- a/js/GUIController.js +++ b/js/GUIController.js @@ -17,6 +17,7 @@ function GUIController(cfg) { const ORIGINAL_LEFT_WIDTH = left.offsetWidth; const MULTICHOICE_LABEL_MAX_LENGTH_FOR_HALFWIDTH_FIELDSET = 22; // More characters than this will result in a full-width div/fieldset. + const FLOAT_RANGE_PREFERENCE_STEP = 0.01; let currentMessages = []; @@ -125,10 +126,17 @@ function GUIController(cfg) { // Range else if (preference instanceof RangePreference) { + const isIntegerRange = preference instanceof IntegerRangePreference; div.appendChild(label); const input = document.createElement("input"); input.type = "number"; + input.dataset.key = preference.key; + input.dataset.numberType = isIntegerRange ? STRINGS.pref_number_type_integer : STRINGS.pref_number_type_float; input.name = STRINGS.html_name_preference_prefix + preference.key; + input.setAttribute("step", isIntegerRange ? 1 : FLOAT_RANGE_PREFERENCE_STEP); + input.setAttribute("min", preference.min); + input.setAttribute("max", preference.max); + input.value = preferenceValue; div.appendChild(input); } @@ -246,8 +254,11 @@ function GUIController(cfg) { if (input.checked === true) { newSettings.push({ key: input.dataset.key, value: input.value }); } + } else if (input.type === "number") { + // number + newSettings.push({ key: input.dataset.key, value: (input.dataset.numberType === STRINGS.pref_number_type_integer ? parseInt : parseFloat)(input.value) }); } else { - // text, number etc + // text newSettings.push({ key: input.dataset.key, value: input.value.toString() }); } }); diff --git a/js/strings.js b/js/strings.js index c19d8c0..80f46ba 100644 --- a/js/strings.js +++ b/js/strings.js @@ -22,6 +22,9 @@ const STRINGS = (() => Object.freeze({ id_fullscreen_hint: "fullscreen-hint", id_popup_hint: "popup-hint", + pref_number_type_integer: "integer", + pref_number_type_float: "float", + 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", From 032d57099d6cbf0f8df93b8b6c0294787077ccb4 Mon Sep 17 00:00:00 2001 From: Simon Alling Date: Mon, 13 Mar 2017 19:41:27 +0100 Subject: [PATCH 04/14] Add checkbox/radio hover effect This commit also slightly buffs the opacity of fieldsets in the settings form and removes leftover PNGs from before sprites were used. --- Zatacka.css | 29 +++++++++++++++++++---- resources/kurve-checkbox-tickmark.png | Bin 2837 -> 0 bytes resources/kurve-icons.png | Bin 1187 -> 1366 bytes resources/kurve-icons.psd | Bin 0 -> 25897 bytes resources/kurve-radio-button-checked.png | Bin 2850 -> 0 bytes resources/kurve-radio-button.png | Bin 2845 -> 0 bytes 6 files changed, 25 insertions(+), 4 deletions(-) delete mode 100644 resources/kurve-checkbox-tickmark.png create mode 100644 resources/kurve-icons.psd delete mode 100644 resources/kurve-radio-button-checked.png delete mode 100644 resources/kurve-radio-button.png diff --git a/Zatacka.css b/Zatacka.css index 090c3a2..74e4104 100644 --- a/Zatacka.css +++ b/Zatacka.css @@ -64,12 +64,23 @@ input[type="radio"] + label::before { /* Actual checkbox: */ input[type="checkbox"] + label::before { border: 1px white solid; - background-position: 0 0; + background-position: 0 -48px; +} + +/* Actual checkbox on hover/focus: */ +input[type="checkbox"] + label:hover::before, input[type="checkbox"] + label:focus::before { + border: 1px white solid; + background-position: -16px -48px; } /* Actual checkbox when checked: */ input[type="checkbox"]:checked + label::before { - background-position: 0 -48px; + background-position: -32px -48px; +} + +/* Actual checkbox when checked on hover/focus: */ +input[type="checkbox"]:checked + label:hover::before, input[type="checkbox"]:checked + label:focus::before { + background-position: -48px -48px; } /* Actual radio button: */ @@ -77,9 +88,19 @@ input[type="radio"] + label::before { background-position: 0 -64px; } +/* Actual radio button on hover/focus: */ +input[type="radio"] + label:hover::before, input[type="radio"] + label:focus::before { + background-position: -16px -64px; +} + /* Actual radio button when checked: */ input[type="radio"]:checked + label::before { - background-position: 0 -80px; + background-position: -32px -64px; +} + +/* Actual radio button when checked on hover/focus: */ +input[type="radio"]:checked + label:hover::before, input[type="radio"]:checked + label:focus::before { + background-position: -48px -64px; } input[type="checkbox"] + label, @@ -316,7 +337,7 @@ select { #settings-form > div { box-sizing: border-box; margin: 12px 0; - opacity: 0.6; + opacity: 0.7; } #settings-form > div.half-width { diff --git a/resources/kurve-checkbox-tickmark.png b/resources/kurve-checkbox-tickmark.png deleted file mode 100644 index 36f5028fd809cab6e8ab4c1fa7833e88f602fa11..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 2837 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 diff --git a/resources/kurve-icons.png b/resources/kurve-icons.png index 9fc64c15cab4fba2c740b68fc13326078e77f954..b5d707be4a304423e18e83086158f68675e84bd2 100644 GIT binary patch delta 507 zcmZ3?d5uf4Gr-TCmrII^fq{Y7)59eQNIL*=0|yI`6zo|3aHC=dbNvoa7srr_TW@B1 zPHHybas3-RN69YP?{a!cymRCQPPr#R6U~ge&nqc5c==B=7tqFfsEkysF8f+O+tWfI`)kjY`}P*8fW@b=VWUfvrN{_J@*- z>c<$tMYAgUINTLih1=~ZVywR-d!Nx^71x6K)|C&|OWKOJF)*%cHOR?+wqPRrgS*_$ z9mfSe#a!%lwvoE<@UQ%HQ-yzJ)8&2~i4|k);rn=!>%=;i)?4Mq$DUu_$hYHqOCiUc ztV5sjcGM``JfkjmYU_PrDW0D{6*t7pS|7`NZJXV8j*m>Q6C>uQF?TGq>%V%Qp?<=t zj}lg~q4vxSZJ#gS_!X(xV0e7Vv)@fR8_bF_m(<0HMc67A=6f(O-T2%f!C89o@7kHI zuRjM_NPgcx<^J(caST`2Uon;7nQy1L-6JM3SXAbri>ID* zEt!xmb&rAJm3`XN>c?wzG)!JEtvi3`^zv1P0r8Vqzopr0FlhkX8-^I delta 327 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 From bf498a6601e7f744048ead364a42c3acce91299c Mon Sep 17 00:00:00 2001 From: Simon Alling Date: Mon, 13 Mar 2017 19:46:32 +0100 Subject: [PATCH 05/14] Fix 404 bug for favicons etc The `` markup was generated with a generator that used absolute paths for the favicons. While it is sensible, it is not what we want here. --- ZATACKA.html | 30 +++++++++++++++--------------- index.html | 30 +++++++++++++++--------------- 2 files changed, 30 insertions(+), 30 deletions(-) diff --git a/ZATACKA.html b/ZATACKA.html index 8367950..b14742e 100644 --- a/ZATACKA.html +++ b/ZATACKA.html @@ -17,22 +17,22 @@ - - - - - - - - - - - - - - + + + + + + + + + + + + + + - + diff --git a/index.html b/index.html index 7fc5267..77f98e0 100644 --- a/index.html +++ b/index.html @@ -18,22 +18,22 @@ - - - - - - - - - - - - - - + + + + + + + + + + + + + + - + From 7c712575df552afba9f411bb78a924c138992cd5 Mon Sep 17 00:00:00 2001 From: Simon Alling Date: Mon, 13 Mar 2017 20:12:19 +0100 Subject: [PATCH 06/14] Add proceed/quit key constants to locale file --- js/locales/Zatacka.en_US.js | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/js/locales/Zatacka.en_US.js b/js/locales/Zatacka.en_US.js index f7f85e4..62c3708 100644 --- a/js/locales/Zatacka.en_US.js +++ b/js/locales/Zatacka.en_US.js @@ -3,15 +3,17 @@ const TEXT = (() => { const KEY_SHIFT = "⇧"; const KEY_CMD = "⌘"; + const KEY_PROCEED = "Space or Enter"; + const KEY_QUIT = "Esc"; return Object.freeze({ hint_unload: `Are you sure you want to unload the page?`, hint_start: `Press Space to start`, hint_popup: `It is recommended to run Kurve in its own window without history (to avoid switching tabs or navigating back in history mid-game). To do that, please allow popups or click here.`, hint_pick: `Pick your desired color by pressing the corresponding LEFT key (e.g. M for Orange).`, - hint_proceed: `Press Space or Enter to start!`, - hint_next: `Press Space or Enter to proceed, or Esc to quit.`, - hint_quit: `Press Space or Enter to start over.`, + hint_proceed: `Press ${KEY_PROCEED} to start!`, + hint_next: `Press ${KEY_PROCEED} to proceed, or ${KEY_QUIT} to quit.`, + hint_quit: `Press ${KEY_PROCEED} to start over.`, hint_alt: `Alt plus some other keys may cause undesired behavior (e.g. switching windows).`, hint_ctrl: `Ctrl plus some other keys may cause undesired behavior (e.g. closing the tab).`, hint_mouse: `Make sure to keep the mouse cursor inside the browser window.`, From 3278e254e4fbbb771367c6908e041302da537c5a Mon Sep 17 00:00:00 2001 From: Simon Alling Date: Mon, 13 Mar 2017 20:27:23 +0100 Subject: [PATCH 07/14] Add data check to `PreferenceManager.getCached()` It now throws a descriptive exception if the requested preference does not exist, just like `getSaved()`. --- js/lib/preferences/PreferenceManager.js | 3 +++ 1 file changed, 3 insertions(+) diff --git a/js/lib/preferences/PreferenceManager.js b/js/lib/preferences/PreferenceManager.js index 4bb0460..57b579e 100644 --- a/js/lib/preferences/PreferenceManager.js +++ b/js/lib/preferences/PreferenceManager.js @@ -129,6 +129,9 @@ function PreferenceManager(preferencesData) { } function getCached(key) { + if (!preferenceExists(key)) { + throw new Error(`There is no preference with key '${key}'.`); + } return getCachedPreference(key).value; } From 83ca2595037f462f6467d196d15ae146aaeec5b9 Mon Sep 17 00:00:00 2001 From: Simon Alling Date: Thu, 16 Mar 2017 02:56:24 +0100 Subject: [PATCH 08/14] Dialogs, confirm quit and new event handling This commit adds the ability to show dialogs, including alerts and confirms, with the help of `GUIController`. This new functionality made it necessary to rewrite the event handling, which also became simpler overall in the process. As of this commit, the dialog functionality is used to give the player(s) the optional requirement to confirm their quit action before actually quitting the game, so as to avoid deleting an entire match by accidentally hitting the Escape button (which, of course, has _never_ happened to yours truly ...). --- ZATACKA.html | 3 + Zatacka.css | 67 +++++++++++- js/GUIController.js | 195 +++++++++++++++++++++++++++++++++++ js/Zatacka.js | 91 ++++++++-------- js/lib/ConfirmationDialog.js | 11 ++ js/lib/Dialog.js | 10 ++ js/locales/Zatacka.en_US.js | 9 ++ js/strings.js | 5 + 8 files changed, 344 insertions(+), 47 deletions(-) create mode 100644 js/lib/ConfirmationDialog.js create mode 100644 js/lib/Dialog.js diff --git a/ZATACKA.html b/ZATACKA.html index b14742e..67c8eaf 100644 --- a/ZATACKA.html +++ b/ZATACKA.html @@ -107,6 +107,7 @@

Achtung, die Kurve!

+ @@ -138,6 +139,8 @@

Achtung, die Kurve!

+ + diff --git a/Zatacka.css b/Zatacka.css index 74e4104..a299bfe 100644 --- a/Zatacka.css +++ b/Zatacka.css @@ -116,6 +116,29 @@ select { cursor: pointer; } +button { + border: 1px solid rgba(255, 255, 255, 0.5); + background-color: rgba(0, 0, 0, 0); + color: white; + height: 32px; + cursor: pointer; + width: calc(50% - 8px); +} + +button:hover, button:focus, button:focus:active { + outline: none; +} + +button:focus { + border-color: rgba(255, 255, 255, 1); + background-color: rgba(255, 255, 255, 0.03); +} + +button:hover { + border-color: rgba(255, 255, 255, 1); + background-color: rgba(255, 255, 255, 0.15); +} + .button { align-items: center; background-color: rgba(255, 255, 255, 0.16); @@ -174,6 +197,10 @@ select { cursor: none; } +.nocursor.tempcursor { + cursor: auto; +} + .icon-button { background-image: url("resources/kurve-icons.png"); cursor: pointer; @@ -261,7 +288,7 @@ select { } .hidden { - display: none; + display: none !important; } .unobtrusive { @@ -331,6 +358,9 @@ select { #settings { background-color: rgba(0, 0, 0, 0.95); +} + +#settings { padding: 50px 80px 50px 80px; } @@ -439,6 +469,41 @@ select { display: block; } +#dialogs { + align-items: center; + background-color: rgba(0, 0, 0, 0.6); + display: flex; + justify-content: center; +} + +.dialog { + background-color: rgba(0, 0, 0, 0.9); + border: 1px solid white; + cursor: auto !important; + padding: 16px; + width: 240px; +} + +.dialog p { + min-height: 64px; +} + +.alert.dialog button { + display: block; + margin-left: auto; + margin-right: auto; +} + +.confirmation.dialog button { + margin-right: 16px; +} + +.confirmation.dialog button:last-of-type { + margin-right: 0; +} + + + .canvasHeight { height: 480px; } diff --git a/js/GUIController.js b/js/GUIController.js index 5dd7571..d0d9785 100644 --- a/js/GUIController.js +++ b/js/GUIController.js @@ -13,13 +13,31 @@ function GUIController(cfg) { const KONEC_HRY = byID("KONEC_HRY"); const messagesContainer = byID("messages"); const settingsContainer = byID("settings"); + const dialogsOverlay = byID("dialogs"); const settingsForm = byID("settings-form"); const ORIGINAL_LEFT_WIDTH = left.offsetWidth; const MULTICHOICE_LABEL_MAX_LENGTH_FOR_HALFWIDTH_FIELDSET = 22; // More characters than this will result in a full-width div/fieldset. const FLOAT_RANGE_PREFERENCE_STEP = 0.01; + const BUTTON_TAG_NAME = "button"; + const BUTTON_NODE_CLASS = HTMLButtonElement; + + const LABEL_ALERT_OK = TEXT.label_button_alert_ok; + const LABEL_CONFIRM_YES = TEXT.label_button_confirm_yes; + const LABEL_CONFIRM_NO = TEXT.label_button_confirm_no; + + let showingSettings = false; let currentMessages = []; + let queuedDialogs = []; + let currentDialogWithBox = null; + + class DialogWithBox { + constructor(dialog, box) { + this.dialog = dialog; + this.box = box; + } + } // PRIVATE FUNCTIONS @@ -34,10 +52,116 @@ function GUIController(cfg) { lobby.classList.remove(STRINGS.class_hidden); } + function showDialogsOverlay() { + dialogsOverlay.classList.remove(STRINGS.class_hidden); + } + + function hideDialogsOverlay() { + dialogsOverlay.classList.add(STRINGS.class_hidden); + } + function isLobbyEntry(element) { return isHTMLElement(element) && element.children.length >= 2; } + function isButton(element) { + return element instanceof BUTTON_NODE_CLASS; + } + + function showDialogRightAway(dialog) { + const dialogBox = dialogHTMLElement(dialog); + currentDialogWithBox = new DialogWithBox(dialog, dialogBox); + setTemporaryCursorVisibility(true); + showDialogsOverlay(); + dialogsOverlay.appendChild(dialogBox); + // A dialog should have its last button focused: + dialogBox.querySelector("button:last-of-type").focus(); + } + + function currentDialogClosed() { + currentDialogWithBox.box.remove(); + currentDialogWithBox = null; + if (anyQueuedDialogs()) { + // There is at least one dialog waiting. Show it: + showDialogRightAway(queuedDialogs.shift()); + } else { + // No more dialogs. Restore: + setTemporaryCursorVisibility(false); + hideDialogsOverlay(); + } + } + + function isShowingDialog() { + return currentDialogWithBox !== null; + } + + function anyQueuedDialogs() { + return queuedDialogs.length > 0; + } + + function escapeShouldCloseCurrentDialog() { + return currentDialogWithBox.dialog instanceof ConfirmationDialog; + } + + function dialogHTMLElementBoilerplate(text) { + const dialogBox = document.createElement("div"); + dialogBox.classList.add(STRINGS.class_dialog); + const label = document.createElement("p"); + label.textContent = text; + dialogBox.appendChild(label); + return dialogBox; + } + + function alertDialogHTMLElement(dialog) { + const alertDialogBox = dialogHTMLElementBoilerplate(dialog.text); + alertDialogBox.classList.add(STRINGS.class_dialog_alert); + const buttonOK = document.createElement("button"); + buttonOK.textContent = LABEL_ALERT_OK; + alertDialogBox.appendChild(buttonOK); + + function alertDialogEventHandler() { + currentDialogClosed(); + } + // Event listener: + buttonOK.addEventListener("click", alertDialogEventHandler); + + return alertDialogBox; + } + + function confirmationDialogHTMLElement(confirmationDialog) { + const confirmationDialogBox = dialogHTMLElementBoilerplate(confirmationDialog.text); + confirmationDialogBox.classList.add(STRINGS.class_dialog_confirmation); + const buttonYes = document.createElement("button"); + buttonYes.textContent = LABEL_CONFIRM_YES; + const buttonNo = document.createElement("button"); + buttonNo.textContent = LABEL_CONFIRM_NO; + confirmationDialogBox.appendChild(buttonYes); + confirmationDialogBox.appendChild(buttonNo); + + function confirmationDialogEventHandler(response) { + return () => { + currentDialogClosed(); + if (response === true) { + confirmationDialog.callback(); + } + }; + } + // Event listeners: + buttonYes.addEventListener("click", confirmationDialogEventHandler(true)); + buttonNo.addEventListener("click", confirmationDialogEventHandler(false)); + + return confirmationDialogBox; + } + + function dialogHTMLElement(dialog) { + if (dialog instanceof ConfirmationDialog) { + return confirmationDialogHTMLElement(dialog); + } else if (dialog instanceof Dialog) { + return alertDialogHTMLElement(dialog); + } + throw new TypeError(`${dialog} is not a valid dialog.`); + } + function resetScoreboardEntry(entry) { entry.classList.remove("active"); } @@ -63,6 +187,14 @@ function GUIController(cfg) { } } + function setTemporaryCursorVisibility(tempCursorActive) { + if (tempCursorActive) { + document.body.classList.add(STRINGS.class_tempcursor); + } else { + document.body.classList.remove(STRINGS.class_tempcursor); + } + } + function settingsEntryShouldBeHalfWidth(preference) { if (preference instanceof MultichoicePreference) { const longestValueLabel = preference.labels.reduce((acc, current) => current.length > acc.length ? current : acc); @@ -147,6 +279,59 @@ function GUIController(cfg) { // PUBLIC API + function keyPressed(event, callback) { + if (isShowingDialog()) { + const currentlyFocusedButton = currentDialogWithBox.box.querySelector(`${BUTTON_TAG_NAME}:focus`); + let previousButton, nextButton; + if (isButton(currentlyFocusedButton)) { + previousButton = isButton(currentlyFocusedButton.previousSibling) ? currentlyFocusedButton.previousSibling : null; + nextButton = isButton(currentlyFocusedButton.nextSibling) ? currentlyFocusedButton.nextSibling : null; + } + switch (event.keyCode) { + case KEY.SPACE: + // Necessary because buttons do not automatically react to Space until keyup, making them feel sluggish compared to when Enter is used. + if (isButton(currentlyFocusedButton)) { + currentlyFocusedButton.click(); + } + break; + case KEY.ESCAPE: + if (escapeShouldCloseCurrentDialog()) { + currentDialogClosed(); + } + break; + case KEY.LEFT_ARROW: + if (isButton(previousButton)) { + previousButton.focus(); + } + break; + case KEY.RIGHT_ARROW: + if (isButton(nextButton)) { + nextButton.focus(); + } + break; + default: + } + } else { + callback(event); + } + } + + function mouseClicked(event, callback) { + if (isShowingDialog()) { + // Do nothing particular, but consume the event. + } else { + callback(event); + } + } + + function showDialog(dialog) { + if (isShowingDialog()) { + queuedDialogs.push(dialog); + } else { + showDialogRightAway(dialog); + } + } + function setEdgePadding(padding) { left.style.width = `${ORIGINAL_LEFT_WIDTH + padding}px`; } @@ -212,13 +397,19 @@ function GUIController(cfg) { } function showSettings() { + showingSettings = true; settings.classList.remove(STRINGS.class_hidden); } function hideSettings() { + showingSettings = false; settings.classList.add(STRINGS.class_hidden); } + function isShowingSettings() { + return showingSettings; + } + function updateSettingsForm(preferencesWithData) { flush(settingsForm); let settingsEntries = preferencesWithData.map((preferenceWithData) => settingsEntryHTMLElement(preferenceWithData.preference, preferenceWithData.value)); @@ -339,13 +530,17 @@ function GUIController(cfg) { } return { + keyPressed, + mouseClicked, playerReady, playerUnready, gameStarted, gameQuit, konecHry, + showDialog, showSettings, hideSettings, + isShowingSettings, updateSettingsForm, parseSettingsForm, updateScoreOfPlayer, diff --git a/js/Zatacka.js b/js/Zatacka.js index 8207631..d1348b2 100644 --- a/js/Zatacka.js +++ b/js/Zatacka.js @@ -41,6 +41,9 @@ const Zatacka = ((window, document) => { preferences_access_denied: new WarningMessage(TEXT.hint_preferences_access_denied), preferences_localstorage_failed: new WarningMessage(TEXT.hint_preferences_localstorage_failed), }), + dialogs: Object.freeze({ + confirmation_quit: new ConfirmationDialog(TEXT.label_text_confirm_quit, quitGame), + }), defaultPlayers: Object.freeze([ { id: 1, name: "Red" , color: "#FF2800", keyL: KEY["1"] , keyR: KEY.Q }, { id: 2, name: "Yellow", color: "#C3C300", keyL: [ KEY.CTRL, KEY.Z ] , keyR: [ KEY.ALT, KEY.X ] }, @@ -59,6 +62,13 @@ const Zatacka = ((window, document) => { description: TEXT.pref_label_description_prevent_spawnkill, default: false, }, + { + type: BooleanPreference, + key: STRINGS.pref_key_confirm_quit, + label: TEXT.pref_label_confirm_quit, + description: TEXT.pref_label_description_confirm_quit, + default: true, + }, { type: MultichoicePreference, key: STRINGS.pref_key_cursor, @@ -217,8 +227,6 @@ const Zatacka = ((window, document) => { const numberOfReadyPlayers = game.getNumberOfPlayers(); if (numberOfReadyPlayers > 0) { clearMessages(); - removeLobbyEventListeners(); - addGameEventListeners(); applyCursorBehavior(); game.setMode(numberOfReadyPlayers === 1 ? Game.PRACTICE : Game.COMPETITIVE); game.start(); @@ -291,6 +299,7 @@ const Zatacka = ((window, document) => { function eventConsumer(event) { event.stopPropagation(); + event.preventDefault(); } function keyPressedInLobby(pressedKey) { @@ -305,6 +314,24 @@ const Zatacka = ((window, document) => { }); } + function keyHandler(event) { + const callback = game.isStarted() ? gameKeyHandler + : guiController.isShowingSettings() ? settingsKeyHandler + : lobbyKeyHandler; + guiController.keyPressed(event, callback); + } + + function mouseHandler(event) { + const callback = game.isStarted() ? gameMouseHandler : lobbyMouseHandler; + guiController.mouseClicked(event, callback); + } + + function unloadHandler(event) { + if (game.isStarted()) { + gameUnloadHandler(); + } + } + function lobbyKeyHandler(event) { const pressedKey = event.keyCode; if (shouldPreventDefault(pressedKey)) { @@ -323,8 +350,6 @@ const Zatacka = ((window, document) => { } function quitGame() { - removeGameEventListeners(); - addLobbyEventListeners(); game.quit(); guiController.gameQuit(); game = newGame(); @@ -342,7 +367,11 @@ const Zatacka = ((window, document) => { game.proceedKeyPressed(); } } else if (isQuitKey(pressedKey) && game.shouldQuitOnQuitKey()) { - quitGame(); + if (preferenceManager.getCached(STRINGS.pref_key_confirm_quit) === true) { + guiController.showDialog(config.dialogs.confirmation_quit); + } else { + quitGame(); + } } } @@ -374,15 +403,10 @@ const Zatacka = ((window, document) => { guiController.updateSettingsForm(preferenceManager.getAllPreferencesWithValues_cached()); handleSettingsAccessError(e); } - removeLobbyEventListeners(); - addHideSettingsButtonEventListener(); - document.addEventListener("keydown", settingsKeyHandler); guiController.showSettings(); } function hideSettings() { - document.removeEventListener("keydown", settingsKeyHandler); - addLobbyEventListeners(); guiController.parseSettingsForm().forEach((newSetting) => { try { preferenceManager.set(newSetting.key, newSetting.value); @@ -442,54 +466,29 @@ const Zatacka = ((window, document) => { } } - function removeShowSettingsButtonEventListener() { - const showSettingsButton = byID("button-show-settings"); - if (showSettingsButton instanceof HTMLElement) { - showSettingsButton.removeEventListener("mousedown", eventConsumer); - showSettingsButton.removeEventListener("click", showSettings); - } - } + function addEventListeners() { + log("Adding event listeners ..."); - function addLobbyEventListeners() { - log("Adding lobby event listeners ..."); + // Hide/show settings: addShowSettingsButtonEventListener(); - document.addEventListener("keydown", lobbyKeyHandler); - document.addEventListener("mousedown", lobbyMouseHandler); - document.addEventListener("contextmenu", lobbyMouseHandler); - log("Done."); - } + addHideSettingsButtonEventListener(); - function removeLobbyEventListeners() { - log("Removing lobby event listeners ..."); - removeShowSettingsButtonEventListener(); - document.removeEventListener("keydown", lobbyKeyHandler); - document.removeEventListener("mousedown", lobbyMouseHandler); - document.removeEventListener("contextmenu", lobbyMouseHandler); - log("Done."); - } + // General event handlers: + document.addEventListener("keydown", keyHandler); + document.addEventListener("mousedown", mouseHandler); + document.addEventListener("contextmenu", eventConsumer); + window.addEventListener("beforeunload", unloadHandler); - function addGameEventListeners() { - log("Adding game event listeners ..."); + // Player input: document.addEventListener("keydown", Keyboard.onKeydown.bind(Keyboard)); document.addEventListener("keyup", Keyboard.onKeyup.bind(Keyboard)); document.addEventListener("mousedown", Mouse.onMousedown.bind(Mouse)); document.addEventListener("mouseup", Mouse.onMouseup.bind(Mouse)); - document.addEventListener("keydown", gameKeyHandler); - document.addEventListener("mousedown", gameMouseHandler); - document.addEventListener("contextmenu", gameMouseHandler); - window.addEventListener("beforeunload", gameUnloadHandler); - log("Done."); - } - function removeGameEventListeners() { - log("Removing game event listeners ..."); - document.removeEventListener("keydown", gameKeyHandler); - document.removeEventListener("mousedown", gameMouseHandler); - window.removeEventListener("beforeunload", gameUnloadHandler); log("Done."); } - addLobbyEventListeners(); + addEventListeners(); function newGame() { return new Game(config, Renderer(canvas_main, canvas_overlay), guiController); diff --git a/js/lib/ConfirmationDialog.js b/js/lib/ConfirmationDialog.js new file mode 100644 index 0000000..fbc6c54 --- /dev/null +++ b/js/lib/ConfirmationDialog.js @@ -0,0 +1,11 @@ +"use strict"; + +class ConfirmationDialog extends Dialog { + constructor(question, callback) { + if (!(callback instanceof Function)) { + throw new TypeError(`callback must be a function (found ${callback}).`); + } + super(question); + this.callback = callback; + } +} diff --git a/js/lib/Dialog.js b/js/lib/Dialog.js new file mode 100644 index 0000000..6ed2301 --- /dev/null +++ b/js/lib/Dialog.js @@ -0,0 +1,10 @@ +"use strict"; + +class Dialog { + constructor(text) { + if (!isString(text)) { + throw new TypeError(`text must be a string (found ${text}).`); + } + this.text = text; + } +} diff --git a/js/locales/Zatacka.en_US.js b/js/locales/Zatacka.en_US.js index 62c3708..645be56 100644 --- a/js/locales/Zatacka.en_US.js +++ b/js/locales/Zatacka.en_US.js @@ -25,6 +25,12 @@ const TEXT = (() => { getFullscreenHint: (shortcut) => `Press ${shortcut} to toggle fullscreen`, + label_button_alert_ok: `OK`, + label_button_confirm_yes: `Yes`, + label_button_confirm_no: `No`, + + label_text_confirm_quit: `Are you sure you want to quit?`, + pref_label_cursor: `Cursor`, pref_label_description_cursor: `Control how the cursor behaves when the game is running.`, pref_label_cursor_always_visible: `Always visible`, @@ -45,5 +51,8 @@ const TEXT = (() => { pref_label_prevent_spawnkill: `Prevent spawnkills`, pref_label_description_prevent_spawnkill: `Enforce a minimum distance between player spawns.`, + + pref_label_confirm_quit: `Confirm quit`, + pref_label_description_confirm_quit: `Ask for confirmation before quitting to the main menu.`, }); })(); diff --git a/js/strings.js b/js/strings.js index 80f46ba..a191ea1 100644 --- a/js/strings.js +++ b/js/strings.js @@ -7,10 +7,14 @@ const STRINGS = (() => Object.freeze({ class_hidden: "hidden", class_active: "active", + class_dialog: "dialog", + class_dialog_alert: "alert", + class_dialog_confirmation: "confirmation", class_description: "description", class_half_width: "half-width", class_right_hand_side: "right-hand-side", class_nocursor: "nocursor", + class_tempcursor: "tempcursor", class_hints_warnings_only: "hints-warnings-only", class_hints_none: "hints-none", html_name_preference_prefix: "preference-", @@ -40,5 +44,6 @@ const STRINGS = (() => Object.freeze({ pref_value_hints_warnings_only: "warnings", pref_value_hints_none: "none", + pref_key_confirm_quit: "confirm_quit", pref_key_prevent_spawnkill: "prevent_spawnkill", }))(); From adcc0398a2c5d37c2a338ab8243c749336b05b49 Mon Sep 17 00:00:00 2001 From: Simon Alling Date: Thu, 16 Mar 2017 18:52:58 +0100 Subject: [PATCH 09/14] Rewrite key handling, prevent accidental reload --- js/GUIController.js | 10 ++++++++++ js/Game.js | 4 ++++ js/Zatacka.js | 27 +++++++++++++++++++-------- js/lib/Utilities.js | 2 +- js/locales/Zatacka.en_US.js | 1 + 5 files changed, 35 insertions(+), 9 deletions(-) diff --git a/js/GUIController.js b/js/GUIController.js index d0d9785..d1faed0 100644 --- a/js/GUIController.js +++ b/js/GUIController.js @@ -289,6 +289,7 @@ function GUIController(cfg) { } switch (event.keyCode) { case KEY.SPACE: + case KEY.ENTER: // Necessary because buttons do not automatically react to Space until keyup, making them feel sluggish compared to when Enter is used. if (isButton(currentlyFocusedButton)) { currentlyFocusedButton.click(); @@ -299,6 +300,15 @@ function GUIController(cfg) { currentDialogClosed(); } break; + case KEY.TAB: + if (Keyboard.isDown(KEY.SHIFT)) { + if (isButton(previousButton)) { + previousButton.focus(); + } + } else if (isButton(nextButton)) { + nextButton.focus(); + } + break; case KEY.LEFT_ARROW: if (isButton(previousButton)) { previousButton.focus(); diff --git a/js/Game.js b/js/Game.js index f8de2c4..c8128eb 100644 --- a/js/Game.js +++ b/js/Game.js @@ -619,6 +619,10 @@ class Game { } } + shouldShowReloadConfirmationOnReloadKey() { + return this.isPostRound(); + } + shouldQuitOnQuitKey() { return this.isPostRound() && !this.isGameOver(); } diff --git a/js/Zatacka.js b/js/Zatacka.js index d1348b2..a5a510b 100644 --- a/js/Zatacka.js +++ b/js/Zatacka.js @@ -7,6 +7,9 @@ const Zatacka = ((window, document) => { const ORIGINAL_WIDTH = canvas_main.width; const ORIGINAL_HEIGHT = canvas_main.height; const TOTAL_BORDER_THICKNESS = 4; + const KEY_RELOAD = KEY.F5; + const KEY_FULLSCREEN = KEY.F11; + const ALLOWED_KEYS = [KEY_FULLSCREEN]; // not to be intercepted by our event handler const config = Object.freeze({ tickrate: 600, // Hz @@ -43,6 +46,7 @@ const Zatacka = ((window, document) => { }), dialogs: Object.freeze({ confirmation_quit: new ConfirmationDialog(TEXT.label_text_confirm_quit, quitGame), + confirmation_reload: new ConfirmationDialog(TEXT.label_text_confirm_reload, reload), }), defaultPlayers: Object.freeze([ { id: 1, name: "Red" , color: "#FF2800", keyL: KEY["1"] , keyR: KEY.Q }, @@ -133,7 +137,7 @@ const Zatacka = ((window, document) => { } function shouldPreventDefault(key) { - return !isFKey(key); + return !(ALLOWED_KEYS.includes(key)); } function setEdgePadding(padding) { @@ -315,6 +319,9 @@ const Zatacka = ((window, document) => { } function keyHandler(event) { + if (shouldPreventDefault(event.keyCode)) { + event.preventDefault(); + } const callback = game.isStarted() ? gameKeyHandler : guiController.isShowingSettings() ? settingsKeyHandler : lobbyKeyHandler; @@ -334,10 +341,9 @@ const Zatacka = ((window, document) => { function lobbyKeyHandler(event) { const pressedKey = event.keyCode; - if (shouldPreventDefault(pressedKey)) { - event.preventDefault(); - } - if (isProceedKey(pressedKey)) { + if (pressedKey === KEY_RELOAD) { + reload(); + } else if (isProceedKey(pressedKey)) { proceedKeyPressedInLobby(); } else { keyPressedInLobby(pressedKey); @@ -349,6 +355,10 @@ const Zatacka = ((window, document) => { mouseClickedInLobby(event.button); } + function reload() { + window.location.reload(); + } + function quitGame() { game.quit(); guiController.gameQuit(); @@ -357,9 +367,6 @@ const Zatacka = ((window, document) => { function gameKeyHandler(event) { const pressedKey = event.keyCode; - if (shouldPreventDefault(pressedKey)) { - event.preventDefault(); - } if (isProceedKey(pressedKey)) { if (game.shouldQuitOnProceedKey()) { quitGame(); @@ -372,6 +379,8 @@ const Zatacka = ((window, document) => { } else { quitGame(); } + } else if (pressedKey === KEY_RELOAD && game.shouldShowReloadConfirmationOnReloadKey()) { + guiController.showDialog(config.dialogs.confirmation_reload, reload); } } @@ -390,6 +399,8 @@ const Zatacka = ((window, document) => { const pressedKey = event.keyCode; if (isQuitKey(pressedKey)) { hideSettings(); + } else if (pressedKey === KEY_RELOAD) { + reload(); } } diff --git a/js/lib/Utilities.js b/js/lib/Utilities.js index 3409783..a6fd28b 100644 --- a/js/lib/Utilities.js +++ b/js/lib/Utilities.js @@ -141,7 +141,7 @@ function forfor(y_start, y_end, x_start, x_end, func, arg) { const Keyboard = { pressed: {}, isDown: function(keyCode) { - return this.pressed[keyCode]; + return !!this.pressed[keyCode]; }, onKeydown: function(event) { this.pressed[event.keyCode] = true; diff --git a/js/locales/Zatacka.en_US.js b/js/locales/Zatacka.en_US.js index 645be56..a400a73 100644 --- a/js/locales/Zatacka.en_US.js +++ b/js/locales/Zatacka.en_US.js @@ -30,6 +30,7 @@ const TEXT = (() => { label_button_confirm_no: `No`, label_text_confirm_quit: `Are you sure you want to quit?`, + label_text_confirm_reload: `Are you sure you want to reload the application?`, pref_label_cursor: `Cursor`, pref_label_description_cursor: `Control how the cursor behaves when the game is running.`, From 031224c0e9266b57c1c1b6d7a605b2077dc7257a Mon Sep 17 00:00:00 2001 From: Simon Alling Date: Fri, 17 Mar 2017 23:57:12 +0100 Subject: [PATCH 10/14] Remove settings checkbox left margin --- Zatacka.css | 4 ---- 1 file changed, 4 deletions(-) diff --git a/Zatacka.css b/Zatacka.css index a299bfe..43694f5 100644 --- a/Zatacka.css +++ b/Zatacka.css @@ -404,10 +404,6 @@ button:hover { padding: 0 4px; } -#settings-form input[type="checkbox"] + label { - margin-left: 13px; /* to align with fieldset content */ -} - #settings-form fieldset input[type="radio"] + label { margin-left: 4px; /* to align with legend */ } From d9b148f9bfb56a83b8a6ae6b52d1a1de565ce1a9 Mon Sep 17 00:00:00 2001 From: Simon Alling Date: Sat, 18 Mar 2017 00:06:21 +0100 Subject: [PATCH 11/14] Fix L_keys/R_keys bug in Player If a player was added without both L and R keys, the mouse check (to determine mouse behavior) would fail because L_keys and/or R_keys would be undefined. --- js/Player.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/js/Player.js b/js/Player.js index 7d81be0..8cbe426 100644 --- a/js/Player.js +++ b/js/Player.js @@ -32,6 +32,7 @@ class Player { this.L_keys = L_keys; } else { logWarning(`Creating player "${this.name}" without any LEFT key(s).`); + this.L_keys = []; } if (isPositiveInt(R_keys)) { @@ -40,6 +41,7 @@ class Player { this.R_keys = R_keys; } else { logWarning(`Creating player "${this.name}" without any RIGHT key(s).`); + this.R_keys = []; } } From 5c2c60f47ccb9248f2d968db4ec316f94064b9f5 Mon Sep 17 00:00:00 2001 From: Simon Alling Date: Sat, 18 Mar 2017 00:10:13 +0100 Subject: [PATCH 12/14] Remove debugging code --- ZATACKA.html | 9 --------- Zatacka.css | 23 ----------------------- js/Game.js | 6 ------ 3 files changed, 38 deletions(-) diff --git a/ZATACKA.html b/ZATACKA.html index 67c8eaf..486bae7 100644 --- a/ZATACKA.html +++ b/ZATACKA.html @@ -123,15 +123,6 @@

Achtung, die Kurve!

-
- - - - - - -
- diff --git a/Zatacka.css b/Zatacka.css index 43694f5..3b30dfd 100644 --- a/Zatacka.css +++ b/Zatacka.css @@ -219,29 +219,6 @@ button:hover { top: 16px; } -#debug { - background-color: black; - border: 1px white solid; - display: none; - left: 0; - position: fixed; - top: 0; - z-index: 50; -} - -#debug span { - font-family: monospace; - font-size: 16px; - display: block; -} - -#debug_red { color: #FF2800; } -#debug_yellow { color: #C3C300; } -#debug_orange { color: #FF7900; } -#debug_green { color: #00CB00; } -#debug_pink { color: #DF51B6; } -#debug_blue { color: #00A2CB; } - #wrapper { align-items: center; display: flex; diff --git a/js/Game.js b/js/Game.js index c8128eb..89e31dd 100644 --- a/js/Game.js +++ b/js/Game.js @@ -680,14 +680,8 @@ class Game { updatePlayer(player, delta) { if (player.isAlive()) { - // Debugging: - const debugFieldID = "debug_" + player.getName().toLowerCase(); - const debugField = document.getElementById(debugFieldID); const angleChange = this.computeAngleChange(); let direction = player.getDirection(); - if (isHTMLElement(debugField)) { - debugField.textContent = "x ~ "+Math.round(player.x)+", y ~ "+Math.round(player.y)+", dir = "+round(radToDeg(player.direction), 2); - } if (player.isPressingLeft()) { direction = direction + angleChange; // let compound assignment not optimizable in V8 } From 27da313f936a1307917875910e580db543221aef Mon Sep 17 00:00:00 2001 From: Simon Alling Date: Sat, 18 Mar 2017 00:13:44 +0100 Subject: [PATCH 13/14] Move hardcoded IDs (settings button) to STRINGS --- js/Zatacka.js | 4 ++-- js/strings.js | 2 ++ 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/js/Zatacka.js b/js/Zatacka.js index a5a510b..3f13045 100644 --- a/js/Zatacka.js +++ b/js/Zatacka.js @@ -462,7 +462,7 @@ const Zatacka = ((window, document) => { } function addShowSettingsButtonEventListener() { - const showSettingsButton = byID("button-show-settings"); + const showSettingsButton = byID(STRINGS.id_button_show_settings); if (showSettingsButton instanceof HTMLElement) { showSettingsButton.addEventListener("mousedown", eventConsumer); showSettingsButton.addEventListener("click", showSettings); @@ -470,7 +470,7 @@ const Zatacka = ((window, document) => { } function addHideSettingsButtonEventListener() { - const hideSettingsButton = byID("button-hide-settings"); + const hideSettingsButton = byID(STRINGS.id_button_hide_settings); if (hideSettingsButton instanceof HTMLElement) { hideSettingsButton.addEventListener("mousedown", eventConsumer); hideSettingsButton.addEventListener("click", hideSettings); diff --git a/js/strings.js b/js/strings.js index a191ea1..f845075 100644 --- a/js/strings.js +++ b/js/strings.js @@ -25,6 +25,8 @@ const STRINGS = (() => Object.freeze({ id_start_hint: "start-hint", id_fullscreen_hint: "fullscreen-hint", id_popup_hint: "popup-hint", + id_button_show_settings: "button-show-settings", + id_button_hide_settings: "button-hide-settings", pref_number_type_integer: "integer", pref_number_type_float: "float", From bee69b6157fc03825e186a44dd9e54f5770c8d02 Mon Sep 17 00:00:00 2001 From: Simon Alling Date: Sat, 18 Mar 2017 00:33:26 +0100 Subject: [PATCH 14/14] Fix key/mouse handling when settings visible When the settings menu was being shown, the mouse and keyboard would not work as expected; for example, neither could be used to modify the value of an input[type="number"] field. Instead, a mouse click would be interpreted as Blue getting (un)ready, which is not a desired behavior in the settings menu. --- js/Zatacka.js | 17 +++++++++++++---- 1 file changed, 13 insertions(+), 4 deletions(-) diff --git a/js/Zatacka.js b/js/Zatacka.js index 3f13045..65e70f8 100644 --- a/js/Zatacka.js +++ b/js/Zatacka.js @@ -319,9 +319,6 @@ const Zatacka = ((window, document) => { } function keyHandler(event) { - if (shouldPreventDefault(event.keyCode)) { - event.preventDefault(); - } const callback = game.isStarted() ? gameKeyHandler : guiController.isShowingSettings() ? settingsKeyHandler : lobbyKeyHandler; @@ -329,7 +326,9 @@ const Zatacka = ((window, document) => { } function mouseHandler(event) { - const callback = game.isStarted() ? gameMouseHandler : lobbyMouseHandler; + const callback = game.isStarted() ? gameMouseHandler + : guiController.isShowingSettings() ? settingsMouseHandler + : lobbyMouseHandler; guiController.mouseClicked(event, callback); } @@ -340,6 +339,9 @@ const Zatacka = ((window, document) => { } function lobbyKeyHandler(event) { + if (shouldPreventDefault(event.keyCode)) { + event.preventDefault(); + } const pressedKey = event.keyCode; if (pressedKey === KEY_RELOAD) { reload(); @@ -366,6 +368,9 @@ const Zatacka = ((window, document) => { } function gameKeyHandler(event) { + if (shouldPreventDefault(event.keyCode)) { + event.preventDefault(); + } const pressedKey = event.keyCode; if (isProceedKey(pressedKey)) { if (game.shouldQuitOnProceedKey()) { @@ -404,6 +409,10 @@ const Zatacka = ((window, document) => { } } + function settingsMouseHandler(event) { + // Intentionally empty. + } + function showSettings() { clearTimeout(hintPickTimer); clearTimeout(hintProceedTimer);