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