Rewrite how connections work

- Teleporters now connect on the fly, rather than having fixed
  connections (important because dynamite can destroy teleporters!)

- If custom connections are present, red and brown buttons ONLY use
  those, rather than falling back to CC2 connection rules

- Multiple brown buttons connected to the same trap should now work
  correctly
This commit is contained in:
Eevee (Evelyn Woods) 2020-09-30 02:11:17 -06:00
parent 6df0c96d1b
commit f4363b8fda
4 changed files with 131 additions and 103 deletions

View File

@ -121,6 +121,7 @@ const TILE_ENCODING = {
function parse_level(buf, number) {
let level = new util.StoredLevel(number);
level.has_custom_connections = true;
// Map size is always fixed as 32x32 in CC1
level.size_x = 32;
level.size_y = 32;

View File

@ -24,6 +24,7 @@ export class StoredLevel {
// Maps of button positions to trap/cloner positions, as scalar indexes
// in the linear cell list
// TODO merge these imo
this.has_custom_connections = false;
this.custom_trap_wiring = {};
this.custom_cloner_wiring = {};

View File

@ -272,60 +272,37 @@ export class Level {
let x = cell.x;
let y = cell.y;
let goal = connectable.type.connects_to;
let found = false;
// Check for custom wiring, for MSCC .DAT levels
let n = x + y * this.width;
let target_cell_n = null;
if (goal === 'trap') {
target_cell_n = this.stored_level.custom_trap_wiring[n] ?? null;
}
else if (goal === 'cloner') {
target_cell_n = this.stored_level.custom_cloner_wiring[n] ?? null;
}
if (target_cell_n) {
// TODO this N could be outside the map bounds
let target_cell_x = target_cell_n % this.width;
let target_cell_y = Math.floor(target_cell_n / this.width);
for (let tile of this.cells[target_cell_y][target_cell_x]) {
if (tile.type.name === goal) {
connectable.connection = tile;
found = true;
break;
if (this.stored_level.has_custom_connections) {
let n = this.stored_level.coords_to_scalar(x, y);
let target_cell_n = null;
if (goal === 'trap') {
target_cell_n = this.stored_level.custom_trap_wiring[n] ?? null;
}
else if (goal === 'cloner') {
target_cell_n = this.stored_level.custom_cloner_wiring[n] ?? null;
}
if (target_cell_n && target_cell_n < this.width * this.height) {
let [tx, ty] = this.stored_level.scalar_to_coords(target_cell_n);
for (let tile of this.cells[ty][tx]) {
if (tile.type.name === goal) {
connectable.connection = tile;
break;
}
}
}
if (found)
continue;
continue;
}
// Otherwise, look in reading order
let direction = 1;
if (connectable.type.connect_order === 'backward') {
direction = -1;
for (let tile of this.iter_tiles_in_reading_order(cell, goal)) {
// TODO ideally this should be a weak connection somehow, since dynamite can destroy
// empty cloners and probably traps too
connectable.connection = tile;
// Just grab the first
break;
}
for (let i = 0; i < num_cells - 1; i++) {
x += direction;
if (x >= this.width) {
x -= this.width;
y = (y + 1) % this.height;
}
else if (x < 0) {
x += this.width;
y = (y - 1 + this.height) % this.height;
}
for (let tile of this.cells[y][x]) {
if (tile.type.name === goal) {
// TODO should be weak, but you can't destroy cloners so in practice not a concern
connectable.connection = tile;
found = true;
break;
}
}
if (found)
break;
}
// TODO soft warn for e.g. a button with no cloner? (or a cloner with no button?)
}
}
@ -876,7 +853,7 @@ export class Level {
}
this.remove_tile(tile);
}
else if (tile.type.is_teleporter) {
else if (tile.type.teleport_dest_order) {
teleporter = tile;
}
else if (tile.type.on_arrive) {
@ -887,33 +864,23 @@ export 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?
if (teleporter) {
let goal = teleporter;
// TODO in pathological cases this might infinite loop
while (true) {
goal = goal.connection;
for (let dest of teleporter.type.teleport_dest_order(teleporter, this)) {
// Teleporters already containing an actor are blocked and unusable
if (goal.cell.some(tile => tile.type.is_actor && tile !== actor))
if (dest.cell.some(tile => tile.type.is_actor && tile !== actor))
continue;
// 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 not especially undo-efficient
this.remove_tile(actor);
this.add_tile(actor, goal.cell);
this.add_tile(actor, dest.cell);
if (this.attempt_step(actor, actor.direction)) {
// Success, teleportation complete
// Sound plays from the origin cell simply because that's where the sfx player
// thinks the player is currently
this.sfx.play_once('teleport', cell);
// thinks the player is currently; position isn't updated til next turn
this.sfx.play_once('teleport', dest.cell);
break;
}
if (goal === teleporter)
// We've tried every teleporter, including the one they
// stepped on, so leave them on it
break;
// Otherwise, try the next one
}
}
}
@ -930,6 +897,39 @@ export class Level {
}
}
// Iterates over the grid in (reverse?) reading order and yields all tiles with the given name.
// The starting cell is iterated last.
*iter_tiles_in_reading_order(start_cell, name, reverse = false) {
let x = start_cell.x;
let y = start_cell.y;
while (true) {
if (reverse) {
x -= 1;
if (x < 0) {
x = this.width - 1;
y = (y - 1 + this.height) % this.height;
}
}
else {
x += 1;
if (x >= this.width) {
x = 0;
y = (y + 1) % this.height;
}
}
let cell = this.cells[y][x];
for (let tile of cell) {
if (tile.type.name === name) {
yield tile;
}
}
if (cell === start_cell)
return;
}
}
// -------------------------------------------------------------------------
// Undo handling

View File

@ -1,4 +1,5 @@
import { DIRECTIONS } from './defs.js';
import { random_choice } from './util.js';
// Draw layers
const LAYER_TERRAIN = 0;
@ -633,7 +634,7 @@ const TILE_TYPES = {
trap: {
draw_layer: LAYER_TERRAIN,
on_arrive(me, level, other) {
if (me.open) {
if (me.presses) {
// Lynx: Traps immediately eject their contents, if possible
// TODO compat this, cc2 doens't do it!
//level.attempt_step(other, other.direction);
@ -642,8 +643,33 @@ const TILE_TYPES = {
level.set_actor_stuck(other, true);
}
},
increment_presses(me, level, is_begin) {
level._set_prop(me, 'presses', (me.presses ?? 0) + 1);
if (me.presses === 1) {
// Free everything on us, if we went from 0 to 1 presses (i.e. closed to open)
for (let tile of Array.from(me.cell)) {
if (tile.type.is_actor) {
level.set_actor_stuck(tile, false);
// Forcibly move anything released from a trap, to keep it in sync with
// whatever pushed the button
level.attempt_step(tile, tile.direction);
}
}
}
},
decrement_presses(me, level) {
level._set_prop(me, 'presses', me.presses - 1);
if (me.presses === 0) {
// Trap everything on us, if we went from 1 to 0 presses (i.e. open to closed)
for (let tile of me.cell) {
if (tile.type.is_actor) {
level.set_actor_stuck(tile, true);
}
}
}
},
visual_state(me) {
if (me && me.open) {
if (me && me.presses) {
return 'open';
}
else {
@ -690,29 +716,32 @@ const TILE_TYPES = {
},
teleport_blue: {
draw_layer: LAYER_TERRAIN,
connects_to: 'teleport_blue',
connect_order: 'backward',
is_teleporter: true,
teleport_dest_order(me, level) {
return level.iter_tiles_in_reading_order(me.cell, 'teleport_blue', true);
},
},
teleport_red: {
draw_layer: LAYER_TERRAIN,
connects_to: 'teleport_red',
connect_order: 'forward',
is_teleporter: true,
teleport_dest_order(me, level) {
// FIXME you can control your exit direction from red teleporters
return level.iter_tiles_in_reading_order(me.cell, 'teleport_red');
},
},
teleport_green: {
draw_layer: LAYER_TERRAIN,
// connects_to: 'teleport_red',
// connect_order: 'forward',
// is_teleporter: true,
// FIXME completely different behavior from other teleporters
teleport_dest_order(me, level) {
// FIXME exit direction is random; unclear if it's any direction or only unblocked ones
let all = Array.from(level.iter_tiles_in_reading_order(me.cell, 'teleport_green'));
// FIXME this should use the lynxish rng
return [random_choice(all), me];
},
},
teleport_yellow: {
draw_layer: LAYER_TERRAIN,
connects_to: 'teleport_yellow',
connect_order: 'backward',
is_teleporter: true,
// FIXME special pickup behavior
teleport_dest_order(me, level) {
// FIXME special pickup behavior
return level.iter_tiles_in_reading_order(me.cell, 'teleport_yellow', true);
},
},
// FIXME do i want these as separate objects? what would they do, turn into each other? or should it be one with state?
flame_jet_off: {
@ -791,36 +820,32 @@ const TILE_TYPES = {
draw_layer: LAYER_TERRAIN,
connects_to: 'trap',
connect_order: 'forward',
on_arrive(me, level, other) {
level.sfx.play_once('button-press', me.cell);
if (me.connection && me.connection.cell) {
let trap = me.connection;
level._set_prop(trap, 'open', true);
for (let tile of trap.cell) {
if (tile.type.is_actor) {
if (tile.stuck) {
level.set_actor_stuck(tile, false);
}
// Forcibly move anything released from a trap, to keep
// it in sync with whatever pushed the button
level.attempt_step(tile, tile.direction);
on_begin(me, level) {
// Inform the trap of any actors that start out holding us down
let traps = Array.from(level.find_connections(me, 'trap'));
for (let tile of me.cell) {
if (tile.type.is_actor) {
for (let trap of traps) {
// FIXME this will try to move stuff and also populate undo buffer ~_~
trap.type.increment_presses(trap, level);
}
}
}
},
on_arrive(me, level, other) {
level.sfx.play_once('button-press', me.cell);
let trap = me.connection;
if (trap && trap.cell) {
trap.type.increment_presses(trap, level);
}
},
on_depart(me, level, other) {
// TODO this doesn't play if you walk straight across
level.sfx.play_once('button-release', me.cell);
if (me.connection && me.connection.cell) {
let trap = me.connection;
level._set_prop(trap, 'open', false);
for (let tile of trap.cell) {
if (tile.type.is_actor) {
level.set_actor_stuck(tile, true);
}
}
let trap = me.connection;
if (trap && trap.cell) {
trap.type.decrement_presses(trap, level);
}
},
},
@ -831,8 +856,9 @@ const TILE_TYPES = {
on_arrive(me, level, other) {
level.sfx.play_once('button-press', me.cell);
if (me.connection && me.connection.cell) {
me.connection.type.activate(me.connection, level);
let cloner = me.connection;
if (cloner && cloner.cell) {
cloner.type.activate(cloner, level);
}
},
on_depart(me, level, other) {