Implement the remaining logic gates and /most/ of their rendering!

This commit is contained in:
Eevee (Evelyn Woods) 2020-11-25 03:14:06 -07:00
parent ac6e33bb6c
commit 3a454d77f5
3 changed files with 263 additions and 80 deletions

View File

@ -428,7 +428,7 @@ const TILE_ENCODING = {
// Counter, which can't be rotated // Counter, which can't be rotated
tile.direction = 'north'; tile.direction = 'north';
tile.gate_type = 'counter'; tile.gate_type = 'counter';
tile.counter_value = modifier - 0x1e; tile.memory = modifier - 0x1e;
} }
else { else {
tile.direction = ['north', 'east', 'south', 'west'][modifier & 0x03]; tile.direction = ['north', 'east', 'south', 'west'][modifier & 0x03];

View File

@ -401,58 +401,52 @@ export const CC2_TILESET_LAYOUT = {
], ],
logic_gate: { logic_gate: {
// TODO currently, 'wired' can't coexist with visual state etc... special: 'logic-gate',
// TODO *long sigh* of course, logic gates have parts with independent current too logic_gate_tiles: {
// for a north-facing gate with two inputs, we have: 'latch-ccw': {
// - north output: just a wire, doesn't include center. -r 0 w -r north: [8, 21],
// - west output: bottom left quadrant, except for the middle wire part, but including the east: [9, 21],
// horizontal wire. 0 -r -r +r south: [10, 21],
// - east output: same but includes middle wire. -r -r +r +r west: [11, 21],
// TODO check if they're flipped for latches facing the other way },
'latch-ccw': { not: {
north: [8, 21], north: [0, 25],
east: [9, 21], east: [1, 25],
south: [10, 21], south: [2, 25],
west: [11, 21], 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],
},
counter: [14, 26],
}, },
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],
@ -731,6 +725,94 @@ export class Tileset {
this.draw_type(tile.type.name, tile, tic, blit); this.draw_type(tile.type.name, tile, tic, blit);
} }
// Draw a "standard" drawspec, which is either:
// - a single tile: [x, y]
// - an animation: [[x0, y0], [x1, y1], ...]
// - a directional tile: { north: T, east: T, ... } where T is either of the above
_draw_standard(drawspec, tile, tic, blit, mask = []) {
// If we have an object, it must be a table of directions
let coords = drawspec;
if (!(coords instanceof Array)) {
coords = coords[(tile && tile.direction) ?? 'south'];
}
// Deal with animation
if (coords[0] instanceof Array) {
if (tic !== null) {
if (tile && tile.animation_speed) {
// This tile reports its own animation timing (in tics), so trust that, and just
// use the current tic's fraction.
// That said: adjusting animation speed complicates this slightly. Consider the
// player's walk animation, which takes 4 tics to complete, during which time we
// cycle through 8 frames. Playing that at half speed means only half the
// animation actually plays, but if the player continues walking, then on the
// NEXT four tics, we should play the other half. To make this work, use the
// tic as a global timer as well: if the animation started on tics 0-4, play the
// first half; if it started on tics 5-8, play the second half. They could get
// out of sync if the player hesitates, but no one will notice that, and this
// approach minimizes storing extra state.
let i = (tile.animation_progress + tic % 1) / tile.animation_speed;
// But do NOT do this for explosions or splashes, which have a fixed duration
// and only play once
if (this.animation_slowdown > 1 && ! tile.type.ttl) {
// i ranges from [0, 1), but a slowdown of N means we'll only play the first
// 1/N of it before the game ends (or loops) the animation.
// So increase by [0..N-1] to get it in some other range, then divide by N
// to scale back down to [0, 1)
i += Math.floor(tic / tile.animation_speed % this.animation_slowdown);
i /= this.animation_slowdown;
}
coords = coords[Math.floor(i * coords.length)];
}
else {
// This tile animates on a global timer, one cycle every quarter of a second
coords = coords[Math.floor(tic / this.animation_slowdown % 5 / 5 * coords.length)];
}
}
else {
coords = coords[0];
}
}
blit(coords[0], coords[1], ...mask);
}
_draw_logic_gate(drawspec, tile, tic, blit) {
// Layer 1: wiring state
// Always draw the unpowered wire base
let unpowered_coords = this.layout['#unpowered'];
let powered_coords = this.layout['#powered'];
blit(...unpowered_coords);
if (tile && tile.cell) {
// What goes on top varies a bit...
// FIXME implement for NOT and counter!
let r = this.layout['#wire-width'] / 2;
if (tile.cell.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) {
// Right input, which includes the middle
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) {
// Left input, which does not include the middle
let [x0, y0, x1, y1] = this._rotate(tile.direction, 0, 0.5 - r, 0.5 - r, 1);
blit(powered_coords[0], powered_coords[1], x0, y0, x1 - x0, y1 - y0);
}
}
// Layer 2: the tile itself
this._draw_standard(drawspec.logic_gate_tiles[tile.gate_type], tile, tic, blit);
// Layer 3: counter number
if (tile.gate_type === 'counter') {
blit(0, 3, tile.memory * 0.75, 0, 0.75, 1, 0.125, 0);
}
}
// Draws a tile type, given by name. Passing in a tile is optional, but // Draws a tile type, given by name. Passing in a tile is optional, but
// without it you'll get defaults. // without it you'll get defaults.
draw_type(name, tile, tic, blit) { draw_type(name, tile, tic, blit) {
@ -755,6 +837,14 @@ export class Tileset {
} }
} }
// TODO shift everything to use this style, this is ridiculous
if (drawspec.special) {
if (drawspec.special === 'logic-gate') {
this._draw_logic_gate(drawspec, tile, tic, blit);
return;
}
}
let coords = drawspec; let coords = drawspec;
if (drawspec.mask) { if (drawspec.mask) {
// Some tiles (OK, just the thin walls) don't actually draw a full // Some tiles (OK, just the thin walls) don't actually draw a full
@ -991,4 +1081,19 @@ export class Tileset {
blit(sx, sy, 0, 0, 0.5, 0.5, offset, offset); blit(sx, sy, 0, 0, 0.5, 0.5, offset, offset);
} }
} }
_rotate(direction, x0, y0, x1, y1) {
if (direction === 'east') {
return [1 - y1, x0, 1 - y0, x1];
}
else if (direction === 'south') {
return [1 - x1, 1 - y1, 1 - x0, 1 - y0];
}
else if (direction === 'west') {
return [y0, 1 - x1, y1, 1 - x0];
}
else {
return [x0, y0, x1, y1];
}
}
} }

