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
tile.direction = 'north';
tile.gate_type = 'counter';
tile.counter_value = modifier - 0x1e;
tile.memory = modifier - 0x1e;
}
else {
tile.direction = ['north', 'east', 'south', 'west'][modifier & 0x03];

View File

@ -401,14 +401,8 @@ export const CC2_TILESET_LAYOUT = {
],
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
// for a north-facing gate with two inputs, we have:
// - north output: just a wire, doesn't include center. -r 0 w -r
// - west output: bottom left quadrant, except for the middle wire part, but including the
// horizontal wire. 0 -r -r +r
// - east output: same but includes middle wire. -r -r +r +r
// TODO check if they're flipped for latches facing the other way
special: 'logic-gate',
logic_gate_tiles: {
'latch-ccw': {
north: [8, 21],
east: [9, 21],
@ -451,9 +445,9 @@ export const CC2_TILESET_LAYOUT = {
south: [6, 26],
west: [7, 26],
},
// FIXME need to draw the number as well
counter: [14, 26],
},
},
'#unpowered': [13, 26],
'#powered': [15, 26],
@ -731,6 +725,94 @@ export class Tileset {
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
// without it you'll get defaults.
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;
if (drawspec.mask) {
// 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);
}
}
_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) {
level.transmute_tile(me, 'green_wall');
},
on_power(me, level) {
me.type.on_gray_button(me, level);
},
},
green_wall: {
draw_layer: DRAW_LAYERS.terrain,
@ -661,6 +664,9 @@ const TILE_TYPES = {
on_gray_button(me, level) {
level.transmute_tile(me, 'green_floor');
},
on_power(me, level) {
me.type.on_gray_button(me, level);
},
},
green_chip: {
draw_layer: DRAW_LAYERS.item,
@ -1116,42 +1122,114 @@ const TILE_TYPES = {
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': [],
not: ['out0', null, 'in0', null],
and: ['out0', 'in0', null, 'in1'],
or: ['out0', 'in0', null, 'in1'],
xor: ['out0', 'in0', null, 'in1'],
nand: ['out0', 'in0', null, 'in1'],
// in0 is the trigger, in1 is the input
'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,
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;
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 (name) {
vars[name] = (me.cell.powered_edges & dirinfo.bit) !== 0;
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) {
// 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.cell.powered_edges & dirinfo.bit) !== 0;
}
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) {
return out_bit;
if (me.gate_type === 'not') {
output0 = ! input0;
}
else {
return 0;
else if (me.gate_type === 'and') {
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 {
return 0;
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) {
return me.gate_type;