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 {