From a3b283b51eaf58fa2fa368264c87af6cf262195e Mon Sep 17 00:00:00 2001 From: "Eevee (Evelyn Woods)" Date: Thu, 11 Apr 2024 23:41:48 -0600 Subject: [PATCH] Allow holding R (for one second) to restart the level --- index.html | 3 +-- js/main.js | 79 +++++++++++++++++++++++++++++++++++++++++++----------- style.css | 10 ++++++- 3 files changed, 74 insertions(+), 18 deletions(-) diff --git a/index.html b/index.html index 8990fb8..2a40e50 100644 --- a/index.html +++ b/index.html @@ -227,8 +227,7 @@ pause p + retry r diff --git a/js/main.js b/js/main.js index 2113d57..495a28f 100644 --- a/js/main.js +++ b/js/main.js @@ -21,6 +21,7 @@ const PAGE_TITLE = "Lexy's Labyrinth"; // This prefix is LLDEMO in base64, used to be somewhat confident that a string is a valid demo // (it's 6 characters so it becomes exactly 8 base64 chars with no leftovers to entangle) const REPLAY_PREFIX = "TExERU1P"; +const RESTART_KEY_DELAY = 1.0; function format_replay_duration(t) { return `${t} tics (${util.format_duration(t / TICS_PER_SECOND)})`; @@ -715,7 +716,6 @@ class Player extends PrimaryView { this.inventory_el.append(img); } - let last_key; this.pending_player_move = null; this.next_player_move = null; this.player_used_move = false; @@ -750,6 +750,11 @@ class Player extends PrimaryView { return; } + if (ev.key === 'r') { + this.start_restarting(); + return; + } + // Per-tic navigation; only useful if the game isn't running if (ev.key === ',') { if (this.state === 'stopped' || this.state === 'paused' || this.turn_based_mode) { @@ -849,10 +854,16 @@ class Player extends PrimaryView { if (! this.active) return; + if (ev.key === 'r') { + this.stop_restarting(); + return; + } + if (ev.key === 'z') { if (this.state === 'rewinding') { this.set_state('playing'); } + return; } if (this.key_mapping[ev.key]) { @@ -939,23 +950,13 @@ class Player extends PrimaryView { // When we lose focus, act as though every key was released, and pause the game window.addEventListener('blur', () => { - this.current_keys.clear(); - this.current_touches = {}; - - if (this.state === 'playing' || this.state === 'rewinding') { - this.autopause(); - } + this.enter_background(); }); // Same when the window becomes hidden (especially important on phones, where this covers // turning the screen off!) document.addEventListener('visibilitychange', ev => { if (document.visibilityState === 'hidden') { - this.current_keys.clear(); - this.current_touches = {}; - - if (this.state === 'playing' || this.state === 'rewinding') { - this.autopause(); - } + this.enter_background(); } }); @@ -1402,6 +1403,17 @@ class Player extends PrimaryView { this.captions_el.textContent = ''; } + // Called when we lose focus; assume all keys are released, since we can't be sure any more + enter_background() { + this.stop_restarting(); + this.current_keys.clear(); + this.current_touches = {}; + + if (this.state === 'playing' || this.state === 'rewinding') { + this.autopause(); + } + } + reload_options(options) { this.music_audio_el.volume = options.music_volume ?? 1.0; // TODO hide music info when disabled? @@ -1946,6 +1958,38 @@ class Player extends PrimaryView { this.set_state('paused'); } + start_restarting() { + this.stop_restarting(); + + if (! (this.state === 'playing' || this.state === 'paused' || this.state === 'rewinding')) + return; + + let t0 = performance.now(); + let update = () => { + let t = performance.now(); + let p = (t - t0) / 1000 / RESTART_KEY_DELAY; + this.restart_button.style.setProperty('--restart-progress', p); + + if (p < 1) { + this._restart_handle = requestAnimationFrame(update); + } + else { + this.restart_level(); + this.stop_restarting(); + this._restart_handle = null; + } + }; + update(); + } + + stop_restarting() { + if (this._restart_handle) { + cancelAnimationFrame(this._restart_handle); + this._restart_handle = null; + } + this.restart_button.style.setProperty('--restart-progress', 0); + } + // waiting: haven't yet pressed a key so the timer isn't going // playing: playing normally // paused: um, paused @@ -2217,8 +2261,13 @@ class Player extends PrimaryView { this.update_music_playback_state(); - // The advance and redraw methods run in a loop, but they cancel - // themselves if the game isn't running, so restart them here + // Restarting makes no sense if we're not playing + if (this.state === 'waiting' || this.state === 'stopped' || this.state === 'ended') { + this.stop_restarting(); + } + + // The advance and redraw methods run in a loop, but they cancel themselves if the game + // isn't running, so restart them here if (this.state === 'playing' || this.state === 'rewinding') { if (! this._advance_handle) { this.advance(); diff --git a/style.css b/style.css index 120b354..a9dc1a7 100644 --- a/style.css +++ b/style.css @@ -19,6 +19,7 @@ body { --panel-bg-color: hsl(220, 10%, 15%); --button-bg-color: hsl(220, 20%, 25%); + --button-bg-gradient: linear-gradient(to bottom, var(--button-bg-shadow-color), transparent 75%); --button-bg-shadow-color: #fff1; --button-bg-hover-color: hsl(220, 30%, 30%); --generic-bg-hover-on-white: hsl(220, 60%, 90%); @@ -50,7 +51,7 @@ button, font-family: inherit; color: white; background-color: var(--button-bg-color); - background-image: linear-gradient(to bottom, var(--button-bg-shadow-color), transparent 75%); + background-image: var(--button-bg-gradient); border: 1px solid hsl(220, 10%, 7.5%); box-shadow: inset 0 0 1px 1px #fff2, @@ -1238,6 +1239,13 @@ ol.packtest-summary > li { #player button:disabled .keyhint { display: none; } +#player-controls button:enabled.control-restart { + /* Special shenanigans for holding R to restart */ + --restart-progress: 0; + background-image: var(--button-bg-gradient), conic-gradient( + hsl(345, 60%, 40%) 0deg calc(var(--restart-progress) * 360deg), + transparent calc(var(--restart-progress) * 360deg) 360deg) +} @media (orientation: portrait) { /* On a portrait screen, put the controls on top */ #player-main {