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>
|
||||
<div class="-middle"></div>
|
||||
<p class="-bottom"></p>
|
||||
<p class="-keyhint"></p>
|
||||
</div>
|
||||
<div class="message"></div>
|
||||
<div class="chips">
|
||||
|
||||
@ -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;
|
||||
|
||||
71
js/main.js
71
js/main.js
@ -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();
|
||||
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') {
|
||||
|
||||
@ -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) {
|
||||
|
||||
@ -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;
|
||||
}
|
||||
|
||||
Loading…
Reference in New Issue
Block a user