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:
parent
6df0c96d1b
commit
f4363b8fda
@ -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;
|
||||
|
||||
@ -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 = {};
|
||||
|
||||
|
||||
124
js/game.js
124
js/game.js
@ -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
|
||||
|
||||
|
||||
108
js/tiletypes.js
108
js/tiletypes.js
@ -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) {
|
||||
|
||||
Loading…
Reference in New Issue
Block a user