diff --git a/js/algorithms.js b/js/algorithms.js index ae5b563..97f3d7a 100644 --- a/js/algorithms.js +++ b/js/algorithms.js @@ -157,7 +157,7 @@ export function trace_floor_circuit(levelish, actor_mode, start_cell, start_edge 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? circuit.add_input_edge(tile, connections); } diff --git a/js/game.js b/js/game.js index 8b207f4..050f8f7 100644 --- a/js/game.js +++ b/js/game.js @@ -416,6 +416,7 @@ export class Level extends LevelInterface { let n = 0; let connectables = []; + this.power_sources = []; this.remaining_players = 0; this.ankh_tile = null; // 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); } + if (tile.type.update_power_emission) { + this.power_sources.push(tile); + } + if (tile.type.name === 'teleport_yellow' && ! this.allow_taking_yellow_teleporters) { 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 this.circuits = []; - this.power_sources = []; let wired_outputs = new Set; let seen_edges = new Map; for (let cell of this.linear_cells) { @@ -572,10 +576,6 @@ export class Level extends LevelInterface { if (! terrain) // ?! continue; - if (terrain.type.is_power_source) { - this.power_sources.push(terrain); - } - let actor = cell.get_actor(); let wire_directions = terrain.wire_directions; 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 // update them during each wire phase 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) { wired_outputs.add(tile); } @@ -2359,62 +2359,50 @@ export class Level extends LevelInterface { // Wiring ----------------------------------------------------------------------------------------- _do_wire_phase() { - let force_next_wire_phase = false; 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 // 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 if (this.undo_enabled) { 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) return; - // 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 = 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); - } + for (let circuit of this.circuits) { + circuit._was_powered = circuit.is_powered; + circuit.is_powered = false; } - // 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); + + // Update the state of any tiles that can generate power. First, static power sources + for (let tile of this.power_sources) { + if (tile.type.update_power_emission) { + tile.type.update_power_emission(tile, this); } } - if (! any_changed && ! force_next_wire_phase) { - return; + // Next, actors who are standing still, on floor/electrified, and holding a lightning bolt + 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) { @@ -2426,29 +2414,24 @@ export class Level extends LevelInterface { // Go through every circuit and recompute whether it's powered let circuit_changes = new Map; for (let circuit of this.circuits) { - let is_powered = false; - - if (externally_powered_circuits.has(circuit)) { - is_powered = true; - } - else { + if (! circuit.is_powered) { for (let [input_tile, edges] of circuit.inputs.entries()) { - if (input_tile.emitting_edges & edges) { - is_powered = true; + if (input_tile.type.is_emitting && input_tile.type.is_emitting(input_tile, this, edges)) { + circuit.is_powered = true; break; } } } - if (is_powered !== circuit.is_powered) { - if (this.undo_enabled) { - circuit_changes.set(circuit, circuit.is_powered); - } - circuit.is_powered = is_powered; + if (this.undo_enabled && circuit.is_powered !== circuit._was_powered) { + circuit_changes.set(circuit, circuit._was_powered); } } 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 for (let tile of this.wired_outputs) { @@ -2459,10 +2442,6 @@ export class Level extends LevelInterface { 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 @@ -2507,7 +2486,7 @@ export class Level extends LevelInterface { continue; 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) continue; @@ -2603,6 +2582,7 @@ export class Level extends LevelInterface { for (let [circuit, is_powered] of entry.circuit_power_changes.entries()) { 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(); } diff --git a/js/tiletypes.js b/js/tiletypes.js index e18c6fe..de9273c 100644 --- a/js/tiletypes.js +++ b/js/tiletypes.js @@ -2284,17 +2284,13 @@ const TILE_TYPES = { button_pink: { layer: LAYERS.terrain, contains_wire: true, - is_power_source: true, 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 let actor = me.cell.get_actor(); - if (actor && actor.movement_cooldown === 0) { - return me.wire_directions; - } - else { - return 0; - } + return (actor && actor.movement_cooldown === 0); + }, + update_power_emission(me, level) { }, on_arrive(me, level, other) { level.sfx.play_once('button-press', me.cell); @@ -2307,31 +2303,30 @@ const TILE_TYPES = { button_black: { layer: LAYERS.terrain, contains_wire: true, - is_power_source: true, wire_propagation_mode: 'cross', - get_emitting_edges(me, level) { - // TODO weird and inconsistent with pink buttons, but cc2 has a single-frame delay here! + on_ready(me, level) { + 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 + 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 held = (actor && actor.movement_cooldown === 0); - if (me.is_first_frame) { - held = ! held; - level._set_tile_prop(me, 'is_first_frame', false); - } - - if (held) { - return 0; - } - else { - return me.wire_directions; - } + let ret = me.emitting; + level._set_tile_prop(me, 'emitting', me.next_emitting); + level._set_tile_prop(me, 'next_emitting', ! (actor && actor.movement_cooldown === 0)); + return ret; }, on_arrive(me, level, other) { - level._set_tile_prop(me, 'is_first_frame', true); level.sfx.play_once('button-press', me.cell); }, on_depart(me, level, other) { - level._set_tile_prop(me, 'is_first_frame', true); level.sfx.play_once('button-release', me.cell); }, visual_state: button_visual_state, @@ -2379,7 +2374,6 @@ const TILE_TYPES = { counter: ['out1', 'in0', 'in1', 'out0'], }, layer: LAYERS.terrain, - is_power_source: true, on_ready(me, level) { me.gate_def = me.type._gate_types[me.gate_type]; if (me.gate_type === 'latch-cw' || me.gate_type === 'latch-ccw') { @@ -2392,6 +2386,27 @@ const TILE_TYPES = { me.underflowing = false; 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 get_wires(me) { @@ -2417,30 +2432,16 @@ const TILE_TYPES = { } 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 // direction, matching _gate_types - let input0 = false, input1 = false; - let output0 = false, output1 = false; - let outbit0 = 0, outbit1 = 0; - let dir = me.direction; - 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; - } + let input0 = !! (me.in0 && (me.powered_edges & DIRECTIONS[me.in0].bit)); + let input1 = !! (me.in1 && (me.powered_edges & DIRECTIONS[me.in1].bit)); + let output0 = false; + let output1 = false; if (me.gate_type === 'not') { output0 = ! input0; @@ -2480,14 +2481,14 @@ const TILE_TYPES = { level._set_tile_prop(me, 'underflowing', false); } if (inc && ! dec) { - mem++; + mem += 1; if (mem > 9) { mem = 0; output0 = true; } } else if (dec && ! inc) { - mem--; + mem -= 1; if (mem < 0) { mem = 9; // Underflow is persistent until the next pulse @@ -2500,7 +2501,9 @@ const TILE_TYPES = { 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) { return me.gate_type; @@ -2510,15 +2513,22 @@ const TILE_TYPES = { light_switch_off: { layer: LAYERS.terrain, contains_wire: true, - is_power_source: true, wire_propagation_mode: 'none', - get_emitting_edges(me, level) { - // TODO weird and inconsistent with pink buttons, but cc2 has a single-frame delay here! + on_ready(me, level) { + 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) { + level._set_tile_prop(me, 'emitting', true); 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) { // TODO distinct sfx? more clicky? @@ -2530,15 +2540,22 @@ const TILE_TYPES = { light_switch_on: { layer: LAYERS.terrain, contains_wire: true, - is_power_source: true, wire_propagation_mode: 'none', - get_emitting_edges(me, level) { - // TODO weird and inconsistent with pink buttons, but cc2 has a single-frame delay here! + on_ready(me, level) { + 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) { + level._set_tile_prop(me, 'emitting', 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) { level.sfx.play_once('button-press', me.cell);