Implement rewind, add a key for it, and suggest keys in general
This commit is contained in:
parent
57810da581
commit
063d9f9ef9
@ -56,6 +56,7 @@
|
|||||||
<h1 class="-top"></h1>
|
<h1 class="-top"></h1>
|
||||||
<div class="-middle"></div>
|
<div class="-middle"></div>
|
||||||
<p class="-bottom"></p>
|
<p class="-bottom"></p>
|
||||||
|
<p class="-keyhint"></p>
|
||||||
</div>
|
</div>
|
||||||
<div class="message"></div>
|
<div class="message"></div>
|
||||||
<div class="chips">
|
<div class="chips">
|
||||||
|
|||||||
@ -745,6 +745,7 @@ export class Level {
|
|||||||
if (actor.cell === goal_cell)
|
if (actor.cell === goal_cell)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
|
// TODO undo this stuff?
|
||||||
actor.previous_cell = actor.cell;
|
actor.previous_cell = actor.cell;
|
||||||
actor.animation_speed = speed;
|
actor.animation_speed = speed;
|
||||||
actor.animation_progress = 0;
|
actor.animation_progress = 0;
|
||||||
|
|||||||
71
js/main.js
71
js/main.js
@ -268,6 +268,12 @@ class Player extends PrimaryView {
|
|||||||
this._redraw();
|
this._redraw();
|
||||||
ev.target.blur();
|
ev.target.blur();
|
||||||
});
|
});
|
||||||
|
this.rewind_button = this.root.querySelector('.controls .control-rewind');
|
||||||
|
this.rewind_button.addEventListener('click', ev => {
|
||||||
|
if (this.level.undo_stack.length > 0) {
|
||||||
|
this.state = 'rewinding';
|
||||||
|
}
|
||||||
|
});
|
||||||
// Demo playback
|
// Demo playback
|
||||||
this.root.querySelector('.demo-controls .demo-play').addEventListener('click', ev => {
|
this.root.querySelector('.demo-controls .demo-play').addEventListener('click', ev => {
|
||||||
if (this.state === 'playing' || this.state === 'paused' || this.state === 'rewinding') {
|
if (this.state === 'playing' || this.state === 'paused' || this.state === 'rewinding') {
|
||||||
@ -339,6 +345,14 @@ class Player extends PrimaryView {
|
|||||||
ev.preventDefault();
|
ev.preventDefault();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (ev.key === 'z') {
|
||||||
|
if (this.level.undo_stack.length > 0 &&
|
||||||
|
(this.state === 'stopped' || this.state === 'playing' || this.state === 'paused'))
|
||||||
|
{
|
||||||
|
this.set_state('rewinding');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (this.key_mapping[ev.key]) {
|
if (this.key_mapping[ev.key]) {
|
||||||
this.current_keys.add(ev.key);
|
this.current_keys.add(ev.key);
|
||||||
ev.stopPropagation();
|
ev.stopPropagation();
|
||||||
@ -350,6 +364,12 @@ class Player extends PrimaryView {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
key_target.addEventListener('keyup', ev => {
|
key_target.addEventListener('keyup', ev => {
|
||||||
|
if (ev.key === 'z') {
|
||||||
|
if (this.state === 'rewinding') {
|
||||||
|
this.set_state('playing');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (this.key_mapping[ev.key]) {
|
if (this.key_mapping[ev.key]) {
|
||||||
this.current_keys.delete(ev.key);
|
this.current_keys.delete(ev.key);
|
||||||
ev.stopPropagation();
|
ev.stopPropagation();
|
||||||
@ -357,6 +377,15 @@ class Player extends PrimaryView {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// When we lose focus, act as though every key was released, and pause the game
|
||||||
|
window.addEventListener('blur', ev => {
|
||||||
|
this.current_keys.clear();
|
||||||
|
|
||||||
|
if (this.state === 'playing' || this.state === 'rewinding') {
|
||||||
|
this.set_state('paused');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
// Populate input debugger
|
// Populate input debugger
|
||||||
this.input_el = this.root.querySelector('.input');
|
this.input_el = this.root.querySelector('.input');
|
||||||
this.input_action_elements = {};
|
this.input_action_elements = {};
|
||||||
@ -366,13 +395,6 @@ class Player extends PrimaryView {
|
|||||||
this.input_action_elements[action] = el;
|
this.input_action_elements[action] = el;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Auto pause when we lose focus
|
|
||||||
window.addEventListener('blur', ev => {
|
|
||||||
if (this.state === 'playing' || this.state === 'rewinding') {
|
|
||||||
this.set_state('paused');
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
this._advance_bound = this.advance.bind(this);
|
this._advance_bound = this.advance.bind(this);
|
||||||
this._redraw_bound = this.redraw.bind(this);
|
this._redraw_bound = this.redraw.bind(this);
|
||||||
// Used to determine where within a tic we are, for animation purposes
|
// Used to determine where within a tic we are, for animation purposes
|
||||||
@ -554,7 +576,22 @@ class Player extends PrimaryView {
|
|||||||
}
|
}
|
||||||
|
|
||||||
this.last_advance = performance.now();
|
this.last_advance = performance.now();
|
||||||
|
if (this.state === 'playing') {
|
||||||
this.advance_by(1);
|
this.advance_by(1);
|
||||||
|
}
|
||||||
|
else if (this.state === 'rewinding') {
|
||||||
|
if (this.level.undo_stack.length === 0) {
|
||||||
|
// TODO detect if we hit the start of the level (rather than just running the undo
|
||||||
|
// buffer dry) and change to 'waiting' instead
|
||||||
|
// TODO pausing seems rude actually, it should just hover in-place?
|
||||||
|
this._advance_handle = null;
|
||||||
|
this.set_state('paused');
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
// Rewind by undoing one tic every tic
|
||||||
|
this.level.undo();
|
||||||
|
}
|
||||||
|
}
|
||||||
this._advance_handle = window.setTimeout(this._advance_bound, 1000 / TICS_PER_SECOND);
|
this._advance_handle = window.setTimeout(this._advance_bound, 1000 / TICS_PER_SECOND);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -566,6 +603,9 @@ class Player extends PrimaryView {
|
|||||||
// TODO i'm not sure it'll be right when rewinding either
|
// TODO i'm not sure it'll be right when rewinding either
|
||||||
// TODO or if the game's speed changes. wow!
|
// TODO or if the game's speed changes. wow!
|
||||||
this.tic_offset = Math.min(0.9999, (performance.now() - this.last_advance) / 1000 / (1 / TICS_PER_SECOND));
|
this.tic_offset = Math.min(0.9999, (performance.now() - this.last_advance) / 1000 / (1 / TICS_PER_SECOND));
|
||||||
|
if (this.state === 'rewinding') {
|
||||||
|
this.tic_offset = 1 - this.tic_offset;
|
||||||
|
}
|
||||||
|
|
||||||
this._redraw();
|
this._redraw();
|
||||||
|
|
||||||
@ -637,7 +677,7 @@ class Player extends PrimaryView {
|
|||||||
if (this.state === 'paused') {
|
if (this.state === 'paused') {
|
||||||
this.set_state('playing');
|
this.set_state('playing');
|
||||||
}
|
}
|
||||||
else if (this.state === 'playing') {
|
else if (this.state === 'playing' || this.state === 'rewinding') {
|
||||||
this.set_state('paused');
|
this.set_state('paused');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -653,6 +693,7 @@ class Player extends PrimaryView {
|
|||||||
let overlay_top = '';
|
let overlay_top = '';
|
||||||
let overlay_middle = null;
|
let overlay_middle = null;
|
||||||
let overlay_bottom = '';
|
let overlay_bottom = '';
|
||||||
|
let overlay_keyhint = '';
|
||||||
if (this.state === 'waiting') {
|
if (this.state === 'waiting') {
|
||||||
overlay_reason = 'waiting';
|
overlay_reason = 'waiting';
|
||||||
overlay_middle = "Ready!";
|
overlay_middle = "Ready!";
|
||||||
@ -660,6 +701,7 @@ class Player extends PrimaryView {
|
|||||||
else if (this.state === 'paused') {
|
else if (this.state === 'paused') {
|
||||||
overlay_reason = 'paused';
|
overlay_reason = 'paused';
|
||||||
overlay_bottom = "/// paused ///";
|
overlay_bottom = "/// paused ///";
|
||||||
|
overlay_keyhint = "press P to resume";
|
||||||
}
|
}
|
||||||
else if (this.state === 'stopped') {
|
else if (this.state === 'stopped') {
|
||||||
if (this.level.state === 'failure') {
|
if (this.level.state === 'failure') {
|
||||||
@ -667,6 +709,7 @@ class Player extends PrimaryView {
|
|||||||
overlay_top = "whoops";
|
overlay_top = "whoops";
|
||||||
let obits = OBITUARIES[this.level.fail_reason] ?? OBITUARIES['generic'];
|
let obits = OBITUARIES[this.level.fail_reason] ?? OBITUARIES['generic'];
|
||||||
overlay_bottom = random_choice(obits);
|
overlay_bottom = random_choice(obits);
|
||||||
|
overlay_keyhint = "press space to try again, or Z to rewind";
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
overlay_reason = 'success';
|
overlay_reason = 'success';
|
||||||
@ -706,7 +749,7 @@ class Player extends PrimaryView {
|
|||||||
"alphanumeric!", "nice dynamic typing!",
|
"alphanumeric!", "nice dynamic typing!",
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
overlay_bottom = "press spacebar to continue";
|
overlay_keyhint = "press space to move on";
|
||||||
|
|
||||||
overlay_middle = mk('dl.score-chart',
|
overlay_middle = mk('dl.score-chart',
|
||||||
mk('dt', "base score"),
|
mk('dt', "base score"),
|
||||||
@ -727,12 +770,22 @@ class Player extends PrimaryView {
|
|||||||
this.overlay_message_el.setAttribute('data-reason', overlay_reason);
|
this.overlay_message_el.setAttribute('data-reason', overlay_reason);
|
||||||
this.overlay_message_el.querySelector('.-top').textContent = overlay_top;
|
this.overlay_message_el.querySelector('.-top').textContent = overlay_top;
|
||||||
this.overlay_message_el.querySelector('.-bottom').textContent = overlay_bottom;
|
this.overlay_message_el.querySelector('.-bottom').textContent = overlay_bottom;
|
||||||
|
this.overlay_message_el.querySelector('.-keyhint').textContent = overlay_keyhint;
|
||||||
let middle = this.overlay_message_el.querySelector('.-middle');
|
let middle = this.overlay_message_el.querySelector('.-middle');
|
||||||
middle.textContent = '';
|
middle.textContent = '';
|
||||||
if (overlay_middle) {
|
if (overlay_middle) {
|
||||||
middle.append(overlay_middle);
|
middle.append(overlay_middle);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Ask the renderer to apply a rewind effect only when rewinding, or when paused from
|
||||||
|
// rewinding
|
||||||
|
if (this.state === 'rewinding') {
|
||||||
|
this.renderer.use_rewind_effect = true;
|
||||||
|
}
|
||||||
|
else if (this.state !== 'paused') {
|
||||||
|
this.renderer.use_rewind_effect = false;
|
||||||
|
}
|
||||||
|
|
||||||
// The advance and redraw methods run in a loop, but they cancel
|
// The advance and redraw methods run in a loop, but they cancel
|
||||||
// themselves if the game isn't running, so restart them here
|
// themselves if the game isn't running, so restart them here
|
||||||
if (this.state === 'playing' || this.state === 'rewinding') {
|
if (this.state === 'playing' || this.state === 'rewinding') {
|
||||||
|
|||||||
@ -25,6 +25,7 @@ export class CanvasRenderer {
|
|||||||
this.ctx = this.canvas.getContext('2d');
|
this.ctx = this.canvas.getContext('2d');
|
||||||
this.viewport_x = 0;
|
this.viewport_x = 0;
|
||||||
this.viewport_y = 0;
|
this.viewport_y = 0;
|
||||||
|
this.use_rewind_effect = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
set_level(level) {
|
set_level(level) {
|
||||||
@ -129,6 +130,24 @@ export class CanvasRenderer {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (this.use_rewind_effect) {
|
||||||
|
this.draw_rewind_effect(tic);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
draw_rewind_effect(tic) {
|
||||||
|
// Shift several rows over
|
||||||
|
let rewind_start = 1 - tic / 20 % 1;
|
||||||
|
for (let chunk = 0; chunk < 4; chunk++) {
|
||||||
|
let y = Math.floor(this.canvas.height * (chunk + rewind_start) / 4);
|
||||||
|
for (let dy = 1; dy < 5; dy++) {
|
||||||
|
this.ctx.drawImage(
|
||||||
|
this.canvas,
|
||||||
|
0, y + dy, this.canvas.width, 1,
|
||||||
|
-dy * dy, y + dy, this.canvas.width, 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
create_tile_type_canvas(name) {
|
create_tile_type_canvas(name) {
|
||||||
|
|||||||
@ -328,13 +328,13 @@ body[data-mode=player] #editor-play {
|
|||||||
place-self: stretch;
|
place-self: stretch;
|
||||||
|
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-rows: 1fr 3fr 1fr;
|
grid-template-rows: 2fr 6fr 2fr 1fr;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
|
||||||
z-index: 1;
|
z-index: 1;
|
||||||
font-size: calc(0.5 * var(--tile-width) * var(--scale));
|
font-size: calc(0.5 * var(--tile-width) * var(--scale));
|
||||||
padding: 6.25%;
|
padding: 2%;
|
||||||
background: #0009;
|
background: #0009;
|
||||||
color: white;
|
color: white;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
@ -350,6 +350,11 @@ body[data-mode=player] #editor-play {
|
|||||||
}
|
}
|
||||||
#player .overlay-message .-bottom {
|
#player .overlay-message .-bottom {
|
||||||
}
|
}
|
||||||
|
#player .overlay-message .-keyhint {
|
||||||
|
align-self: end;
|
||||||
|
font-size: 0.5em;
|
||||||
|
color: #c0c0c0;
|
||||||
|
}
|
||||||
#player .overlay-message[data-reason=""] {
|
#player .overlay-message[data-reason=""] {
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user