diff --git a/js/main.js b/js/main.js
index 18a7ca9..15af887 100644
--- a/js/main.js
+++ b/js/main.js
@@ -39,13 +39,15 @@ class Tile {
visual_position(tic_offset = 0) {
let x = this.cell.x;
let y = this.cell.y;
- if (! this.movement_cooldown) {
+ if (! this.animation_speed) {
return [x, y];
}
else {
- let p = (- this.movement_cooldown + tic_offset) / this.movement_speed;
- let motion = DIRECTIONS[this.direction].movement;
- return [x + p * motion[0], y + p * motion[1]];
+ let p = (this.animation_progress + tic_offset) / this.animation_speed;
+ return [
+ (1 - p) * this.previous_cell.x + p * x,
+ (1 - p) * this.previous_cell.y + p * y,
+ ];
}
}
@@ -299,18 +301,35 @@ class Level {
// XXX this entire turn order is rather different in ms rules
for (let actor of this.actors) {
+ // Actors with no cell were destroyed
+ if (! actor.cell)
+ continue;
+
+ // Decrement the cooldown here, but only check it later because
+ // stepping on cells in the next block might reset it
if (actor.movement_cooldown > 0) {
this._set_prop(actor, 'movement_cooldown', actor.movement_cooldown - 1);
- if (actor.movement_cooldown > 0) {
- continue;
- }
- else if (! this.compat.tiles_react_instantly) {
- // For delayed arrival (usually paired with smooth
- // scrolling), tiles only react once actors finish moving
- // onto them
- this.step_on_cell(actor);
+ }
+
+ if (actor.animation_speed) {
+ // Deal with movement animation
+ actor.animation_progress += 1;
+ if (actor.animation_progress >= actor.animation_speed) {
+ actor.previous_cell = null;
+ actor.animation_progress = null;
+ actor.animation_speed = null;
+ if (! this.compat.tiles_react_instantly) {
+ this.step_on_cell(actor);
+ // May have been destroyed here, too!
+ if (! actor.cell)
+ continue;
+ }
}
}
+
+ if (actor.movement_cooldown > 0)
+ continue;
+
// XXX does the cooldown drop while in a trap? is this even right?
// TODO should still attempt to move (so chip turns), just will be stuck (but wait, do monsters turn? i don't think so)
if (actor.stuck)
@@ -432,17 +451,6 @@ class Level {
}
}
- // Only set movement cooldown if we actually moved!
- if (moved) {
- // Speed multiplier is based on the tile we landed /on/.
- let speed = actor.type.movement_speed;
- if (actor.slide_mode !== null) {
- speed /= 2;
- }
- this._set_prop(actor, 'movement_cooldown', speed);
- this._set_prop(actor, 'movement_speed', speed);
- }
-
// TODO do i need to do this more aggressively?
if (this.state === 'success' || this.state === 'failure')
break;
@@ -466,15 +474,38 @@ class Level {
}
}
+ // Strip out any destroyed actors from the acting order
+ let p = 0;
+ for (let i = 0, l = this.actors.length; i < l; i++) {
+ let actor = this.actors[i];
+ if (actor.cell) {
+ if (p !== i) {
+ this.actors[p] = actor;
+ }
+ p++;
+ }
+ }
+ this.actors.length = p;
+
// Commit the undo state at the end of each tic
this.commit();
}
// Try to move the given actor one tile in the given direction and update
// their cooldown. Return true if successful.
- attempt_step(actor, direction) {
+ attempt_step(actor, direction, speed = null) {
+ // If speed is given, we're being pushed by something so we're using
+ // its speed. Otherwise, use our movement speed. If we're moving onto
+ // a sliding tile, we'll halve it later
+ let check_for_slide = false;
+ if (speed === null) {
+ speed = actor.type.movement_speed;
+ check_for_slide = true;
+ }
+
let move = DIRECTIONS[direction].movement;
let original_cell = actor.cell;
+ if (!original_cell) console.error(actor);
let goal_x = original_cell.x + move[0];
let goal_y = original_cell.y + move[1];
@@ -500,11 +531,16 @@ class Level {
if (! blocked) {
let goal_cell = this.cells[goal_y][goal_x];
for (let tile of Array.from(goal_cell)) {
+ if (check_for_slide && tile.type.slide_mode) {
+ check_for_slide = false;
+ speed /= 2;
+ }
+
if (! tile.blocks(actor, direction))
continue;
if (actor.type.pushes && actor.type.pushes[tile.type.name]) {
- if (this.attempt_step(tile, direction))
+ if (this.attempt_step(tile, direction, speed))
// It moved out of the way!
continue;
}
@@ -533,21 +569,28 @@ class Level {
}
// We're clear!
- this.move_to(actor, goal_x, goal_y);
+ this.move_to(actor, goal_x, goal_y, speed);
+
+ // Set movement cooldown since we just moved
+ this._set_prop(actor, 'movement_cooldown', speed);
return true;
}
// Move the given actor to the given position and perform any appropriate
// tile interactions. Does NOT check for whether the move is actually
// legal; use attempt_step for that!
- move_to(actor, x, y) {
+ move_to(actor, x, y, speed) {
let original_cell = actor.cell;
if (x === original_cell.x && y === original_cell.y)
return;
+ actor.previous_cell = actor.cell;
+ actor.animation_speed = speed;
+ actor.animation_progress = 0;
+
let goal_cell = this.cells[y][x];
this.remove_tile(actor);
- actor.slide_mode = null;
+ this.make_slide(actor, null);
this.add_tile(actor, goal_cell);
// Announce we're leaving, for the handful of tiles that care about it
@@ -562,11 +605,13 @@ class Level {
}
}
- // Check for hitting a monster, which is always instant and ends the
- // game right here
+ // Check for a couple effects that always apply immediately
// TODO i guess this covers blocks too
// TODO do blocks smash monsters?
for (let tile of goal_cell) {
+ if (tile.type.slide_mode) {
+ this.make_slide(actor, tile.type.slide_mode);
+ }
if ((actor.type.is_player && tile.type.is_monster) ||
(actor.type.is_monster && tile.type.is_player))
{
@@ -609,6 +654,7 @@ class Level {
}
// Handle teleporting, now that the dust has cleared
+ // FIXME something funny happening here, your input isn't ignore while walking out of it?
let current_cell = actor.cell;
if (teleporter) {
let goal = teleporter.connection;
@@ -759,6 +805,9 @@ class Overlay {
}
open() {
+ // FIXME ah, but keystrokes can still go to the game, including
+ // spacebar to begin it if it was waiting. how do i completely disable
+ // an entire chunk of the page?
if (this.game.state === 'playing') {
this.game.set_state('paused');
}
@@ -990,20 +1039,18 @@ const GAME_UI_HTML = `
-
-
-
-
-
-
-
Solution demo available
+
+
+
+
+
+
-
+
-
+
-
`;
@@ -1125,14 +1172,23 @@ class Game {
ev.target.blur();
});
// Demo playback
- this.container.querySelector('.demo .demo-step-1').addEventListener('click', ev => {
+ this.container.querySelector('.demo-controls .demo-play').addEventListener('click', ev => {
+ if (this.state === 'playing' || this.state === 'paused' || this.state === 'rewinding') {
+ new ConfirmOverlay(this, "Abandon your progress and watch the replay?", () => {
+ this.play_demo();
+ });
+ }
+ else {
+ this.play_demo();
+ }
+ });
+ this.container.querySelector('.demo-controls .demo-step-1').addEventListener('click', ev => {
this.advance_by(1);
+ this._redraw();
});
- this.container.querySelector('.demo .demo-step-4').addEventListener('click', ev => {
+ this.container.querySelector('.demo-controls .demo-step-4').addEventListener('click', ev => {
this.advance_by(4);
- });
- this.container.querySelector('.demo .demo-step-20').addEventListener('click', ev => {
- this.advance_by(20);
+ this._redraw();
});
// Populate inventory
@@ -1224,17 +1280,6 @@ class Game {
// Done with UI, now we can load a level
this.load_level(0);
- if (false && this.level.stored_level.demo) {
- this.demo = this.level.stored_level.demo[Symbol.iterator]();
- // FIXME should probably start playback on first input
- this.set_state('playing');
- }
- else {
- // TODO update these, as appropriate, when loading a level
- this.input_el.style.display = 'none';
- this.demo_el.style.display = 'none';
- }
-
// Auto-size the level canvas, both now and on resize
this.adjust_scale();
window.addEventListener('resize', ev => {
@@ -1265,6 +1310,9 @@ class Game {
this.nav_prev_button.disabled = level_index <= 0;
this.nav_next_button.disabled = level_index >= this.stored_game.levels.length;
+ this.demo_faucet = null;
+ this.container.classList.toggle('--has-demo', !!this.level.stored_level.demo);
+
this.update_ui();
// Force a redraw, which won't happen on its own since the game isn't running
this._redraw();
@@ -1274,11 +1322,19 @@ class Game {
this.level.restart(this.compat);
this.set_state('waiting');
this.update_ui();
+ this._redraw();
+ }
+
+ play_demo() {
+ this.demo_faucet = this.level.stored_level.demo[Symbol.iterator]();
+ this.restart_level();
+ // FIXME should probably start playback on first real input
+ this.set_state('playing');
}
get_input() {
- if (this.demo) {
- let step = this.demo.next();
+ if (this.demo_faucet) {
+ let step = this.demo_faucet.next();
if (step.done) {
return new Set;
}
diff --git a/js/tileset.js b/js/tileset.js
index 3213554..604fe9d 100644
--- a/js/tileset.js
+++ b/js/tileset.js
@@ -487,7 +487,7 @@ export class Tileset {
// TODO this is getting really ad-hoc and clumsy lol, maybe
// have tiles expose a single 'state' prop or something
if (coords.moving) {
- if (tile.movement_cooldown) {
+ if (tile.animation_speed) {
coords = coords.moving;
}
else {
diff --git a/js/tiletypes.js b/js/tiletypes.js
index 506f237..cb8b38a 100644
--- a/js/tiletypes.js
+++ b/js/tiletypes.js
@@ -150,12 +150,11 @@ const TILE_TYPES = {
turtle: {
},
ice: {
- on_arrive(me, level, other) {
- level.make_slide(other, 'ice');
- },
+ slide_mode: 'ice',
},
ice_sw: {
thin_walls: new Set(['south', 'west']),
+ slide_mode: 'ice',
on_arrive(me, level, other) {
if (other.direction === 'south') {
other.direction = 'east';
@@ -163,11 +162,11 @@ const TILE_TYPES = {
else {
other.direction = 'north';
}
- level.make_slide(other, 'ice');
},
},
ice_nw: {
thin_walls: new Set(['north', 'west']),
+ slide_mode: 'ice',
on_arrive(me, level, other) {
if (other.direction === 'north') {
other.direction = 'east';
@@ -175,11 +174,11 @@ const TILE_TYPES = {
else {
other.direction = 'south';
}
- level.make_slide(other, 'ice');
},
},
ice_ne: {
thin_walls: new Set(['north', 'east']),
+ slide_mode: 'ice',
on_arrive(me, level, other) {
if (other.direction === 'north') {
other.direction = 'west';
@@ -187,11 +186,11 @@ const TILE_TYPES = {
else {
other.direction = 'south';
}
- level.make_slide(other, 'ice');
},
},
ice_se: {
thin_walls: new Set(['south', 'east']),
+ slide_mode: 'ice',
on_arrive(me, level, other) {
if (other.direction === 'south') {
other.direction = 'west';
@@ -199,38 +198,37 @@ const TILE_TYPES = {
else {
other.direction = 'north';
}
- level.make_slide(other, 'ice');
},
},
force_floor_n: {
+ slide_mode: 'force',
on_arrive(me, level, other) {
other.direction = 'north';
- level.make_slide(other, 'force');
},
},
force_floor_e: {
+ slide_mode: 'force',
on_arrive(me, level, other) {
other.direction = 'east';
- level.make_slide(other, 'force');
},
},
force_floor_s: {
+ slide_mode: 'force',
on_arrive(me, level, other) {
other.direction = 'south';
- level.make_slide(other, 'force');
},
},
force_floor_w: {
+ slide_mode: 'force',
on_arrive(me, level, other) {
other.direction = 'west';
- level.make_slide(other, 'force');
},
},
force_floor_all: {
+ slide_mode: 'force',
// TODO ms: this is random, and an acting wall to monsters (!)
on_arrive(me, level, other) {
other.direction = level.get_force_floor_direction();
- level.make_slide(other, 'force');
},
},
bomb: {
@@ -325,7 +323,6 @@ const TILE_TYPES = {
connects_to: 'teleport_blue',
connect_order: 'backward',
is_teleporter: true,
- // TODO implement 'backward'
// TODO to make this work, i need to be able to check if a spot is blocked /ahead of time/
},
// Buttons
@@ -565,6 +562,14 @@ const TILE_TYPES = {
chip_extra: {
is_chip: true,
is_object: true,
+ blocks_monsters: true,
+ blocks_blocks: true,
+ on_arrive(me, level, other) {
+ if (other.type.is_player) {
+ level.collect_chip();
+ level.remove_tile(me);
+ }
+ },
},
score_10: {
is_object: true,
diff --git a/style.css b/style.css
index 4a6bc8d..51794b1 100644
--- a/style.css
+++ b/style.css
@@ -152,9 +152,8 @@ main {
"level time"
"level bonus"
"level message" 1fr
- "level inventory"
- "level controls"
- "demo demo"
+ "level inventory"
+ "controls controls"
/* Need explicit min-content to force the hint to wrap */
/ min-content min-content
;
@@ -319,18 +318,28 @@ dl.score-chart .-sum {
.controls {
grid-area: controls;
display: flex;
+}
+.play-controls,
+.demo-controls {
+ display: flex;
gap: 0.25em;
}
-.controls > button {
- flex: 1;
+.play-controls {
+ align-self: start;
}
-.demo {
- grid-area: demo;
+.demo-controls {
+ display: none;
+ flex: 1;
+ justify-content: end;
+}
+main.--has-demo .demo-controls {
+ display: flex;
}
/* Debug stuff */
.input {
display: grid;
+ display: none;
grid:
"drop up cycle" 1.5em
"left swap right" 1.5em