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',
},
0x5c: {
// TODO (modifier chooses logic gate) name: 'doppelganger2',
// TODO modifier: ...
error: "Logic gates are not yet implemented, sorry!",
name: 'logic_gate',
modifier: {
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: {
name: 'button_pink',
@ -503,10 +527,10 @@ const TILE_ENCODING = {
},
},
0x72: {
name: 'purple_wall',
name: 'purple_floor',
},
0x73: {
name: 'purple_floor',
name: 'purple_wall',
},
0x76: {
name: '#mod8',

View File

@ -95,6 +95,7 @@ export class Tile {
}
}
}
Tile.prototype.emitting_edges = 0;
export class Cell extends Array {
constructor(x, y) {
@ -159,8 +160,8 @@ export class Cell extends Array {
return false;
}
}
Cell.prototype.was_powered = false;
Cell.prototype.is_powered = false;
Cell.prototype.prev_powered_edges = 0;
Cell.prototype.powered_edges = 0;
class GameEnded extends Error {}
@ -228,9 +229,7 @@ export class Level {
let n = 0;
let connectables = [];
// Handle CC2 wiring; a contiguous region of wire is all updated as a single unit, so detect
// those units ahead of time for simplicity and call them "clusters"
this.wire_clusters = [];
this.power_sources = [];
// FIXME handle traps correctly:
// - 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++) {
@ -251,6 +250,10 @@ export class Level {
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
// with a specific pass at the beginning of the level?
// TODO also assumes a specific order...
@ -272,9 +275,9 @@ export class Level {
// list?
tile.stuck = true;
}
}
if (! tile.stuck) {
this.actors.push(tile);
if (! tile.stuck) {
this.actors.push(tile);
}
}
cell._add(tile);
@ -991,7 +994,8 @@ export class Level {
continue;
// 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))
{
if (tile.type.is_key) {
@ -1075,87 +1079,132 @@ export class Level {
// this needs to happen at all
// FIXME none of this is currently undoable
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
// 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)
for (let row of this.cells) {
for (let cell of row) {
cell.was_powered = cell.is_powered;
cell.is_powered = false;
cell.prev_powered_edges = cell.powered_edges;
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
for (let row of this.cells) {
for (let cell of row) {
// TODO probably this should set a prop on the tile
if (! cell.some(tile => tile.type.is_emitting && tile.type.is_emitting(tile, this)))
while (neighbors.length > 0) {
let [cell, source_direction] = neighbors.shift();
let wire = cell.get_wired_tile();
// 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;
// We have an emitter! Flood-fill outwards
let neighbors = [cell];
for (let neighbor of neighbors) {
// Power it even if it's not wired itself, so that e.g. purple tiles work
neighbor.is_powered = true;
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
// 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();
if (! wire)
continue;
// Emit along every wire direction, and add any unpowered neighbors to the
// 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);
let candidate = this.cells[y][x];
neighbor_wire = candidate.get_wired_tile();
if (neighbor_wire && ((neighbor_wire.wire_tunnel_directions ?? 0) & opposite_bit)) {
neighbor = candidate;
break;
}
}
}
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
for (let row of this.cells) {
for (let cell of row) {
if (cell.was_powered !== cell.is_powered) {
let method = cell.is_powered ? 'on_power' : 'on_depower';
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);

View File

@ -485,6 +485,11 @@ const EDITOR_PALETTE = [{
'wall_invisible', 'wall_appearing',
'gravel',
'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',
'water', 'turtle', 'fire',
'ice', 'ice_nw', 'ice_ne', 'ice_sw', 'ice_se',
@ -495,6 +500,7 @@ const EDITOR_PALETTE = [{
tiles: [
'key_blue', 'key_red', 'key_yellow', 'key_green',
'flippers', 'fire_boots', 'cleats', 'suction_boots',
'no_sign', // 'bestowal_bow',
],
}, {
title: "Creatures",

View File

@ -35,6 +35,7 @@ export const CC2_TILESET_LAYOUT = {
// Wiring!
base: [0, 2],
wired: [8, 26],
wired_cross: [10, 26],
is_wired_optional: true,
},
wall_invisible: [0, 2],
@ -206,6 +207,7 @@ export const CC2_TILESET_LAYOUT = {
// Wiring!
base: [15, 10],
wired: [9, 26],
wired_cross: [11, 26],
is_wired_optional: true,
},
@ -369,6 +371,55 @@ export const CC2_TILESET_LAYOUT = {
[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],
'#powered': [15, 26],
@ -600,7 +651,6 @@ export const LL_TILESET_LAYOUT = Object.assign({}, CC2_TILESET_LAYOUT, {
teeth: Object.assign({}, CC2_TILESET_LAYOUT.teeth, {
north: [[0, 32], [1, 32], [2, 32], [1, 32]],
}),
popwall2: [9, 32],
// Extra player sprites
player: Object.assign({}, CC2_TILESET_LAYOUT.player, {
@ -627,6 +677,10 @@ export const LL_TILESET_LAYOUT = Object.assign({}, CC2_TILESET_LAYOUT, {
overlay: [6, 33],
base: 'floor',
},
// Custom tiles
popwall2: [9, 32],
bestowal_bow: [10, 32],
});
export class Tileset {
@ -673,31 +727,51 @@ export class Tileset {
coords = drawspec.tile;
}
else if (drawspec.wired) {
if (tile && tile.wire_directions) {
// TODO all four is a different thing entirely with two separate parts, ugh
// Draw the appropriate wire underlay
this.draw_type(tile.cell.is_powered ? '#powered' : '#unpowered', tile, tic, blit);
// This /should/ match CC2's draw order exactly, based on experimentation
let wire_radius = this.layout['#wire-width'] / 2;
if (tile && tile.wire_directions === 0x0f) {
// 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
let wiredir = tile.wire_directions;
let wire_radius = this.layout['#wire-width'] / 2;
let wire0 = 0.5 - wire_radius;
let wire1 = 0.5 + wire_radius;
let [bx, by] = drawspec.base;
if ((wiredir & DIRECTIONS['north'].bit) === 0) {
blit(bx, by, 0, 0, 1, wire0);
}
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);
}
// Draw the two wires as separate rectangles, NS then EW
let wire_inset = 0.5 - wire_radius;
let wire_coords_ns = this.layout[
tile.cell && tile.cell.powered_edges & DIRECTIONS['north'].bit ? '#powered' : '#unpowered'];
let wire_coords_ew = this.layout[
tile.cell && tile.cell.powered_edges & DIRECTIONS['east'].bit ? '#powered' : '#unpowered'];
blit(wire_coords_ns[0] + wire_inset, wire_coords_ns[1], wire_inset, 0, wire_radius * 2, 1);
blit(wire_coords_ew[0], wire_coords_ew[1] + wire_inset, 0, wire_inset, 1, wire_radius * 2);
// 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;
}
else {

View File

@ -4,7 +4,7 @@ import { random_choice } from './util.js';
// Draw layers
const LAYER_TERRAIN = 0;
const LAYER_ITEM = 1;
const LAYER_NO_SIGN = 2;
const LAYER_ITEM_MOD = 2;
const LAYER_ACTOR = 3;
const LAYER_OVERLAY = 4;
// 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: {
draw_layer: LAYER_NO_SIGN,
draw_layer: LAYER_ITEM_MOD,
disables_pickup: true,
blocks(me, level, other) {
let item;
@ -562,6 +562,10 @@ const TILE_TYPES = {
return item && other.has_item(item);
},
},
bestowal_bow: {
draw_layer: LAYER_ITEM_MOD,
allows_all_pickup: true,
},
// Mechanisms
dirt_block: {
@ -1046,9 +1050,15 @@ const TILE_TYPES = {
},
button_pink: {
draw_layer: LAYER_TERRAIN,
is_emitting(me, level) {
// We emit current as long as there's an actor on us
return me.cell.some(tile => tile.type.is_actor);
is_power_source: true,
get_emitting_edges(me, level) {
// 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) {
level.sfx.play_once('button-press', me.cell);
@ -1060,6 +1070,16 @@ const TILE_TYPES = {
button_black: {
// TODO not implemented
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: {
// TODO only partially implemented
@ -1086,6 +1106,51 @@ const TILE_TYPES = {
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
stopwatch_bonus: {
@ -1390,6 +1455,14 @@ const TILE_TYPES = {
blocks_monsters: 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
player: {