Implement (PARTIAL) undo; remove 'doomed'; fix a few small tile bugs

This commit is contained in:
Eevee (Evelyn Woods) 2020-09-02 05:20:23 -06:00
parent 1cc631c27e
commit 0ba5ecc7e3
3 changed files with 264 additions and 132 deletions

View File

@ -69,11 +69,10 @@ async function fetch(url) {
const PAGE_TITLE = "Lexy's Labyrinth"; const PAGE_TITLE = "Lexy's Labyrinth";
class Tile { class Tile {
constructor(type, x, y, direction = 'south') { constructor(type, direction = 'south') {
this.type = type; this.type = type;
this.x = x;
this.y = y;
this.direction = direction; this.direction = direction;
this.cell = null;
this.slide_mode = null; this.slide_mode = null;
this.movement_cooldown = 0; this.movement_cooldown = 0;
@ -83,10 +82,10 @@ class Tile {
} }
} }
static from_template(tile_template, x, y) { static from_template(tile_template) {
let type = TILE_TYPES[tile_template.name]; let type = TILE_TYPES[tile_template.name];
if (! type) console.error(tile_template.name); if (! type) console.error(tile_template.name);
let tile = new this(type, x, y, tile_template.direction); let tile = new this(type, tile_template.direction);
if (type.load) { if (type.load) {
type.load(tile, tile_template); type.load(tile, tile_template);
} }
@ -129,15 +128,6 @@ class Tile {
return false; return false;
} }
become(name) {
this.type = TILE_TYPES[name];
// TODO adjust anything else?
}
destroy() {
this.doomed = true;
}
// Inventory stuff // Inventory stuff
give_item(name) { give_item(name) {
this.inventory[name] = (this.inventory[name] ?? 0) + 1; this.inventory[name] = (this.inventory[name] ?? 0) + 1;
@ -162,13 +152,20 @@ class Tile {
} }
class Cell extends Array { class Cell extends Array {
constructor() { constructor(x, y) {
super(); super();
this.is_dirty = false; this.x = x;
this.y = y;
} }
_add(tile) { _add(tile, index = null) {
this.push(tile); if (index === null) {
this.push(tile);
}
else {
this.splice(index, 0, tile);
}
tile.cell = this;
} }
// DO NOT use me to remove a tile permanently, only to move it! // DO NOT use me to remove a tile permanently, only to move it!
@ -179,14 +176,15 @@ class Cell extends Array {
throw new Error("Asked to remove tile that doesn't seem to exist"); throw new Error("Asked to remove tile that doesn't seem to exist");
this.splice(layer, 1); this.splice(layer, 1);
return layer;
} }
each(f) { each(f) {
for (let i = this.length - 1; i >= 0; i--) { let copy = Array.from(this);
if (f(this[i]) === false) for (let i = 0, l = copy.length; i < l; i++) {
if (f(copy[i]) === false)
break; break;
} }
this._gc();
} }
_gc() { _gc() {
@ -237,13 +235,16 @@ class Level {
// TODO in lynx/steam, this carries over between levels; in tile world, you can set it manually // TODO in lynx/steam, this carries over between levels; in tile world, you can set it manually
this.force_floor_direction = 'north'; this.force_floor_direction = 'north';
this.undo_stack = [];
this.pending_undo = [];
let n = 0; let n = 0;
let connectables = []; let connectables = [];
for (let y = 0; y < this.height; y++) { for (let y = 0; y < this.height; y++) {
let row = []; let row = [];
this.cells.push(row); this.cells.push(row);
for (let x = 0; x < this.width; x++) { for (let x = 0; x < this.width; x++) {
let cell = new Cell; let cell = new Cell(x, y);
row.push(cell); row.push(cell);
let stored_cell = this.stored_level.linear_cells[n]; let stored_cell = this.stored_level.linear_cells[n];
@ -251,7 +252,7 @@ class Level {
let has_cloner, has_forbidden; let has_cloner, has_forbidden;
for (let template_tile of stored_cell) { for (let template_tile of stored_cell) {
let tile = Tile.from_template(template_tile, x, y); let tile = Tile.from_template(template_tile);
if (tile.type.is_hint) { if (tile.type.is_hint) {
// Copy over the tile-specific hint, if any // Copy over the tile-specific hint, if any
tile.specific_hint = template_tile.specific_hint ?? null; tile.specific_hint = template_tile.specific_hint ?? null;
@ -278,7 +279,7 @@ class Level {
this.actors.push(tile); this.actors.push(tile);
} }
} }
cell.push(tile); cell._add(tile);
if (tile.type.connects_to) { if (tile.type.connects_to) {
connectables.push(tile); connectables.push(tile);
@ -289,17 +290,15 @@ class Level {
// Connect buttons and teleporters // Connect buttons and teleporters
let num_cells = this.width * this.height; let num_cells = this.width * this.height;
console.log(this.stored_level.custom_trap_wiring);
console.log(this.stored_level.custom_cloner_wiring);
for (let connectable of connectables) { for (let connectable of connectables) {
let x = connectable.x; let cell = connectable.cell;
let y = connectable.y; let x = cell.x;
let y = cell.y;
let goal = connectable.type.connects_to; let goal = connectable.type.connects_to;
let found = false; let found = false;
// Check for custom wiring, for MSCC .DAT levels // Check for custom wiring, for MSCC .DAT levels
let n = x + y * this.width; let n = x + y * this.width;
console.log(x, y, n);
let target_cell_n = null; let target_cell_n = null;
if (goal === 'trap') { if (goal === 'trap') {
target_cell_n = this.stored_level.custom_trap_wiring[n] ?? null; target_cell_n = this.stored_level.custom_trap_wiring[n] ?? null;
@ -367,7 +366,7 @@ class Level {
continue; continue;
if (actor.movement_cooldown > 0) { if (actor.movement_cooldown > 0) {
actor.movement_cooldown -= 1; this._set_prop(actor, 'movement_cooldown', actor.movement_cooldown - 1);
if (actor.movement_cooldown > 0) if (actor.movement_cooldown > 0)
continue; continue;
} }
@ -394,19 +393,19 @@ class Level {
actor.last_move_was_force) actor.last_move_was_force)
{ {
direction_preference = [player_direction]; direction_preference = [player_direction];
actor.last_move_was_force = false; this._set_prop(actor, 'last_move_was_force', false);
} }
else { else {
direction_preference = [actor.direction]; direction_preference = [actor.direction];
if (actor === this.player) { if (actor === this.player) {
actor.last_move_was_force = true; this._set_prop(actor, 'last_move_was_force', true);
} }
} }
} }
else if (actor === this.player) { else if (actor === this.player) {
if (player_direction) { if (player_direction) {
direction_preference = [player_direction]; direction_preference = [player_direction];
actor.last_move_was_force = false; this._set_prop(actor, 'last_move_was_force', false);
} }
} }
else if (actor.type.movement_mode === 'forward') { else if (actor.type.movement_mode === 'forward') {
@ -452,8 +451,8 @@ class Level {
} }
else if (actor.type.movement_mode === 'pursue') { else if (actor.type.movement_mode === 'pursue') {
// teeth behavior: always move towards the player // teeth behavior: always move towards the player
let dx = actor.x - this.player.x; let dx = actor.cell.x - this.player.cell.x;
let dy = actor.y - this.player.y; let dy = actor.cell.y - this.player.cell.y;
// Chooses the furthest direction, vertical wins ties // Chooses the furthest direction, vertical wins ties
if (Math.abs(dx) > Math.abs(dy)) { if (Math.abs(dx) > Math.abs(dy)) {
// Horizontal // Horizontal
@ -485,7 +484,7 @@ class Level {
let moved = false; let moved = false;
for (let direction of direction_preference) { for (let direction of direction_preference) {
actor.direction = direction; this.set_actor_direction(actor, direction);
if (this.attempt_step(actor, direction)) { if (this.attempt_step(actor, direction)) {
moved = true; moved = true;
break; break;
@ -499,7 +498,7 @@ class Level {
if (actor.slide_mode !== null) { if (actor.slide_mode !== null) {
speed_multiplier = 2; speed_multiplier = 2;
} }
actor.movement_cooldown = actor.type.movement_speed / speed_multiplier; this._set_prop(actor, 'movement_cooldown', actor.type.movement_speed / speed_multiplier);
} }
// TODO do i need to do this more aggressively? // TODO do i need to do this more aggressively?
@ -508,6 +507,13 @@ class Level {
} }
if (this.time_remaining !== null) { if (this.time_remaining !== null) {
let tic_counter = this.tic_counter;
let time_remaining = this.time_remaining;
this.pending_undo.push(() => {
this.tic_counter = tic_counter;
this.time_remaining = time_remaining;
});
this.tic_counter++; this.tic_counter++;
while (this.tic_counter > 20) { while (this.tic_counter > 20) {
this.tic_counter -= 20; this.tic_counter -= 20;
@ -517,28 +523,22 @@ class Level {
} }
} }
} }
}
fail(message) { // Commit the undo state at the end of each tic
this.state = 'failure'; this.commit();
this.fail_message = message;
}
win() {
this.state = 'success';
} }
// Try to move the given actor one tile in the given direction and update // Try to move the given actor one tile in the given direction and update
// their cooldown. Return true if successful. // their cooldown. Return true if successful.
attempt_step(actor, direction) { attempt_step(actor, direction) {
let move = DIRECTIONS[direction].movement; let move = DIRECTIONS[direction].movement;
let goal_x = actor.x + move[0]; let original_cell = actor.cell;
let goal_y = actor.y + move[1]; let goal_x = original_cell.x + move[0];
let goal_y = original_cell.y + move[1];
let blocked; let blocked;
if (goal_x >= 0 && goal_x < this.width && goal_y >= 0 && goal_y < this.height) { if (goal_x >= 0 && goal_x < this.width && goal_y >= 0 && goal_y < this.height) {
// Check for a thin wall in our current cell first // Check for a thin wall in our current cell first
let original_cell = this.cells[actor.y][actor.x];
original_cell.each(tile => { original_cell.each(tile => {
if (tile !== actor && tile.type.thin_walls && if (tile !== actor && tile.type.thin_walls &&
tile.type.thin_walls.has(direction)) tile.type.thin_walls.has(direction))
@ -594,19 +594,14 @@ class Level {
// tile interactions. Does NOT check for whether the move is actually // tile interactions. Does NOT check for whether the move is actually
// legal; use attempt_step for that! // legal; use attempt_step for that!
move_to(actor, x, y) { move_to(actor, x, y) {
if (x === actor.x && y === actor.y) let original_cell = actor.cell;
if (x === original_cell.x && y === original_cell.y)
return; return;
let goal_cell = this.cells[y][x]; let goal_cell = this.cells[y][x];
let original_cell = this.cells[actor.y][actor.x]; this.remove_tile(actor);
original_cell._remove(actor);
actor.slide_mode = null; actor.slide_mode = null;
goal_cell._add(actor); this.add_tile(actor, goal_cell);
actor.x = x;
actor.y = y;
original_cell.is_dirty = true;
goal_cell.is_dirty = true;
// Announce we're leaving, for the handful of tiles that care about it // Announce we're leaving, for the handful of tiles that care about it
original_cell.each(tile => { original_cell.each(tile => {
@ -622,7 +617,7 @@ class Level {
// Step on all the tiles in the new cell // Step on all the tiles in the new cell
if (actor === this.player) { if (actor === this.player) {
this.hint_shown = null; this._set_prop(this, 'hint_shown', null);
} }
let teleporter; let teleporter;
goal_cell.each(tile => { goal_cell.each(tile => {
@ -632,12 +627,11 @@ class Level {
return; return;
if (actor === this.player && tile.type.is_hint) { if (actor === this.player && tile.type.is_hint) {
this.hint_shown = tile.specific_hint ?? this.stored_level.hint; this._set_prop(this, 'hint_shown', tile.specific_hint ?? this.stored_level.hint);
} }
if (tile.type.is_item && actor.type.has_inventory) { if (tile.type.is_item && this.give_actor(actor, tile.type.name)) {
actor.give_item(tile.type.name); this.remove_tile(tile);
tile.destroy();
} }
else if (tile.type.is_teleporter) { else if (tile.type.is_teleporter) {
teleporter = tile; teleporter = tile;
@ -663,12 +657,10 @@ class Level {
// Physically move the actor to the new teleporter // Physically move the actor to the new teleporter
// XXX is this right, compare with tile world? i overhear it's actually implemented as a slide? // XXX is this right, compare with tile world? i overhear it's actually implemented as a slide?
// XXX will probably play badly with undo lol // XXX will probably play badly with undo lol
let tele_cell = this.cells[goal.y][goal.x]; let tele_cell = goal.cell;
current_cell._remove(actor); current_cell._remove(actor);
tele_cell._add(actor); tele_cell._add(actor);
current_cell = tele_cell; current_cell = tele_cell;
actor.x = goal.x;
actor.y = goal.y;
if (this.attempt_step(actor, actor.direction)) if (this.attempt_step(actor, actor.direction))
// Success, teleportation complete // Success, teleportation complete
break; break;
@ -683,17 +675,56 @@ class Level {
} }
} }
// -------------------------------------------------------------------------
// Undo handling
commit() {
this.undo_stack.push(this.pending_undo);
this.pending_undo = [];
}
undo() {
let entry = this.undo_stack.pop();
// Undo in reverse order! There's no redo, so it's okay to destroy this
entry.reverse();
for (let undo of entry) {
undo();
}
}
// ------------------------------------------------------------------------- // -------------------------------------------------------------------------
// Level alteration methods. EVERYTHING that changes the state of a level, // Level alteration methods. EVERYTHING that changes the state of a level,
// including the state of a single tile, should do it through one of these // including the state of a single tile, should do it through one of these
// for undo/rewind purposes // for undo/rewind purposes
_set_prop(obj, key, val) {
let old_val = obj[key];
this.pending_undo.push(() => obj[key] = old_val);
obj[key] = val;
}
collect_chip() { collect_chip() {
if (this.chips_remaining > 0) { let current = this.chips_remaining;
if (current > 0) {
this.pending_undo.push(() => this.chips_remaining = current);
this.chips_remaining--; this.chips_remaining--;
} }
} }
fail(message) {
this.pending_undo.push(() => {
this.state = 'playing';
this.fail_message = null;
});
this.state = 'failure';
this.fail_message = message;
}
win() {
this.pending_undo.push(() => this.state = 'playing');
this.state = 'success';
}
// Get the next direction a random force floor will use. They share global // Get the next direction a random force floor will use. They share global
// state and cycle clockwise. // state and cycle clockwise.
get_force_floor_direction() { get_force_floor_direction() {
@ -702,11 +733,55 @@ class Level {
return d; return d;
} }
// TODO make a set of primitives for actually altering the level that also // Tile stuff in particular
// record how to undo themselves
remove_tile(tile) {
let cell = tile.cell;
let layer = cell._remove(tile);
this.pending_undo.push(() => cell._add(tile, layer));
}
add_tile(tile, cell, layer = null) {
cell._add(tile, layer);
this.pending_undo.push(() => cell._remove(tile));
}
transmute_tile(tile, name) {
let current = tile.type.name;
this.pending_undo.push(() => tile.type = TILE_TYPES[current]);
tile.type = TILE_TYPES[name];
// TODO adjust anything else?
}
give_actor(actor, name) {
if (! actor.type.has_inventory)
return false;
let current = actor.inventory[name];
this.pending_undo.push(() => actor.inventory[name] = current);
actor.inventory[name] = (current ?? 0) + 1;
return true;
}
// Mark an actor as sliding
make_slide(actor, mode) { make_slide(actor, mode) {
actor.slide_mode = mode; actor.slide_mode = mode;
} }
// Change an actor's direction
set_actor_direction(actor, direction) {
let current = actor.direction;
this.pending_undo.push(() => actor.direction = current);
actor.direction = direction;
}
set_actor_stuck(actor, is_stuck) {
let current = actor.stuck;
if (current === is_stuck)
return;
this.pending_undo.push(() => actor.stuck = current);
actor.stuck = is_stuck;
}
} }
@ -915,8 +990,8 @@ const GAME_UI_HTML = `
<div class="controls"> <div class="controls">
<button class="control-pause" type="button">Pause</button> <button class="control-pause" type="button">Pause</button>
<button class="control-restart" type="button">Restart</button> <button class="control-restart" type="button">Restart</button>
<button class="control-undo" type="button" disabled>Undo</button> <button class="control-undo" type="button">Undo</button>
<button class="control-rewind" type="button" disabled>Rewind</button> <button class="control-rewind" type="button">Rewind</button>
</div> </div>
<div class="demo"> <div class="demo">
<h2>Solution demo available</h2> <h2>Solution demo available</h2>
@ -966,6 +1041,7 @@ class Game {
// TODO obey level options; allow overriding // TODO obey level options; allow overriding
this.viewport_size_x = 9; this.viewport_size_x = 9;
this.viewport_size_y = 9; this.viewport_size_y = 9;
this.scale = 1;
document.body.innerHTML = GAME_UI_HTML; document.body.innerHTML = GAME_UI_HTML;
this.container = document.body.querySelector('main'); this.container = document.body.querySelector('main');
@ -1026,6 +1102,23 @@ class Game {
}).open(); }).open();
ev.target.blur(); ev.target.blur();
}); });
this.undo_button = this.container.querySelector('.controls .control-undo');
this.undo_button.addEventListener('click', ev => {
let player_cell = this.level.player.cell;
while (player_cell === this.level.player.cell && this.level.undo_stack.length > 0) {
this.level.undo();
}
if (this.level.undo_stack.length === 0) {
this.set_state('waiting');
}
else {
// Be sure to undo any success or failure
this.set_state('playing');
}
this.update_ui();
this.redraw();
ev.target.blur();
});
// Demo playback // Demo playback
this.container.querySelector('.demo .demo-step-1').addEventListener('click', ev => { this.container.querySelector('.demo .demo-step-1').addEventListener('click', ev => {
this.advance_by(1); this.advance_by(1);
@ -1050,8 +1143,8 @@ class Game {
return; return;
let rect = this.level_canvas.getBoundingClientRect(); let rect = this.level_canvas.getBoundingClientRect();
let x = Math.floor((ev.clientX - rect.x) / 2 / this.tileset.size_x + this.viewport_x); let x = Math.floor((ev.clientX - rect.x) / this.scale / this.tileset.size_x + this.viewport_x);
let y = Math.floor((ev.clientY - rect.y) / 2 / this.tileset.size_y + this.viewport_y); let y = Math.floor((ev.clientY - rect.y) / this.scale / this.tileset.size_y + this.viewport_y);
this.level.move_to(this.level.player, x, y); this.level.move_to(this.level.player, x, y);
}); });
@ -1261,7 +1354,7 @@ class Game {
if (! this._inventory_tiles[name]) { if (! this._inventory_tiles[name]) {
// TODO reuse the canvas // TODO reuse the canvas
let canvas = mk('canvas', {width: this.tileset.size_x, height: this.tileset.size_y}); let canvas = mk('canvas', {width: this.tileset.size_x, height: this.tileset.size_y});
this.tileset.draw({type: TILE_TYPES[name]}, canvas.getContext('2d'), 0, 0); this.tileset.draw({type: TILE_TYPES[name]}, null, canvas.getContext('2d'), 0, 0);
this._inventory_tiles[name] = canvas.toDataURL(); this._inventory_tiles[name] = canvas.toDataURL();
} }
return this._inventory_tiles[name]; return this._inventory_tiles[name];
@ -1348,29 +1441,34 @@ class Game {
} }
redraw() { redraw() {
// TODO split this out to a renderer, call it every frame, have the level flag itself as dirty
let ctx = this.level_canvas.getContext('2d'); let ctx = this.level_canvas.getContext('2d');
ctx.clearRect(0, 0, this.level_canvas.width, this.level_canvas.height); ctx.clearRect(0, 0, this.level_canvas.width, this.level_canvas.height);
let xmargin = (this.viewport_size_x - 1) / 2; let xmargin = (this.viewport_size_x - 1) / 2;
let ymargin = (this.viewport_size_y - 1) / 2; let ymargin = (this.viewport_size_y - 1) / 2;
let x0 = this.level.player.x - xmargin; let player_cell = this.level.player.cell;
let y0 = this.level.player.y - ymargin; let x0 = player_cell.x - xmargin;
let y0 = player_cell.y - ymargin;
x0 = Math.max(0, Math.min(this.level.width - this.viewport_size_x, x0)); x0 = Math.max(0, Math.min(this.level.width - this.viewport_size_x, x0));
y0 = Math.max(0, Math.min(this.level.height - this.viewport_size_y, y0)); y0 = Math.max(0, Math.min(this.level.height - this.viewport_size_y, y0));
this.viewport_x = x0; this.viewport_x = x0;
this.viewport_y = y0; this.viewport_y = y0;
for (let dx = 0; dx < this.viewport_size_x; dx++) { // Draw in layers, so animated objects aren't overdrawn by neighboring terrain
for (let dy = 0; dy < this.viewport_size_y; dy++) { let any_drawn = true;
let cell = this.level.cells[dy + y0][dx + x0]; let i = -1;
/* while (any_drawn) {
if (! cell.is_dirty) i++;
continue; any_drawn = false;
*/ for (let dx = 0; dx < this.viewport_size_x; dx++) {
cell.is_dirty = false; for (let dy = 0; dy < this.viewport_size_y; dy++) {
let cell = this.level.cells[dy + y0][dx + x0];
for (let tile of cell) { let tile = cell[i];
if (! tile.doomed) { if (tile) {
this.tileset.draw(tile, ctx, dx, dy); any_drawn = true;
if (! tile.doomed) {
this.tileset.draw(tile, this.level, ctx, dx, dy);
}
} }
} }
} }
@ -1399,7 +1497,9 @@ class Game {
if (scale <= 0) { if (scale <= 0) {
scale = 1; scale = 1;
} }
// FIXME this doesn't take into account the inventory, which is also affected by scale
// FIXME the above logic doesn't take into account the inventory, which is also affected by scale
this.scale = scale;
this.container.style.setProperty('--scale', scale); this.container.style.setProperty('--scale', scale);
} }
} }

View File

@ -1,3 +1,5 @@
import { DIRECTIONS } from './defs.js';
// TODO really need to specify this format more concretely, whoof // TODO really need to specify this format more concretely, whoof
// XXX special kinds of drawing i know this has for a fact: // XXX special kinds of drawing i know this has for a fact:
// - letter tiles draw from a block of half-tiles onto the center of the base // - letter tiles draw from a block of half-tiles onto the center of the base
@ -129,8 +131,10 @@ export const CC2_TILESET_LAYOUT = {
base: 'purple_floor', base: 'purple_floor',
overlay: [8, 9], overlay: [8, 9],
}, },
// TODO state (10 is closed) trap: {
trap: [9, 9], closed: [9, 9],
open: [10, 9],
},
button_gray: [11, 9], button_gray: [11, 9],
fireball: [[12, 9], [13, 9], [14, 9], [15, 9]], fireball: [[12, 9], [13, 9], [14, 9], [15, 9]],
@ -427,27 +431,35 @@ export class Tileset {
dx * this.size_x, dy * this.size_y, w, h); dx * this.size_x, dy * this.size_y, w, h);
} }
draw(tile, ctx, x, y) { draw(tile, level, ctx, x, y) {
this.draw_type(tile.type.name, tile, ctx, x, y); this.draw_type(tile.type.name, tile, level, ctx, x, y);
} }
// Draws a tile type, given by name. Passing in a tile is optional, but // Draws a tile type, given by name. Passing in a tile is optional, but
// without it you'll get defaults. // without it you'll get defaults.
draw_type(name, tile, ctx, x, y) { draw_type(name, tile, level, ctx, x, y) {
let drawspec = this.layout[name]; let drawspec = this.layout[name];
if (! drawspec) { if (! drawspec) {
console.error(`Don't know how to draw tile type ${type.name}!`); console.error(`Don't know how to draw tile type ${type.name}!`);
return; return;
} }
/*
if (tile && tile.movement_cooldown) {
let offset = DIRECTIONS[tile.direction].movement;
x -= tile.movement_cooldown / tile.type.movement_speed * offset[0];
y -= tile.movement_cooldown / tile.type.movement_speed * offset[1];
}
*/
if (drawspec.overlay) { if (drawspec.overlay) {
// Goofy overlay thing used for green/purple toggle tiles and // Goofy overlay thing used for green/purple toggle tiles and
// southeast thin walls. Draw the base (a type name), then draw // southeast thin walls. Draw the base (a type name), then draw
// the overlay (either a type name or a regular draw spec). // the overlay (either a type name or a regular draw spec).
// TODO chance of infinite recursion here // TODO chance of infinite recursion here
this.draw_type(drawspec.base, tile, ctx, x, y); this.draw_type(drawspec.base, tile, level, ctx, x, y);
if (typeof drawspec.overlay === 'string') { if (typeof drawspec.overlay === 'string') {
this.draw_type(drawspec.overlay, tile, ctx, x, y); this.draw_type(drawspec.overlay, tile, level, ctx, x, y);
return; return;
} }
else { else {
@ -464,11 +476,26 @@ export class Tileset {
// Unwrap animations etc. // Unwrap animations etc.
if (!(coords instanceof Array)) { if (!(coords instanceof Array)) {
// Must be an object of directions // Must be an object of either tile-specific state, or directions
coords = coords[(tile && tile.direction) ?? 'south']; if (name === 'trap') {
if (tile && tile.open) {
coords = coords.open;
}
else {
coords = coords.closed;
}
}
else {
coords = coords[(tile && tile.direction) ?? 'south'];
}
} }
if (coords[0] instanceof Array) { if (coords[0] instanceof Array) {
coords = coords[0]; if (level) {
coords = coords[Math.floor(level.tic_counter % 5 / 5 * coords.length)];
}
else {
coords = coords[0];
}
} }
if (drawspec.mask) { if (drawspec.mask) {

View File

@ -19,12 +19,14 @@ const TILE_TYPES = {
wall_appearing: { wall_appearing: {
blocks: true, blocks: true,
on_bump(me, level, other) { on_bump(me, level, other) {
me.become('wall'); level.transmute_tile(me, 'wall');
}, },
}, },
popwall: { popwall: {
blocks_monsters: true,
blocks_blocks: true,
on_depart(me, level, other) { on_depart(me, level, other) {
me.become('wall'); level.transmute_tile(me, 'wall');
}, },
}, },
thinwall_n: { thinwall_n: {
@ -45,13 +47,13 @@ const TILE_TYPES = {
fake_wall: { fake_wall: {
blocks: true, blocks: true,
on_bump(me, level, other) { on_bump(me, level, other) {
me.become('wall'); level.transmute_tile(me, 'wall');
}, },
}, },
fake_floor: { fake_floor: {
blocks: true, blocks: true,
on_bump(me, level, other) { on_bump(me, level, other) {
me.become('floor'); level.transmute_tile(me, 'floor');
}, },
}, },
@ -110,7 +112,7 @@ const TILE_TYPES = {
blocks_blocks: true, blocks_blocks: true,
// TODO block melinda only without the hiking boots; can't use ignore because then she wouldn't step on it :S also ignore doesn't apply to blocks anyway. // TODO block melinda only without the hiking boots; can't use ignore because then she wouldn't step on it :S also ignore doesn't apply to blocks anyway.
on_arrive(me, level, other) { on_arrive(me, level, other) {
me.become('floor'); level.transmute_tile(me, 'floor');
}, },
}, },
gravel: { gravel: {
@ -122,10 +124,10 @@ const TILE_TYPES = {
on_arrive(me, level, other) { on_arrive(me, level, other) {
if (other.type.is_player) { if (other.type.is_player) {
level.fail("Oops! You can't walk on fire without fire boots!"); level.fail("Oops! You can't walk on fire without fire boots!");
other.become('player_burned'); level.transmute_tile(other, 'player_burned');
} }
else { else {
other.destroy(); level.remove_tile(other);
} }
}, },
}, },
@ -133,15 +135,15 @@ const TILE_TYPES = {
on_arrive(me, level, other) { on_arrive(me, level, other) {
// TODO cc1 allows items under water, i think; water was on the upper layer // TODO cc1 allows items under water, i think; water was on the upper layer
if (other.type.name == 'dirt_block' || other.type.name == 'clone_block') { if (other.type.name == 'dirt_block' || other.type.name == 'clone_block') {
other.destroy(); level.remove_tile(other);
me.become('dirt'); level.transmute_tile(me, 'dirt');
} }
else if (other.type.is_player) { else if (other.type.is_player) {
level.fail("Oops! You can't swim without flippers!"); level.fail("swimming with the fishes");
other.become('player_drowned'); level.transmute_tile(other, 'player_drowned');
} }
else { else {
other.destroy(); level.remove_tile(other);
} }
}, },
}, },
@ -234,8 +236,11 @@ const TILE_TYPES = {
bomb: { bomb: {
// TODO explode // TODO explode
on_arrive(me, level, other) { on_arrive(me, level, other) {
me.destroy(); level.remove_tile(me);
other.destroy(); level.remove_tile(other);
if (other.type.is_player) {
level.fail("watch where you step");
}
}, },
}, },
thief_tools: { thief_tools: {
@ -267,6 +272,7 @@ const TILE_TYPES = {
dirt_block: { dirt_block: {
blocks: true, blocks: true,
is_object: true, is_object: true,
is_actor: true,
is_block: true, is_block: true,
ignores: new Set(['fire']), ignores: new Set(['fire']),
}, },
@ -274,6 +280,7 @@ const TILE_TYPES = {
// TODO is this in any way distinct from dirt block // TODO is this in any way distinct from dirt block
blocks: true, blocks: true,
is_object: true, is_object: true,
is_actor: true,
is_block: true, is_block: true,
ignores: new Set(['fire']), ignores: new Set(['fire']),
}, },
@ -284,10 +291,9 @@ const TILE_TYPES = {
cloner: { cloner: {
blocks: true, blocks: true,
activate(me, level) { activate(me, level) {
let cell = level.cells[me.y][me.x]; let cell = me.cell;
// Clone so we don't end up repeatedly cloning the same object // Copy, so we don't end up repeatedly cloning the same object
let current_tiles = Array.from(cell); for (let tile of Array.from(cell)) {
for (let tile of current_tiles) {
if (tile !== me && tile.type.is_actor) { if (tile !== me && tile.type.is_actor) {
// Copy this stuff in case the movement changes it // Copy this stuff in case the movement changes it
let type = tile.type; let type = tile.type;
@ -295,16 +301,14 @@ const TILE_TYPES = {
// Unstick and try to move the actor; if it's blocked, // Unstick and try to move the actor; if it's blocked,
// abort the clone // abort the clone
tile.stuck = false; level.set_actor_stuck(tile, false);
if (level.attempt_step(tile, direction)) { if (level.attempt_step(tile, direction)) {
level.actors.push(tile); level.actors.push(tile);
// FIXME rearrange to make this possible lol
// FIXME go through level for this, and everything else of course
// FIXME add this underneath, just above the cloner // FIXME add this underneath, just above the cloner
cell._add(new tile.constructor(type, me.x, me.y, direction)); level.add_tile(new tile.constructor(type, direction), cell);
} }
else { else {
tile.stuck = true; level.set_actor_stuck(tile, true);
} }
} }
} }
@ -313,7 +317,7 @@ const TILE_TYPES = {
trap: { trap: {
on_arrive(me, level, other) { on_arrive(me, level, other) {
if (! me.open) { if (! me.open) {
other.stuck = true; level.set_actor_stuck(other, true);
} }
}, },
}, },
@ -331,7 +335,7 @@ const TILE_TYPES = {
for (let actor of level.actors) { for (let actor of level.actors) {
// TODO generify somehow?? // TODO generify somehow??
if (actor.type.name === 'tank_blue') { if (actor.type.name === 'tank_blue') {
actor.direction = DIRECTIONS[actor.direction].opposite; level.set_actor_direction(actor, DIRECTIONS[actor.direction].opposite);
} }
} }
}, },
@ -339,20 +343,21 @@ const TILE_TYPES = {
button_green: { button_green: {
on_arrive(me, level, other) { on_arrive(me, level, other) {
// Swap green floors and walls // Swap green floors and walls
// TODO could probably make this more compact for undo purposes
for (let row of level.cells) { for (let row of level.cells) {
for (let cell of row) { for (let cell of row) {
for (let tile of cell) { for (let tile of cell) {
if (tile.type.name === 'green_floor') { if (tile.type.name === 'green_floor') {
tile.become('green_wall'); level.transmute_tile(tile, 'green_wall');
} }
else if (tile.type.name === 'green_wall') { else if (tile.type.name === 'green_wall') {
tile.become('green_floor'); level.transmute_tile(tile, 'green_floor');
} }
else if (tile.type.name === 'green_chip') { else if (tile.type.name === 'green_chip') {
tile.become('green_bomb'); level.transmute_tile(tile, 'green_bomb');
} }
else if (tile.type.name === 'green_bomb') { else if (tile.type.name === 'green_bomb') {
tile.become('green_chip'); level.transmute_tile(tile, 'green_chip');
} }
} }
} }
@ -366,9 +371,9 @@ const TILE_TYPES = {
if (me.connection && ! me.connection.doomed) { if (me.connection && ! me.connection.doomed) {
let trap = me.connection; let trap = me.connection;
trap.open = true; trap.open = true;
for (let tile of level.cells[trap.y][trap.x]) { for (let tile of trap.cell) {
if (tile.stuck) { if (tile.stuck) {
tile.stuck = false; level.set_actor_stuck(tile, false);
} }
} }
} }
@ -377,9 +382,9 @@ const TILE_TYPES = {
if (me.connection && ! me.connection.doomed) { if (me.connection && ! me.connection.doomed) {
let trap = me.connection; let trap = me.connection;
trap.open = false; trap.open = false;
for (let tile of level.cells[trap.y][trap.x]) { for (let tile of trap.cell) {
if (tile.is_actor) { if (tile.is_actor) {
tile.stuck = false; level.set_actor_stuck(tile, true);
} }
} }
} }
@ -553,7 +558,7 @@ const TILE_TYPES = {
on_arrive(me, level, other) { on_arrive(me, level, other) {
if (other.type.is_player) { if (other.type.is_player) {
level.collect_chip(); level.collect_chip();
me.destroy(); level.remove_tile(me);
} }
}, },
}, },