Implement rewind, add a key for it, and suggest keys in general

This commit is contained in:
Eevee (Evelyn Woods) 2020-09-16 19:40:38 -06:00
parent 57810da581
commit 063d9f9ef9
5 changed files with 91 additions and 12 deletions

View File

@ -56,6 +56,7 @@
<h1 class="-top"></h1>
<div class="-middle"></div>
<p class="-bottom"></p>
<p class="-keyhint"></p>
</div>
<div class="message"></div>
<div class="chips">

View File

@ -745,6 +745,7 @@ export class Level {
if (actor.cell === goal_cell)
return;
// TODO undo this stuff?
actor.previous_cell = actor.cell;
actor.animation_speed = speed;
actor.animation_progress = 0;

View File

@ -268,6 +268,12 @@ class Player extends PrimaryView {
this._redraw();
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
this.root.querySelector('.demo-controls .demo-play').addEventListener('click', ev => {
if (this.state === 'playing' || this.state === 'paused' || this.state === 'rewinding') {
@ -339,6 +345,14 @@ class Player extends PrimaryView {
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]) {
this.current_keys.add(ev.key);
ev.stopPropagation();
@ -350,6 +364,12 @@ class Player extends PrimaryView {
}
});
key_target.addEventListener('keyup', ev => {
if (ev.key === 'z') {
if (this.state === 'rewinding') {
this.set_state('playing');
}
}
if (this.key_mapping[ev.key]) {
this.current_keys.delete(ev.key);
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
this.input_el = this.root.querySelector('.input');
this.input_action_elements = {};
@ -366,13 +395,6 @@ class Player extends PrimaryView {
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._redraw_bound = this.redraw.bind(this);
// 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.advance_by(1);
if (this.state === 'playing') {
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);
}
@ -566,6 +603,9 @@ class Player extends PrimaryView {
// TODO i'm not sure it'll be right when rewinding either
// 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));
if (this.state === 'rewinding') {
this.tic_offset = 1 - this.tic_offset;
}
this._redraw();
@ -637,7 +677,7 @@ class Player extends PrimaryView {
if (this.state === 'paused') {
this.set_state('playing');
}
else if (this.state === 'playing') {
else if (this.state === 'playing' || this.state === 'rewinding') {
this.set_state('paused');
}
}
@ -653,6 +693,7 @@ class Player extends PrimaryView {
let overlay_top = '';
let overlay_middle = null;
let overlay_bottom = '';
let overlay_keyhint = '';
if (this.state === 'waiting') {
overlay_reason = 'waiting';
overlay_middle = "Ready!";
@ -660,6 +701,7 @@ class Player extends PrimaryView {
else if (this.state === 'paused') {
overlay_reason = 'paused';
overlay_bottom = "/// paused ///";
overlay_keyhint = "press P to resume";
}
else if (this.state === 'stopped') {
if (this.level.state === 'failure') {
@ -667,6 +709,7 @@ class Player extends PrimaryView {
overlay_top = "whoops";
let obits = OBITUARIES[this.level.fail_reason] ?? OBITUARIES['generic'];
overlay_bottom = random_choice(obits);
overlay_keyhint = "press space to try again, or Z to rewind";
}
else {
overlay_reason = 'success';
@ -706,7 +749,7 @@ class Player extends PrimaryView {
"alphanumeric!", "nice dynamic typing!",
]);
}
overlay_bottom = "press spacebar to continue";
overlay_keyhint = "press space to move on";
overlay_middle = mk('dl.score-chart',
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.querySelector('.-top').textContent = overlay_top;
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');
middle.textContent = '';
if (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
// themselves if the game isn't running, so restart them here
if (this.state === 'playing' || this.state === 'rewinding') {

View File

@ -25,6 +25,7 @@ export class CanvasRenderer {
this.ctx = this.canvas.getContext('2d');
this.viewport_x = 0;
this.viewport_y = 0;
this.use_rewind_effect = false;
}
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) {

View File

@ -328,13 +328,13 @@ body[data-mode=player] #editor-play {
place-self: stretch;
display: grid;
grid-template-rows: 1fr 3fr 1fr;
grid-template-rows: 2fr 6fr 2fr 1fr;
justify-content: center;
align-items: center;
z-index: 1;
font-size: calc(0.5 * var(--tile-width) * var(--scale));
padding: 6.25%;
padding: 2%;
background: #0009;
color: white;
text-align: center;
@ -350,6 +350,11 @@ body[data-mode=player] #editor-play {
}
#player .overlay-message .-bottom {
}
#player .overlay-message .-keyhint {
align-self: end;
font-size: 0.5em;
color: #c0c0c0;
}
#player .overlay-message[data-reason=""] {
display: none;
}