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) { function parse_level(buf, number) {
let level = new util.StoredLevel(number); let level = new util.StoredLevel(number);
level.has_custom_connections = true;
// Map size is always fixed as 32x32 in CC1 // Map size is always fixed as 32x32 in CC1
level.size_x = 32; level.size_x = 32;
level.size_y = 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 // Maps of button positions to trap/cloner positions, as scalar indexes
// in the linear cell list // in the linear cell list
// TODO merge these imo // TODO merge these imo
this.has_custom_connections = false;
this.custom_trap_wiring = {}; this.custom_trap_wiring = {};
this.custom_cloner_wiring = {}; this.custom_cloner_wiring = {};

View File

@ -272,60 +272,37 @@ export class Level {
let x = cell.x; let x = cell.x;
let y = cell.y; let y = cell.y;
let goal = connectable.type.connects_to; let goal = connectable.type.connects_to;
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; if (this.stored_level.has_custom_connections) {
let target_cell_n = null; let n = this.stored_level.coords_to_scalar(x, y);
if (goal === 'trap') { let target_cell_n = null;
target_cell_n = this.stored_level.custom_trap_wiring[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; 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 if (target_cell_n && target_cell_n < this.width * this.height) {
let target_cell_x = target_cell_n % this.width; let [tx, ty] = this.stored_level.scalar_to_coords(target_cell_n);
let target_cell_y = Math.floor(target_cell_n / this.width); for (let tile of this.cells[ty][tx]) {
for (let tile of this.cells[target_cell_y][target_cell_x]) { if (tile.type.name === goal) {
if (tile.type.name === goal) { connectable.connection = tile;
connectable.connection = tile; break;
found = true; }
break;
} }
} }
if (found) continue;
continue;
} }
// Otherwise, look in reading order // Otherwise, look in reading order
let direction = 1; for (let tile of this.iter_tiles_in_reading_order(cell, goal)) {
if (connectable.type.connect_order === 'backward') { // TODO ideally this should be a weak connection somehow, since dynamite can destroy
direction = -1; // 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); this.remove_tile(tile);
} }
else if (tile.type.is_teleporter) { else if (tile.type.teleport_dest_order) {
teleporter = tile; teleporter = tile;
} }
else if (tile.type.on_arrive) { else if (tile.type.on_arrive) {
@ -887,33 +864,23 @@ export class Level {
// Handle teleporting, now that the dust has cleared // Handle teleporting, now that the dust has cleared
// FIXME something funny happening here, your input isn't ignore while walking out of it? // FIXME something funny happening here, your input isn't ignore while walking out of it?
if (teleporter) { if (teleporter) {
let goal = teleporter; for (let dest of teleporter.type.teleport_dest_order(teleporter, this)) {
// TODO in pathological cases this might infinite loop
while (true) {
goal = goal.connection;
// Teleporters already containing an actor are blocked and unusable // 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; continue;
// 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 not especially undo-efficient // XXX not especially undo-efficient
this.remove_tile(actor); this.remove_tile(actor);
this.add_tile(actor, goal.cell); this.add_tile(actor, dest.cell);
if (this.attempt_step(actor, actor.direction)) { if (this.attempt_step(actor, actor.direction)) {
// Success, teleportation complete // Success, teleportation complete
// Sound plays from the origin cell simply because that's where the sfx player // Sound plays from the origin cell simply because that's where the sfx player
// thinks the player is currently // thinks the player is currently; position isn't updated til next turn
this.sfx.play_once('teleport', cell); this.sfx.play_once('teleport', dest.cell);
break; 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 // Undo handling

View File

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