Refactor to using cells with fixed slots

This better matches CC2 behavior and also makes some very common
operations, like grabbing a cell's actor or terrain, way faster.

It also allows me to efficiently implement CC2's layer order when
checking for collisions; thin walls are checked before terrain, and
actors only afterwards.  The upshot is that bowling balls no longer
destroy stuff on the other side of a thin wall!

I also did some minor optimizing, mostly by turning loops over an entire
cell's contents into checks for a single layer; Chromium now performs a
bulk test about 30% faster.

Downsides of this change:
- All kinds of stuff may have broken!
- It'll be a little difficult to ever emulate MSCC's curious behavior
  when stacking terrain on top of items or other terrain.  But not
  impossible.
- It'll be far more difficult to emulate buggy Lynx (or maybe it's just
  Tile World?) behavior where some combination of cloners and teleports
  allow a ton of monsters to accumulate in a few cells.  I guess I
  wasn't planning on doing that anyway.
This commit is contained in:
Eevee (Evelyn Woods) 2021-01-03 17:19:27 -07:00
parent cff756597c
commit 323ed3ee18
8 changed files with 429 additions and 389 deletions

View File

@ -57,7 +57,7 @@ export const INPUT_BITS = {
wait: 0x8000, wait: 0x8000,
}; };
export const DRAW_LAYERS = { export const LAYERS = {
terrain: 0, terrain: 0,
item: 1, item: 1,
item_mod: 2, item_mod: 2,
@ -66,6 +66,7 @@ export const DRAW_LAYERS = {
swivel: 5, swivel: 5,
thin_wall: 6, thin_wall: 6,
canopy: 7, canopy: 7,
MAX: 8, MAX: 8,
}; };

View File

@ -1,4 +1,4 @@
import { DIRECTIONS, DIRECTION_ORDER } from './defs.js'; import { DIRECTIONS, DIRECTION_ORDER, LAYERS } from './defs.js';
import * as format_base from './format-base.js'; import * as format_base from './format-base.js';
import TILE_TYPES from './tiletypes.js'; import TILE_TYPES from './tiletypes.js';
import * as util from './util.js'; import * as util from './util.js';
@ -1438,7 +1438,7 @@ export function synthesize_level(stored_level) {
// save it until we reach the terrain layer, and then sub it in instead. // save it until we reach the terrain layer, and then sub it in instead.
// TODO if i follow in tyler's footsteps and give swivel its own layer then i'll need to // TODO if i follow in tyler's footsteps and give swivel its own layer then i'll need to
// complicate this somewhat // complicate this somewhat
if (tile.type.draw_layer === 0 && dummy_terrain_tile) { if (tile.type.layer === LAYERS.terrain && dummy_terrain_tile) {
tile = dummy_terrain_tile; tile = dummy_terrain_tile;
spec = REVERSE_TILE_ENCODING[tile.type.name]; spec = REVERSE_TILE_ENCODING[tile.type.name];
} }

View File

@ -1,4 +1,4 @@
import { DIRECTIONS } from './defs.js'; import { DIRECTIONS, LAYERS } from './defs.js';
import * as format_base from './format-base.js'; import * as format_base from './format-base.js';
import TILE_TYPES from './tiletypes.js'; import TILE_TYPES from './tiletypes.js';
import * as util from './util.js'; import * as util from './util.js';
@ -266,7 +266,7 @@ function parse_level(bytes, number) {
// Fix the "floor/empty" nonsense here by adding floor to any cell with no terrain on bottom // Fix the "floor/empty" nonsense here by adding floor to any cell with no terrain on bottom
for (let cell of level.linear_cells) { for (let cell of level.linear_cells) {
if (cell.length === 0 || cell[0].type.draw_layer !== 0) { if (cell.length === 0 || cell[0].type.layer !== LAYERS.terrain) {
// No terrain; insert a floor // No terrain; insert a floor
cell.unshift({ type: TILE_TYPES['floor'] }); cell.unshift({ type: TILE_TYPES['floor'] });
} }

View File

@ -1,5 +1,5 @@
import * as algorithms from './algorithms.js'; import * as algorithms from './algorithms.js';
import { DIRECTIONS, DIRECTION_ORDER, INPUT_BITS, TICS_PER_SECOND } from './defs.js'; import { DIRECTIONS, DIRECTION_ORDER, LAYERS, INPUT_BITS, TICS_PER_SECOND } from './defs.js';
import { LevelInterface } from './format-base.js'; import { LevelInterface } from './format-base.js';
import TILE_TYPES from './tiletypes.js'; import TILE_TYPES from './tiletypes.js';
@ -54,9 +54,15 @@ export class Tile {
// Extremely awkward special case: items don't block monsters if the cell also contains an // Extremely awkward special case: items don't block monsters if the cell also contains an
// item modifier (i.e. "no" sign) or a real player // item modifier (i.e. "no" sign) or a real player
// TODO would love to get this outta here // TODO would love to get this outta here
if (this.type.is_item && if (this.type.is_item) {
this.cell.some(tile => tile.type.item_modifier || tile.type.is_real_player)) let item_mod = this.cell.get_item_mod();
return false; if (item_mod && item_mod.type.item_modifier)
return false;
let actor = this.cell.get_actor();
if (actor && actor.type.is_real_player)
return false;
}
if (this.type.blocks_collision & other.type.collision_mask) if (this.type.blocks_collision & other.type.collision_mask)
return true; return true;
@ -122,6 +128,7 @@ export class Tile {
direction = tile.cell.redirect_exit(tile, direction); direction = tile.cell.redirect_exit(tile, direction);
// Need to explicitly check this here, otherwise you could /attempt/ to push a block, // Need to explicitly check this here, otherwise you could /attempt/ to push a block,
// which would fail, but it would still change the block's direction // which would fail, but it would still change the block's direction
// XXX this expects to take a level but it only matters with push_mode === 'push'
return tile.cell.try_leaving(tile, direction); return tile.cell.try_leaving(tile, direction);
} }
@ -142,37 +149,36 @@ Tile.prototype.wire_tunnel_directions = 0;
export class Cell extends Array { export class Cell extends Array {
constructor(x, y) { constructor(x, y) {
super(); super(LAYERS.MAX);
this.x = x; this.x = x;
this.y = y; this.y = y;
} }
_add(tile, index = null) { _add(tile) {
if (index === null) { let index = tile.type.layer;
this.push(tile); if (this[index]) {
} console.error("ATTEMPTING TO ADD", tile, "TO CELL", this, "WHICH ERASES EXISTING TILE", this[index]);
else { this[index].cell = null;
this.splice(index, 0, tile);
} }
this[index] = tile;
tile.cell = this; 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!
// Should only be called from Level, which handles some bookkeeping! // Should only be called from Level, which handles some bookkeeping!
_remove(tile) { _remove(tile) {
let index = this.indexOf(tile); let index = tile.type.layer;
if (index < 0) if (this[index] !== tile)
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(index, 1); this[index] = null;
tile.cell = null; tile.cell = null;
return index;
} }
get_wired_tile() { get_wired_tile() {
let ret = null; let ret = null;
for (let tile of this) { for (let tile of this) {
if ((tile.wire_directions || tile.wire_tunnel_directions) && ! tile.movement_cooldown) { if (tile && (tile.wire_directions || tile.wire_tunnel_directions) && ! tile.movement_cooldown) {
ret = tile; ret = tile;
// Don't break; we want the topmost tile! // Don't break; we want the topmost tile!
} }
@ -181,52 +187,53 @@ export class Cell extends Array {
} }
get_terrain() { get_terrain() {
for (let tile of this) { return this[LAYERS.terrain] ?? null;
if (tile.type.draw_layer === 0)
return tile;
}
return null;
} }
get_actor() { get_actor() {
for (let tile of this) { return this[LAYERS.actor] ?? null;
if (tile.type.is_actor)
return tile;
}
return null;
} }
get_item() { get_item() {
for (let tile of this) { return this[LAYERS.item] ?? null;
if (tile.type.is_item)
return tile;
}
return null;
} }
get_item_mod() { get_item_mod() {
for (let tile of this) { return this[LAYERS.item_mod] ?? null;
if (tile.type.item_modifier)
return tile;
}
return null;
} }
has(name) { has(name) {
return this.some(tile => tile.type.name === name); let current = this[TILE_TYPES[name].layer];
return current && current.type.name === name;
} }
try_leaving(actor, direction) { // FIXME honestly no longer sure why these two are on Cell, or even separate really
for (let tile of this) { try_leaving(actor, direction, level, push_mode) {
if (tile === actor) // The only tiles that can trap us are thin walls and terrain, so for perf (this is very hot
continue; // code), only bother checking those)
let terrain = this[LAYERS.terrain];
let thin_walls = this[LAYERS.thin_wall];
let blocker;
if (tile.type.traps && tile.type.traps(tile, actor)) if (thin_walls && thin_walls.type.blocks_leaving && thin_walls.type.blocks_leaving(thin_walls, actor, direction)) {
return false; blocker = thin_walls;
if (tile.type.blocks_leaving && tile.type.blocks_leaving(tile, actor, direction))
return false;
} }
else if (terrain.type.traps && terrain.type.traps(terrain, actor)) {
blocker = terrain;
}
else if (terrain.type.blocks_leaving && terrain.type.blocks_leaving(terrain, actor, direction)) {
blocker = terrain;
}
if (blocker) {
if (push_mode === 'push') {
if (actor.type.on_blocked) {
actor.type.on_blocked(actor, level, direction, blocker);
}
}
return false;
}
return true; return true;
} }
@ -238,7 +245,6 @@ export class Cell extends Array {
// - 'push': Fire bump triggers. Attempt to move pushable objects out of the way immediately. // - 'push': Fire bump triggers. Attempt to move pushable objects out of the way immediately.
try_entering(actor, direction, level, push_mode = null) { try_entering(actor, direction, level, push_mode = null) {
let pushable_tiles = []; let pushable_tiles = [];
let blocked = false;
// Subtleties ahoy! This is **EXTREMELY** sensitive to ordering. Consider: // Subtleties ahoy! This is **EXTREMELY** sensitive to ordering. Consider:
// - An actor with foil MUST NOT bump a wall on the other side of a thin wall. // - An actor with foil MUST NOT bump a wall on the other side of a thin wall.
// - A ghost with foil MUST bump a wall (even on the other side of a thin wall) and be // - A ghost with foil MUST bump a wall (even on the other side of a thin wall) and be
@ -252,19 +258,17 @@ export class Cell extends Array {
// It seems the order is thus: canopy + thin wall; terrain; actor; item. Which is the usual // It seems the order is thus: canopy + thin wall; terrain; actor; item. Which is the usual
// ordering from the top down, except that terrain is checked before actors. Really, the // ordering from the top down, except that terrain is checked before actors. Really, the
// ordering is from "outermost" to "innermost", which makes physical sense. // ordering is from "outermost" to "innermost", which makes physical sense.
// FIXME make that work, then. i think i may need to shift to fixed slots unfortunately for (let layer of [
// (Note that here, and anywhere else that has any chance of altering the cell's contents, LAYERS.canopy, LAYERS.thin_wall, LAYERS.terrain, LAYERS.swivel,
// we iterate over a copy of the cell to insulate ourselves from tiles appearing or LAYERS.actor, LAYERS.item_mod, LAYERS.item])
// disappearing mid-iteration.) {
for (let tile of Array.from(this).reverse()) { let tile = this[layer];
if (! tile)
continue;
// TODO check ignores here? // TODO check ignores here?
// Note that if they can't enter this cell because of a thin wall, then they can't bump if (tile.type.on_bumped) {
// any of our other tiles either. (This is my best guess at the actual behavior, seeing tile.type.on_bumped(tile, level, actor);
// as walls also block everything but players can obviously bump /those/.)
if (! blocked) {
if (tile.type.on_bumped) {
tile.type.on_bumped(tile, level, actor);
}
} }
if (! tile.blocks(actor, direction, level)) if (! tile.blocks(actor, direction, level))
@ -274,23 +278,19 @@ export class Cell extends Array {
return false; return false;
if (! actor.can_push(tile, direction)) { if (! actor.can_push(tile, direction)) {
// It's in our way and we can't push it, so we're done here
if (push_mode === 'push') { if (push_mode === 'push') {
// Track this instead of returning immediately, because 'push' mode also bumps if (actor.type.on_blocked) {
// every tile in the cell actor.type.on_blocked(actor, level, direction, tile);
blocked = true; }
}
else {
return false;
} }
return false;
} }
// Collect pushables for later, so we don't inadvertently push through a wall // Collect pushables for later, so we don't inadvertently push through a wall
pushable_tiles.push(tile); pushable_tiles.push(tile);
} }
if (blocked)
return false;
// If we got this far, all that's left is to deal with pushables // If we got this far, all that's left is to deal with pushables
if (pushable_tiles.length > 0) { if (pushable_tiles.length > 0) {
// This ends recursive push attempts, which can happen with a row of ice clogged by ice // This ends recursive push attempts, which can happen with a row of ice clogged by ice
@ -340,7 +340,7 @@ export class Cell extends Array {
// BLOX replay, and right at the end ice blocks spring mine each other. also, the wiki // BLOX replay, and right at the end ice blocks spring mine each other. also, the wiki
// suggests something about another actor moving away at the same time? // suggests something about another actor moving away at the same time?
if (! (level.compat.emulate_spring_mining && actor.type.is_real_player) && if (! (level.compat.emulate_spring_mining && actor.type.is_real_player) &&
push_mode === 'push' && this.some(tile => tile.blocks(actor, direction, level))) push_mode === 'push' && this.some(tile => tile && tile.blocks(actor, direction, level)))
return false; return false;
} }
@ -349,10 +349,9 @@ export class Cell extends Array {
// Special railroad ability: change the direction we attempt to leave // Special railroad ability: change the direction we attempt to leave
redirect_exit(actor, direction) { redirect_exit(actor, direction) {
for (let tile of this) { let terrain = this.get_terrain();
if (tile.type.redirect_exit) { if (terrain && terrain.type.redirect_exit) {
return tile.type.redirect_exit(tile, actor, direction); return terrain.type.redirect_exit(terrain, actor, direction);
}
} }
return direction; return direction;
} }
@ -455,6 +454,7 @@ export class Level extends LevelInterface {
let stored_cell = this.stored_level.linear_cells[n]; let stored_cell = this.stored_level.linear_cells[n];
n++; n++;
// FIXME give this same treatment to stored cells (otherwise the editor is fucked)
for (let template_tile of stored_cell) { for (let template_tile of stored_cell) {
let tile = Tile.from_template(template_tile); let tile = Tile.from_template(template_tile);
if (tile.type.is_hint) { if (tile.type.is_hint) {
@ -508,7 +508,7 @@ export class Level extends LevelInterface {
if (target_cell_n && target_cell_n < this.width * this.height) { if (target_cell_n && target_cell_n < this.width * this.height) {
let [tx, ty] = this.stored_level.scalar_to_coords(target_cell_n); let [tx, ty] = this.stored_level.scalar_to_coords(target_cell_n);
for (let tile of this.cell(tx, ty)) { for (let tile of this.cell(tx, ty)) {
if (goals === tile.type.name) { if (tile && goals === tile.type.name) {
connectable.connection = tile; connectable.connection = tile;
break; break;
} }
@ -522,7 +522,7 @@ export class Level extends LevelInterface {
for (let cell of this.iter_cells_in_diamond(connectable.cell)) { for (let cell of this.iter_cells_in_diamond(connectable.cell)) {
let target = null; let target = null;
for (let tile of cell) { for (let tile of cell) {
if (goals.has(tile.type.name)) { if (tile && goals.has(tile.type.name)) {
target = tile; target = tile;
break; break;
} }
@ -625,7 +625,10 @@ export class Level extends LevelInterface {
// Dead end handling (potentially logic gates, etc.) // Dead end handling (potentially logic gates, etc.)
(cell, edge) => { (cell, edge) => {
for (let tile of cell) { for (let tile of cell) {
if (tile.type.name === 'logic_gate') { if (! tile) {
continue;
}
else if (tile.type.name === 'logic_gate') {
// Logic gates are the one non-wired tile that get attached to circuits, // Logic gates are the one non-wired tile that get attached to circuits,
// mostly so blue teleporters can follow them // mostly so blue teleporters can follow them
if (! tile.circuits) { if (! tile.circuits) {
@ -659,6 +662,8 @@ export class Level extends LevelInterface {
for (let i = this.linear_cells.length - 1; i >= 0; i--) { for (let i = this.linear_cells.length - 1; i >= 0; i--) {
let cell = this.linear_cells[i]; let cell = this.linear_cells[i];
for (let tile of cell) { for (let tile of cell) {
if (! tile)
continue;
if (tile.type.on_ready) { if (tile.type.on_ready) {
tile.type.on_ready(tile, this); tile.type.on_ready(tile, this);
} }
@ -737,7 +742,7 @@ export class Level extends LevelInterface {
for (let i = this.linear_cells.length - 1; i >= 0; i--) { for (let i = this.linear_cells.length - 1; i >= 0; i--) {
let cell = this.linear_cells[i]; let cell = this.linear_cells[i];
for (let tile of cell) { for (let tile of cell) {
if (tile.type.on_begin) { if (tile && tile.type.on_begin) {
tile.type.on_begin(tile, this); tile.type.on_begin(tile, this);
} }
} }
@ -1116,6 +1121,7 @@ export class Level extends LevelInterface {
this.commit(); this.commit();
} }
// TODO this only has one caller
_extract_player_directions(input) { _extract_player_directions(input) {
// Extract directions from an input mask // Extract directions from an input mask
let dir1 = null, dir2 = null; let dir1 = null, dir2 = null;
@ -1322,7 +1328,7 @@ export class Level extends LevelInterface {
} }
if (forced_only) if (forced_only)
return; return;
if (actor.cell.some(tile => tile.type.traps && tile.type.traps(tile, actor))) { if (terrain.type.traps && terrain.type.traps(terrain, actor)) {
// An actor in a cloner or a closed trap can't turn // An actor in a cloner or a closed trap can't turn
// TODO because of this, if a tank is trapped when a blue button is pressed, then // TODO because of this, if a tank is trapped when a blue button is pressed, then
// when released, it will make one move out of the trap and /then/ turn around and // when released, it will make one move out of the trap and /then/ turn around and
@ -1366,7 +1372,7 @@ export class Level extends LevelInterface {
check_movement(actor, orig_cell, direction, push_mode) { check_movement(actor, orig_cell, direction, push_mode) {
let dest_cell = this.get_neighboring_cell(orig_cell, direction); let dest_cell = this.get_neighboring_cell(orig_cell, direction);
let success = (dest_cell && let success = (dest_cell &&
orig_cell.try_leaving(actor, direction) && orig_cell.try_leaving(actor, direction, this, push_mode) &&
dest_cell.try_entering(actor, direction, this, push_mode)); dest_cell.try_entering(actor, direction, this, push_mode));
// If we have the hook, pull anything behind us, now that we're out of the way. // If we have the hook, pull anything behind us, now that we're out of the way.
@ -1439,12 +1445,8 @@ export class Level extends LevelInterface {
let speed = actor.type.movement_speed; let speed = actor.type.movement_speed;
let move = DIRECTIONS[direction].movement; let move = DIRECTIONS[direction].movement;
if (! this.check_movement(actor, actor.cell, direction, 'push')) { if (! this.check_movement(actor, actor.cell, direction, 'push'))
if (actor.type.on_blocked) {
actor.type.on_blocked(actor, this, direction);
}
return false; return false;
}
// We're clear! Compute our speed and move us // We're clear! Compute our speed and move us
// FIXME this feels clunky // FIXME this feels clunky
@ -1497,6 +1499,8 @@ export class Level extends LevelInterface {
// XXX that's still not perfect; if actor X is tic-misaligned, like if there's a chain // XXX that's still not perfect; if actor X is tic-misaligned, like if there's a chain
// of 3 or more actors cloning directly onto red buttons for other cloners, then this // of 3 or more actors cloning directly onto red buttons for other cloners, then this
// cannot possibly work // cannot possibly work
// TODO now that i have steam-strict mode this is largely pointless, just do what seems
// correct
actor.cooldown_delay_hack = 1; actor.cooldown_delay_hack = 1;
return true; return true;
} }
@ -1513,14 +1517,21 @@ export class Level extends LevelInterface {
return; return;
let original_cell = actor.cell; let original_cell = actor.cell;
// Physically remove the actor first, so that it won't get in the way of e.g. a splash
// spawned from stepping off of a lilypad
this.remove_tile(actor);
// 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
for (let tile of Array.from(original_cell)) { for (let tile of original_cell) {
if (! tile)
continue;
if (tile === actor) if (tile === actor)
continue; continue;
if (actor.ignores(tile.type.name)) if (actor.ignores(tile.type.name))
continue; continue;
// FIXME ah, stepping off a lilypad will add a splash but we're still here? but then
// why did the warning not catch it
if (tile.type.on_depart) { if (tile.type.on_depart) {
tile.type.on_depart(tile, this, actor); tile.type.on_depart(tile, this, actor);
} }
@ -1532,7 +1543,7 @@ export class Level extends LevelInterface {
} }
for (let tile of goal_cell) { for (let tile of goal_cell) {
// FIXME this could go in on_approach now // FIXME this could go in on_approach now
if (actor === this.player && tile.type.is_hint) { if (tile && actor === this.player && tile.type.is_hint) {
this.hint_shown = tile.hint_text ?? this.stored_level.hint; this.hint_shown = tile.hint_text ?? this.stored_level.hint;
} }
} }
@ -1540,7 +1551,9 @@ export class Level extends LevelInterface {
// Announce we're approaching. Slide mode is set here, since it's about the tile we're // Announce we're approaching. Slide mode is set here, since it's about the tile we're
// moving towards and needs to last through our next decision // moving towards and needs to last through our next decision
this.make_slide(actor, null); this.make_slide(actor, null);
for (let tile of Array.from(goal_cell)) { for (let tile of goal_cell) {
if (! tile)
continue;
if (tile === actor) if (tile === actor)
continue; continue;
if (actor.ignores(tile.type.name)) if (actor.ignores(tile.type.name))
@ -1570,10 +1583,17 @@ export class Level extends LevelInterface {
} }
} }
// Now physically move the actor; we wait until here in case some of those callbacks handled // Now add the actor back; we have to wait this long because e.g. monsters erase splashes
// interactions between actors on the same layer (e.g. monsters erasing splashes) if (goal_cell.get_actor()) {
this.remove_tile(actor); // FIXME a monster or block killing the player will still move into her cell!!! i don't
this.add_tile(actor, goal_cell); // know what to do about this, i feel like i tried making monster/player block each
// other before and it did not go well. maybe it was an ordering issue though?
this.add_tile(actor, original_cell);
return;
}
else {
this.add_tile(actor, goal_cell);
}
// If we're a monster stepping on the player's tail, that also kills her immediately; the // If we're a monster stepping on the player's tail, that also kills her immediately; the
// player and a monster must be strictly more than 4 tics apart // player and a monster must be strictly more than 4 tics apart
@ -1598,7 +1618,11 @@ export class Level extends LevelInterface {
// Step on every tile in a cell we just arrived in // Step on every tile in a cell we just arrived in
step_on_cell(actor, cell) { step_on_cell(actor, cell) {
// Step on topmost things first -- notably, it's safe to step on water with flippers on top // Step on topmost things first -- notably, it's safe to step on water with flippers on top
for (let tile of Array.from(cell).reverse()) { // TODO is there a custom order here similar to collision checking?
for (let layer = LAYERS.MAX - 1; layer >= 0; layer--) {
let tile = cell[layer];
if (! tile)
continue;
if (tile === actor) if (tile === actor)
continue; continue;
if (actor.ignores(tile.type.name)) if (actor.ignores(tile.type.name))
@ -1607,7 +1631,7 @@ export class Level extends LevelInterface {
if (tile.type.is_item && if (tile.type.is_item &&
// FIXME implement item priority i'm begging you // FIXME implement item priority i'm begging you
((actor.type.has_inventory && ! (tile.type.name === 'key_red' && ! actor.type.is_player)) || ((actor.type.has_inventory && ! (tile.type.name === 'key_red' && ! actor.type.is_player)) ||
cell.some(t => t.type.item_modifier === 'pickup')) && (cell.get_item_mod() && cell.get_item_mod().type.item_modifier === 'pickup')) &&
this.attempt_take(actor, tile)) this.attempt_take(actor, tile))
{ {
if (tile.type.is_key) { if (tile.type.is_key) {
@ -1684,7 +1708,7 @@ export class Level extends LevelInterface {
for ([dest, direction] of teleporter.type.teleport_dest_order(teleporter, this, actor)) { for ([dest, direction] of teleporter.type.teleport_dest_order(teleporter, this, actor)) {
// Teleporters already containing an actor are blocked and unusable // Teleporters already containing an actor are blocked and unusable
// FIXME should check collision? otherwise this catches non-blocking vfx... // FIXME should check collision? otherwise this catches non-blocking vfx...
if (dest.cell.some(tile => tile.type.is_actor && tile !== actor && ! tile.type.ttl)) if (dest.cell.some(tile => tile && tile.type.is_actor && tile !== actor && ! tile.type.ttl))
continue; continue;
// XXX lynx treats this as a slide and does it in a pass in the main loop // XXX lynx treats this as a slide and does it in a pass in the main loop
@ -1767,7 +1791,7 @@ export class Level extends LevelInterface {
// Attempt to place an item in the world, as though dropped by an actor // Attempt to place an item in the world, as though dropped by an actor
_place_dropped_item(name, cell, dropping_actor) { _place_dropped_item(name, cell, dropping_actor) {
let type = TILE_TYPES[name]; let type = TILE_TYPES[name];
if (type.draw_layer === 0) { if (type.layer === LAYERS.terrain) {
// Terrain items (i.e., yellow teleports) can only be dropped on regular floor // Terrain items (i.e., yellow teleports) can only be dropped on regular floor
let terrain = cell.get_terrain(); let terrain = cell.get_terrain();
if (terrain.type.name !== 'floor') if (terrain.type.name !== 'floor')
@ -1794,12 +1818,19 @@ export class Level extends LevelInterface {
if (type.is_actor) { if (type.is_actor) {
// This is tricky -- the item has become an actor, but whatever dropped it is // This is tricky -- the item has become an actor, but whatever dropped it is
// already in this cell's actor layer. But we also know for sure that there's no // already in this cell's actor layer. But we also know for sure that there's no
// item in this cell, so we'll cheat a little: add it in the item layer, set it // item in this cell, so we'll cheat a little: remove the dropping actor, set the
// rolling (which should shift it into the next cell over), then switch it to the // item moving, then put the dropping actor back before anyone notices.
// actor layer. cell._remove(dropping_actor);
// TODO do that this.add_tile(tile, cell);
this.add_actor(tile); if (! this.attempt_out_of_turn_step(tile, dropping_actor.direction)) {
this.attempt_out_of_turn_step(tile, dropping_actor.direction); // It was unable to move, so there's nothing we can do but destroy it
// TODO maybe blow it up with a nonblocking vfx? in cc2 it just vanishes
this.remove_tile(tile);
}
else {
this.add_actor(tile);
}
cell._add(dropping_actor);
} }
else { else {
this.add_tile(tile, cell); this.add_tile(tile, cell);
@ -1946,7 +1977,9 @@ export class Level extends LevelInterface {
// Some non-actor tiles still want to act every tic. Note that this should happen AFTER wiring. // Some non-actor tiles still want to act every tic. Note that this should happen AFTER wiring.
_do_static_phase() { _do_static_phase() {
for (let tile of this.static_on_tic_tiles) { for (let tile of this.static_on_tic_tiles) {
tile.type.on_tic(tile, this); if (tile.type.on_tic) {
tile.type.on_tic(tile, this);
}
} }
} }
@ -1964,6 +1997,7 @@ export class Level extends LevelInterface {
// The starting cell is iterated last. // The starting cell is iterated last.
*iter_tiles_in_reading_order(start_cell, name, reverse = false) { *iter_tiles_in_reading_order(start_cell, name, reverse = false) {
let i = this.coords_to_scalar(start_cell.x, start_cell.y); let i = this.coords_to_scalar(start_cell.x, start_cell.y);
let index = TILE_TYPES[name].layer;
while (true) { while (true) {
if (reverse) { if (reverse) {
i -= 1; i -= 1;
@ -1977,10 +2011,9 @@ export class Level extends LevelInterface {
} }
let cell = this.linear_cells[i]; let cell = this.linear_cells[i];
for (let tile of cell) { let tile = cell[index];
if (tile.type.name === name) { if (tile && tile.type.name === name) {
yield tile; yield tile;
}
} }
if (cell === start_cell) if (cell === start_cell)
@ -2228,12 +2261,12 @@ export class Level extends LevelInterface {
remove_tile(tile) { remove_tile(tile) {
let cell = tile.cell; let cell = tile.cell;
let index = cell._remove(tile); cell._remove(tile);
this._push_pending_undo(() => cell._add(tile, index)); this._push_pending_undo(() => cell._add(tile));
} }
add_tile(tile, cell, index = null) { add_tile(tile, cell) {
cell._add(tile, index); cell._add(tile);
this._push_pending_undo(() => cell._remove(tile)); this._push_pending_undo(() => cell._remove(tile));
} }
@ -2260,13 +2293,28 @@ export class Level extends LevelInterface {
} }
transmute_tile(tile, name) { transmute_tile(tile, name) {
let current = tile.type.name; let old_type = tile.type;
this._push_pending_undo(() => tile.type = TILE_TYPES[current]); let new_type = TILE_TYPES[name];
tile.type = TILE_TYPES[name]; if (old_type.layer !== new_type.layer) {
// Move it to the right layer!
let cell = tile.cell;
cell._remove(tile);
tile.type = new_type;
cell._add(tile);
this._push_pending_undo(() => {
cell._remove(tile);
tile.type = old_type;
cell._add(tile);
});
}
else {
tile.type = new_type;
this._push_pending_undo(() => tile.type = old_type);
}
// For transmuting into an animation, set up the timer immediately // For transmuting into an animation, set up the timer immediately
if (tile.type.ttl) { if (tile.type.ttl) {
if (! TILE_TYPES[current].is_actor) { if (! old_type.is_actor) {
console.warn("Transmuting a non-actor into an animation!"); console.warn("Transmuting a non-actor into an animation!");
} }
this._set_tile_prop(tile, 'previous_cell', null); this._set_tile_prop(tile, 'previous_cell', null);
@ -2293,7 +2341,7 @@ export class Level extends LevelInterface {
let dropped_item; let dropped_item;
if (! tile.type.is_key && actor.toolbelt && actor.toolbelt.length >= 4) { if (! tile.type.is_key && actor.toolbelt && actor.toolbelt.length >= 4) {
let oldest_item_type = TILE_TYPES[actor.toolbelt[0]]; let oldest_item_type = TILE_TYPES[actor.toolbelt[0]];
if (oldest_item_type.draw_layer === 0 && cell.get_terrain().type.name !== 'floor') { if (oldest_item_type.layer === LAYERS.terrain && cell.get_terrain().type.name !== 'floor') {
// This is a yellow teleporter, and we are not standing on floor; abort! // This is a yellow teleporter, and we are not standing on floor; abort!
return false; return false;
} }
@ -2304,7 +2352,7 @@ export class Level extends LevelInterface {
} }
if (this.give_actor(actor, tile.type.name)) { if (this.give_actor(actor, tile.type.name)) {
if (tile.type.draw_layer === 0) { if (tile.type.layer === LAYERS.terrain) {
// This should only happen for the yellow teleporter // This should only happen for the yellow teleporter
this.transmute_tile(tile, 'floor'); this.transmute_tile(tile, 'floor');
} }

View File

@ -1,6 +1,6 @@
import * as fflate from 'https://unpkg.com/fflate/esm/index.mjs'; import * as fflate from 'https://unpkg.com/fflate/esm/index.mjs';
import { DIRECTIONS, TICS_PER_SECOND } from './defs.js'; import { DIRECTIONS, LAYERS, TICS_PER_SECOND } from './defs.js';
import { TILES_WITH_PROPS } from './editor-tile-overlays.js'; import { TILES_WITH_PROPS } from './editor-tile-overlays.js';
import * as format_base from './format-base.js'; import * as format_base from './format-base.js';
import * as c2g from './format-c2g.js'; import * as c2g from './format-c2g.js';
@ -472,7 +472,7 @@ class PencilOperation extends DrawOperation {
let cell = this.cell(x, y); let cell = this.cell(x, y);
cell.length = 0; cell.length = 0;
let type = this.editor.palette_selection.type; let type = this.editor.palette_selection.type;
if (type.draw_layer !== 0) { if (type.layer !== LAYERS.terrain) {
cell.push({type: TILE_TYPES.floor}); cell.push({type: TILE_TYPES.floor});
} }
this.editor.place_in_cell(x, y, template); this.editor.place_in_cell(x, y, template);
@ -3111,13 +3111,13 @@ export class Editor extends PrimaryView {
return; return;
} }
if (cell[i].type.draw_layer === tile.type.draw_layer) { if (cell[i].type.layer === tile.type.layer) {
cell.splice(i, 1); cell.splice(i, 1);
} }
} }
cell.push(Object.assign({}, tile)); cell.push(Object.assign({}, tile));
cell.sort((a, b) => a.type.draw_layer - b.type.draw_layer); cell.sort((a, b) => a.type.layer - b.type.layer);
} }
erase_tile(cell, tile = null) { erase_tile(cell, tile = null) {
@ -3127,7 +3127,7 @@ export class Editor extends PrimaryView {
tile = this.palette_selection; tile = this.palette_selection;
} }
let layer = tile.type.draw_layer; let layer = tile.type.layer;
for (let i = cell.length - 1; i >= 0; i--) { for (let i = cell.length - 1; i >= 0; i--) {
// If we find a tile of the same type as the one being drawn, see if it has custom // If we find a tile of the same type as the one being drawn, see if it has custom
// combine behavior (only the case if it came from the palette) // combine behavior (only the case if it came from the palette)
@ -3142,7 +3142,7 @@ export class Editor extends PrimaryView {
return; return;
} }
if (cell[i].type.draw_layer === layer) { if (cell[i].type.layer === layer) {
cell.splice(i, 1); cell.splice(i, 1);
} }
} }

View File

@ -2622,6 +2622,7 @@ class PackTestDialog extends DialogOverlay {
try { try {
stored_level = pack.load_level(i); stored_level = pack.load_level(i);
console.log(i + 1, stored_level.title);
if (! stored_level.has_replay) { if (! stored_level.has_replay) {
record_result('no-replay', "No replay"); record_result('no-replay', "No replay");
continue; continue;

View File

@ -1,4 +1,4 @@
import { DIRECTIONS, DRAW_LAYERS } from './defs.js'; import { DIRECTIONS, LAYERS } from './defs.js';
import { mk } from './util.js'; import { mk } from './util.js';
import TILE_TYPES from './tiletypes.js'; import TILE_TYPES from './tiletypes.js';
@ -157,46 +157,45 @@ export class CanvasRenderer {
// FIXME this is a bit inefficient when there are a lot of rarely-used layers; consider // FIXME this is a bit inefficient when there are a lot of rarely-used layers; consider
// instead drawing everything under actors, then actors, then everything above actors? // instead drawing everything under actors, then actors, then everything above actors?
// (note: will need to first fix the game to ensure everything is stacked correctly!) // (note: will need to first fix the game to ensure everything is stacked correctly!)
for (let layer = 0; layer < DRAW_LAYERS.MAX; layer++) { for (let layer = 0; layer < LAYERS.MAX; layer++) {
for (let x = xf0; x <= x1; x++) { for (let x = xf0; x <= x1; x++) {
for (let y = yf0; y <= y1; y++) { for (let y = yf0; y <= y1; y++) {
let cell = this.level.cell(x, y); let cell = this.level.cell(x, y);
for (let tile of cell) { let tile = cell[layer];
if (tile.type.draw_layer !== layer) if (! tile)
continue; continue;
let vx, vy; let vx, vy;
if (tile.type.is_actor && if (tile.type.is_actor &&
// FIXME kind of a hack for the editor, which uses bare tile objects // FIXME kind of a hack for the editor, which uses bare tile objects
tile.visual_position) tile.visual_position)
{ {
// Handle smooth scrolling // Handle smooth scrolling
[vx, vy] = tile.visual_position(tic_offset); [vx, vy] = tile.visual_position(tic_offset);
// Round this to the pixel grid too! // Round this to the pixel grid too!
vx = Math.floor(vx * tw + 0.5) / tw; vx = Math.floor(vx * tw + 0.5) / tw;
vy = Math.floor(vy * th + 0.5) / th; vy = Math.floor(vy * th + 0.5) / th;
}
else {
// Non-actors can't move
vx = x;
vy = y;
}
// For actors (i.e., blocks), perception only applies if there's something
// of potential interest underneath
let perception = this.perception;
if (perception !== 'normal' && tile.type.is_actor &&
! cell.some(t =>
t.type.draw_layer < layer &&
! (t.type.name === 'floor' && (t.wire_directions | t.wire_tunnel_directions) === 0)))
{
perception = 'normal';
}
this.tileset.draw(
tile, tic, perception,
this._make_tileset_blitter(this.ctx, vx - x0, vy - y0));
} }
else {
// Non-actors can't move
vx = x;
vy = y;
}
// For actors (i.e., blocks), perception only applies if there's something
// of potential interest underneath
let perception = this.perception;
if (perception !== 'normal' && tile.type.is_actor &&
! cell.some(t =>
t && t.type.layer < layer &&
! (t.type.name === 'floor' && (t.wire_directions | t.wire_tunnel_directions) === 0)))
{
perception = 'normal';
}
this.tileset.draw(
tile, tic, perception,
this._make_tileset_blitter(this.ctx, vx - x0, vy - y0));
} }
} }
} }

File diff suppressed because it is too large Load Diff