From 3a454d77f5c130bf8a3b3802028320890383e326 Mon Sep 17 00:00:00 2001 From: "Eevee (Evelyn Woods)" Date: Wed, 25 Nov 2020 03:14:06 -0700 Subject: [PATCH] Implement the remaining logic gates and /most/ of their rendering! --- js/format-c2g.js | 2 +- js/tileset.js | 207 +++++++++++++++++++++++++++++++++++------------ js/tiletypes.js | 134 +++++++++++++++++++++++------- 3 files changed, 263 insertions(+), 80 deletions(-) diff --git a/js/format-c2g.js b/js/format-c2g.js index fc8f68e..0ff4cd6 100644 --- a/js/format-c2g.js +++ b/js/format-c2g.js @@ -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]; diff --git a/js/tileset.js b/js/tileset.js index f1ead22..cbfc342 100644 --- a/js/tileset.js +++ b/js/tileset.js @@ -401,58 +401,52 @@ 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 - 'latch-ccw': { - north: [8, 21], - east: [9, 21], - south: [10, 21], - west: [11, 21], + special: 'logic-gate', + logic_gate_tiles: { + '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], + }, + 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], @@ -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]; + } + } } diff --git a/js/tiletypes.js b/js/tiletypes.js index d2ae20a..c73396b 100644 --- a/js/tiletypes.js +++ b/js/tiletypes.js @@ -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, + 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) { - 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; + // 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; - } - else { - return 0; - } + 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 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;