Clean up wiring drawing and logic (zero gates is now a no-op!); begin implementing logic gates

This commit is contained in:
Eevee (Evelyn Woods) 2020-11-01 11:36:17 -07:00
parent 37b44bcca4
commit 49ff0d9723
5 changed files with 329 additions and 103 deletions

View File

@ -421,9 +421,33 @@ const TILE_ENCODING = {
name: 'no_player1_sign', name: 'no_player1_sign',
}, },
0x5c: { 0x5c: {
// TODO (modifier chooses logic gate) name: 'doppelganger2', name: 'logic_gate',
// TODO modifier: ... modifier: {
error: "Logic gates are not yet implemented, sorry!", decode(tile, modifier) {
if (modifier >= 0x1e && modifier <= 0x27) {
// Counter, which can't be rotated
tile.direction = 'north';
tile.gate_type = 'counter';
tile.counter_value = modifier - 0x1e;
}
else {
tile.direction = ['north', 'east', 'south', 'west'][modifier & 0x03];
let type = modifier >> 2;
if (type < 6) {
tile.gate_type = ['not', 'and', 'or', 'xor', 'latch-cw', 'nand'][type];
}
else if (type === 16) {
tile.gate_type = 'latch-ccw';
}
else {
tile.gate_type = 'bogus';
}
}
},
encode(tile) {
// FIXME implement
},
},
}, },
0x5e: { 0x5e: {
name: 'button_pink', name: 'button_pink',
@ -503,10 +527,10 @@ const TILE_ENCODING = {
}, },
}, },
0x72: { 0x72: {
name: 'purple_wall', name: 'purple_floor',
}, },
0x73: { 0x73: {
name: 'purple_floor', name: 'purple_wall',
}, },
0x76: { 0x76: {
name: '#mod8', name: '#mod8',

View File

@ -95,6 +95,7 @@ export class Tile {
} }
} }
} }
Tile.prototype.emitting_edges = 0;
export class Cell extends Array { export class Cell extends Array {
constructor(x, y) { constructor(x, y) {
@ -159,8 +160,8 @@ export class Cell extends Array {
return false; return false;
} }
} }
Cell.prototype.was_powered = false; Cell.prototype.prev_powered_edges = 0;
Cell.prototype.is_powered = false; Cell.prototype.powered_edges = 0;
class GameEnded extends Error {} class GameEnded extends Error {}
@ -228,9 +229,7 @@ export class Level {
let n = 0; let n = 0;
let connectables = []; let connectables = [];
// Handle CC2 wiring; a contiguous region of wire is all updated as a single unit, so detect this.power_sources = [];
// those units ahead of time for simplicity and call them "clusters"
this.wire_clusters = [];
// FIXME handle traps correctly: // FIXME handle traps correctly:
// - if an actor is in the cell, set the trap to open and unstick everything in it // - if an actor is in the cell, set the trap to open and unstick everything in it
for (let y = 0; y < this.height; y++) { for (let y = 0; y < this.height; y++) {
@ -251,6 +250,10 @@ export class Level {
tile.specific_hint = template_tile.specific_hint ?? null; tile.specific_hint = template_tile.specific_hint ?? null;
} }
if (tile.type.is_power_source) {
this.power_sources.push(tile);
}
// TODO well this is pretty special-casey. maybe come up // TODO well this is pretty special-casey. maybe come up
// with a specific pass at the beginning of the level? // with a specific pass at the beginning of the level?
// TODO also assumes a specific order... // TODO also assumes a specific order...
@ -272,9 +275,9 @@ export class Level {
// list? // list?
tile.stuck = true; tile.stuck = true;
} }
} if (! tile.stuck) {
if (! tile.stuck) { this.actors.push(tile);
this.actors.push(tile); }
} }
cell._add(tile); cell._add(tile);
@ -991,7 +994,8 @@ export class Level {
continue; continue;
// TODO some actors can pick up some items... // TODO some actors can pick up some items...
if (actor.type.is_player && tile.type.is_item && if (tile.type.is_item &&
(actor.type.is_player || cell.some(t => t.allows_all_pickup)) &&
this.attempt_take(actor, tile)) this.attempt_take(actor, tile))
{ {
if (tile.type.is_key) { if (tile.type.is_key) {
@ -1075,87 +1079,132 @@ export class Level {
// this needs to happen at all // this needs to happen at all
// FIXME none of this is currently undoable // FIXME none of this is currently undoable
update_wiring() { update_wiring() {
// 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 = [];
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;
}
}
// Also check actors, since any of them might be holding a lightning bolt (argh)
for (let actor of this.actors) {
if (! actor.cell)
continue;
// Only count when they're on a tile, not in transit!
let emitting = actor.movement_cooldown === 0 && actor.has_item('lightning_bolt');
if (emitting) {
neighbors.push([actor.cell, emitting]);
}
if (emitting !== actor.emitting_edges) {
any_changed = true;
actor.emitting_edges = emitting;
}
}
// If none changed, we're done
if (! any_changed)
return;
// Turn off power to every cell // Turn off power to every cell
// TODO wonder if i need a linear cell list, or even a flat list of all tiles (that sounds // TODO wonder if i need a linear cell list, or even a flat list of all tiles (that sounds
// like hell to keep updated though) // like hell to keep updated though)
for (let row of this.cells) { for (let row of this.cells) {
for (let cell of row) { for (let cell of row) {
cell.was_powered = cell.is_powered; cell.prev_powered_edges = cell.powered_edges;
cell.is_powered = false; cell.powered_edges = 0;
} }
} }
// Iterate through the grid looking for emitters — tiles that are generating current — and // Iterate over emitters and flood-fill outwards one edge at a time
// propagated it via flood-fill through neighboring wires // propagated it via flood-fill through neighboring wires
for (let row of this.cells) { while (neighbors.length > 0) {
for (let cell of row) { let [cell, source_direction] = neighbors.shift();
// TODO probably this should set a prop on the tile let wire = cell.get_wired_tile();
if (! cell.some(tile => tile.type.is_emitting && tile.type.is_emitting(tile, this)))
// 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;
}
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. The only
// special case is that four-way wiring is two separate wires, N/S and E/W
if (wire.wire_directions === 0x0f) {
cell.powered_edges |= bit;
cell.powered_edges |= DIRECTIONS[DIRECTIONS[source_direction].opposite].bit;
}
else {
cell.powered_edges = wire.wire_directions;
}
}
// 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; continue;
// We have an emitter! Flood-fill outwards let neighbor, neighbor_wire;
let neighbors = [cell]; let opposite_bit = DIRECTIONS[dirinfo.opposite].bit;
for (let neighbor of neighbors) { if (wire && (wire.wire_tunnel_directions & dirinfo.bit)) {
// Power it even if it's not wired itself, so that e.g. purple tiles work // Search in the given direction until we find a matching tunnel
neighbor.is_powered = true; // FIXME these act like nested parens!
let x = cell.x;
let y = cell.y;
let nesting = 0;
while (true) {
x += dirinfo.movement[0];
y += dirinfo.movement[1];
if (! this.is_point_within_bounds(x, y))
break;
let wire = neighbor.get_wired_tile(); let candidate = this.cells[y][x];
if (! wire) neighbor_wire = candidate.get_wired_tile();
continue; if (neighbor_wire && ((neighbor_wire.wire_tunnel_directions ?? 0) & opposite_bit)) {
neighbor = candidate;
// Emit along every wire direction, and add any unpowered neighbors to the break;
// pending list to continue the floodfill
// TODO but only if wires connect
// TODO handle wire tunnels
for (let [direction, dirinfo] of Object.entries(DIRECTIONS)) {
if (! (wire.wire_directions & dirinfo.bit))
continue;
let neighbor2, wire2;
let opposite_bit = DIRECTIONS[dirinfo.opposite].bit;
if (wire.wire_tunnel_directions & dirinfo.bit) {
// Search in the given direction until we find a matching tunnel
// FIXME these act like nested parens!
let x = neighbor.x;
let y = neighbor.y;
let nesting = 0;
while (true) {
x += dirinfo.movement[0];
y += dirinfo.movement[1];
if (! this.is_point_within_bounds(x, y))
break;
let candidate = this.cells[y][x];
wire2 = candidate.get_wired_tile();
if (wire2 && (wire2.wire_tunnel_directions ?? 0) & opposite_bit) {
neighbor2 = candidate;
break;
}
}
}
else {
// Otherwise this is easy
neighbor2 = this.get_neighboring_cell(neighbor, direction);
wire2 = neighbor2.get_wired_tile();
}
if (neighbor2 && ! neighbor2.is_powered &&
// Unwired tiles are OK; they might be something activated by power.
// Wired tiles that do NOT connect to us are ignored.
(! wire2 || wire2.wire_directions & opposite_bit))
{
neighbors.push(neighbor2);
} }
} }
} }
else {
// No tunnel; this is easy
neighbor = this.get_neighboring_cell(cell, direction);
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]);
}
} }
} }
// Inform any affected cells of power changes // Inform any affected cells of power changes
for (let row of this.cells) { for (let row of this.cells) {
for (let cell of row) { for (let cell of row) {
if (cell.was_powered !== cell.is_powered) { if ((cell.prev_powered_edges === 0) !== (cell.powered_edges === 0)) {
let method = cell.is_powered ? 'on_power' : 'on_depower'; let method = cell.powered_edges ? 'on_power' : 'on_depower';
for (let tile of cell) { for (let tile of cell) {
if (tile.type[method]) { if (tile.type[method]) {
tile.type[method](tile, this); tile.type[method](tile, this);

View File

@ -485,6 +485,11 @@ const EDITOR_PALETTE = [{
'wall_invisible', 'wall_appearing', 'wall_invisible', 'wall_appearing',
'gravel', 'gravel',
'dirt', 'dirt',
'dirt',
'floor_custom_green', 'floor_custom_pink', 'floor_custom_yellow', 'floor_custom_blue',
'wall_custom_green', 'wall_custom_pink', 'wall_custom_yellow', 'wall_custom_blue',
'door_blue', 'door_red', 'door_yellow', 'door_green', 'door_blue', 'door_red', 'door_yellow', 'door_green',
'water', 'turtle', 'fire', 'water', 'turtle', 'fire',
'ice', 'ice_nw', 'ice_ne', 'ice_sw', 'ice_se', 'ice', 'ice_nw', 'ice_ne', 'ice_sw', 'ice_se',
@ -495,6 +500,7 @@ const EDITOR_PALETTE = [{
tiles: [ tiles: [
'key_blue', 'key_red', 'key_yellow', 'key_green', 'key_blue', 'key_red', 'key_yellow', 'key_green',
'flippers', 'fire_boots', 'cleats', 'suction_boots', 'flippers', 'fire_boots', 'cleats', 'suction_boots',
'no_sign', // 'bestowal_bow',
], ],
}, { }, {
title: "Creatures", title: "Creatures",

View File

@ -35,6 +35,7 @@ export const CC2_TILESET_LAYOUT = {
// Wiring! // Wiring!
base: [0, 2], base: [0, 2],
wired: [8, 26], wired: [8, 26],
wired_cross: [10, 26],
is_wired_optional: true, is_wired_optional: true,
}, },
wall_invisible: [0, 2], wall_invisible: [0, 2],
@ -206,6 +207,7 @@ export const CC2_TILESET_LAYOUT = {
// Wiring! // Wiring!
base: [15, 10], base: [15, 10],
wired: [9, 26], wired: [9, 26],
wired_cross: [11, 26],
is_wired_optional: true, is_wired_optional: true,
}, },
@ -369,6 +371,55 @@ export const CC2_TILESET_LAYOUT = {
[15, 24], [15, 24],
], ],
logic_gate: {
// TODO currently, 'wired' can't coexist with visual state etc...
// TODO *long sigh* of course, logic gates have parts with independent current too
'latch-ccw': {
north: [8, 21],
east: [9, 21],
south: [10, 21],
west: [11, 21],
},
not: {
north: [0, 25],
east: [1, 25],
south: [2, 25],
west: [3, 25],
},
and: {
north: [4, 25],
east: [5, 25],
south: [6, 25],
west: [7, 25],
},
or: {
north: [8, 25],
east: [9, 25],
south: [10, 25],
west: [11, 25],
},
xor: {
north: [12, 25],
east: [13, 25],
south: [14, 25],
west: [15, 25],
},
'latch-cw': {
north: [0, 26],
east: [1, 26],
south: [2, 26],
west: [3, 26],
},
nand: {
north: [4, 26],
east: [5, 26],
south: [6, 26],
west: [7, 26],
},
// FIXME need to draw the number as well
counter: [14, 26],
},
'#unpowered': [13, 26], '#unpowered': [13, 26],
'#powered': [15, 26], '#powered': [15, 26],
@ -600,7 +651,6 @@ export const LL_TILESET_LAYOUT = Object.assign({}, CC2_TILESET_LAYOUT, {
teeth: Object.assign({}, CC2_TILESET_LAYOUT.teeth, { teeth: Object.assign({}, CC2_TILESET_LAYOUT.teeth, {
north: [[0, 32], [1, 32], [2, 32], [1, 32]], north: [[0, 32], [1, 32], [2, 32], [1, 32]],
}), }),
popwall2: [9, 32],
// Extra player sprites // Extra player sprites
player: Object.assign({}, CC2_TILESET_LAYOUT.player, { player: Object.assign({}, CC2_TILESET_LAYOUT.player, {
@ -627,6 +677,10 @@ export const LL_TILESET_LAYOUT = Object.assign({}, CC2_TILESET_LAYOUT, {
overlay: [6, 33], overlay: [6, 33],
base: 'floor', base: 'floor',
}, },
// Custom tiles
popwall2: [9, 32],
bestowal_bow: [10, 32],
}); });
export class Tileset { export class Tileset {
@ -673,31 +727,51 @@ export class Tileset {
coords = drawspec.tile; coords = drawspec.tile;
} }
else if (drawspec.wired) { else if (drawspec.wired) {
if (tile && tile.wire_directions) { // This /should/ match CC2's draw order exactly, based on experimentation
// TODO all four is a different thing entirely with two separate parts, ugh let wire_radius = this.layout['#wire-width'] / 2;
// Draw the appropriate wire underlay if (tile && tile.wire_directions === 0x0f) {
this.draw_type(tile.cell.is_powered ? '#powered' : '#unpowered', tile, tic, blit); // This is a wired tile with crossing wires, which acts a little differently
// Draw the base tile
blit(drawspec.base[0], drawspec.base[1]);
// Draw a masked part of the base tile // Draw the two wires as separate rectangles, NS then EW
let wiredir = tile.wire_directions; let wire_inset = 0.5 - wire_radius;
let wire_radius = this.layout['#wire-width'] / 2; let wire_coords_ns = this.layout[
let wire0 = 0.5 - wire_radius; tile.cell && tile.cell.powered_edges & DIRECTIONS['north'].bit ? '#powered' : '#unpowered'];
let wire1 = 0.5 + wire_radius; let wire_coords_ew = this.layout[
let [bx, by] = drawspec.base; tile.cell && tile.cell.powered_edges & DIRECTIONS['east'].bit ? '#powered' : '#unpowered'];
if ((wiredir & DIRECTIONS['north'].bit) === 0) { blit(wire_coords_ns[0] + wire_inset, wire_coords_ns[1], wire_inset, 0, wire_radius * 2, 1);
blit(bx, by, 0, 0, 1, wire0); blit(wire_coords_ew[0], wire_coords_ew[1] + wire_inset, 0, wire_inset, 1, wire_radius * 2);
}
if ((wiredir & DIRECTIONS['south'].bit) === 0) {
blit(bx, by + wire1, 0, wire1, 1, wire0);
}
if ((wiredir & DIRECTIONS['west'].bit) === 0) {
blit(bx, by, 0, 0, wire0, 1);
}
if ((wiredir & DIRECTIONS['east'].bit) === 0) {
blit(bx + wire1, by, wire1, 0, wire0, 1);
}
// Then draw the wired tile as normal // Draw the cross tile on top
coords = drawspec.wired_cross ?? drawspec.wired;
}
else if (tile && tile.wire_directions) {
// Draw the base tile
blit(drawspec.base[0], drawspec.base[1]);
// Draw the wire part as a single rectangle, initially just a small dot in the
// center, but extending out to any edge that has a wire present
let x0 = 0.5 - wire_radius;
let x1 = 0.5 + wire_radius;
let y0 = 0.5 - wire_radius;
let y1 = 0.5 + wire_radius;
if (tile.wire_directions & DIRECTIONS['north'].bit) {
y0 = 0;
}
if (tile.wire_directions & DIRECTIONS['east'].bit) {
x1 = 1;
}
if (tile.wire_directions & DIRECTIONS['south'].bit) {
y1 = 1;
}
if (tile.wire_directions & DIRECTIONS['west'].bit) {
x0 = 0;
}
let wire_coords = this.layout[tile.cell && tile.cell.powered_edges ? '#powered' : '#unpowered'];
blit(wire_coords[0] + x0, wire_coords[1] + y0, x0, y0, x1 - x0, y1 - y0);
// Then draw the wired tile on top of it all
coords = drawspec.wired; coords = drawspec.wired;
} }
else { else {

View File

@ -4,7 +4,7 @@ import { random_choice } from './util.js';
// Draw layers // Draw layers
const LAYER_TERRAIN = 0; const LAYER_TERRAIN = 0;
const LAYER_ITEM = 1; const LAYER_ITEM = 1;
const LAYER_NO_SIGN = 2; const LAYER_ITEM_MOD = 2;
const LAYER_ACTOR = 3; const LAYER_ACTOR = 3;
const LAYER_OVERLAY = 4; const LAYER_OVERLAY = 4;
// TODO cc2 order is: swivel, thinwalls, canopy (and yes you can have them all in the same tile) // TODO cc2 order is: swivel, thinwalls, canopy (and yes you can have them all in the same tile)
@ -549,7 +549,7 @@ const TILE_TYPES = {
}, },
}, },
no_sign: { no_sign: {
draw_layer: LAYER_NO_SIGN, draw_layer: LAYER_ITEM_MOD,
disables_pickup: true, disables_pickup: true,
blocks(me, level, other) { blocks(me, level, other) {
let item; let item;
@ -562,6 +562,10 @@ const TILE_TYPES = {
return item && other.has_item(item); return item && other.has_item(item);
}, },
}, },
bestowal_bow: {
draw_layer: LAYER_ITEM_MOD,
allows_all_pickup: true,
},
// Mechanisms // Mechanisms
dirt_block: { dirt_block: {
@ -1046,9 +1050,15 @@ const TILE_TYPES = {
}, },
button_pink: { button_pink: {
draw_layer: LAYER_TERRAIN, draw_layer: LAYER_TERRAIN,
is_emitting(me, level) { is_power_source: true,
// We emit current as long as there's an actor on us get_emitting_edges(me, level) {
return me.cell.some(tile => tile.type.is_actor); // We emit current as long as there's an actor fully on us
if (me.cell.some(tile => tile.type.is_actor && tile.movement_cooldown === 0)) {
return me.wire_directions;
}
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);
@ -1060,6 +1070,16 @@ const TILE_TYPES = {
button_black: { button_black: {
// TODO not implemented // TODO not implemented
draw_layer: LAYER_TERRAIN, draw_layer: LAYER_TERRAIN,
is_power_source: true,
get_emitting_edges(me, level) {
// We emit current as long as there's NOT an actor fully on us
if (! me.cell.some(tile => tile.type.is_actor && tile.movement_cooldown === 0)) {
return me.wire_directions;
}
else {
return 0;
}
},
}, },
button_gray: { button_gray: {
// TODO only partially implemented // TODO only partially implemented
@ -1086,6 +1106,51 @@ const TILE_TYPES = {
level.sfx.play_once('button-release', me.cell); level.sfx.play_once('button-release', me.cell);
}, },
}, },
// Logic gates, all consolidated into a single tile type
logic_gate: {
// gate_type: not, and, or, xor, nand, latch-cw, latch-ccw, counter, bogus
_gate_types: {
not: ['out', null, 'in1', null],
and: ['out', 'in2', null, 'in1'],
or: [],
xor: [],
nand: [],
'latch-cw': [],
'latch-ccw': [],
},
draw_layer: LAYER_TERRAIN,
is_power_source: true,
get_emitting_edges(me, level) {
if (me.gate_type === 'and') {
let vars = {};
let out_bit = 0;
let dir = me.direction;
for (let name of me.type._gate_types[me.gate_type]) {
let dirinfo = DIRECTIONS[dir];
if (name === 'out') {
out_bit |= dirinfo.bit;
}
else if (name) {
vars[name] = (me.cell.powered_edges & dirinfo.bit) !== 0;
}
dir = dirinfo.right;
}
if (vars.in1 && vars.in2) {
return out_bit;
}
else {
return 0;
}
}
else {
return 0;
}
},
visual_state(me) {
return me.gate_type;
},
},
// Time alteration // Time alteration
stopwatch_bonus: { stopwatch_bonus: {
@ -1390,6 +1455,14 @@ const TILE_TYPES = {
blocks_monsters: true, blocks_monsters: true,
blocks_blocks: true, blocks_blocks: true,
}, },
lightning_bolt: {
// TODO not implemented
draw_layer: LAYER_ITEM,
is_item: true,
is_tool: true,
blocks_monsters: true,
blocks_blocks: true,
},
// Progression // Progression
player: { player: {