Add partial wiring support

This commit is contained in:
Eevee (Evelyn Woods) 2020-10-01 06:46:07 -06:00
parent 4cd0585d0b
commit 8adb630862
6 changed files with 248 additions and 21 deletions

View File

@ -69,6 +69,7 @@ class CC2Demo {
let modifier_wire = { let modifier_wire = {
decode(tile, modifier) { decode(tile, modifier) {
tile.wire_directions = modifier & 0x0f; tile.wire_directions = modifier & 0x0f;
// TODO wait, what happens if you use wire tunnels on steel or something other than floor?
tile.wire_tunnel_directions = (modifier & 0xf0) >> 4; tile.wire_tunnel_directions = (modifier & 0xf0) >> 4;
}, },
encode(tile) { encode(tile) {

View File

@ -125,6 +125,17 @@ export class Cell extends Array {
return index; return index;
} }
get_wired_tile() {
let ret = null;
for (let tile of this) {
if (tile.wire_directions || tile.wire_tunnel_directions) {
ret = tile;
// Don't break; we want the topmost tile!
}
}
return ret;
}
blocks_leaving(actor, direction) { blocks_leaving(actor, direction) {
for (let tile of this) { for (let tile of this) {
if (tile !== actor && if (tile !== actor &&
@ -148,6 +159,8 @@ export class Cell extends Array {
return false; return false;
} }
} }
Cell.prototype.was_powered = false;
Cell.prototype.is_powered = false;
class GameEnded extends Error {} class GameEnded extends Error {}
@ -205,6 +218,9 @@ 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
// 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++) {
@ -398,6 +414,9 @@ export class Level {
this.step_on_cell(actor, cell); this.step_on_cell(actor, cell);
} }
// Now we handle wiring
this.update_wiring();
// Only reset the player's is_pushing between movement, so it lasts for the whole push // Only reset the player's is_pushing between movement, so it lasts for the whole push
if (this.player.movement_cooldown <= 0) { if (this.player.movement_cooldown <= 0) {
this.player.is_pushing = false; this.player.is_pushing = false;
@ -558,7 +577,7 @@ export class Level {
} }
for (let direction of direction_preference) { for (let direction of direction_preference) {
let dest_cell = this.cell_with_offset(actor.cell, direction); let dest_cell = this.get_neighboring_cell(actor.cell, direction);
if (! dest_cell) if (! dest_cell)
continue; continue;
@ -600,7 +619,7 @@ export class Level {
if (actor.type.is_player && dir2 && if (actor.type.is_player && dir2 &&
! old_cell.blocks_leaving(actor, dir2)) ! old_cell.blocks_leaving(actor, dir2))
{ {
let neighbor = this.cell_with_offset(old_cell, dir2); let neighbor = this.get_neighboring_cell(old_cell, dir2);
if (neighbor) { if (neighbor) {
let could_push = ! neighbor.blocks_entering(actor, dir2, this, true); let could_push = ! neighbor.blocks_entering(actor, dir2, this, true);
for (let tile of Array.from(neighbor)) { for (let tile of Array.from(neighbor)) {
@ -677,7 +696,7 @@ export class Level {
let move = DIRECTIONS[direction].movement; let move = DIRECTIONS[direction].movement;
if (!actor.cell) console.error(actor); if (!actor.cell) console.error(actor);
let goal_cell = this.cell_with_offset(actor.cell, direction); let goal_cell = this.get_neighboring_cell(actor.cell, direction);
// TODO this could be a lot simpler if i could early-return! should ice bumping be // TODO this could be a lot simpler if i could early-return! should ice bumping be
// somewhere else? // somewhere else?
@ -903,11 +922,126 @@ export class Level {
} }
} }
cell_with_offset(cell, direction) { // Update the state of all wired tiles in the game.
// XXX need to be clear on the order of events here. say everything starts out unpowered.
// then:
// 1. you step on a pink button, which flags itself as going to be powered next frame
// 2. this pass happens. every unpowered-but-wired cell is inspected. if a powered one is
// found, floodfill from there
// FIXME can probably skip this if we know there are no wires at all, like in a CCL, or just an
// unwired map
// FIXME this feels inefficient. most of the time none of the inputs have changed so none of
// this needs to happen at all
// FIXME none of this is currently undoable
update_wiring() {
// 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;
}
}
// Iterate through the grid looking for emitters — tiles that are generating current — and
// 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)))
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 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);
}
}
}
}
}
// 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';
for (let tile of cell) {
if (tile.type[method]) {
tile.type[method](tile, this);
}
}
}
}
}
}
// Performs a depth-first search for connected wires and wire objects, extending out from the
// given starting cell
*follow_circuit(cell) {
}
// -------------------------------------------------------------------------
// Board inspection
is_point_within_bounds(x, y) {
return (x >= 0 && x < this.width && y >= 0 && y < this.height);
}
get_neighboring_cell(cell, direction) {
let move = DIRECTIONS[direction].movement; let move = DIRECTIONS[direction].movement;
let goal_x = cell.x + move[0]; let goal_x = cell.x + move[0];
let goal_y = cell.y + move[1]; let goal_y = cell.y + move[1];
if (goal_x >= 0 && goal_x < this.width && goal_y >= 0 && goal_y < this.height) { if (this.is_point_within_bounds(goal_x, goal_y)) {
return this.cells[goal_y][goal_x]; return this.cells[goal_y][goal_x];
} }
else { else {

View File

@ -200,7 +200,12 @@ export const CC2_TILESET_LAYOUT = {
popwall2: [8, 10], popwall2: [8, 10],
gravel: [9, 10], gravel: [9, 10],
ball: [[10, 10], [11, 10], [12, 10], [13, 10], [14, 10]], ball: [[10, 10], [11, 10], [12, 10], [13, 10], [14, 10]],
steel: [15, 10], steel: {
// Wiring!
base: [15, 10],
wired: [9, 26],
is_wired_optional: true,
},
// TODO only animates while moving // TODO only animates while moving
teeth: { teeth: {
@ -216,7 +221,7 @@ export const CC2_TILESET_LAYOUT = {
swivel_ne: [11, 11], swivel_ne: [11, 11],
swivel_se: [12, 11], swivel_se: [12, 11],
swivel_floor: [13, 11], swivel_floor: [13, 11],
// TODO some kinda four-edges thing again '#wire-tunnel': [14, 11],
stopwatch_penalty: [15, 11], stopwatch_penalty: [15, 11],
paramecium: { paramecium: {
north: [[0, 12], [1, 12], [2, 12]], north: [[0, 12], [1, 12], [2, 12]],
@ -666,10 +671,10 @@ export class Tileset {
coords = drawspec.tile; coords = drawspec.tile;
} }
else if (drawspec.wired) { else if (drawspec.wired) {
if (tile && tile.wire_directions !== undefined && tile.wire_directions !== 0) { if (tile && tile.wire_directions) {
// TODO all four is a different thing entirely // TODO all four is a different thing entirely with two separate parts, ugh
// Draw the appropriate wire underlay // Draw the appropriate wire underlay
this.draw_type('#unpowered', tile, tic, blit); this.draw_type(tile.cell.is_powered ? '#powered' : '#unpowered', tile, tic, blit);
// Draw a masked part of the base tile // Draw a masked part of the base tile
let wiredir = tile.wire_directions; let wiredir = tile.wire_directions;
@ -726,7 +731,7 @@ export class Tileset {
} }
// Apply custom per-type visual states // Apply custom per-type visual states
if (TILE_TYPES[name].visual_state) { if (TILE_TYPES[name] && TILE_TYPES[name].visual_state) {
// Note that these accept null, too, and return a default // Note that these accept null, too, and return a default
let state = TILE_TYPES[name].visual_state(tile); let state = TILE_TYPES[name].visual_state(tile);
// If it's a string, that's an alias for another state // If it's a string, that's an alias for another state
@ -797,6 +802,30 @@ export class Tileset {
blit(coords[0], coords[1]); blit(coords[0], coords[1]);
} }
// Wired tiles may also have tunnels, drawn on top of everything else
if (drawspec.wired && tile && tile.wire_tunnel_directions) {
let tunnel_coords = this.layout['#wire-tunnel'];
let tunnel_width = 6/32;
let tunnel_length = 12/32;
let tunnel_offset = (1 - tunnel_width) / 2;
if (tile.wire_tunnel_directions & DIRECTIONS['north'].bit) {
blit(tunnel_coords[0] + tunnel_offset, tunnel_coords[1],
tunnel_offset, 0, tunnel_width, tunnel_length);
}
if (tile.wire_tunnel_directions & DIRECTIONS['south'].bit) {
blit(tunnel_coords[0] + tunnel_offset, tunnel_coords[1] + 1 - tunnel_length,
tunnel_offset, 1 - tunnel_length, tunnel_width, tunnel_length);
}
if (tile.wire_tunnel_directions & DIRECTIONS['west'].bit) {
blit(tunnel_coords[0], tunnel_coords[1] + tunnel_offset,
0, tunnel_offset, tunnel_length, tunnel_width);
}
if (tile.wire_tunnel_directions & DIRECTIONS['east'].bit) {
blit(tunnel_coords[0] + 1 - tunnel_length, tunnel_coords[1] + tunnel_offset,
1 - tunnel_length, tunnel_offset, tunnel_length, tunnel_width);
}
}
// Special behavior for special objects // Special behavior for special objects
// TODO? hardcode this less? // TODO? hardcode this less?
if (name === 'floor_letter') { if (name === 'floor_letter') {

View File

@ -588,10 +588,16 @@ const TILE_TYPES = {
}, },
green_floor: { green_floor: {
draw_layer: LAYER_TERRAIN, draw_layer: LAYER_TERRAIN,
on_gray_button(me, level) {
level.transmute_tile(me, 'green_wall');
},
}, },
green_wall: { green_wall: {
draw_layer: LAYER_TERRAIN, draw_layer: LAYER_TERRAIN,
blocks_all: true, blocks_all: true,
on_gray_button(me, level) {
level.transmute_tile(me, 'green_floor');
},
}, },
green_chip: { green_chip: {
draw_layer: LAYER_ITEM, draw_layer: LAYER_ITEM,
@ -605,6 +611,7 @@ const TILE_TYPES = {
level.remove_tile(me); level.remove_tile(me);
} }
}, },
// Not affected by gray buttons
}, },
green_bomb: { green_bomb: {
draw_layer: LAYER_ITEM, draw_layer: LAYER_ITEM,
@ -619,15 +626,32 @@ const TILE_TYPES = {
level.transmute_tile(other, 'explosion'); level.transmute_tile(other, 'explosion');
} }
}, },
// Not affected by gray buttons
}, },
purple_floor: { purple_floor: {
// TODO wired
draw_layer: LAYER_TERRAIN, draw_layer: LAYER_TERRAIN,
on_gray_button(me, level) {
level.transmute_tile(me, 'purple_wall');
},
on_power(me, level) {
me.type.on_gray_button(me, level);
},
on_depower(me, level) {
me.type.on_gray_button(me, level);
},
}, },
purple_wall: { purple_wall: {
// TODO wired
draw_layer: LAYER_TERRAIN, draw_layer: LAYER_TERRAIN,
blocks_all: true, blocks_all: true,
on_gray_button(me, level) {
level.transmute_tile(me, 'purple_floor');
},
on_power(me, level) {
me.type.on_gray_button(me, level);
},
on_depower(me, level) {
me.type.on_gray_button(me, level);
},
}, },
cloner: { cloner: {
draw_layer: LAYER_TERRAIN, draw_layer: LAYER_TERRAIN,
@ -658,6 +682,13 @@ const TILE_TYPES = {
} }
} }
}, },
// Also clones on rising pulse or gray button
on_power(me, level) {
me.type.activate(me, level);
},
on_gray_button(me, level) {
me.type.activate(me, level);
},
}, },
trap: { trap: {
draw_layer: LAYER_TERRAIN, draw_layer: LAYER_TERRAIN,
@ -693,7 +724,7 @@ const TILE_TYPES = {
level.set_actor_stuck(other, true); level.set_actor_stuck(other, true);
} }
}, },
add_press(me, level, is_begin) { add_press(me, level) {
level._set_prop(me, 'presses', (me.presses ?? 0) + 1); level._set_prop(me, 'presses', (me.presses ?? 0) + 1);
if (me.presses === 1) { if (me.presses === 1) {
// Free everything on us, if we went from 0 to 1 presses (i.e. closed to open) // Free everything on us, if we went from 0 to 1 presses (i.e. closed to open)
@ -718,6 +749,15 @@ const TILE_TYPES = {
} }
} }
}, },
on_power(me, level) {
// Treat being powered or not as an extra kind of brown button press
me.type.add_press(me, level);
console.log("powering UP", me.presses);
},
on_depower(me, level) {
me.type.remove_press(me, level);
console.log("powering down...", me.presses);
},
visual_state(me) { visual_state(me) {
if (me && me.presses) { if (me && me.presses) {
return 'open'; return 'open';
@ -793,13 +833,30 @@ const TILE_TYPES = {
return level.iter_tiles_in_reading_order(me.cell, 'teleport_yellow', true); return level.iter_tiles_in_reading_order(me.cell, 'teleport_yellow', true);
}, },
}, },
// FIXME do i want these as separate objects? what would they do, turn into each other? or should it be one with state?
flame_jet_off: { flame_jet_off: {
draw_layer: LAYER_TERRAIN, draw_layer: LAYER_TERRAIN,
activate(me, level) {
level.transmute_tile(me, 'flame_jet_on');
},
on_gray_button(me, level) {
me.type.activate(me, level);
},
on_power(me, level) {
me.type.activate(me, level);
},
}, },
flame_jet_on: { flame_jet_on: {
draw_layer: LAYER_TERRAIN, draw_layer: LAYER_TERRAIN,
// FIXME every tic, kills every actor in the cell // FIXME every tic, kills every actor in the cell
activate(me, level) {
level.transmute_tile(me, 'flame_jet_off');
},
on_gray_button(me, level) {
me.type.activate(me, level);
},
on_power(me, level) {
me.type.activate(me, level);
},
}, },
// Buttons // Buttons
button_blue: { button_blue: {
@ -920,8 +977,17 @@ const TILE_TYPES = {
// FIXME toggles flame jets, connected somehow, ??? // FIXME toggles flame jets, connected somehow, ???
}, },
button_pink: { button_pink: {
// TODO not implemented
draw_layer: LAYER_TERRAIN, 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);
},
on_arrive(me, level, other) {
level.sfx.play_once('button-press', me.cell);
},
on_depart(me, level, other) {
level.sfx.play_once('button-release', me.cell);
},
}, },
button_black: { button_black: {
// TODO not implemented // TODO not implemented
@ -941,11 +1007,8 @@ const TILE_TYPES = {
continue; continue;
for (let tile of cell) { for (let tile of cell) {
if (tile.type.name === 'green_floor') { if (tile.type.on_gray_button) {
level.transmute_tile(tile, 'green_wall'); tile.type.on_gray_button(tile, level);
}
else if (tile.type.name === 'green_wall') {
level.transmute_tile(tile, 'green_floor');
} }
} }
} }

Binary file not shown.

Before

Width:  |  Height:  |  Size: 53 KiB

After

Width:  |  Height:  |  Size: 54 KiB

Binary file not shown.