View File

@ -654,6 +654,9 @@ const TILE_TYPES = {
on_gray_button(me, level) { on_gray_button(me, level) {
level.transmute_tile(me, 'green_wall'); level.transmute_tile(me, 'green_wall');
}, },
on_power(me, level) {
me.type.on_gray_button(me, level);
},
}, },
green_wall: { green_wall: {
draw_layer: DRAW_LAYERS.terrain, draw_layer: DRAW_LAYERS.terrain,
@ -661,6 +664,9 @@ const TILE_TYPES = {
on_gray_button(me, level) { on_gray_button(me, level) {
level.transmute_tile(me, 'green_floor'); level.transmute_tile(me, 'green_floor');
}, },
on_power(me, level) {
me.type.on_gray_button(me, level);
},
}, },
green_chip: { green_chip: {
draw_layer: DRAW_LAYERS.item, draw_layer: DRAW_LAYERS.item,
@ -1116,42 +1122,114 @@ const TILE_TYPES = {
logic_gate: { logic_gate: {
// gate_type: not, and, or, xor, nand, latch-cw, latch-ccw, counter, bogus // gate_type: not, and, or, xor, nand, latch-cw, latch-ccw, counter, bogus
_gate_types: { _gate_types: {
not: ['out', null, 'in1', null], not: ['out0', null, 'in0', null],
and: ['out', 'in2', null, 'in1'], and: ['out0', 'in0', null, 'in1'],
or: [], or: ['out0', 'in0', null, 'in1'],
xor: [], xor: ['out0', 'in0', null, 'in1'],
nand: [], nand: ['out0', 'in0', null, 'in1'],
'latch-cw': [], // in0 is the trigger, in1 is the input
'latch-ccw': [], 'latch-cw': ['out0', 'in0', null, 'in1'],
// in0 is the input, in1 is the trigger
'latch-ccw': ['out0', 'in0', null, 'in1'],
// inputs: inc, dec; outputs: overflow, underflow
counter: ['out1', 'in0', 'in1', 'out0'],
}, },
draw_layer: DRAW_LAYERS.terrain, draw_layer: DRAW_LAYERS.terrain,
is_power_source: true, 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') {
me.memory = false;
}
else if (me.gate_type === 'counter') {
me.memory = me.memory ?? 0;
me.incrementing = false;
me.decrementing = false;
me.underflowing = false;
me.direction = 'north';
}
},
get_emitting_edges(me, level) { get_emitting_edges(me, level) {
if (me.gate_type === 'and') { // Collect which of our edges are powered, in clockwise order starting from our
let vars = {}; // direction, matching _gate_types
let out_bit = 0; let input0 = false, input1 = false;
let dir = me.direction; let output0 = false, output1 = false;
for (let name of me.type._gate_types[me.gate_type]) { let outbit0 = 0, outbit1 = 0;
let dirinfo = DIRECTIONS[dir]; let dir = me.direction;
if (name === 'out') { for (let i = 0; i < 4; i++) {
out_bit |= dirinfo.bit; let cxn = me.gate_def[i];
} let dirinfo = DIRECTIONS[dir];
else if (name) { if (cxn === 'in0') {
vars[name] = (me.cell.powered_edges & dirinfo.bit) !== 0; input0 = (me.cell.powered_edges & dirinfo.bit) !== 0;
}
dir = dirinfo.right;
} }
else if (cxn === 'in1') {
input1 = (me.cell.powered_edges & dirinfo.bit) !== 0;
}
else if (cxn === 'out0') {
outbit0 = dirinfo.bit;
}
else if (cxn === 'out1') {
outbit1 = dirinfo.bit;
}
dir = dirinfo.right;
}
if (vars.in1 && vars.in2) { if (me.gate_type === 'not') {
return out_bit; output0 = ! input0;
}
else {
return 0;
}
} }
else { else if (me.gate_type === 'and') {
return 0; output0 = input0 && input1;
} }
else if (me.gate_type === 'or') {
output0 = input0 || input1;
}
else if (me.gate_type === 'xor') {
output0 = input0 !== input1;
}
else if (me.gate_type === 'nand') {
output0 = ! (input0 && input1);
}
else if (me.gate_type === 'latch-cw') {
if (input0) {
level._set_tile_prop(me, 'memory', input1);
}
output0 = me.memory;
}
else if (me.gate_type === 'latch-ccw') {
if (input1) {
level._set_tile_prop(me, 'memory', input0);
}
output0 = me.memory;
}
else if (me.gate_type === 'counter') {
let inc = input0 && ! me.incrementing;
let dec = input1 && ! me.decrementing;
let mem = me.memory;
if (inc || dec) {
level._set_tile_prop(me, 'underflowing', false);
}
if (inc && ! dec) {
mem++;
if (mem > 9) {
mem = 0;
output0 = true;
}
}
else if (dec && ! inc) {
mem--;
if (mem < 0) {
mem = 9;
// Underflow is persistent until the next pulse
level._set_tile_prop(me, 'underflowing', true);
}
}
output1 = me.underflowing;
level._set_tile_prop(me, 'memory', mem);
level._set_tile_prop(me, 'incrementing', input0);
level._set_tile_prop(me, 'decrementing', input1);
}
return (output0 ? outbit0 : 0) | (output1 ? outbit1 : 0);
}, },
visual_state(me) { visual_state(me) {
return me.gate_type; return me.gate_type;