Allow holding R (for one second) to restart the level

This commit is contained in:
Eevee (Evelyn Woods) 2024-04-11 23:41:48 -06:00
parent 1df89884ed
commit a3b283b51e
3 changed files with 74 additions and 18 deletions

View File

@ -227,8 +227,7 @@
<span class="-optional-label">pause</span> <span class="keyhint"><kbd>p</kbd></span></button>
<button class="control-restart" type="button" title="restart">
<svg class="svg-icon" viewBox="0 0 16 16"><path d="M13,13 A 7,7 270 1,1 13,3 L15,1 15,7 9,7 11,5 A 4,4 270 1,0 11,11 z"></path></svg>
<span class="-optional-label">retry</span>
</button>
<span class="-optional-label">retry</span> <span class="keyhint"><kbd>r</kbd></span></button>
<button class="control-undo" type="button" title="undo">
<svg class="svg-icon" viewBox="0 0 16 16"><path d="M6,5 6,2 1,7 6,12 6,9 A 10,10 60 0,1 15,12 A 10,10 90 0,0 6,5"></path></svg>
<span class="-optional-label">undo</span> <span class="keyhint"><kbd>u</kbd></span></button>

View File

@ -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();

View File

@ -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 {