Add partial wiring support
This commit is contained in:
parent
4cd0585d0b
commit
8adb630862
@ -69,6 +69,7 @@ class CC2Demo {
|
||||
let modifier_wire = {
|
||||
decode(tile, modifier) {
|
||||
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;
|
||||
},
|
||||
encode(tile) {
|
||||
|
||||
144
js/game.js
144
js/game.js
@ -125,6 +125,17 @@ export class Cell extends Array {
|
||||
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) {
|
||||
for (let tile of this) {
|
||||
if (tile !== actor &&
|
||||
@ -148,6 +159,8 @@ export class Cell extends Array {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
Cell.prototype.was_powered = false;
|
||||
Cell.prototype.is_powered = false;
|
||||
|
||||
class GameEnded extends Error {}
|
||||
|
||||
@ -205,6 +218,9 @@ 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 = [];
|
||||
// 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++) {
|
||||
@ -398,6 +414,9 @@ export class Level {
|
||||
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
|
||||
if (this.player.movement_cooldown <= 0) {
|
||||
this.player.is_pushing = false;
|
||||
@ -558,7 +577,7 @@ export class Level {
|
||||
}
|
||||
|
||||
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)
|
||||
continue;
|
||||
|
||||
@ -600,7 +619,7 @@ export class Level {
|
||||
if (actor.type.is_player && 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) {
|
||||
let could_push = ! neighbor.blocks_entering(actor, dir2, this, true);
|
||||
for (let tile of Array.from(neighbor)) {
|
||||
@ -677,7 +696,7 @@ export class Level {
|
||||
|
||||
let move = DIRECTIONS[direction].movement;
|
||||
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
|
||||
// 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 goal_x = cell.x + move[0];
|
||||
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];
|
||||
}
|
||||
else {
|
||||
|
||||
@ -200,7 +200,12 @@ export const CC2_TILESET_LAYOUT = {
|
||||
popwall2: [8, 10],
|
||||
gravel: [9, 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
|
||||
teeth: {
|
||||
@ -216,7 +221,7 @@ export const CC2_TILESET_LAYOUT = {
|
||||
swivel_ne: [11, 11],
|
||||
swivel_se: [12, 11],
|
||||
swivel_floor: [13, 11],
|
||||
// TODO some kinda four-edges thing again
|
||||
'#wire-tunnel': [14, 11],
|
||||
stopwatch_penalty: [15, 11],
|
||||
paramecium: {
|
||||
north: [[0, 12], [1, 12], [2, 12]],
|
||||
@ -666,10 +671,10 @@ export class Tileset {
|
||||
coords = drawspec.tile;
|
||||
}
|
||||
else if (drawspec.wired) {
|
||||
if (tile && tile.wire_directions !== undefined && tile.wire_directions !== 0) {
|
||||
// TODO all four is a different thing entirely
|
||||
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('#unpowered', tile, tic, blit);
|
||||
this.draw_type(tile.cell.is_powered ? '#powered' : '#unpowered', tile, tic, blit);
|
||||
|
||||
// Draw a masked part of the base tile
|
||||
let wiredir = tile.wire_directions;
|
||||
@ -726,7 +731,7 @@ export class Tileset {
|
||||
}
|
||||
|
||||
// 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
|
||||
let state = TILE_TYPES[name].visual_state(tile);
|
||||
// If it's a string, that's an alias for another state
|
||||
@ -797,6 +802,30 @@ export class Tileset {
|
||||
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
|
||||
// TODO? hardcode this less?
|
||||
if (name === 'floor_letter') {
|
||||
|
||||
@ -588,10 +588,16 @@ const TILE_TYPES = {
|
||||
},
|
||||
green_floor: {
|
||||
draw_layer: LAYER_TERRAIN,
|
||||
on_gray_button(me, level) {
|
||||
level.transmute_tile(me, 'green_wall');
|
||||
},
|
||||
},
|
||||
green_wall: {
|
||||
draw_layer: LAYER_TERRAIN,
|
||||
blocks_all: true,
|
||||
on_gray_button(me, level) {
|
||||
level.transmute_tile(me, 'green_floor');
|
||||
},
|
||||
},
|
||||
green_chip: {
|
||||
draw_layer: LAYER_ITEM,
|
||||
@ -605,6 +611,7 @@ const TILE_TYPES = {
|
||||
level.remove_tile(me);
|
||||
}
|
||||
},
|
||||
// Not affected by gray buttons
|
||||
},
|
||||
green_bomb: {
|
||||
draw_layer: LAYER_ITEM,
|
||||
@ -619,15 +626,32 @@ const TILE_TYPES = {
|
||||
level.transmute_tile(other, 'explosion');
|
||||
}
|
||||
},
|
||||
// Not affected by gray buttons
|
||||
},
|
||||
purple_floor: {
|
||||
// TODO wired
|
||||
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: {
|
||||
// TODO wired
|
||||
draw_layer: LAYER_TERRAIN,
|
||||
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: {
|
||||
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: {
|
||||
draw_layer: LAYER_TERRAIN,
|
||||
@ -693,7 +724,7 @@ const TILE_TYPES = {
|
||||
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);
|
||||
if (me.presses === 1) {
|
||||
// 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) {
|
||||
if (me && me.presses) {
|
||||
return 'open';
|
||||
@ -793,13 +833,30 @@ const TILE_TYPES = {
|
||||
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: {
|
||||
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: {
|
||||
draw_layer: LAYER_TERRAIN,
|
||||
// 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
|
||||
button_blue: {
|
||||
@ -920,8 +977,17 @@ const TILE_TYPES = {
|
||||
// FIXME toggles flame jets, connected somehow, ???
|
||||
},
|
||||
button_pink: {
|
||||
// TODO not implemented
|
||||
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: {
|
||||
// TODO not implemented
|
||||
@ -941,11 +1007,8 @@ const TILE_TYPES = {
|
||||
continue;
|
||||
|
||||
for (let tile of cell) {
|
||||
if (tile.type.name === 'green_floor') {
|
||||
level.transmute_tile(tile, 'green_wall');
|
||||
}
|
||||
else if (tile.type.name === 'green_wall') {
|
||||
level.transmute_tile(tile, 'green_floor');
|
||||
if (tile.type.on_gray_button) {
|
||||
tile.type.on_gray_button(tile, level);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
BIN
tileset-lexy.png
BIN
tileset-lexy.png
Binary file not shown.
|
Before Width: | Height: | Size: 53 KiB After Width: | Height: | Size: 54 KiB |
Binary file not shown.
Loading…
Reference in New Issue
Block a user