From f4363b8fda3e160b69de6c94d3ef3db40c680ef2 Mon Sep 17 00:00:00 2001 From: "Eevee (Evelyn Woods)" Date: Wed, 30 Sep 2020 02:11:17 -0600 Subject: [PATCH] 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 --- js/format-dat.js | 1 + js/format-util.js | 1 + js/game.js | 124 +++++++++++++++++++++++----------------------- js/tiletypes.js | 108 +++++++++++++++++++++++++--------------- 4 files changed, 131 insertions(+), 103 deletions(-) diff --git a/js/format-dat.js b/js/format-dat.js index d77c7fa..8238b5d 100644 --- a/js/format-dat.js +++ b/js/format-dat.js @@ -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; diff --git a/js/format-util.js b/js/format-util.js index 04de65c..c74eb49 100644 --- a/js/format-util.js +++ b/js/format-util.js @@ -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 = {}; diff --git a/js/game.js b/js/game.js index 084947a..83aa781 100644 --- a/js/game.js +++ b/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 diff --git a/js/tiletypes.js b/js/tiletypes.js index 569d02e..743f1e9 100644 --- a/js/tiletypes.js +++ b/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) {