Merge pull request #41 from Patashu/custom-tiles
Custom tiles: Terraformer and Global Cycler
This commit is contained in:
commit
e8a6ae4a27
@ -15,11 +15,18 @@ export function trace_floor_circuit(level, start_cell, start_edge, on_wire, on_d
|
||||
let seen_edges = seen_cells.get(cell) ?? 0;
|
||||
if (seen_edges & edgeinfo.bit)
|
||||
continue;
|
||||
|
||||
let actor = cell.get_actor();
|
||||
let wire_directions = terrain.wire_directions;
|
||||
if ((actor?.wire_directions ?? null !== null) && (actor.movement_cooldown === 0 || level.compat.tiles_react_instantly))
|
||||
{
|
||||
wire_directions = actor.wire_directions;
|
||||
}
|
||||
|
||||
// The wire comes in from this edge towards the center; see how it connects within this
|
||||
// cell, then check for any neighbors
|
||||
let connections = edgeinfo.bit;
|
||||
if (! is_first && ((terrain.wire_directions ?? 0) & edgeinfo.bit) === 0) {
|
||||
if (! is_first && ((wire_directions ?? 0) & edgeinfo.bit) === 0) {
|
||||
// There's not actually a wire here (but not if this is our starting cell, in which
|
||||
// case we trust the caller)
|
||||
if (on_dead_end) {
|
||||
@ -31,16 +38,16 @@ export function trace_floor_circuit(level, start_cell, start_edge, on_wire, on_d
|
||||
// The wires in this tile never connect to each other
|
||||
}
|
||||
else if (terrain.type.wire_propagation_mode === 'cross' ||
|
||||
(terrain.wire_directions === 0x0f && terrain.type.wire_propagation_mode !== 'all'))
|
||||
(wire_directions === 0x0f && terrain.type.wire_propagation_mode !== 'all'))
|
||||
{
|
||||
// This is a cross pattern, so only opposite edges connect
|
||||
if (terrain.wire_directions & edgeinfo.opposite_bit) {
|
||||
if (wire_directions & edgeinfo.opposite_bit) {
|
||||
connections |= edgeinfo.opposite_bit;
|
||||
}
|
||||
}
|
||||
else {
|
||||
// Everything connects
|
||||
connections |= terrain.wire_directions;
|
||||
connections |= wire_directions;
|
||||
}
|
||||
|
||||
seen_cells.set(cell, seen_edges | connections);
|
||||
|
||||
101
js/format-c2g.js
101
js/format-c2g.js
@ -530,8 +530,8 @@ const TILE_ENCODING = {
|
||||
else {
|
||||
tile.direction = DIRECTION_ORDER[modifier & 0x03];
|
||||
let type = modifier >> 2;
|
||||
if (type < 6) {
|
||||
tile.gate_type = ['not', 'and', 'or', 'xor', 'latch-cw', 'nand'][type];
|
||||
if (type < 7) {
|
||||
tile.gate_type = ['not', 'and', 'or', 'xor', 'latch-cw', 'nand', 'diode'][type];
|
||||
}
|
||||
else if (type === 16) {
|
||||
tile.gate_type = 'latch-ccw';
|
||||
@ -561,6 +561,9 @@ const TILE_ENCODING = {
|
||||
else if (tile.gate_type === 'nand') {
|
||||
return 20 + direction_offset;
|
||||
}
|
||||
else if (tile.gate_type === 'diode') {
|
||||
return 24 + direction_offset;
|
||||
}
|
||||
else if (tile.gate_type === 'counter') {
|
||||
return 30 + tile.memory;
|
||||
}
|
||||
@ -787,6 +790,56 @@ const TILE_ENCODING = {
|
||||
},
|
||||
|
||||
// LL-specific tiles
|
||||
0xd0: {
|
||||
name: 'electrified_floor',
|
||||
is_extension: true,
|
||||
},
|
||||
0xd1: {
|
||||
name: 'hole',
|
||||
is_extension: true,
|
||||
},
|
||||
0xd2: {
|
||||
name: 'cracked_floor',
|
||||
is_extension: true,
|
||||
},
|
||||
0xd3: {
|
||||
name: 'cracked_ice',
|
||||
is_extension: true,
|
||||
},
|
||||
0xd4: {
|
||||
name: 'score_5x',
|
||||
has_next: true,
|
||||
is_extension: true,
|
||||
},
|
||||
0xd5: {
|
||||
name: 'spikes',
|
||||
is_extension: true,
|
||||
},
|
||||
0xd6: {
|
||||
name: 'boulder',
|
||||
has_next: true,
|
||||
extra_args: [arg_direction],
|
||||
},
|
||||
0xd7: {
|
||||
name: 'item_lock',
|
||||
has_next: true,
|
||||
is_extension: true,
|
||||
},
|
||||
0xd8: {
|
||||
name: 'dash_floor',
|
||||
is_extension: true,
|
||||
},
|
||||
0xd9: {
|
||||
name: 'teleport_blue_exit',
|
||||
modifier: modifier_wire,
|
||||
is_extension: true,
|
||||
},
|
||||
0xda: {
|
||||
name: 'glass_block',
|
||||
has_next: true,
|
||||
extra_args: [arg_direction],
|
||||
is_extension: true,
|
||||
},
|
||||
0xe0: {
|
||||
name: 'gift_bow',
|
||||
has_next: true,
|
||||
@ -799,6 +852,50 @@ const TILE_ENCODING = {
|
||||
extra_args: [arg_direction],
|
||||
is_extension: true,
|
||||
},
|
||||
0xe2: {
|
||||
name: 'skeleton_key',
|
||||
has_next: true,
|
||||
is_extension: true,
|
||||
},
|
||||
0xe3: {
|
||||
name: 'gate_red',
|
||||
has_next: true,
|
||||
is_extension: true,
|
||||
},
|
||||
0xe4: {
|
||||
name: 'gate_blue',
|
||||
has_next: true,
|
||||
is_extension: true,
|
||||
},
|
||||
0xe5: {
|
||||
name: 'gate_yellow',
|
||||
has_next: true,
|
||||
is_extension: true,
|
||||
},
|
||||
0xe6: {
|
||||
name: 'gate_green',
|
||||
has_next: true,
|
||||
is_extension: true,
|
||||
},
|
||||
0xe7: {
|
||||
name: 'sand',
|
||||
is_extension: true,
|
||||
},
|
||||
0xed: {
|
||||
name: 'halo',
|
||||
has_next: true,
|
||||
is_extension: true,
|
||||
},
|
||||
0xef: {
|
||||
name: 'turntable_cw',
|
||||
modifier: modifier_wire,
|
||||
is_extension: true,
|
||||
},
|
||||
0xf0: {
|
||||
name: 'turntable_ccw',
|
||||
modifier: modifier_wire,
|
||||
is_extension: true,
|
||||
},
|
||||
};
|
||||
const REVERSE_TILE_ENCODING = {};
|
||||
for (let [tile_byte, spec] of Object.entries(TILE_ENCODING)) {
|
||||
|
||||
297
js/game.js
297
js/game.js
@ -109,6 +109,21 @@ export class Tile {
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
slide_ignores(name) {
|
||||
if (this.type.slide_ignores && this.type.slide_ignores.has(name))
|
||||
return true;
|
||||
|
||||
if (this.toolbelt) {
|
||||
for (let item of this.toolbelt) {
|
||||
let item_type = TILE_TYPES[item];
|
||||
if (item_type.item_slide_ignores && item_type.item_slide_ignores.has(name))
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
can_push(tile, direction) {
|
||||
// This tile already has a push queued, sorry
|
||||
@ -293,6 +308,10 @@ export class Cell extends Array {
|
||||
if (! tile.blocks(actor, direction, level))
|
||||
continue;
|
||||
|
||||
if (tile.type.on_after_bumped) {
|
||||
tile.type.on_after_bumped(tile, level, actor);
|
||||
}
|
||||
|
||||
if (push_mode === null)
|
||||
return false;
|
||||
|
||||
@ -525,66 +544,99 @@ export class Level extends LevelInterface {
|
||||
// Connect buttons and teleporters
|
||||
let num_cells = this.width * this.height;
|
||||
for (let connectable of connectables) {
|
||||
let cell = connectable.cell;
|
||||
let x = cell.x;
|
||||
let y = cell.y;
|
||||
// FIXME this is a single string for red/brown buttons (to match iter_tiles_in_RO) but a
|
||||
// set for orange buttons (because flame jet states are separate tiles), which sucks ass
|
||||
let goals = connectable.type.connects_to;
|
||||
this.connect_button(connectable);
|
||||
}
|
||||
|
||||
// Check for custom wiring, for MSCC .DAT levels
|
||||
// TODO would be neat if this applied to orange buttons too
|
||||
if (this.stored_level.has_custom_connections) {
|
||||
let n = this.stored_level.coords_to_scalar(x, y);
|
||||
let target_cell_n = null;
|
||||
if (connectable.type.name === 'button_brown') {
|
||||
target_cell_n = this.stored_level.custom_trap_wiring[n] ?? null;
|
||||
this.recalculate_circuitry_next_wire_phase = false;
|
||||
this.undid_past_recalculate_circuitry = false;
|
||||
this.recalculate_circuitry(true);
|
||||
|
||||
// Finally, let all tiles do custom init behavior... but backwards, to match actor order
|
||||
for (let i = this.linear_cells.length - 1; i >= 0; i--) {
|
||||
let cell = this.linear_cells[i];
|
||||
for (let tile of cell) {
|
||||
if (! tile)
|
||||
continue;
|
||||
if (tile.type.on_ready) {
|
||||
tile.type.on_ready(tile, this);
|
||||
}
|
||||
else if (connectable.type.name === 'button_red') {
|
||||
target_cell_n = this.stored_level.custom_cloner_wiring[n] ?? null;
|
||||
}
|
||||
if (target_cell_n && target_cell_n < this.width * this.height) {
|
||||
let [tx, ty] = this.stored_level.scalar_to_coords(target_cell_n);
|
||||
for (let tile of this.cell(tx, ty)) {
|
||||
if (tile && goals === tile.type.name) {
|
||||
connectable.connection = tile;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
continue;
|
||||
}
|
||||
}
|
||||
// Erase undo, in case any on_ready added to it (we don't want to undo initialization!)
|
||||
this.pending_undo = this.create_undo_entry();
|
||||
}
|
||||
|
||||
connect_button(connectable) {
|
||||
let cell = connectable.cell;
|
||||
let x = cell.x;
|
||||
let y = cell.y;
|
||||
// FIXME this is a single string for red/brown buttons (to match iter_tiles_in_RO) but a
|
||||
// set for orange buttons (because flame jet states are separate tiles), which sucks ass
|
||||
let goals = connectable.type.connects_to;
|
||||
|
||||
// Orange buttons do a really weird diamond search
|
||||
if (connectable.type.connect_order === 'diamond') {
|
||||
for (let cell of this.iter_cells_in_diamond(connectable.cell)) {
|
||||
let target = null;
|
||||
for (let tile of cell) {
|
||||
if (tile && goals.has(tile.type.name)) {
|
||||
target = tile;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (target !== null) {
|
||||
connectable.connection = target;
|
||||
// Check for custom wiring, for MSCC .DAT levels
|
||||
// TODO would be neat if this applied to orange buttons too
|
||||
if (this.stored_level.has_custom_connections) {
|
||||
let n = this.stored_level.coords_to_scalar(x, y);
|
||||
let target_cell_n = null;
|
||||
if (connectable.type.name === 'button_brown') {
|
||||
target_cell_n = this.stored_level.custom_trap_wiring[n] ?? null;
|
||||
}
|
||||
else if (connectable.type.name === 'button_red') {
|
||||
target_cell_n = this.stored_level.custom_cloner_wiring[n] ?? null;
|
||||
}
|
||||
if (target_cell_n && target_cell_n < this.width * this.height) {
|
||||
let [tx, ty] = this.stored_level.scalar_to_coords(target_cell_n);
|
||||
for (let tile of this.cell(tx, ty)) {
|
||||
if (tile && goals === tile.type.name) {
|
||||
connectable.connection = tile;
|
||||
break;
|
||||
}
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
// Otherwise, look in reading order
|
||||
for (let tile of this.iter_tiles_in_reading_order(cell, goals)) {
|
||||
// TODO ideally this should be a weak connection somehow, since dynamite can destroy
|
||||
// empty cloners and probably traps too
|
||||
connectable.connection = tile;
|
||||
// Just grab the first
|
||||
break;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// Orange buttons do a really weird diamond search
|
||||
if (connectable.type.connect_order === 'diamond') {
|
||||
for (let cell of this.iter_cells_in_diamond(connectable.cell)) {
|
||||
let target = null;
|
||||
for (let tile of cell) {
|
||||
if (tile && goals.has(tile.type.name)) {
|
||||
target = tile;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (target !== null) {
|
||||
connectable.connection = target;
|
||||
break;
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// Otherwise, look in reading order
|
||||
for (let tile of this.iter_tiles_in_reading_order(cell, goals)) {
|
||||
// TODO ideally this should be a weak connection somehow, since dynamite can destroy
|
||||
// empty cloners and probably traps too
|
||||
connectable.connection = tile;
|
||||
// Just grab the first
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
recalculate_circuitry(first_time = false, undoing = false) {
|
||||
// Build circuits out of connected wires
|
||||
// TODO document this idea
|
||||
|
||||
if (!first_time) {
|
||||
for (let circuit of this.circuits) {
|
||||
for (let tile of circuit.tiles) {
|
||||
tile[0].circuits = [null, null, null, null];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
this.circuits = [];
|
||||
this.power_sources = [];
|
||||
let wired_outputs = new Set;
|
||||
@ -594,6 +646,7 @@ export class Level extends LevelInterface {
|
||||
};
|
||||
for (let cell of this.linear_cells) {
|
||||
// We're interested in static circuitry, which means terrain
|
||||
// OR circuit blocks on top
|
||||
let terrain = cell.get_terrain();
|
||||
if (! terrain) // ?!
|
||||
continue;
|
||||
@ -602,7 +655,13 @@ export class Level extends LevelInterface {
|
||||
this.power_sources.push(terrain);
|
||||
}
|
||||
|
||||
let actor = cell.get_actor();
|
||||
let wire_directions = terrain.wire_directions;
|
||||
if ((actor?.wire_directions ?? null !== null) && (actor.movement_cooldown === 0 || this.compat.tiles_react_instantly))
|
||||
{
|
||||
wire_directions = actor.wire_directions;
|
||||
}
|
||||
|
||||
if (! wire_directions && ! terrain.wire_tunnel_directions) {
|
||||
// No wires, not interesting... unless it's a logic gate, which defines its own
|
||||
// wires! We only care about outgoing ones here, on the off chance that they point
|
||||
@ -635,7 +694,7 @@ export class Level extends LevelInterface {
|
||||
continue;
|
||||
|
||||
let circuit = {
|
||||
is_powered: false,
|
||||
is_powered: first_time ? false : null,
|
||||
tiles: new Map,
|
||||
inputs: new Map,
|
||||
};
|
||||
@ -700,20 +759,25 @@ export class Level extends LevelInterface {
|
||||
}
|
||||
this.wired_outputs = Array.from(wired_outputs);
|
||||
this.wired_outputs.sort((a, b) => this.coords_to_scalar(a.cell.x, a.cell.y) - this.coords_to_scalar(b.cell.x, b.cell.y));
|
||||
|
||||
// Finally, let all tiles do custom init behavior... but backwards, to match actor order
|
||||
for (let i = this.linear_cells.length - 1; i >= 0; i--) {
|
||||
let cell = this.linear_cells[i];
|
||||
for (let tile of cell) {
|
||||
if (! tile)
|
||||
continue;
|
||||
if (tile.type.on_ready) {
|
||||
tile.type.on_ready(tile, this);
|
||||
|
||||
if (!first_time) {
|
||||
//update wireables
|
||||
for (var i = 0; i < this.width; ++i)
|
||||
{
|
||||
for (var j = 0; j < this.height; ++j)
|
||||
{
|
||||
let terrain = this.cell(i, j).get_terrain();
|
||||
if (terrain.is_wired !== undefined)
|
||||
{
|
||||
terrain.type.on_begin(terrain, this);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!undoing) {
|
||||
this._push_pending_undo(() => this.undid_past_recalculate_circuitry = true);
|
||||
}
|
||||
}
|
||||
// Erase undo, in case any on_ready added to it (we don't want to undo initialization!)
|
||||
this.pending_undo = this.create_undo_entry();
|
||||
}
|
||||
|
||||
can_accept_input() {
|
||||
@ -1521,12 +1585,15 @@ export class Level extends LevelInterface {
|
||||
// FIXME this feels clunky
|
||||
let goal_cell = this.get_neighboring_cell(actor.cell, direction);
|
||||
let terrain = goal_cell.get_terrain();
|
||||
if (actor.has_item('speed_boots')) {
|
||||
speed /= 2;
|
||||
}
|
||||
else if (terrain && terrain.type.speed_factor && ! actor.ignores(terrain.type.name)) {
|
||||
if (terrain && terrain.type.speed_factor && ! actor.ignores(terrain.type.name) && !actor.slide_ignores(terrain.type.name)) {
|
||||
speed /= terrain.type.speed_factor;
|
||||
}
|
||||
//speed boots speed us up UNLESS we're entering a terrain with a speed factor and an unignored slide mode (so e.g. we gain 2x on teleports, ice + ice skates, force floors + suction boots, sand and dash floors, but we don't gain 2x sliding on ice or force floors unless it's the turn we're leaving them)
|
||||
if (actor.has_item('speed_boots')
|
||||
&& !(terrain.type.speed_factor && terrain.type.slide_mode && !actor.ignores(terrain.type.name) && !actor.slide_ignores(terrain.type.name)))
|
||||
{
|
||||
speed /= 2;
|
||||
}
|
||||
|
||||
let orig_cell = actor.cell;
|
||||
this._set_tile_prop(actor, 'previous_cell', orig_cell);
|
||||
@ -1577,11 +1644,15 @@ export class Level extends LevelInterface {
|
||||
move_to(actor, goal_cell, speed) {
|
||||
if (actor.cell === goal_cell)
|
||||
return;
|
||||
|
||||
if (actor.type.on_starting_move) {
|
||||
actor.type.on_starting_move(actor, this);
|
||||
}
|
||||
|
||||
let original_cell = actor.cell;
|
||||
// Physically remove the actor first, so that it won't get in the way of e.g. a splash
|
||||
// spawned from stepping off of a lilypad
|
||||
this.remove_tile(actor);
|
||||
this.remove_tile(actor, true);
|
||||
|
||||
// Announce we're leaving, for the handful of tiles that care about it
|
||||
for (let tile of original_cell) {
|
||||
@ -1609,21 +1680,23 @@ export class Level extends LevelInterface {
|
||||
continue;
|
||||
if (actor.ignores(tile.type.name))
|
||||
continue;
|
||||
if (actor.slide_ignores(tile.type.name))
|
||||
continue;
|
||||
|
||||
// Possibly kill a player
|
||||
if (actor.has_item('helmet') || tile.has_item('helmet')) {
|
||||
// Helmet disables this, do nothing
|
||||
}
|
||||
else if (actor.type.is_real_player && tile.type.is_monster) {
|
||||
this.fail(tile.type.name, actor);
|
||||
this.fail(tile.type.name, tile, actor);
|
||||
}
|
||||
else if (actor.type.is_monster && tile.type.is_real_player) {
|
||||
this.fail(actor.type.name, tile);
|
||||
this.fail(actor.type.name, actor, tile);
|
||||
}
|
||||
else if (actor.type.is_block && tile.type.is_real_player && ! actor.is_pulled) {
|
||||
// Note that blocks squish players if they move for ANY reason, even if pushed by
|
||||
// another player! The only exception is being pulled
|
||||
this.fail('squished', tile);
|
||||
this.fail('squished', actor, tile);
|
||||
}
|
||||
|
||||
if (tile.type.on_approach) {
|
||||
@ -1656,7 +1729,7 @@ export class Level extends LevelInterface {
|
||||
this.player.movement_cooldown === this.player.movement_speed &&
|
||||
! actor.has_item('helmet') && ! this.player.has_item('helmet'))
|
||||
{
|
||||
this.fail(actor.type.name);
|
||||
this.fail(actor.type.name, actor, this.player);
|
||||
}
|
||||
|
||||
if (this.compat.tiles_react_instantly) {
|
||||
@ -1666,6 +1739,10 @@ export class Level extends LevelInterface {
|
||||
|
||||
// Step on every tile in a cell we just arrived in
|
||||
step_on_cell(actor, cell) {
|
||||
if (actor.type.on_finishing_move) {
|
||||
actor.type.on_finishing_move(actor, this);
|
||||
}
|
||||
|
||||
// Step on topmost things first -- notably, it's safe to step on water with flippers on top
|
||||
// TODO is there a custom order here similar to collision checking?
|
||||
for (let layer = LAYERS.MAX - 1; layer >= 0; layer--) {
|
||||
@ -1812,7 +1889,7 @@ export class Level extends LevelInterface {
|
||||
}
|
||||
|
||||
// Now physically move the actor, but their movement waits until next decision phase
|
||||
this.remove_tile(actor);
|
||||
this.remove_tile(actor, true);
|
||||
this.add_tile(actor, dest.cell);
|
||||
}
|
||||
|
||||
@ -1911,6 +1988,14 @@ export class Level extends LevelInterface {
|
||||
}
|
||||
|
||||
_do_wire_phase() {
|
||||
let force_next_wire_phase = false;
|
||||
if (this.recalculate_circuitry_next_wire_phase)
|
||||
{
|
||||
this.recalculate_circuitry();
|
||||
this.recalculate_circuitry_next_wire_phase = false;
|
||||
force_next_wire_phase = true;
|
||||
}
|
||||
|
||||
if (this.circuits.length === 0)
|
||||
return;
|
||||
|
||||
@ -1956,7 +2041,7 @@ export class Level extends LevelInterface {
|
||||
this._set_tile_prop(tile, 'emitting_edges', emitting);
|
||||
}
|
||||
}
|
||||
// Next, actors who are standing still, on floor, and holding a lightning bolt
|
||||
// Next, actors who are standing still, on floor/electrified, and holding a lightning bolt
|
||||
let externally_powered_circuits = new Set;
|
||||
for (let actor of this.actors) {
|
||||
if (! actor.cell)
|
||||
@ -1964,7 +2049,7 @@ export class Level extends LevelInterface {
|
||||
let emitting = 0;
|
||||
if (actor.movement_cooldown === 0 && actor.has_item('lightning_bolt')) {
|
||||
let wired_tile = actor.cell.get_wired_tile();
|
||||
if (wired_tile && (wired_tile === actor || wired_tile.type.name === 'floor')) {
|
||||
if (wired_tile && (wired_tile === actor || wired_tile.type.name === 'floor' || wired_tile.type.name === 'electrified_floor')) {
|
||||
emitting = wired_tile.wire_directions;
|
||||
for (let circuit of wired_tile.circuits) {
|
||||
if (circuit) {
|
||||
@ -1979,8 +2064,9 @@ export class Level extends LevelInterface {
|
||||
}
|
||||
}
|
||||
|
||||
if (! any_changed)
|
||||
if (! any_changed && !force_next_wire_phase) {
|
||||
return;
|
||||
}
|
||||
|
||||
for (let tile of this.wired_outputs) {
|
||||
// This is only used within this function, no need to undo
|
||||
@ -2090,6 +2176,33 @@ export class Level extends LevelInterface {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
//same as above, but accepts multiple tiles
|
||||
*iter_tiles_in_reading_order_multiple(start_cell, names, reverse = false) {
|
||||
let i = this.coords_to_scalar(start_cell.x, start_cell.y);
|
||||
let index = TILE_TYPES[names[0]].layer;
|
||||
while (true) {
|
||||
if (reverse) {
|
||||
i -= 1;
|
||||
if (i < 0) {
|
||||
i += this.size_x * this.size_y;
|
||||
}
|
||||
}
|
||||
else {
|
||||
i += 1;
|
||||
i %= this.size_x * this.size_y;
|
||||
}
|
||||
|
||||
let cell = this.linear_cells[i];
|
||||
let tile = cell[index];
|
||||
if (tile && names.indexOf(tile.type.name) >= 0) {
|
||||
yield tile;
|
||||
}
|
||||
|
||||
if (cell === start_cell)
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Iterates over the grid in a diamond pattern, spreading out from the given start cell (but not
|
||||
// including it). Only used for connecting orange buttons.
|
||||
@ -2183,6 +2296,11 @@ export class Level extends LevelInterface {
|
||||
}
|
||||
this._undo_entry(this.undo_buffer[this.undo_buffer_index]);
|
||||
this.undo_buffer[this.undo_buffer_index] = null;
|
||||
|
||||
if (this.undid_past_recalculate_circuitry) {
|
||||
this.recalculate_circuitry_next_wire_phase = true;
|
||||
this.undid_past_recalculate_circuitry = false;
|
||||
}
|
||||
}
|
||||
|
||||
// Reverse a single undo entry
|
||||
@ -2280,7 +2398,7 @@ export class Level extends LevelInterface {
|
||||
}
|
||||
}
|
||||
|
||||
fail(reason, player = null) {
|
||||
fail(reason, killer = null, player = null) {
|
||||
if (this.state !== 'playing')
|
||||
return;
|
||||
|
||||
@ -2294,14 +2412,34 @@ export class Level extends LevelInterface {
|
||||
if (player === null) {
|
||||
player = this.player;
|
||||
}
|
||||
|
||||
if (player != null && this.take_tool_from_actor(player, 'halo')) {
|
||||
this.sfx.play_once('revive');
|
||||
if (reason === 'time')
|
||||
{
|
||||
this.pause_timer();
|
||||
}
|
||||
else if (killer !== null)
|
||||
{
|
||||
if (killer.type.is_actor || killer.type.is_item)
|
||||
{
|
||||
this.remove_tile(killer);
|
||||
}
|
||||
else //presumably terrain
|
||||
{
|
||||
this.transmute_tile(killer, 'floor');
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
this._push_pending_undo(() => {
|
||||
this.fail_reason = null;
|
||||
player.fail_reason = null;
|
||||
if (player != null) { player.fail_reason = null; }
|
||||
});
|
||||
this.state = 'failure';
|
||||
this.fail_reason = reason;
|
||||
player.fail_reason = reason;
|
||||
if (player != null) { player.fail_reason = reason; }
|
||||
}
|
||||
|
||||
win() {
|
||||
@ -2424,6 +2562,9 @@ export class Level extends LevelInterface {
|
||||
this._set_tile_prop(tile, 'last_extra_cooldown_tic', null);
|
||||
}
|
||||
this._do_extra_cooldown(tile);
|
||||
if (old_type.on_death) {
|
||||
old_type.on_death(tile, this);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -2537,9 +2678,9 @@ export class Level extends LevelInterface {
|
||||
return true;
|
||||
}
|
||||
|
||||
take_key_from_actor(actor, name) {
|
||||
take_key_from_actor(actor, name, ignore_infinity = false) {
|
||||
if (actor.keyring && (actor.keyring[name] ?? 0) > 0) {
|
||||
if (actor.type.infinite_items && actor.type.infinite_items[name]) {
|
||||
if (!ignore_infinity && actor.type.infinite_items && actor.type.infinite_items[name]) {
|
||||
// Some items can't be taken away normally, by which I mean, green or yellow keys
|
||||
return true;
|
||||
}
|
||||
|
||||
@ -1072,7 +1072,7 @@ class WireOperation extends DrawOperation {
|
||||
// TODO probably a better way to do this
|
||||
if (! tile)
|
||||
continue;
|
||||
if (['floor', 'steel', 'button_pink', 'button_black', 'teleport_blue', 'teleport_red', 'light_switch_on', 'light_switch_off', 'circuit_block'].indexOf(tile.type.name) < 0)
|
||||
if (['floor', 'steel', 'button_pink', 'button_black', 'teleport_blue', 'teleport_red', 'light_switch_on', 'light_switch_off', 'circuit_block', 'teleport_blue_exit', 'turntable_cw', 'turntable_ccw'].indexOf(tile.type.name) < 0)
|
||||
continue;
|
||||
|
||||
tile = {...tile};
|
||||
@ -1598,6 +1598,21 @@ const EDITOR_PALETTE = [{
|
||||
'gate_yellow',
|
||||
'gate_green',
|
||||
'sand',
|
||||
'dash_floor',
|
||||
'spikes',
|
||||
'cracked_ice',
|
||||
'hole',
|
||||
'cracked_floor',
|
||||
'turntable_cw',
|
||||
'turntable_ccw',
|
||||
'teleport_blue_exit',
|
||||
'electrified_floor',
|
||||
'halo',
|
||||
'item_lock',
|
||||
'score_5x',
|
||||
'boulder',
|
||||
'glass_block',
|
||||
'logic_gate/diode',
|
||||
],
|
||||
}];
|
||||
|
||||
@ -2094,6 +2109,10 @@ const EDITOR_TILE_DESCRIPTIONS = {
|
||||
name: "NOT gate",
|
||||
desc: "Emits power only when not receiving power.",
|
||||
},
|
||||
'logic_gate/diode': {
|
||||
name: "Diode",
|
||||
desc: "Emits power only when receiving power. (Effectively, this delays power by one frame.)",
|
||||
},
|
||||
'logic_gate/and': {
|
||||
name: "AND gate",
|
||||
desc: "Emits power while both inputs are receiving power.",
|
||||
@ -2154,7 +2173,7 @@ const EDITOR_TILE_DESCRIPTIONS = {
|
||||
// Experimental
|
||||
circuit_block: {
|
||||
name: "Circuit block",
|
||||
desc: "(Currently non-functional.) May contain wires, which will connect to any adjacent wires and conduct power as normal.",
|
||||
desc: "May contain wires, which will connect to any adjacent wires and conduct power as normal. When pushed into water, turns into floor with the same wires.",
|
||||
},
|
||||
gift_bow: {
|
||||
name: "Gift bow",
|
||||
@ -2171,7 +2190,63 @@ const EDITOR_TILE_DESCRIPTIONS = {
|
||||
sand: {
|
||||
name: "Sand",
|
||||
desc: "Anything walking on it moves at half speed. Stops all blocks.",
|
||||
}
|
||||
},
|
||||
halo: {
|
||||
name: "Halo",
|
||||
desc: "Protects the player from death once, destroying the would-be killer in the process.",
|
||||
},
|
||||
turntable_cw: {
|
||||
name: "Turntable (clockwise)",
|
||||
desc: "Rotates anything entering this tile clockwise. Frame blocks are rotated too. If connected to wire, only functions while receiving power.",
|
||||
},
|
||||
turntable_ccw: {
|
||||
name: "Turntable (counterclockwise)",
|
||||
desc: "Rotates anything entering this tile counterclockwise. Frame blocks are rotated too. If connected to wire, only functions while receiving power.",
|
||||
},
|
||||
electrified_floor: {
|
||||
name: "Electrified floor",
|
||||
desc: "Conducts power (like a 4-way wire). While powered, destroys anything not wearing lightning boots (except dirt blocks).",
|
||||
},
|
||||
hole: {
|
||||
name: "Hole",
|
||||
desc: "A bottomless pit. Destroys everything (except ghosts).",
|
||||
},
|
||||
cracked_floor: {
|
||||
name: "Cracked floor",
|
||||
desc: "Turns into a hole when something steps off of it (except ghosts).",
|
||||
},
|
||||
cracked_ice: {
|
||||
name: "Cracked ice",
|
||||
desc: "Turns into water when something steps off of it (except ghosts).",
|
||||
},
|
||||
score_5x: {
|
||||
name: "×5 bonus",
|
||||
desc: "Quintuples the player's current bonus points. Can be collected by doppelgangers, rovers, and bowling balls, but will not grant bonus points.",
|
||||
},
|
||||
spikes: {
|
||||
name: "Spikes",
|
||||
desc: "Stops players (and doppelgangers) unless they have hiking boots. Everything else can pass.",
|
||||
},
|
||||
boulder: {
|
||||
name: "Boulder",
|
||||
desc: "Similar to a dirt block, but rolls when pushed. Boulders transfer momentum to each other. Has ice block/frame block collision. Turns into gravel in water. Spreads slime.",
|
||||
},
|
||||
item_lock: {
|
||||
name: "Item lock",
|
||||
desc: "When placed atop an item, you must have that item to enter the tile. When you do, pay the item and destroy the item lock. Also can be placed on top of a bonus, and you must pay that amount of bonus to enter.",
|
||||
},
|
||||
dash_floor: {
|
||||
name: "Dash floor",
|
||||
desc: "Anything walking on it moves at double speed. Stacks with speed shoes!",
|
||||
},
|
||||
teleport_blue_exit: {
|
||||
name: "Blue teleporter exit",
|
||||
desc: "A blue teleporter for all intents and purposes except it can only be exited, not entered.",
|
||||
},
|
||||
glass_block: {
|
||||
name: "Glass block",
|
||||
desc: "Similar to a dirt block, but stores the first item it moves over, dropping it when destroyed and cloning it in a cloning machine. Has ice block/frame block collision. Turns into floor in water. Doesn't have dirt block immunities.",
|
||||
},
|
||||
};
|
||||
|
||||
const SPECIAL_PALETTE_ENTRIES = {
|
||||
@ -2187,6 +2262,7 @@ const SPECIAL_PALETTE_ENTRIES = {
|
||||
'railroad/curve': { name: 'railroad', tracks: 1 << 0, track_switch: null, entered_direction: 'north' },
|
||||
'railroad/switch': { name: 'railroad', tracks: 0, track_switch: 0, entered_direction: 'north' },
|
||||
'logic_gate/not': { name: 'logic_gate', direction: 'north', gate_type: 'not' },
|
||||
'logic_gate/diode': { name: 'logic_gate', direction: 'north', gate_type: 'diode' },
|
||||
'logic_gate/and': { name: 'logic_gate', direction: 'north', gate_type: 'and' },
|
||||
'logic_gate/or': { name: 'logic_gate', direction: 'north', gate_type: 'or' },
|
||||
'logic_gate/xor': { name: 'logic_gate', direction: 'north', gate_type: 'xor' },
|
||||
@ -2418,6 +2494,8 @@ for (let cycle of [
|
||||
['force_floor_n', 'force_floor_e', 'force_floor_s', 'force_floor_w'],
|
||||
['ice_nw', 'ice_ne', 'ice_se', 'ice_sw'],
|
||||
['swivel_nw', 'swivel_ne', 'swivel_se', 'swivel_sw'],
|
||||
['terraformer_n', 'terraformer_e', 'terraformer_s', 'terraformer_w'],
|
||||
['turntable_cw', 'turntable_ccw'],
|
||||
]) {
|
||||
for (let [i, name] of cycle.entries()) {
|
||||
let left = cycle[(i - 1 + cycle.length) % cycle.length];
|
||||
|
||||
43
js/main.js
43
js/main.js
@ -25,6 +25,33 @@ function format_replay_duration(t) {
|
||||
return `${t} tics (${util.format_duration(t / TICS_PER_SECOND)})`;
|
||||
}
|
||||
|
||||
function simplify_number(number) {
|
||||
if (number < 1e6)
|
||||
{
|
||||
return number.toString();
|
||||
}
|
||||
else if (number < 1e9)
|
||||
{
|
||||
return (number/1e6).toPrecision(4) + "M";
|
||||
}
|
||||
else if (number < 1e12)
|
||||
{
|
||||
return (number/1e9).toPrecision(4) + "B";
|
||||
}
|
||||
else if (number < 1e15)
|
||||
{
|
||||
return (number/1e12).toPrecision(4) + "T";
|
||||
}
|
||||
else if (number < 1e18)
|
||||
{
|
||||
return (number/1e15).toPrecision(4) + "Q";
|
||||
}
|
||||
else
|
||||
{
|
||||
return number.toPrecision(2).replace("+","")
|
||||
}
|
||||
}
|
||||
|
||||
// TODO:
|
||||
// - level password, if any
|
||||
const ACTION_LABELS = {
|
||||
@ -85,6 +112,18 @@ const OBITUARIES = {
|
||||
"you're feeling quite alarmed",
|
||||
//"
|
||||
],
|
||||
electrocuted: [
|
||||
"a shocking revelation",
|
||||
"danger: high voltage",
|
||||
"inadequate insulation",
|
||||
"rode the lightning",
|
||||
],
|
||||
fell: [
|
||||
"some say she's still falling",
|
||||
"look before you leap",
|
||||
"where's my ladder",
|
||||
"it's dark down here",
|
||||
],
|
||||
generic: [
|
||||
"you had a bad time",
|
||||
],
|
||||
@ -311,6 +350,8 @@ class SFXPlayer {
|
||||
timeup: 'sfx/timeup.ogg',
|
||||
// https://jummbus.bitbucket.io/#j2N03winn200s0k0l00e00t2wm9a3g00j07i0r1O_U00o32T0v0EL0OD0Ou00q1d1f5y1z1C2w1c2h0T0v0pL0OD0Ou00q0d1f2y1z2C0w2c3h0b4gp1xFyW4xo31pe0MaCHCbwLbM5cFDgapBOyY0
|
||||
win: 'sfx/win.ogg',
|
||||
//from Ableton Retro Synths
|
||||
'revive': 'sfx/revive.ogg',
|
||||
};
|
||||
|
||||
for (let [name, path] of Object.entries(this.sound_sources)) {
|
||||
@ -1548,7 +1589,7 @@ class Player extends PrimaryView {
|
||||
this.time_el.classList.toggle('--danger', this.level.time_remaining < 10 * TICS_PER_SECOND);
|
||||
}
|
||||
|
||||
this.bonus_el.textContent = this.level.bonus_points;
|
||||
this.bonus_el.textContent = simplify_number(this.level.bonus_points);
|
||||
if (this.level.bonus_points > 0) {
|
||||
this.root.classList.add('--bonus-visible');
|
||||
}
|
||||
|
||||
@ -496,6 +496,7 @@ export const CC2_TILESET_LAYOUT = {
|
||||
burned: [1, 5],
|
||||
exploded: [1, 5],
|
||||
failed: [1, 5],
|
||||
fell: [5, 39],
|
||||
},
|
||||
// Do a quick spin I guess??
|
||||
player1_exit: [[0, 22], [8, 22], [0, 23], [8, 23]],
|
||||
@ -547,6 +548,12 @@ export const CC2_TILESET_LAYOUT = {
|
||||
south: [2, 25],
|
||||
west: [3, 25],
|
||||
},
|
||||
diode: {
|
||||
north: [0, 41],
|
||||
east: [1, 41],
|
||||
south: [2, 41],
|
||||
west: [3, 41],
|
||||
},
|
||||
and: {
|
||||
north: [4, 25],
|
||||
east: [5, 25],
|
||||
@ -627,6 +634,7 @@ export const CC2_TILESET_LAYOUT = {
|
||||
burned: [1, 5],
|
||||
exploded: [1, 5],
|
||||
failed: [1, 5],
|
||||
fell: [5, 39],
|
||||
},
|
||||
player2_exit: [[0, 27], [8, 27], [0, 28], [8, 28]],
|
||||
fire: [
|
||||
@ -1031,6 +1039,51 @@ export const LL_TILESET_LAYOUT = Object.assign({}, CC2_TILESET_LAYOUT, {
|
||||
skeleton_key: [4, 40],
|
||||
|
||||
sand: [10, 41],
|
||||
halo: [5, 43],
|
||||
turntable_cw: {
|
||||
__special__: 'wires',
|
||||
base: [0, 2],
|
||||
wired: {
|
||||
__special__: 'visual-state',
|
||||
active: [7, 43],
|
||||
inactive: [9, 43],
|
||||
}
|
||||
},
|
||||
turntable_ccw: {
|
||||
__special__: 'wires',
|
||||
base: [0, 2],
|
||||
wired: {
|
||||
__special__: 'visual-state',
|
||||
active: [8, 43],
|
||||
inactive: [10, 43],
|
||||
}
|
||||
},
|
||||
electrified_floor: {
|
||||
__special__: 'visual-state',
|
||||
active: [[5, 41], [6, 41], [7, 41]],
|
||||
inactive: [4, 41],
|
||||
},
|
||||
hole: {
|
||||
__special__: 'visual-state',
|
||||
north: [8, 41],
|
||||
open: [9, 41],
|
||||
},
|
||||
cracked_floor: [11, 43],
|
||||
cracked_ice: [7, 40],
|
||||
score_5x: [10, 40],
|
||||
spikes: [5, 40],
|
||||
boulder: [8, 40],
|
||||
item_lock: [12, 43],
|
||||
dash_floor: [[0, 44], [1, 44], [2, 44], [3, 44], [4, 44], [5, 44], [6, 44], [7, 44]],
|
||||
teleport_blue_exit: {
|
||||
__special__: 'wires',
|
||||
base: [0, 2],
|
||||
wired: [11, 41],
|
||||
},
|
||||
glass_block: {
|
||||
__special__: 'encased_item',
|
||||
base: [14, 41],
|
||||
}
|
||||
});
|
||||
|
||||
export const TILESET_LAYOUTS = {
|
||||
@ -1473,7 +1526,7 @@ export class Tileset {
|
||||
if (tile && tile.cell) {
|
||||
// What goes on top varies a bit...
|
||||
let r = this.layout['#wire-width'] / 2;
|
||||
if (tile.gate_type === 'not' || tile.gate_type === 'counter') {
|
||||
if (tile.gate_type === 'not' || tile.gate_type === 'counter' || tile.gate_type === 'diode') {
|
||||
this._draw_fourway_tile_power(tile, 0x0f, packet);
|
||||
}
|
||||
else {
|
||||
@ -1542,6 +1595,16 @@ export class Tileset {
|
||||
this.draw_drawspec(drawspec.railroad_switch, name, tile, packet);
|
||||
}
|
||||
}
|
||||
|
||||
_draw_encased_item(drawspec, name, tile, packet) {
|
||||
//draw the encased item
|
||||
if (tile !== null && tile.encased_item !== undefined && tile.encased_item !== null) {
|
||||
this._draw_standard(this.layout[tile.encased_item], tile.encased_item, TILE_TYPES[tile.encased_item], packet);
|
||||
}
|
||||
//then draw the glass block
|
||||
this._draw_standard(drawspec.base, name, tile, packet);
|
||||
}
|
||||
|
||||
|
||||
draw_drawspec(drawspec, name, tile, packet) {
|
||||
if (drawspec.__special__) {
|
||||
@ -1592,6 +1655,9 @@ export class Tileset {
|
||||
else if (drawspec.__special__ === 'railroad') {
|
||||
this._draw_railroad(drawspec, name, tile, packet);
|
||||
}
|
||||
else if (drawspec.__special__ === 'encased_item') {
|
||||
this._draw_encased_item(drawspec, name, tile, packet);
|
||||
}
|
||||
else {
|
||||
console.error(`No such special ${drawspec.__special__} for ${name}`);
|
||||
}
|
||||
|
||||
459
js/tiletypes.js
459
js/tiletypes.js
@ -18,7 +18,7 @@ function on_begin_force_floor(me, level) {
|
||||
|
||||
me.type.on_arrive(me, level, actor);
|
||||
if (me.type.slide_mode) {
|
||||
actor.slide_mode = me.type.slide_mode;
|
||||
level._set_tile_prop(actor, 'slide_mode', me.type.slide_mode);
|
||||
}
|
||||
|
||||
// Item bestowal
|
||||
@ -36,7 +36,7 @@ function on_begin_force_floor(me, level) {
|
||||
if (level.attempt_take(actor, item) && actor.ignores(me.type.name)) {
|
||||
// If they just picked up suction boots, they're no longer sliding
|
||||
// TODO this feels hacky, shouldn't the slide mode be erased some other way?
|
||||
actor.slide_mode = null;
|
||||
level._set_tile_prop(actor, 'slide_mode', null);
|
||||
}
|
||||
}
|
||||
|
||||
@ -87,6 +87,30 @@ function _define_gate(key) {
|
||||
};
|
||||
}
|
||||
|
||||
function update_wireable(me, level) {
|
||||
if (me.is_wired === undefined) {
|
||||
//start of the level/first time, then
|
||||
me.is_wired = level.is_tile_wired(me, false);
|
||||
me.is_active = !me.is_wired;
|
||||
}
|
||||
else {
|
||||
let new_is_wired = level.is_tile_wired(me, false);
|
||||
if (new_is_wired && !me.is_wired)
|
||||
{
|
||||
//connected
|
||||
level._set_tile_prop(me, 'is_wired', true);
|
||||
//TODO: it'll always get on_power called later if it's wired to something already given power, right?
|
||||
level._set_tile_prop(me, 'is_active', false);
|
||||
}
|
||||
else if (!new_is_wired && me.is_wired)
|
||||
{
|
||||
//disconnected
|
||||
level._set_tile_prop(me, 'is_wired', false);
|
||||
level._set_tile_prop(me, 'is_active', true);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function player_visual_state(me) {
|
||||
if (! me) {
|
||||
return 'normal';
|
||||
@ -106,6 +130,12 @@ function player_visual_state(me) {
|
||||
else if (me.fail_reason === 'slimed') {
|
||||
return 'slimed';
|
||||
}
|
||||
else if (me.fail_reason === 'electrocuted') {
|
||||
return 'burned'; //same gfx for now
|
||||
}
|
||||
else if (me.fail_reason === 'fell') {
|
||||
return 'fell';
|
||||
}
|
||||
else if (me.fail_reason) {
|
||||
return 'failed';
|
||||
}
|
||||
@ -190,7 +220,7 @@ const TILE_TYPES = {
|
||||
floor: {
|
||||
layer: LAYERS.terrain,
|
||||
on_approach(me, level, other) {
|
||||
if (other.type.name === 'blob') {
|
||||
if (other.type.name === 'blob' || other.type.name === 'boulder') {
|
||||
// Blobs spread slime onto floor
|
||||
if (other.previous_cell && other.previous_cell.has('slime')) {
|
||||
level.transmute_tile(me, 'slime');
|
||||
@ -618,6 +648,72 @@ const TILE_TYPES = {
|
||||
blocks_collision: COLLISION.block_cc1 | COLLISION.block_cc2,
|
||||
speed_factor: 0.5,
|
||||
},
|
||||
dash_floor: {
|
||||
layer: LAYERS.terrain,
|
||||
speed_factor: 2,
|
||||
},
|
||||
spikes: {
|
||||
layer: LAYERS.terrain,
|
||||
blocks(me, level, other) {
|
||||
return !(!other.type.is_player || other.has_item('hiking_boots'));
|
||||
},
|
||||
},
|
||||
turntable_cw: {
|
||||
layer: LAYERS.terrain,
|
||||
wire_propagation_mode: 'all',
|
||||
on_begin(me, level) {
|
||||
update_wireable(me, level);
|
||||
},
|
||||
on_arrive(me, level, other) {
|
||||
if (! me.is_active)
|
||||
return;
|
||||
other.direction = DIRECTIONS[other.direction].right;
|
||||
if (other.type.on_rotate) {
|
||||
other.type.on_rotate(other, level, 'right');
|
||||
}
|
||||
},
|
||||
on_power(me, level) {
|
||||
if (me.is_wired) {
|
||||
level._set_tile_prop(me, 'is_active', true);
|
||||
}
|
||||
},
|
||||
on_depower(me, level) {
|
||||
if (me.is_wired) {
|
||||
level._set_tile_prop(me, 'is_active', false);
|
||||
}
|
||||
},
|
||||
visual_state(me) {
|
||||
return ! me || me.is_active ? 'active' : 'inactive';
|
||||
},
|
||||
},
|
||||
turntable_ccw: {
|
||||
layer: LAYERS.terrain,
|
||||
wire_propagation_mode: 'all',
|
||||
on_begin(me, level) {
|
||||
update_wireable(me, level);
|
||||
},
|
||||
on_arrive(me, level, other) {
|
||||
if (! me.is_active)
|
||||
return;
|
||||
other.direction = DIRECTIONS[other.direction].left;
|
||||
if (other.type.on_rotate) {
|
||||
other.type.on_rotate(other, level, 'left');
|
||||
}
|
||||
},
|
||||
on_power(me, level) {
|
||||
if (me.is_wired) {
|
||||
level._set_tile_prop(me, 'is_active', true);
|
||||
}
|
||||
},
|
||||
on_depower(me, level) {
|
||||
if (me.is_wired) {
|
||||
level._set_tile_prop(me, 'is_active', false);
|
||||
}
|
||||
},
|
||||
visual_state(me) {
|
||||
return ! me || me.is_active ? 'active' : 'inactive';
|
||||
},
|
||||
},
|
||||
|
||||
// Hazards
|
||||
fire: {
|
||||
@ -639,7 +735,7 @@ const TILE_TYPES = {
|
||||
level.sfx.play_once('splash', me.cell);
|
||||
}
|
||||
else if (other.type.is_real_player) {
|
||||
level.fail('burned', other);
|
||||
level.fail('burned', me, other);
|
||||
}
|
||||
else {
|
||||
level.transmute_tile(other, 'explosion');
|
||||
@ -665,12 +761,26 @@ const TILE_TYPES = {
|
||||
level.transmute_tile(other, 'splash');
|
||||
level.transmute_tile(me, 'floor');
|
||||
}
|
||||
else if (other.type.name === 'glass_block') {
|
||||
level.transmute_tile(other, 'splash');
|
||||
level.transmute_tile(me, 'floor');
|
||||
}
|
||||
else if (other.type.name === 'ice_block') {
|
||||
level.transmute_tile(other, 'splash');
|
||||
level.transmute_tile(me, 'ice');
|
||||
}
|
||||
else if (other.type.name === 'boulder') {
|
||||
level.transmute_tile(other, 'splash');
|
||||
level.transmute_tile(me, 'gravel');
|
||||
}
|
||||
else if (other.type.name === 'circuit_block') {
|
||||
level.transmute_tile(me, 'floor');
|
||||
level._set_tile_prop(me, 'wire_directions', other.wire_directions);
|
||||
level.transmute_tile(other, 'splash');
|
||||
level.recalculate_circuitry_next_wire_phase = true;
|
||||
}
|
||||
else if (other.type.is_real_player) {
|
||||
level.fail('drowned', other);
|
||||
level.fail('drowned', me, other);
|
||||
}
|
||||
else {
|
||||
level.transmute_tile(other, 'splash');
|
||||
@ -686,6 +796,16 @@ const TILE_TYPES = {
|
||||
level.sfx.play_once('splash', me.cell);
|
||||
},
|
||||
},
|
||||
cracked_ice: {
|
||||
layer: LAYERS.terrain,
|
||||
slide_mode: 'ice',
|
||||
speed_factor: 2,
|
||||
on_depart(me, level, other) {
|
||||
level.transmute_tile(me, 'water');
|
||||
level.spawn_animation(me.cell, 'splash');
|
||||
level.sfx.play_once('splash', me.cell);
|
||||
},
|
||||
},
|
||||
ice: {
|
||||
layer: LAYERS.terrain,
|
||||
slide_mode: 'ice',
|
||||
@ -850,7 +970,7 @@ const TILE_TYPES = {
|
||||
slime: {
|
||||
layer: LAYERS.terrain,
|
||||
on_arrive(me, level, other) {
|
||||
if (other.type.name === 'ghost' || other.type.name === 'blob') {
|
||||
if (other.type.name === 'ghost' || other.type.name === 'blob' || other.type.name === 'boulder') {
|
||||
// No effect
|
||||
return;
|
||||
}
|
||||
@ -860,7 +980,7 @@ const TILE_TYPES = {
|
||||
level.transmute_tile(me, 'floor');
|
||||
}
|
||||
else if (other.type.is_real_player) {
|
||||
level.fail('slimed', other);
|
||||
level.fail('slimed', me, other);
|
||||
}
|
||||
else {
|
||||
level.transmute_tile(other, 'splash_slime');
|
||||
@ -884,7 +1004,7 @@ const TILE_TYPES = {
|
||||
on_arrive(me, level, other) {
|
||||
level.remove_tile(me);
|
||||
if (other.type.is_real_player) {
|
||||
level.fail('exploded', other);
|
||||
level.fail('exploded', me, other);
|
||||
}
|
||||
else {
|
||||
level.sfx.play_once('bomb', me.cell);
|
||||
@ -892,6 +1012,45 @@ const TILE_TYPES = {
|
||||
}
|
||||
},
|
||||
},
|
||||
hole: {
|
||||
layer: LAYERS.terrain,
|
||||
on_begin(me, level) {
|
||||
var one_north = level.cell(me.cell.x, me.cell.y - 1);
|
||||
if (one_north === null || one_north.get_terrain().type.name != 'hole') {
|
||||
level._set_tile_prop(me, 'visual_state', 'north');
|
||||
}
|
||||
else {
|
||||
level._set_tile_prop(me, 'visual_state', 'open');
|
||||
}
|
||||
},
|
||||
on_arrive(me, level, other) {
|
||||
if (other.type.is_real_player) {
|
||||
level.fail('fell', me, other);
|
||||
}
|
||||
else {
|
||||
level.transmute_tile(other, 'puff');
|
||||
}
|
||||
},
|
||||
visual_state(me) {
|
||||
return (me && me.visual_state) ?? 'open';
|
||||
},
|
||||
},
|
||||
cracked_floor: {
|
||||
layer: LAYERS.terrain,
|
||||
on_depart(me, level, other) {
|
||||
level.spawn_animation(me.cell, 'puff');
|
||||
level.transmute_tile(me, 'hole');
|
||||
if (other === level.player) {
|
||||
level.sfx.play_once('popwall', me.cell);
|
||||
}
|
||||
//update hole visual state
|
||||
me.type.on_begin(me, level);
|
||||
var one_south = level.cell(me.cell.x, me.cell.y + 1);
|
||||
if (one_south !== null && one_south.get_terrain().type.name == 'hole') {
|
||||
me.type.on_begin(one_south.get_terrain(), level);
|
||||
}
|
||||
},
|
||||
},
|
||||
thief_tools: {
|
||||
layer: LAYERS.terrain,
|
||||
blocks_collision: COLLISION.block_cc1 | COLLISION.monster_solid,
|
||||
@ -951,6 +1110,60 @@ const TILE_TYPES = {
|
||||
layer: LAYERS.item_mod,
|
||||
item_modifier: 'pickup',
|
||||
},
|
||||
item_lock: {
|
||||
layer: LAYERS.item_mod,
|
||||
item_modifier: 'ignore',
|
||||
blocks(me, level, other) {
|
||||
let item = me.cell.get_item();
|
||||
if (item === null) {
|
||||
return false;
|
||||
}
|
||||
if (item.type.name == 'score_10') {
|
||||
return !(other.type.is_real_player && level.bonus_points >= 10);
|
||||
}
|
||||
else if (item.type.name == 'score_100') {
|
||||
return !(other.type.is_real_player && level.bonus_points >= 100);
|
||||
}
|
||||
else if (item.type.name == 'score_1000') {
|
||||
return !(other.type.is_real_player && level.bonus_points >= 1000);
|
||||
}
|
||||
else if (item.type.name == 'score_2x') {
|
||||
return !(other.type.is_real_player && level.bonus_points >= 1);
|
||||
}
|
||||
else if (item.type.name == 'score_5x') {
|
||||
return !(other.type.is_real_player && level.bonus_points >= 1);
|
||||
}
|
||||
return !other.has_item(item.type.name);
|
||||
},
|
||||
on_arrive(me, level, other) {
|
||||
let item = me.cell.get_item();
|
||||
if (item === null) {
|
||||
return;
|
||||
}
|
||||
if (item.type.name == 'score_10') {
|
||||
level.adjust_bonus(-10);
|
||||
}
|
||||
else if (item.type.name == 'score_100') {
|
||||
level.adjust_bonus(-100);
|
||||
}
|
||||
else if (item.type.name == 'score_1000') {
|
||||
level.adjust_bonus(-1000);
|
||||
}
|
||||
else if (item.type.name == 'score_2x') {
|
||||
level.adjust_bonus(0, 1/2);
|
||||
}
|
||||
else if (item.type.name == 'score_5x') {
|
||||
level.adjust_bonus(0, 1/5);
|
||||
}
|
||||
else {
|
||||
level.take_key_from_actor(other, item.type.name, true) || level.take_tool_from_actor(other, item.type.name);
|
||||
}
|
||||
level.sfx.play_once('door', me.cell);
|
||||
level.spawn_animation(me.cell, 'puff');
|
||||
level.remove_tile(me);
|
||||
level.remove_tile(item);
|
||||
},
|
||||
},
|
||||
|
||||
// Mechanisms
|
||||
dirt_block: {
|
||||
@ -959,7 +1172,7 @@ const TILE_TYPES = {
|
||||
blocks_collision: COLLISION.all,
|
||||
is_actor: true,
|
||||
is_block: true,
|
||||
ignores: new Set(['fire', 'flame_jet_on']),
|
||||
ignores: new Set(['fire', 'flame_jet_on', 'electrified_floor']),
|
||||
can_reverse_on_railroad: true,
|
||||
movement_speed: 4,
|
||||
},
|
||||
@ -975,8 +1188,9 @@ const TILE_TYPES = {
|
||||
pushes: {
|
||||
ice_block: true,
|
||||
frame_block: true,
|
||||
boulder: true,
|
||||
},
|
||||
on_bumped(me, level, other) {
|
||||
on_after_bumped(me, level, other) {
|
||||
// Fireballs melt ice blocks on regular floor FIXME and water!
|
||||
// XXX what if i'm in motion?
|
||||
if (other.type.name === 'fireball') {
|
||||
@ -1005,6 +1219,8 @@ const TILE_TYPES = {
|
||||
dirt_block: true,
|
||||
ice_block: true,
|
||||
frame_block: true,
|
||||
boulder: true,
|
||||
glass_block: true,
|
||||
},
|
||||
on_clone(me, original) {
|
||||
me.arrows = new Set(original.arrows);
|
||||
@ -1018,6 +1234,43 @@ const TILE_TYPES = {
|
||||
level._set_tile_prop(me, 'arrows', new_arrows);
|
||||
},
|
||||
},
|
||||
boulder: {
|
||||
layer: LAYERS.actor,
|
||||
collision_mask: COLLISION.block_cc2,
|
||||
blocks_collision: COLLISION.all,
|
||||
is_actor: true,
|
||||
is_block: true,
|
||||
can_reveal_walls: true,
|
||||
pushes: {
|
||||
ice_block: true,
|
||||
frame_block: true,
|
||||
//boulders don't push each other; instead on_bumped will chain through them
|
||||
},
|
||||
ignores: new Set(['fire', 'flame_jet_on', 'electrified_floor']),
|
||||
can_reverse_on_railroad: true,
|
||||
movement_speed: 4,
|
||||
decide_movement(me, level) {
|
||||
if (me.rolling) {
|
||||
level._set_tile_prop(me, 'rolling', false);
|
||||
return [me.direction, null];
|
||||
}
|
||||
else {
|
||||
return null;
|
||||
}
|
||||
},
|
||||
on_bumped(me, level, other) {
|
||||
if (other.type.name === 'boulder') {
|
||||
level._set_tile_prop(me, 'rolling', true);
|
||||
level._set_tile_prop(me, 'direction', other.direction);
|
||||
level._set_tile_prop(other, 'rolling', false);
|
||||
}
|
||||
},
|
||||
on_starting_move(me, level) {
|
||||
if (!me.rolling) {
|
||||
level._set_tile_prop(me, 'rolling', true);
|
||||
}
|
||||
},
|
||||
},
|
||||
glass_block: {
|
||||
layer: LAYERS.actor,
|
||||
collision_mask: COLLISION.block_cc2,
|
||||
@ -1027,25 +1280,55 @@ const TILE_TYPES = {
|
||||
can_reveal_walls: true,
|
||||
can_reverse_on_railroad: true,
|
||||
movement_speed: 4,
|
||||
allows_push(me, direction) {
|
||||
return me.arrows && me.arrows.has(direction);
|
||||
try_pickup_item(me, level) {
|
||||
if (me.encased_item === null) {
|
||||
let item = me.cell.get_item();
|
||||
if (item && !item.type.is_chip) {
|
||||
level.attempt_take(me, item);
|
||||
//then if we picked it up, encase it (so we have max one item at a time and so we can't 'use' the item)
|
||||
if (me.keyring !== undefined && me.keyring !== null && Object.keys(me.keyring).length > 0) {
|
||||
level._set_tile_prop(me, 'encased_item', Object.keys(me.keyring)[0]);
|
||||
level.take_all_keys_from_actor(me);
|
||||
}
|
||||
else if (me.toolbelt !== undefined && me.toolbelt !== null && me.toolbelt.length > 0)
|
||||
{
|
||||
level._set_tile_prop(me, 'encased_item', me.toolbelt[0]);
|
||||
level.take_all_tools_from_actor(me);
|
||||
}
|
||||
}
|
||||
}
|
||||
/*if ((me.keyring === undefined || Object.keys(me.keyring).length == 0) &&
|
||||
(me.toolbelt === undefined || me.toolbelt.length == 0)) {
|
||||
let item = me.cell.get_item();
|
||||
if (item) {
|
||||
level.attempt_take(me, item);
|
||||
}
|
||||
}*/
|
||||
},
|
||||
pushes: {
|
||||
dirt_block: true,
|
||||
ice_block: true,
|
||||
frame_block: true,
|
||||
on_ready(me, level) {
|
||||
level._set_tile_prop(me, 'encased_item', null);
|
||||
this.try_pickup_item(me, level);
|
||||
},
|
||||
on_clone(me, original) {
|
||||
me.arrows = new Set(original.arrows);
|
||||
},
|
||||
on_rotate(me, level, turn) {
|
||||
// We rotate when turned on railroads
|
||||
let new_arrows = new Set;
|
||||
for (let arrow of me.arrows) {
|
||||
new_arrows.add(DIRECTIONS[arrow][turn]);
|
||||
me.encased_item = original.encased_item;
|
||||
/*if (original.keyring !== undefined) {
|
||||
me.keyring = {};
|
||||
Object.assign(me.keyring, original.keyring);
|
||||
}
|
||||
level._set_tile_prop(me, 'arrows', new_arrows);
|
||||
if (original.toolbelt !== undefined) {
|
||||
me.toolbelt = original.toolbelt.map((x) => x);
|
||||
}*/
|
||||
},
|
||||
on_finishing_move(me, level) {
|
||||
this.try_pickup_item(me, level);
|
||||
},
|
||||
on_death(me, level) {
|
||||
//needs to be called by transmute_tile to ttl and by lit_dynamite before remove_tile
|
||||
if (me.encased_item !== null) {
|
||||
level._place_dropped_item(me.encased_item, me.cell, me);
|
||||
level._set_tile_prop(me, 'encased_item', null);
|
||||
}
|
||||
}
|
||||
},
|
||||
green_floor: {
|
||||
layer: LAYERS.terrain,
|
||||
@ -1085,7 +1368,7 @@ const TILE_TYPES = {
|
||||
on_arrive(me, level, other) {
|
||||
level.remove_tile(me);
|
||||
if (other.type.is_real_player) {
|
||||
level.fail('exploded', other);
|
||||
level.fail('exploded', me, other);
|
||||
}
|
||||
else {
|
||||
level.sfx.play_once('bomb', me.cell);
|
||||
@ -1174,13 +1457,18 @@ const TILE_TYPES = {
|
||||
},
|
||||
trap: {
|
||||
layer: LAYERS.terrain,
|
||||
on_begin(me, level) {
|
||||
if (me.presses === undefined) {
|
||||
level._set_tile_prop(me, 'presses', 0);
|
||||
}
|
||||
},
|
||||
add_press_ready(me, level, other) {
|
||||
// Same as below, but without ejection
|
||||
me.presses = (me.presses ?? 0) + 1;
|
||||
level._set_tile_prop(me, 'presses', (me.presses ?? 0) + 1);
|
||||
},
|
||||
// Lynx (not cc2): open traps immediately eject their contents on arrival, if possible
|
||||
add_press(me, level, is_wire = false) {
|
||||
level._set_tile_prop(me, 'presses', (me.presses ?? 0) + 1);
|
||||
level._set_tile_prop(me, 'presses', me.presses + 1);
|
||||
// TODO weird cc2 case that may or may not be a bug: actors aren't ejected if the trap
|
||||
// opened because of wiring
|
||||
if (me.presses === 1 && ! is_wire) {
|
||||
@ -1194,7 +1482,7 @@ const TILE_TYPES = {
|
||||
}
|
||||
},
|
||||
remove_press(me, level) {
|
||||
level._set_tile_prop(me, 'presses', me.presses - 1);
|
||||
level._set_tile_prop(me, 'presses', Math.max(0, me.presses - 1));
|
||||
if (me._initially_open) {
|
||||
level._set_tile_prop(me, '_initially_open', false);
|
||||
}
|
||||
@ -1248,9 +1536,7 @@ const TILE_TYPES = {
|
||||
},
|
||||
_blob_mogrifications: ['glider', 'paramecium', 'fireball', 'bug', 'walker', 'ball', 'teeth', 'tank_blue', 'teeth_timid'],
|
||||
on_begin(me, level) {
|
||||
// TODO if wire destruction is ever allowed, this will need to update somehow
|
||||
me.is_wired = level.is_tile_wired(me, false);
|
||||
me.is_active = ! me.is_wired;
|
||||
update_wireable(me, level);
|
||||
},
|
||||
on_arrive(me, level, other) {
|
||||
// Note: Transmogrifiers technically contain wires the way teleports do, and CC2 uses
|
||||
@ -1300,7 +1586,7 @@ const TILE_TYPES = {
|
||||
// TODO cc2 has a bug where, once it wraps around to the bottom right, it seems to
|
||||
// forget that it was ever looking for an unwired teleport and will just grab the
|
||||
// first one it sees
|
||||
for (let dest of level.iter_tiles_in_reading_order(me.cell, 'teleport_blue', true)) {
|
||||
for (let dest of level.iter_tiles_in_reading_order_multiple(me.cell, ['teleport_blue', 'teleport_blue_exit'], true)) {
|
||||
if (! dest.wire_directions) {
|
||||
yield [dest, exit_direction];
|
||||
}
|
||||
@ -1333,7 +1619,7 @@ const TILE_TYPES = {
|
||||
walked_circuits.add(circuit);
|
||||
|
||||
for (let [tile, edges] of circuit.tiles.entries()) {
|
||||
if (tile.type === me.type) {
|
||||
if (tile.type === me.type || tile.type.name === 'teleport_blue_exit') {
|
||||
candidate_teleporters.add(tile);
|
||||
}
|
||||
else if (tile.type.name === 'logic_gate' && ! circuit.inputs.get(tile)) {
|
||||
@ -1368,18 +1654,20 @@ const TILE_TYPES = {
|
||||
}
|
||||
},
|
||||
},
|
||||
teleport_blue_exit: {
|
||||
layer: LAYERS.terrain,
|
||||
wire_propagation_mode: 'all',
|
||||
},
|
||||
teleport_red: {
|
||||
layer: LAYERS.terrain,
|
||||
slide_mode: 'teleport',
|
||||
wire_propagation_mode: 'none',
|
||||
teleport_allow_override: true,
|
||||
on_begin(me, level) {
|
||||
// TODO if wire destruction is ever allowed, this will need to update somehow
|
||||
// FIXME must be connected to something that can convey current: a wire, a switch, a
|
||||
// blue teleporter, etc; NOT nothing, a wall, a transmogrifier, a force floor, etc.
|
||||
// this is also how blue teleporters, transmogrifiers, and railroads work!
|
||||
me.is_wired = level.is_tile_wired(me);
|
||||
me.is_active = ! me.is_wired;
|
||||
update_wireable(me, level);
|
||||
},
|
||||
*teleport_dest_order(me, level, other) {
|
||||
// Wired red teleporters can be turned off, which disconnects them from every other red
|
||||
@ -1500,7 +1788,7 @@ const TILE_TYPES = {
|
||||
// TODO would be neat if this understood "ignores anything with fire immunity" but that
|
||||
// might be a bit too high-level for this game
|
||||
if (actor.type.is_real_player) {
|
||||
level.fail('burned', actor);
|
||||
level.fail('burned', me, actor);
|
||||
}
|
||||
else {
|
||||
level.sfx.play_once('bomb', me.cell);
|
||||
@ -1509,6 +1797,39 @@ const TILE_TYPES = {
|
||||
}
|
||||
},
|
||||
},
|
||||
electrified_floor: {
|
||||
layer: LAYERS.terrain,
|
||||
wire_propagation_mode: 'all',
|
||||
on_begin(me, level) {
|
||||
level._set_tile_prop(me, 'wire_directions', 15);
|
||||
level.recalculate_circuitry_next_wire_phase = true;
|
||||
},
|
||||
on_tic(me, level) {
|
||||
if (me.is_active)
|
||||
{
|
||||
let actor = me.cell.get_actor();
|
||||
if (actor && actor.movement_cooldown <= 0 && ! actor.ignores(me.type.name)) {
|
||||
if (actor.type.is_real_player) {
|
||||
level.fail('electrocuted', me, actor);
|
||||
}
|
||||
else {
|
||||
level.sfx.play_once('bomb', me.cell);
|
||||
level.transmute_tile(actor, 'explosion');
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
on_power(me, level) {
|
||||
level._set_tile_prop(me, 'is_active', true);
|
||||
},
|
||||
on_depower(me, level) {
|
||||
level._set_tile_prop(me, 'is_active', false);
|
||||
},
|
||||
visual_state(me) {
|
||||
return ! me || me.is_active ? 'active' : 'inactive';
|
||||
},
|
||||
},
|
||||
|
||||
// Buttons
|
||||
button_blue: {
|
||||
layer: LAYERS.terrain,
|
||||
@ -1727,6 +2048,7 @@ const TILE_TYPES = {
|
||||
// gate_type: not, and, or, xor, nand, latch-cw, latch-ccw, counter, bogus
|
||||
_gate_types: {
|
||||
not: ['out0', null, 'in0', null],
|
||||
diode: ['out0', null, 'in0', null],
|
||||
and: ['out0', 'in0', null, 'in1'],
|
||||
or: ['out0', 'in0', null, 'in1'],
|
||||
xor: ['out0', 'in0', null, 'in1'],
|
||||
@ -1781,6 +2103,9 @@ const TILE_TYPES = {
|
||||
if (me.gate_type === 'not') {
|
||||
output0 = ! input0;
|
||||
}
|
||||
else if (me.gate_type === 'diode') {
|
||||
output0 = input0;
|
||||
}
|
||||
else if (me.gate_type === 'and') {
|
||||
output0 = input0 && input1;
|
||||
}
|
||||
@ -1886,6 +2211,15 @@ const TILE_TYPES = {
|
||||
is_block: true,
|
||||
can_reverse_on_railroad: true,
|
||||
movement_speed: 4,
|
||||
on_clone(me, original) {
|
||||
me.wire_directions = original.wire_directions;
|
||||
},
|
||||
on_starting_move(me, level) {
|
||||
level.recalculate_circuitry_next_wire_phase = true;
|
||||
},
|
||||
on_finishing_move(me, level) {
|
||||
level.recalculate_circuitry_next_wire_phase = true;
|
||||
},
|
||||
},
|
||||
|
||||
// Time alteration
|
||||
@ -2015,6 +2349,8 @@ const TILE_TYPES = {
|
||||
ice_block: true,
|
||||
frame_block: true,
|
||||
circuit_block: true,
|
||||
boulder: true,
|
||||
glass_block: true,
|
||||
},
|
||||
movement_speed: 4,
|
||||
decide_movement(me, level) {
|
||||
@ -2120,10 +2456,11 @@ const TILE_TYPES = {
|
||||
ignores: new Set([
|
||||
'bomb', 'green_bomb',
|
||||
'water',
|
||||
'ice', 'ice_nw', 'ice_ne', 'ice_sw', 'ice_se',
|
||||
'ice', 'ice_nw', 'ice_ne', 'ice_sw', 'ice_se', 'cracked_ice',
|
||||
'force_floor_n', 'force_floor_s', 'force_floor_e', 'force_floor_w', 'force_floor_all',
|
||||
// Ghosts don't activate swivels or popwalls
|
||||
'popwall', 'swivel_nw', 'swivel_ne', 'swivel_se', 'swivel_sw',
|
||||
'hole', 'cracked_floor',
|
||||
]),
|
||||
movement_speed: 4,
|
||||
// TODO ignores /most/ walls. collision is basically completely different. has a regular inventory, except red key. good grief
|
||||
@ -2165,6 +2502,8 @@ const TILE_TYPES = {
|
||||
ice_block: true,
|
||||
frame_block: true,
|
||||
circuit_block: true,
|
||||
boulder: true,
|
||||
glass_block: true,
|
||||
},
|
||||
on_ready(me, level) {
|
||||
me.current_emulatee = 0;
|
||||
@ -2240,7 +2579,8 @@ const TILE_TYPES = {
|
||||
is_item: true,
|
||||
is_tool: true,
|
||||
blocks_collision: COLLISION.block_cc1 | (COLLISION.monster_solid & ~COLLISION.rover),
|
||||
item_ignores: new Set(['ice', 'ice_nw', 'ice_ne', 'ice_sw', 'ice_se']),
|
||||
item_ignores: new Set(['ice', 'ice_nw', 'ice_ne', 'ice_sw', 'ice_se', ]),
|
||||
item_slide_ignores: new Set(['cracked_ice']),
|
||||
},
|
||||
suction_boots: {
|
||||
layer: LAYERS.item,
|
||||
@ -2343,10 +2683,13 @@ const TILE_TYPES = {
|
||||
else if (tile.type.is_real_player) {
|
||||
// TODO it would be nice if i didn't have to special-case this every
|
||||
// time
|
||||
level.fail(me.type.name, tile);
|
||||
level.fail(me.type.name, me, tile);
|
||||
}
|
||||
else {
|
||||
// Everything else is destroyed
|
||||
if (tile.type.on_death) {
|
||||
tile.type.on_death(tile, level);
|
||||
}
|
||||
level.remove_tile(tile);
|
||||
removed_anything = true;
|
||||
}
|
||||
@ -2420,7 +2763,7 @@ const TILE_TYPES = {
|
||||
if (me.cell.has('cloner'))
|
||||
return;
|
||||
if (other.type.is_real_player) {
|
||||
level.fail(me.type.name, other);
|
||||
level.fail(me.type.name, me, other);
|
||||
}
|
||||
else {
|
||||
level.transmute_tile(other, 'explosion');
|
||||
@ -2432,7 +2775,7 @@ const TILE_TYPES = {
|
||||
// Blow up anything we run into
|
||||
if (obstacle && obstacle.type.is_actor) {
|
||||
if (obstacle.type.is_real_player) {
|
||||
level.fail(me.type.name, obstacle);
|
||||
level.fail(me.type.name, me, obstacle);
|
||||
}
|
||||
else {
|
||||
level.transmute_tile(obstacle, 'explosion');
|
||||
@ -2479,6 +2822,7 @@ const TILE_TYPES = {
|
||||
is_item: true,
|
||||
is_tool: true,
|
||||
blocks_collision: COLLISION.block_cc1 | (COLLISION.monster_solid & ~COLLISION.rover),
|
||||
item_ignores: new Set(['electrified_floor']),
|
||||
},
|
||||
speed_boots: {
|
||||
layer: LAYERS.item,
|
||||
@ -2504,6 +2848,12 @@ const TILE_TYPES = {
|
||||
is_tool: true,
|
||||
blocks_collision: COLLISION.block_cc1 | (COLLISION.monster_solid & ~COLLISION.rover),
|
||||
},
|
||||
halo: {
|
||||
layer: LAYERS.item,
|
||||
is_item: true,
|
||||
is_tool: true,
|
||||
blocks_collision: COLLISION.block_cc1 | (COLLISION.monster_solid & ~COLLISION.rover),
|
||||
},
|
||||
|
||||
// Progression
|
||||
player: {
|
||||
@ -2521,6 +2871,8 @@ const TILE_TYPES = {
|
||||
ice_block: true,
|
||||
frame_block: true,
|
||||
circuit_block: true,
|
||||
boulder: true,
|
||||
glass_block: true,
|
||||
},
|
||||
infinite_items: {
|
||||
key_green: true,
|
||||
@ -2537,12 +2889,14 @@ const TILE_TYPES = {
|
||||
has_inventory: true,
|
||||
can_reveal_walls: true,
|
||||
movement_speed: 4,
|
||||
ignores: new Set(['ice', 'ice_nw', 'ice_ne', 'ice_sw', 'ice_se']),
|
||||
ignores: new Set(['ice', 'ice_nw', 'ice_ne', 'ice_sw', 'ice_se', 'cracked_ice']),
|
||||
pushes: {
|
||||
dirt_block: true,
|
||||
ice_block: true,
|
||||
frame_block: true,
|
||||
circuit_block: true,
|
||||
boulder: true,
|
||||
glass_block: true,
|
||||
},
|
||||
infinite_items: {
|
||||
key_yellow: true,
|
||||
@ -2564,6 +2918,8 @@ const TILE_TYPES = {
|
||||
ice_block: true,
|
||||
frame_block: true,
|
||||
circuit_block: true,
|
||||
boulder: true,
|
||||
glass_block: true,
|
||||
},
|
||||
infinite_items: {
|
||||
key_green: true,
|
||||
@ -2583,12 +2939,14 @@ const TILE_TYPES = {
|
||||
has_inventory: true,
|
||||
can_reveal_walls: true, // XXX i think?
|
||||
movement_speed: 4,
|
||||
ignores: new Set(['ice', 'ice_nw', 'ice_ne', 'ice_sw', 'ice_se']),
|
||||
ignores: new Set(['ice', 'ice_nw', 'ice_ne', 'ice_sw', 'ice_se', 'cracked_ice']),
|
||||
pushes: {
|
||||
dirt_block: true,
|
||||
ice_block: true,
|
||||
frame_block: true,
|
||||
circuit_block: true,
|
||||
boulder: true,
|
||||
glass_block: true,
|
||||
},
|
||||
infinite_items: {
|
||||
key_yellow: true,
|
||||
@ -2674,6 +3032,19 @@ const TILE_TYPES = {
|
||||
}
|
||||
},
|
||||
},
|
||||
score_5x: {
|
||||
layer: LAYERS.item,
|
||||
blocks_collision: COLLISION.block_cc1 | COLLISION.monster_solid,
|
||||
on_arrive(me, level, other) {
|
||||
if (other.type.is_real_player) {
|
||||
level.adjust_bonus(0, 5);
|
||||
level.sfx.play_once('get-bonus2', me.cell);
|
||||
}
|
||||
if (other.type.is_player || other.type.name === 'rover' || other.type.name === 'bowling_ball') {
|
||||
level.remove_tile(me);
|
||||
}
|
||||
},
|
||||
},
|
||||
|
||||
hint: {
|
||||
layer: LAYERS.terrain,
|
||||
|
||||
BIN
sfx/revive.ogg
Normal file
BIN
sfx/revive.ogg
Normal file
Binary file not shown.
BIN
tileset-lexy.png
BIN
tileset-lexy.png
Binary file not shown.
|
Before Width: | Height: | Size: 76 KiB After Width: | Height: | Size: 174 KiB |
Loading…
Reference in New Issue
Block a user