Fix and clean up wiring

- Fixed a gigantic bug where, due to a typo, a new circuit was created
  for every single wire segment.  Oops!

- The wiring phase now has somewhat fewer intermediate parts.

- Power-generating tiles have an explicit update phase, rather than
  updating in a method whose name starts with "get".

Anyway it's slightly faster than when I started and that's nice.

Only drawback is that circuit recalculation doesn't quite undo
correctly, but I think the effect is only visual.
This commit is contained in:
Eevee (Evelyn Woods) 2024-05-06 20:03:37 -06:00
parent aa4b3f3794
commit 559730eae4
3 changed files with 123 additions and 126 deletions

View File

@ -157,7 +157,7 @@ export function trace_floor_circuit(levelish, actor_mode, start_cell, start_edge
circuit.add_tile_edge(tile, connections); circuit.add_tile_edge(tile, connections);
if (tile.type.is_power_source) { if (tile.type.update_power_emission) {
// TODO could just do this in a pass afterwards? // TODO could just do this in a pass afterwards?
circuit.add_input_edge(tile, connections); circuit.add_input_edge(tile, connections);
} }

View File

@ -416,6 +416,7 @@ export class Level extends LevelInterface {
let n = 0; let n = 0;
let connectables = []; let connectables = [];
this.power_sources = [];
this.remaining_players = 0; this.remaining_players = 0;
this.ankh_tile = null; this.ankh_tile = null;
// If there's exactly one yellow teleporter when the level loads, it cannot be picked up // If there's exactly one yellow teleporter when the level loads, it cannot be picked up
@ -465,6 +466,10 @@ export class Level extends LevelInterface {
connectables.push(tile); connectables.push(tile);
} }
if (tile.type.update_power_emission) {
this.power_sources.push(tile);
}
if (tile.type.name === 'teleport_yellow' && ! this.allow_taking_yellow_teleporters) { if (tile.type.name === 'teleport_yellow' && ! this.allow_taking_yellow_teleporters) {
yellow_teleporter_count += 1; yellow_teleporter_count += 1;
if (yellow_teleporter_count > 1) { if (yellow_teleporter_count > 1) {
@ -562,7 +567,6 @@ export class Level extends LevelInterface {
// TODO moving a circuit block should only need to invalidate the circuits it touches // TODO moving a circuit block should only need to invalidate the circuits it touches
this.circuits = []; this.circuits = [];
this.power_sources = [];
let wired_outputs = new Set; let wired_outputs = new Set;
let seen_edges = new Map; let seen_edges = new Map;
for (let cell of this.linear_cells) { for (let cell of this.linear_cells) {
@ -572,10 +576,6 @@ export class Level extends LevelInterface {
if (! terrain) // ?! if (! terrain) // ?!
continue; continue;
if (terrain.type.is_power_source) {
this.power_sources.push(terrain);
}
let actor = cell.get_actor(); let actor = cell.get_actor();
let wire_directions = terrain.wire_directions; let wire_directions = terrain.wire_directions;
if (actor && actor.contains_wire && if (actor && actor.contains_wire &&
@ -625,7 +625,7 @@ export class Level extends LevelInterface {
// Search the circuit for tiles that act as outputs, so we can check whether to // Search the circuit for tiles that act as outputs, so we can check whether to
// update them during each wire phase // update them during each wire phase
for (let [tile, edges] of circuit.tiles) { for (let [tile, edges] of circuit.tiles) {
seen_edges.set((seen_edges.get(tile) ?? 0) | edges); seen_edges.set(tile, (seen_edges.get(tile) ?? 0) | edges);
if (tile.type.on_power) { if (tile.type.on_power) {
wired_outputs.add(tile); wired_outputs.add(tile);
} }
@ -2359,62 +2359,50 @@ export class Level extends LevelInterface {
// Wiring ----------------------------------------------------------------------------------------- // Wiring -----------------------------------------------------------------------------------------
_do_wire_phase() { _do_wire_phase() {
let force_next_wire_phase = false;
if (this.recalculate_circuitry_next_wire_phase) { if (this.recalculate_circuitry_next_wire_phase) {
this.recalculate_circuitry();
this.recalculate_circuitry_next_wire_phase = false;
force_next_wire_phase = true;
// This property doesn't tend to last beyond a single tic, but if we recalculate now, we // This property doesn't tend to last beyond a single tic, but if we recalculate now, we
// also need to recalculate if we undo beyond this point. So set it as a level prop, // also need to recalculate if we undo beyond this point. So set it as a level prop,
// which after an undo, will then cause us to recalculate the next time we advance // which after an undo, will then cause us to recalculate the next time we advance
if (this.undo_enabled) { if (this.undo_enabled) {
this.pending_undo.level_props.recalculate_circuitry_next_wire_phase = true; this.pending_undo.level_props.recalculate_circuitry_next_wire_phase = true;
// Since we're about to invalidate a bunch of circuitry, be safe and store ALL the
// power states
this.pending_undo.circuit_power_changes = new Map(
this.circuits.map(circuit => [circuit, circuit.is_powered]));
} }
this.recalculate_circuitry();
this.recalculate_circuitry_next_wire_phase = false;
} }
if (this.circuits.length === 0) if (this.circuits.length === 0)
return; return;
// Update the state of any tiles that can generate power. If none of them changed since for (let circuit of this.circuits) {
// last wiring update, stop here. First, static power sources. circuit._was_powered = circuit.is_powered;
let any_changed = false; circuit.is_powered = false;
}
// Update the state of any tiles that can generate power. First, static power sources
for (let tile of this.power_sources) { for (let tile of this.power_sources) {
if (! tile.cell) if (tile.type.update_power_emission) {
continue; tile.type.update_power_emission(tile, this);
let emitting = 0;
if (tile.type.get_emitting_edges) {
// This method may not exist any more, if the tile was destroyed by e.g. dynamite
emitting = tile.type.get_emitting_edges(tile, this);
}
if (emitting !== tile.emitting_edges) {
any_changed = true;
this._set_tile_prop(tile, 'emitting_edges', emitting);
}
}
// Next, actors who are standing still, on floor/electrified, and holding a lightning bolt
let externally_powered_circuits = new Set;
for (let actor of this.actors) {
if (! actor.cell)
continue;
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.can_be_powered_by_actor)) {
emitting = wired_tile.wire_directions;
for (let circuit of this.cells_to_circuits.get(this.cell_to_scalar(wired_tile.cell))) {
externally_powered_circuits.add(circuit);
}
}
}
if (emitting !== actor.emitting_edges) {
any_changed = true;
this._set_tile_prop(actor, 'emitting_edges', emitting);
} }
} }
if (! any_changed && ! force_next_wire_phase) { // Next, actors who are standing still, on floor/electrified, and holding a lightning bolt
return; for (let actor of this.actors) {
if (! actor.cell)
continue;
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.can_be_powered_by_actor)) {
for (let circuit of this.cells_to_circuits.get(this.cell_to_scalar(wired_tile.cell))) {
circuit.is_powered = true;
}
}
}
} }
for (let tile of this.wired_outputs) { for (let tile of this.wired_outputs) {
@ -2426,29 +2414,24 @@ export class Level extends LevelInterface {
// Go through every circuit and recompute whether it's powered // Go through every circuit and recompute whether it's powered
let circuit_changes = new Map; let circuit_changes = new Map;
for (let circuit of this.circuits) { for (let circuit of this.circuits) {
let is_powered = false; if (! circuit.is_powered) {
if (externally_powered_circuits.has(circuit)) {
is_powered = true;
}
else {
for (let [input_tile, edges] of circuit.inputs.entries()) { for (let [input_tile, edges] of circuit.inputs.entries()) {
if (input_tile.emitting_edges & edges) { if (input_tile.type.is_emitting && input_tile.type.is_emitting(input_tile, this, edges)) {
is_powered = true; circuit.is_powered = true;
break; break;
} }
} }
} }
if (is_powered !== circuit.is_powered) { if (this.undo_enabled && circuit.is_powered !== circuit._was_powered) {
if (this.undo_enabled) { circuit_changes.set(circuit, circuit._was_powered);
circuit_changes.set(circuit, circuit.is_powered);
}
circuit.is_powered = is_powered;
} }
} }
this.__apply_circuit_power_to_tiles(); this.__apply_circuit_power_to_tiles();
if (this.undo_enabled && circuit_changes.size > 0 && ! this.pending_undo.circuit_power_changes) {
this.pending_undo.circuit_power_changes = circuit_changes;
}
// Finally, inform every tile of power changes, if any // Finally, inform every tile of power changes, if any
for (let tile of this.wired_outputs) { for (let tile of this.wired_outputs) {
@ -2459,10 +2442,6 @@ export class Level extends LevelInterface {
tile.type.on_depower(tile, this); tile.type.on_depower(tile, this);
} }
} }
if (this.undo_enabled) {
this.pending_undo.circuit_power_changes = circuit_changes;
}
} }
// Recompute which tiles (across the whole level) are powered, based on the power status of the // Recompute which tiles (across the whole level) are powered, based on the power status of the
@ -2507,7 +2486,7 @@ export class Level extends LevelInterface {
continue; continue;
if ((wired.wire_propagation_mode ?? wired.type.wire_propagation_mode) === 'none' && if ((wired.wire_propagation_mode ?? wired.type.wire_propagation_mode) === 'none' &&
! wired.type.is_power_source) ! wired.type.update_power_emission)
{ {
// Being next to e.g. a red teleporter doesn't count (but pink button is ok) // Being next to e.g. a red teleporter doesn't count (but pink button is ok)
continue; continue;
@ -2603,6 +2582,7 @@ export class Level extends LevelInterface {
for (let [circuit, is_powered] of entry.circuit_power_changes.entries()) { for (let [circuit, is_powered] of entry.circuit_power_changes.entries()) {
circuit.is_powered = is_powered; circuit.is_powered = is_powered;
} }
// FIXME ah the power state doesn't undo correctly because the circuits are different
this.__apply_circuit_power_to_tiles(); this.__apply_circuit_power_to_tiles();
} }

View File

@ -2284,17 +2284,13 @@ const TILE_TYPES = {
button_pink: { button_pink: {
layer: LAYERS.terrain, layer: LAYERS.terrain,
contains_wire: true, contains_wire: true,
is_power_source: true,
wire_propagation_mode: 'none', wire_propagation_mode: 'none',
get_emitting_edges(me, level) { is_emitting(me, level) {
// We emit current as long as there's an actor fully on us // We emit current as long as there's an actor fully on us
let actor = me.cell.get_actor(); let actor = me.cell.get_actor();
if (actor && actor.movement_cooldown === 0) { return (actor && actor.movement_cooldown === 0);
return me.wire_directions; },
} update_power_emission(me, level) {
else {
return 0;
}
}, },
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);
@ -2307,31 +2303,30 @@ const TILE_TYPES = {
button_black: { button_black: {
layer: LAYERS.terrain, layer: LAYERS.terrain,
contains_wire: true, contains_wire: true,
is_power_source: true,
wire_propagation_mode: 'cross', wire_propagation_mode: 'cross',
get_emitting_edges(me, level) { on_ready(me, level) {
// TODO weird and inconsistent with pink buttons, but cc2 has a single-frame delay here! me.emitting = true;
me.next_emitting = true;
},
is_emitting(me, level) {
// We emit current as long as there's NOT an actor fully on us // We emit current as long as there's NOT an actor fully on us
return me.emitting;
},
// CC2 has a single frame delay between an actor stepping on/off of the button and the
// output changing; it's not clear why, and I can't figure out how this might have happened
// on accident, but it might be to compensate for logic gates firing quickly...?
// Same applies to light switches, but NOT pink buttons.
update_power_emission(me, level) {
let actor = me.cell.get_actor(); let actor = me.cell.get_actor();
let held = (actor && actor.movement_cooldown === 0); let ret = me.emitting;
if (me.is_first_frame) { level._set_tile_prop(me, 'emitting', me.next_emitting);
held = ! held; level._set_tile_prop(me, 'next_emitting', ! (actor && actor.movement_cooldown === 0));
level._set_tile_prop(me, 'is_first_frame', false); return ret;
}
if (held) {
return 0;
}
else {
return me.wire_directions;
}
}, },
on_arrive(me, level, other) { on_arrive(me, level, other) {
level._set_tile_prop(me, 'is_first_frame', true);
level.sfx.play_once('button-press', me.cell); level.sfx.play_once('button-press', me.cell);
}, },
on_depart(me, level, other) { on_depart(me, level, other) {
level._set_tile_prop(me, 'is_first_frame', true);
level.sfx.play_once('button-release', me.cell); level.sfx.play_once('button-release', me.cell);
}, },
visual_state: button_visual_state, visual_state: button_visual_state,
@ -2379,7 +2374,6 @@ const TILE_TYPES = {
counter: ['out1', 'in0', 'in1', 'out0'], counter: ['out1', 'in0', 'in1', 'out0'],
}, },
layer: LAYERS.terrain, layer: LAYERS.terrain,
is_power_source: true,
on_ready(me, level) { on_ready(me, level) {
me.gate_def = me.type._gate_types[me.gate_type]; me.gate_def = me.type._gate_types[me.gate_type];
if (me.gate_type === 'latch-cw' || me.gate_type === 'latch-ccw') { if (me.gate_type === 'latch-cw' || me.gate_type === 'latch-ccw') {
@ -2392,6 +2386,27 @@ const TILE_TYPES = {
me.underflowing = false; me.underflowing = false;
me.direction = 'north'; me.direction = 'north';
} }
me.in0 = me.in1 = null;
me.out0 = me.out1 = null;
let dir = me.direction;
for (let i = 0; i < 4; i++) {
let cxn = me.gate_def[i];
let dirinfo = DIRECTIONS[dir];
if (cxn === 'in0') {
me.in0 = dir;
}
else if (cxn === 'in1') {
me.in1 = dir;
}
else if (cxn === 'out0') {
me.out0 = dir;
}
else if (cxn === 'out1') {
me.out1 = dir;
}
dir = dirinfo.right;
}
}, },
// Returns [in0, in1, out0, out1] as directions // Returns [in0, in1, out0, out1] as directions
get_wires(me) { get_wires(me) {
@ -2417,30 +2432,16 @@ const TILE_TYPES = {
} }
return ret; return ret;
}, },
get_emitting_edges(me, level) { is_emitting(me, level, edges) {
return edges & me._output;
},
update_power_emission(me, level) {
// Collect which of our edges are powered, in clockwise order starting from our // Collect which of our edges are powered, in clockwise order starting from our
// direction, matching _gate_types // direction, matching _gate_types
let input0 = false, input1 = false; let input0 = !! (me.in0 && (me.powered_edges & DIRECTIONS[me.in0].bit));
let output0 = false, output1 = false; let input1 = !! (me.in1 && (me.powered_edges & DIRECTIONS[me.in1].bit));
let outbit0 = 0, outbit1 = 0; let output0 = false;
let dir = me.direction; let output1 = false;
for (let i = 0; i < 4; i++) {
let cxn = me.gate_def[i];
let dirinfo = DIRECTIONS[dir];
if (cxn === 'in0') {
input0 = (me.powered_edges & dirinfo.bit) !== 0;
}
else if (cxn === 'in1') {
input1 = (me.powered_edges & dirinfo.bit) !== 0;
}
else if (cxn === 'out0') {
outbit0 = dirinfo.bit;
}
else if (cxn === 'out1') {
outbit1 = dirinfo.bit;
}
dir = dirinfo.right;
}
if (me.gate_type === 'not') { if (me.gate_type === 'not') {
output0 = ! input0; output0 = ! input0;
@ -2480,14 +2481,14 @@ const TILE_TYPES = {
level._set_tile_prop(me, 'underflowing', false); level._set_tile_prop(me, 'underflowing', false);
} }
if (inc && ! dec) { if (inc && ! dec) {
mem++; mem += 1;
if (mem > 9) { if (mem > 9) {
mem = 0; mem = 0;
output0 = true; output0 = true;
} }
} }
else if (dec && ! inc) { else if (dec && ! inc) {
mem--; mem -= 1;
if (mem < 0) { if (mem < 0) {
mem = 9; mem = 9;
// Underflow is persistent until the next pulse // Underflow is persistent until the next pulse
@ -2500,7 +2501,9 @@ const TILE_TYPES = {
level._set_tile_prop(me, 'decrementing', input1); level._set_tile_prop(me, 'decrementing', input1);
} }
return (output0 ? outbit0 : 0) | (output1 ? outbit1 : 0); // This should only need to persist for a tic, and can be recomputed during undo, and
// also making it undoable would eat a whole lot of undo space
me._output = (output0 ? DIRECTIONS[me.out0].bit : 0) | (output1 ? DIRECTIONS[me.out1].bit : 0);
}, },
visual_state(me) { visual_state(me) {
return me.gate_type; return me.gate_type;
@ -2510,15 +2513,22 @@ const TILE_TYPES = {
light_switch_off: { light_switch_off: {
layer: LAYERS.terrain, layer: LAYERS.terrain,
contains_wire: true, contains_wire: true,
is_power_source: true,
wire_propagation_mode: 'none', wire_propagation_mode: 'none',
get_emitting_edges(me, level) { on_ready(me, level) {
// TODO weird and inconsistent with pink buttons, but cc2 has a single-frame delay here! me.emitting = false;
},
// See button_black's commentary on the timing here
is_emitting(me, level, edge) {
return me.emitting;
},
update_power_emission(me, level) {
if (me.is_first_frame) { if (me.is_first_frame) {
level._set_tile_prop(me, 'emitting', true);
level._set_tile_prop(me, 'is_first_frame', false); level._set_tile_prop(me, 'is_first_frame', false);
return me.wire_directions;
} }
return 0; else {
level._set_tile_prop(me, 'emitting', false);
}
}, },
on_arrive(me, level, other) { on_arrive(me, level, other) {
// TODO distinct sfx? more clicky? // TODO distinct sfx? more clicky?
@ -2530,15 +2540,22 @@ const TILE_TYPES = {
light_switch_on: { light_switch_on: {
layer: LAYERS.terrain, layer: LAYERS.terrain,
contains_wire: true, contains_wire: true,
is_power_source: true,
wire_propagation_mode: 'none', wire_propagation_mode: 'none',
get_emitting_edges(me, level) { on_ready(me, level) {
// TODO weird and inconsistent with pink buttons, but cc2 has a single-frame delay here! me.emitting = true;
},
// See button_black's commentary on the timing here
is_emitting(me, level, edge) {
return me.emitting;
},
update_power_emission(me, level) {
if (me.is_first_frame) { if (me.is_first_frame) {
level._set_tile_prop(me, 'emitting', false);
level._set_tile_prop(me, 'is_first_frame', false); level._set_tile_prop(me, 'is_first_frame', false);
return 0;
} }
return me.wire_directions; else {
level._set_tile_prop(me, 'emitting', true);
}
}, },
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);