diff --git a/js/algorithms.js b/js/algorithms.js new file mode 100644 index 0000000..6d430e8 --- /dev/null +++ b/js/algorithms.js @@ -0,0 +1,109 @@ +import { DIRECTIONS, DIRECTION_ORDER } from './defs.js'; + +export function trace_floor_circuit(level, start_cell, start_edge, on_wire, on_dead_end) { + let is_first = true; + let pending = [[start_cell, start_edge]]; + let seen_cells = new Map; + while (pending.length > 0) { + let next = []; + for (let [cell, edge] of pending) { + let terrain = cell.get_terrain(); + if (! terrain) + continue; + + let edgeinfo = DIRECTIONS[edge]; + let seen_edges = seen_cells.get(cell) ?? 0; + if (seen_edges & edgeinfo.bit) + continue; + + // The wire comes in from this edge towards the center; see how it connects within this + // cell, then check for any neighbors + let connections = edgeinfo.bit; + if (! is_first && ((terrain.wire_directions ?? 0) & edgeinfo.bit) === 0) { + // There's not actually a wire here (but not if this is our starting cell, in which + // case we trust the caller) + if (on_dead_end) { + on_dead_end(terrain.cell, edge); + } + continue; + } + else if (terrain.type.wire_propagation_mode === 'none') { + // The wires in this tile never connect to each other + } + else if (terrain.wire_directions === 0x0f && terrain.type.wire_propagation_mode !== 'all') { + // This is a cross pattern, so only opposite edges connect + connections |= edgeinfo.opposite_bit; + } + else { + // Everything connects + connections |= terrain.wire_directions; + } + + seen_cells.set(cell, seen_edges | connections); + + if (on_wire) { + on_wire(terrain, connections); + } + + for (let [direction, dirinfo] of Object.entries(DIRECTIONS)) { + // Obviously don't go backwards, but that doesn't apply if this is our first pass + if (direction === edge && ! is_first) + continue; + + if ((connections & dirinfo.bit) === 0) + continue; + + let neighbor; + if ((terrain.wire_tunnel_directions ?? 0) & dirinfo.bit) { + // Search in this direction for a matching tunnel + neighbor = find_matching_wire_tunnel(level, cell.x, cell.y, direction); + } + else { + neighbor = level.get_neighboring_cell(cell, direction); + } + + /* + if (! neighbor || (((neighbor.get_terrain().wire_directions ?? 0) & dirinfo.opposite_bit) === 0)) { + console.log("bailing here", neighbor, direction); + continue; + } + */ + if (! neighbor) + continue; + + next.push([neighbor, dirinfo.opposite]); + } + } + pending = next; + is_first = false; + } +} + +export function find_matching_wire_tunnel(level, x, y, direction) { + let dirinfo = DIRECTIONS[direction]; + let [dx, dy] = dirinfo.movement; + let nesting = 0; + while (true) { + x += dx; + y += dy; + let candidate = level.cell(x, y); + if (! candidate) + return null; + + let neighbor = candidate.get_terrain(); + if (! neighbor) + continue; + + if ((neighbor.wire_tunnel_directions ?? 0) & dirinfo.opposite_bit) { + if (nesting === 0) { + return candidate; + } + else { + nesting -= 1; + } + } + if ((neighbor.wire_tunnel_directions ?? 0) & dirinfo.bit) { + nesting += 1; + } + } +} diff --git a/js/defs.js b/js/defs.js index b71f914..f06c763 100644 --- a/js/defs.js +++ b/js/defs.js @@ -4,6 +4,7 @@ export const DIRECTIONS = { north: { movement: [0, -1], bit: 0x01, + opposite_bit: 0x04, index: 0, action: 'up', left: 'west', @@ -13,6 +14,7 @@ export const DIRECTIONS = { south: { movement: [0, 1], bit: 0x04, + opposite_bit: 0x01, index: 2, action: 'down', left: 'east', @@ -22,6 +24,7 @@ export const DIRECTIONS = { west: { movement: [-1, 0], bit: 0x08, + opposite_bit: 0x02, index: 3, action: 'left', left: 'south', @@ -31,6 +34,7 @@ export const DIRECTIONS = { east: { movement: [1, 0], bit: 0x02, + opposite_bit: 0x08, index: 1, action: 'right', left: 'north', diff --git a/js/game.js b/js/game.js index 6bb9ea8..a148bd9 100644 --- a/js/game.js +++ b/js/game.js @@ -1,3 +1,4 @@ +import * as algorithms from './algorithms.js'; import { DIRECTIONS, DIRECTION_ORDER, INPUT_BITS, TICS_PER_SECOND } from './defs.js'; import { LevelInterface } from './format-base.js'; import TILE_TYPES from './tiletypes.js'; @@ -117,6 +118,9 @@ export class Tile { } } Tile.prototype.emitting_edges = 0; +Tile.prototype.powered_edges = 0; +Tile.prototype.wire_directions = 0; +Tile.prototype.wire_tunnel_directions = 0; export class Cell extends Array { constructor(x, y) { @@ -294,8 +298,6 @@ export class Cell extends Array { return direction; } } -Cell.prototype.prev_powered_edges = 0; -Cell.prototype.powered_edges = 0; // The undo stack is implemented with a ring buffer, and this is its size. One entry per tic. // Based on Chrome measurements made against the pathological level CCLP4 #40 (Periodic Lasers) and @@ -376,7 +378,6 @@ export class Level extends LevelInterface { let n = 0; let connectables = []; - this.power_sources = []; this.players = []; // FIXME handle traps correctly: // - if an actor is in the cell, set the trap to open and unstick everything in it @@ -397,10 +398,6 @@ export class Level extends LevelInterface { tile.hint_text = template_tile.hint_text ?? null; } - if (tile.type.is_power_source) { - this.power_sources.push(tile); - } - if (tile.type.is_real_player) { this.players.push(tile); } @@ -484,6 +481,111 @@ export class Level extends LevelInterface { } } + // Build circuits out of connected wires + // TODO document this idea + this.circuits = []; + this.power_sources = []; + let wired_outputs = new Set; + this.wired_outputs = []; + let add_to_edge_map = (map, item, edges) => { + map.set(item, (map.get(item) ?? 0) | edges); + }; + for (let cell of this.linear_cells) { + // We're interested in static circuitry, which means terrain + let terrain = cell.get_terrain(); + if (! terrain) // ?! + continue; + + if (terrain.type.is_power_source) { + this.power_sources.push(terrain); + } + + let wire_directions = terrain.wire_directions; + if (! wire_directions && ! terrain.wire_tunnel_directions) { + // No wires, not interesting... unless it's a logic gate, which defines its own + // wires! We only care about outgoing ones here, on the off chance that they point + // directly into a non-wired tile, in which case a wire scan won't find them + if (terrain.type.name === 'logic_gate') { + let dir = terrain.direction; + let cxns = terrain.type._gate_types[terrain.gate_type]; + for (let i = 0; i < 4; i++) { + let cxn = cxns[i]; + if (cxn && cxn.match(/^out/)) { + wire_directions |= DIRECTIONS[dir].bit; + } + dir = DIRECTIONS[dir].right; + } + } + else { + continue; + } + } + + for (let [direction, dirinfo] of Object.entries(DIRECTIONS)) { + if (! ((wire_directions | terrain.wire_tunnel_directions) & dirinfo.bit)) + continue; + + if (terrain.circuits && terrain.circuits[dirinfo.index]) + continue; + + let circuit = { + is_powered: false, + tiles: new Map, + inputs: new Map, + }; + this.circuits.push(circuit); + // At last, a wired cell edge we have not yet handled. Floodfill from here + algorithms.trace_floor_circuit( + this, terrain.cell, direction, + // Wire handling + (tile, edges) => { + if (! tile.circuits) { + tile.circuits = [null, null, null, null]; + } + for (let [direction, dirinfo] of Object.entries(DIRECTIONS)) { + if (edges & dirinfo.bit) { + tile.circuits[dirinfo.index] = circuit; + } + } + add_to_edge_map(circuit.tiles, tile, edges); + + if (tile.type.is_power_source) { + // TODO could just do this in a pass afterwards + add_to_edge_map(circuit.inputs, tile, edges); + } + }, + // Dead end handling (potentially logic gates, etc.) + (cell, edge) => { + for (let tile of cell) { + if (tile.type.name === 'logic_gate') { + // Logic gates are the one non-wired tile that get attached to circuits, + // mostly so blue teleporters can follow them + if (! tile.circuits) { + tile.circuits = [null, null, null, null]; + } + tile.circuits[DIRECTIONS[edge].index] = circuit; + + let wire = tile.type._gate_types[tile.gate_type][ + (DIRECTIONS[edge].index - DIRECTIONS[tile.direction].index + 4) % 4]; + if (! wire) + return; + add_to_edge_map(circuit.tiles, tile, DIRECTIONS[edge].bit); + if (wire.match(/^out/)) { + add_to_edge_map(circuit.inputs, tile, DIRECTIONS[edge].bit); + } + } + else if (tile.type.on_power) { + add_to_edge_map(circuit.tiles, tile, DIRECTIONS[edge].bit); + wired_outputs.add(tile); + } + } + }, + ); + } + } + this.wired_outputs = Array.from(wired_outputs); + this.wired_outputs.sort((a, b) => this.coords_to_scalar(a.cell.x, a.cell.y) - this.coords_to_scalar(b.cell.x, b.cell.y)); + // Finally, let all tiles do any custom init behavior for (let cell of this.linear_cells) { for (let tile of cell) { @@ -569,9 +671,8 @@ export class Level extends LevelInterface { this.p1_released |= ~p1_input; // Action keys released since we last checked them this.swap_player1 = false; - // Used for various tic-local effects; don't need to be undoable - // TODO maybe this should be undone anyway so rewind looks better? - this.player.is_blocked = false; + // This effect only lasts one tic, after which we can move again + this._set_tile_prop(this.player, 'is_blocked', false); this.sfx.set_player_position(this.player.cell); @@ -720,7 +821,7 @@ export class Level extends LevelInterface { // Track whether the player is blocked, for visual effect if (actor === this.player && actor.decision && ! success) { this.sfx.play_once('blocked'); - actor.is_blocked = true; + this._set_tile_prop(actor, 'is_blocked', true); } } @@ -1397,178 +1498,130 @@ export class Level extends LevelInterface { } } - // Update the state of all wired tiles in the game. - // XXX need to be clear on the order of events here. say everything starts out unpowered. - // then: - // 1. you step on a pink button, which flags itself as going to be powered next frame - // 2. this pass happens. every unpowered-but-wired cell is inspected. if a powered one is - // found, floodfill from there - // FIXME can probably skip this if we know there are no wires at all, like in a CCL, or just an - // unwired map - // FIXME this feels inefficient. most of the time none of the inputs have changed so none of - // this needs to happen at all - // FIXME none of this is currently undoable update_wiring() { - // FIXME: - // - make this undoable :( - // - blue tele, red tele, and pink button have different connections - // - would like to reuse the walk for blue teles + if (this.circuits.length === 0) + return; - // Gather every tile that's emitting power. Along the way, check whether any of them have - // changed since last tic, so we can skip this work entirely if none did - let neighbors = []; + // Prepare a big slab of undo. The only thing we directly change here (aside from + // emitting_edges, a normal tile property) is Tile.powered_edges, which tends to change for + // large numbers of tiles at a time, so store it all in one map and undo it in one shot. + let powered_edges_changes = new Map; + let _set_edges = (tile, new_edges) => { + if (powered_edges_changes.has(tile)) { + if (powered_edges_changes.get(tile) === new_edges) { + powered_edges_changes.delete(tile); + } + } + else { + powered_edges_changes.set(tile, tile.powered_edges); + } + tile.powered_edges = new_edges; + }; + let power_edges = (tile, edges) => { + let new_edges = tile.powered_edges | edges; + _set_edges(tile, new_edges); + }; + let depower_edges = (tile, edges) => { + let new_edges = tile.powered_edges & ~edges; + _set_edges(tile, new_edges); + }; + + // Update the state of any tiles that can generate power. If none of them changed since + // last wiring update, stop here. First, static power sources. let any_changed = false; for (let tile of this.power_sources) { if (! tile.cell) continue; let emitting = tile.type.get_emitting_edges(tile, this); - if (emitting) { - neighbors.push([tile.cell, emitting]); - } if (emitting !== tile.emitting_edges) { any_changed = true; - tile.emitting_edges = emitting; + this._set_tile_prop(tile, 'emitting_edges', emitting); } } - // Also check actors, since any of them might be holding a lightning bolt (argh) + // Next, actors who are standing still, on floor, and holding a lightning bolt + let externally_powered_circuits = new Set; for (let actor of this.actors) { if (! actor.cell) continue; - // Only count when they're on a floor tile AND not in transit! - let emitting = null; + let emitting = 0; if (actor.movement_cooldown === 0 && actor.has_item('lightning_bolt')) { let wired_tile = actor.cell.get_wired_tile(); if (wired_tile && (wired_tile === actor || wired_tile.type.name === 'floor')) { emitting = wired_tile.wire_directions; - neighbors.push([actor.cell, wired_tile.wire_directions]); + for (let circuit of wired_tile.circuits) { + if (circuit) { + externally_powered_circuits.add(circuit); + } + } } } if (emitting !== actor.emitting_edges) { any_changed = true; - actor.emitting_edges = emitting; + this._set_tile_prop(actor, 'emitting_edges', emitting); } } - // If none changed, we're done + if (! any_changed) return; - // Turn off power to every cell - for (let cell of this.linear_cells) { - cell.prev_powered_edges = cell.powered_edges; - cell.powered_edges = 0; + for (let tile of this.wired_outputs) { + // This is only used within this function, no need to undo + // TODO if this can overlap with power_sources then this is too late? + tile._prev_powered_edges = tile.powered_edges; } - // Iterate over emitters and flood-fill outwards one edge at a time - // propagated it via flood-fill through neighboring wires - while (neighbors.length > 0) { - let [cell, source_direction] = neighbors.shift(); - let wire = cell.get_wired_tile(); + // Now go through every circuit, compute whether it's powered, and if that changed, inform + // its outputs + let circuit_changes = new Map; + for (let circuit of this.circuits) { + let is_powered = false; - // Power this cell - if (typeof(source_direction) === 'number') { - // This cell is emitting power itself, and the source direction is actually a - // bitmask of directions - cell.powered_edges = source_direction; + if (externally_powered_circuits.has(circuit)) { + is_powered = true; } else { - let bit = DIRECTIONS[source_direction].bit; - if (wire === null || (wire.wire_directions & bit) === 0) { - // No wire on this side, so the power doesn't actually propagate, but it DOES - // stay on this edge (so if this is e.g. a purple tile, it'll be powered) - cell.powered_edges |= bit; - continue; - } - - // Common case: power entering a wired edge and propagating outwards. There are a - // couple special cases: - if (wire.type.wire_propagation_mode === 'none') { - // This tile type has wires, but none of them connect to each other - cell.powered_edges |= bit; - continue; - } - else if (wire.wire_directions === 0x0f && wire.type.wire_propagation_mode !== 'all') { - // If all four wires are present, they don't actually make a four-way - // connection, but two straight wires that don't connect to each other (with the - // exception of blue teleporters) - cell.powered_edges |= bit; - cell.powered_edges |= DIRECTIONS[DIRECTIONS[source_direction].opposite].bit; - } - else { - cell.powered_edges = wire.wire_directions; + for (let [input_tile, edges] of circuit.inputs.entries()) { + if (input_tile.emitting_edges & edges) { + is_powered = true; + break; + } } } - // Propagate current to neighbors - for (let [direction, dirinfo] of Object.entries(DIRECTIONS)) { - if (direction === source_direction) - continue; - if ((cell.powered_edges & dirinfo.bit) === 0) - continue; + let was_powered = circuit.is_powered; + if (is_powered === was_powered) + continue; - let neighbor, neighbor_wire; - let opposite_bit = DIRECTIONS[dirinfo.opposite].bit; - if (wire && (wire.wire_tunnel_directions & dirinfo.bit)) { - // Search in the given direction until we find a matching tunnel - let x = cell.x; - let y = cell.y; - let nesting = 0; - while (true) { - x += dirinfo.movement[0]; - y += dirinfo.movement[1]; - let candidate = this.cell(x, y); - if (! candidate) - break; - neighbor_wire = candidate.get_wired_tile(); - if (neighbor_wire) { - if ((neighbor_wire.wire_tunnel_directions ?? 0) & opposite_bit) { - if (nesting === 0) { - neighbor = candidate; - break; - } - else { - nesting -= 1; - } - } - if ((neighbor_wire.wire_tunnel_directions ?? 0) & dirinfo.bit) { - nesting += 1; - } - } - } + circuit.is_powered = is_powered; + circuit_changes.set(circuit, was_powered); + + for (let [tile, edges] of circuit.tiles.entries()) { + if (is_powered) { + power_edges(tile, edges); } else { - // No tunnel; this is easy - neighbor = this.get_neighboring_cell(cell, direction); - if (neighbor) { - neighbor_wire = neighbor.get_wired_tile(); - } - } - - if (neighbor && (neighbor.powered_edges & opposite_bit) === 0 && - // Unwired tiles are OK; they might be something activated by power. - // Wired tiles that do NOT connect to us are ignored. - (! neighbor_wire || neighbor_wire.wire_directions & opposite_bit)) - { - neighbors.push([neighbor, dirinfo.opposite]); + depower_edges(tile, edges); } } } - // Inform any affected cells of power changes - for (let cell of this.linear_cells) { - if ((cell.prev_powered_edges === 0) !== (cell.powered_edges === 0)) { - let method = cell.powered_edges ? 'on_power' : 'on_depower'; - for (let tile of cell) { - if (tile.type[method]) { - tile.type[method](tile, this); - } - } + for (let tile of this.wired_outputs) { + if (tile.powered_edges && ! tile._prev_powered_edges && tile.type.on_power) { + tile.type.on_power(tile, this); + } + else if (! tile.powered_edges && tile._prev_powered_edges && tile.type.on_depower) { + tile.type.on_depower(tile, this); } } - } - // Performs a depth-first search for connected wires and wire objects, extending out from the - // given starting cell - *follow_circuit(cell) { + this.pending_undo.push(() => { + for (let [tile, edges] of powered_edges_changes.entries()) { + tile.powered_edges = edges; + } + for (let [circuit, is_powered] of circuit_changes.entries()) { + circuit.is_powered = is_powered; + } + }); } // ------------------------------------------------------------------------- @@ -1630,9 +1683,14 @@ export class Level extends LevelInterface { } } - is_cell_wired(cell) { - for (let direction of Object.keys(DIRECTIONS)) { - let neighbor = this.get_neighboring_cell(cell, direction); + // FIXME require_stub should really just care whether we ourselves /can/ contain wire, and also + // we should check that on our neighbor + is_tile_wired(tile, require_stub = true) { + for (let [direction, dirinfo] of Object.entries(DIRECTIONS)) { + if (require_stub && (tile.wire_directions & dirinfo.bit) === 0) + continue; + + let neighbor = this.get_neighboring_cell(tile.cell, direction); if (! neighbor) continue; @@ -1640,7 +1698,11 @@ export class Level extends LevelInterface { if (! wired) continue; - if (wired.wire_directions & DIRECTIONS[DIRECTIONS[direction].opposite].bit) + if (wired.type.wire_propagation_mode === 'none' && ! wired.type.is_power_source) + // Being next to e.g. a red teleporter doesn't count (but pink button is ok) + continue; + + if (wired.wire_directions & dirinfo.opposite_bit) return true; } return false; diff --git a/js/tileset.js b/js/tileset.js index d6c9a57..b777e28 100644 --- a/js/tileset.js +++ b/js/tileset.js @@ -985,18 +985,18 @@ export class Tileset { this._draw_fourway_tile_power(tile, 0x0f, blit); } else { - if (tile.cell.powered_edges & DIRECTIONS[tile.direction].bit) { + if (tile.powered_edges & DIRECTIONS[tile.direction].bit) { // Output (on top) let [x0, y0, x1, y1] = this._rotate(tile.direction, 0.5 - r, 0, 0.5 + r, 0.5); blit(powered_coords[0], powered_coords[1], x0, y0, x1 - x0, y1 - y0); } - if (tile.cell.powered_edges & DIRECTIONS[DIRECTIONS[tile.direction].right].bit) { + if (tile.powered_edges & DIRECTIONS[DIRECTIONS[tile.direction].right].bit) { // Right input, which includes the middle // This actually covers the entire lower right corner, for bent inputs. let [x0, y0, x1, y1] = this._rotate(tile.direction, 0.5 - r, 0.5 - r, 1, 1); blit(powered_coords[0], powered_coords[1], x0, y0, x1 - x0, y1 - y0); } - if (tile.cell.powered_edges & DIRECTIONS[DIRECTIONS[tile.direction].left].bit) { + if (tile.powered_edges & DIRECTIONS[DIRECTIONS[tile.direction].left].bit) { // Left input, which does not include the middle // This actually covers the entire lower left corner, for bent inputs. let [x0, y0, x1, y1] = this._rotate(tile.direction, 0, 0.5 - r, 0.5 - r, 1); @@ -1017,7 +1017,7 @@ export class Tileset { _draw_fourway_tile_power(tile, wires, blit) { // Draw the unpowered tile underneath, if any edge is unpowered (and in fact if /none/ of it // is powered then we're done here) - let powered = (tile.cell ? tile.cell.powered_edges : 0) & wires; + let powered = (tile.cell ? tile.powered_edges : 0) & wires; if (! tile.cell || powered !== tile.wire_directions) { this._draw_fourway_power_underlay(this.layout['#unpowered'], wires, blit); if (! tile.cell || powered === 0) diff --git a/js/tiletypes.js b/js/tiletypes.js index 752aae8..64b65cf 100644 --- a/js/tiletypes.js +++ b/js/tiletypes.js @@ -466,7 +466,7 @@ const TILE_TYPES = { level._set_tile_prop(me, 'entered_direction', other.direction); }, on_depart(me, level, other) { - if (! level.is_cell_wired(me.cell)) { + if (! level.is_tile_wired(me, false)) { me.type._switch_track(me, level); } }, @@ -1104,6 +1104,8 @@ const TILE_TYPES = { }, transmogrifier: { draw_layer: DRAW_LAYERS.terrain, + // C2M technically supports wires in transmogrifiers, but they don't do anything + wire_propagation_mode: 'none', _mogrifications: { player: 'player2', player2: 'player', @@ -1128,15 +1130,12 @@ const TILE_TYPES = { teeth_timid: 'teeth', }, _blob_mogrifications: ['ball', 'walker', 'fireball', 'glider', 'paramecium', 'bug', 'tank_blue', 'teeth', 'teeth_timid'], - on_ready(me, level) { - me.is_powered = false; - }, on_arrive(me, level, other) { // Note: Transmogrifiers technically contain wires the way teleports do, and CC2 uses // the presence and poweredness of those wires to determine whether the transmogrifier // should appear to be on or off, but the /functionality/ is controlled entirely by // whether an adjoining cell carries current to our edge, like a railroad or cloner - if (level.is_cell_wired(me.cell) && ! me.is_powered) + if (level.is_tile_wired(me, false) && ! me.powered_edges) return; let name = other.type.name; if (me.type._mogrifications[name]) { @@ -1149,30 +1148,33 @@ const TILE_TYPES = { } }, on_power(me, level) { - level._set_tile_prop(me, 'is_powered', true); - }, - on_depower(me, level) { - level._set_tile_prop(me, 'is_powered', false); + // No need to do anything, we just need this here as a signal that our .powered_edges + // needs to be updated }, }, - // FIXME blue teleporters transmit current 4 ways. red don't transmit it at all teleport_blue: { draw_layer: DRAW_LAYERS.terrain, wire_propagation_mode: 'all', *teleport_dest_order(me, level, other) { let exit_direction = other.direction; + // Note that unlike other tiles that care about whether they're wired, a blue teleporter + // considers itself part of a network if it contains any wires at all, regardless of + // whether they connect to anything if (! me.wire_directions) { // TODO cc2 has a bug where, once it wraps around to the bottom right, it seems to // forget that it was ever looking for an unwired teleport and will just grab the // first one it sees for (let dest of level.iter_tiles_in_reading_order(me.cell, 'teleport_blue', true)) { - yield [dest, exit_direction]; + if (! dest.wire_directions) { + yield [dest, exit_direction]; + } } + return; } - // Wired blue teleports form a network, which means we have to walk all wires from this - // point, collect a list of all possible blue teleports, and then sort them so we can - // try them in the right order. + // Wired blue teleports form an isolated network, so we have to walk the circuit we're + // on, collect a list of all possible blue teleports, and then sort them so we can try + // them in the right order. // Complicating this somewhat, logic gates act as diodes: we can walk through a logic // gate if we're connected to one of its inputs AND its output is enabled, but we can't // walk "backwards" through it. @@ -1185,66 +1187,45 @@ const TILE_TYPES = { // behavior is not and will never be emulated. No level in CC2 or even CC2LP1 uses blue // teleporters wired into logic gates, so even the ordering is not interesting imo.) // Anyway, let's do a breadth-first search for teleporters. - let seeds = [me.cell]; - let found = []; - let seen = new Set; - while (seeds.length > 0) { - let next_seeds = []; - for (let cell of seeds) { - if (seen.has(cell)) - continue; - seen.add(cell); + let walked_circuits = new Set; + let candidate_teleporters = new Set; + let circuits = me.circuits; + for (let i = 0; i < circuits.length; i++) { + let circuit = circuits[i]; + if (! circuit || walked_circuits.has(circuit)) + continue; + walked_circuits.add(circuit); - let wired = cell.get_wired_tile(); - if (! wired || wired.wire_directions === 0) - continue; - - // Check for a blue teleporter - let dest; - for (let tile of cell) { - if (tile.type.name === 'teleport_blue') { - found.push(tile); - break; + for (let [tile, edges] of circuit.tiles.entries()) { + if (tile.type === me.type) { + candidate_teleporters.add(tile); + } + else if (tile.type.name === 'logic_gate' && ! circuit.inputs.get(tile)) { + // This logic gate is functioning as an output, so walk through it and also + // trace any circuits that treat it as an input (as long as those circuits + // are currently powered) + for (let subcircuit of tile.circuits) { + if (subcircuit && subcircuit.is_powered && subcircuit.inputs.get(tile)) { + circuits.push(subcircuit); + } } } - - // Search our neighbors - for (let direction of Object.keys(DIRECTIONS)) { - if (! (wired.wire_directions & DIRECTIONS[direction].bit)) - continue; - let neighbor = level.get_neighboring_cell(cell, direction); - if (! neighbor || seen.has(neighbor)) - continue; - let neighbor_wired = neighbor.get_wired_tile(); - if (! neighbor_wired) - continue; - if (! (neighbor_wired.wire_directions & DIRECTIONS[DIRECTIONS[direction].opposite].bit)) - continue; - - // TODO check for logic gate - // TODO need to know the direction we came in so we can get the right ones - // going out! - // FIXME this needs to understand crossings, and both this and basic wiring - // need to understand how blue/red teleports convey current - - next_seeds.push(neighbor); - } } - seeds = next_seeds; } - // Now that we have a list of candidate exits, sort it in reverse reading order, + // Now that we have a set of candidate destinations, sort it in reverse reading order, // starting from ourselves. Easiest way to do this is to make a map of cell indices, // shifted so that we're at zero, then sort in reverse let dest_indices = new Map; let our_index = me.cell.x + me.cell.y * level.size_x; let level_size = level.size_x * level.size_y; - for (let dest of found) { + for (let dest of candidate_teleporters) { dest_indices.set(dest, ( (dest.cell.x + dest.cell.y * level.size_x) - our_index + level_size ) % level_size); } + let found = Array.from(candidate_teleporters); found.sort((a, b) => dest_indices.get(b) - dest_indices.get(a)); for (let dest of found) { yield [dest, exit_direction]; @@ -1255,8 +1236,11 @@ const TILE_TYPES = { draw_layer: DRAW_LAYERS.terrain, wire_propagation_mode: 'none', teleport_allow_override: true, - _is_active(me) { - return ! (me.wire_directions && (me.cell.powered_edges & me.wire_directions) === 0); + _is_active(me, level) { + // FIXME must be connected to something that can convey current: a wire, a switch, a + // blue teleporter, etc; NOT nothing, a wall, a transmogrifier, a force floor, etc. + // this is also how blue teleporters, transmogrifiers, and railroads work! + return me.powered_edges || ! level.is_tile_wired(me); }, *teleport_dest_order(me, level, other) { // Wired red teleporters can be turned off, which disconnects them from every other red @@ -1264,9 +1248,8 @@ const TILE_TYPES = { // A red teleporter is considered wired only if it has wires itself. However, CC2 also // has the bizarre behavior of NOT considering a red teleporter wired if none of its // wires are directly connected to another neighboring wire. - // FIXME implement that, merge current code with is_cell_wired let iterable; - if (this._is_active(me)) { + if (this._is_active(me, level)) { iterable = level.iter_tiles_in_reading_order(me.cell, 'teleport_red'); } else { @@ -1274,7 +1257,7 @@ const TILE_TYPES = { } let exit_direction = other.direction; for (let tile of iterable) { - if (tile === me || this._is_active(tile)) { + if (tile === me || this._is_active(tile, level)) { // Red teleporters allow exiting in any direction, searching clockwise yield [tile, exit_direction]; yield [tile, DIRECTIONS[exit_direction].right]; @@ -1283,7 +1266,7 @@ const TILE_TYPES = { } } }, - // TODO inactive ones don't animate + // TODO inactive ones don't animate; transmogrifiers too /* visual_state(me) { return this._is_active(me) ? 'active' : 'inactive'; @@ -1615,10 +1598,10 @@ const TILE_TYPES = { let cxn = me.gate_def[i]; let dirinfo = DIRECTIONS[dir]; if (cxn === 'in0') { - input0 = (me.cell.powered_edges & dirinfo.bit) !== 0; + input0 = (me.powered_edges & dirinfo.bit) !== 0; } else if (cxn === 'in1') { - input1 = (me.cell.powered_edges & dirinfo.bit) !== 0; + input1 = (me.powered_edges & dirinfo.bit) !== 0; } else if (cxn === 'out0') { outbit0 = dirinfo.bit;