Fixed several issues with animation and movement; quick stab at replay UI
- Animation now has its own timer and isn't linked to movement cooldown, which is good for blocks since they don't have movement cooldown - Destroyed actors don't crash the game again (oops) - Slide and cooldown handling was reshuffled to better support the CC2 approach of landing on tiles with a delay; in particular, you move at double speed on sliding tiles again! - Demo playback got some rough UI so I don't have to keep editing the source code to decide whether to play a demo
This commit is contained in:
parent
bee6ba4c80
commit
2df8607243
158
js/main.js
158
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) {
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
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.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,21 +1039,19 @@ const GAME_UI_HTML = `
|
||||
</div>
|
||||
<div class="inventory"></div>
|
||||
<div class="controls">
|
||||
<div class="play-controls">
|
||||
<button class="control-pause" type="button">Pause</button>
|
||||
<button class="control-restart" type="button">Restart</button>
|
||||
<button class="control-undo" type="button">Undo</button>
|
||||
<button class="control-rewind" type="button">Rewind</button>
|
||||
</div>
|
||||
<div class="demo">
|
||||
<h2>Solution demo available</h2>
|
||||
<div class="demo-controls">
|
||||
<button class="demo-play" type="button">Restart and play</button>
|
||||
<button class="demo-play" type="button">View replay</button>
|
||||
<button class="demo-step-1" type="button">Step 1 tic</button>
|
||||
<button class="demo-step-4" type="button">Step 1 move</button>
|
||||
<button class="demo-step-20" type="button">Step 1 second</button>
|
||||
</div>
|
||||
<div class="input"></div>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
`;
|
||||
const ACTION_LABELS = {
|
||||
@ -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;
|
||||
}
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -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,
|
||||
|
||||
21
style.css
21
style.css
@ -153,8 +153,7 @@ main {
|
||||
"level bonus"
|
||||
"level message" 1fr
|
||||
"level inventory"
|
||||
"level controls"
|
||||
"demo demo"
|
||||
"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
|
||||
|
||||
Loading…
Reference in New Issue
Block a user