Improve the connection tool somewhat; show implicit connections
For example, you can now make connections with the connection tool. Remarkable. Unfortunately, implicit connections aren't updated as you edit the level yet. Also came with some refactors for searching a level and whatnot.
This commit is contained in:
parent
5e2dfdd926
commit
3cf81b53ad
@ -1,4 +1,65 @@
|
||||
import { DIRECTIONS } from './defs.js';
|
||||
import { DIRECTIONS, LAYERS } from './defs.js';
|
||||
|
||||
// Iterates over every terrain tile in the grid that has one of the given types (a Set of type
|
||||
// names), in linear order, optionally in reverse. The starting cell is checked last.
|
||||
// Yields [tile, cell].
|
||||
export function* find_terrain_linear(levelish, start_cell, type_names, reverse = false) {
|
||||
let i = levelish.coords_to_scalar(start_cell.x, start_cell.y);
|
||||
while (true) {
|
||||
if (reverse) {
|
||||
i -= 1;
|
||||
if (i < 0) {
|
||||
i += levelish.size_x * levelish.size_y;
|
||||
}
|
||||
}
|
||||
else {
|
||||
i += 1;
|
||||
i %= levelish.size_x * levelish.size_y;
|
||||
}
|
||||
|
||||
let cell = levelish.linear_cells[i];
|
||||
let tile = cell[LAYERS.terrain];
|
||||
if (tile && type_names.has(tile.type.name)) {
|
||||
yield [tile, cell];
|
||||
}
|
||||
|
||||
if (cell === start_cell)
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Iterates over every terrain tile in the grid that has one of the given types (a Set of type
|
||||
// names), spreading outward in a diamond pattern. The starting cell is not included.
|
||||
// Only used by orange buttons.
|
||||
// Yields [tile, cell].
|
||||
export function* find_terrain_diamond(levelish, start_cell, type_names) {
|
||||
let max_search_radius = (
|
||||
Math.max(start_cell.x, levelish.size_x - start_cell.x) +
|
||||
Math.max(start_cell.y, levelish.size_y - start_cell.y));
|
||||
for (let dist = 1; dist <= max_search_radius; dist++) {
|
||||
// Start east and move counterclockwise
|
||||
let sx = start_cell.x + dist;
|
||||
let sy = start_cell.y;
|
||||
for (let direction of [[-1, -1], [-1, 1], [1, 1], [1, -1]]) {
|
||||
for (let i = 0; i < dist; i++) {
|
||||
let cell = levelish.cell(sx, sy);
|
||||
sx += direction[0];
|
||||
sy += direction[1];
|
||||
|
||||
if (! cell)
|
||||
continue;
|
||||
let terrain = cell[LAYERS.terrain];
|
||||
if (type_names.has(terrain.type.name)) {
|
||||
yield [terrain, cell];
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TODO make this guy work generically for orange, red, brown buttons? others...?
|
||||
export function find_implicit_connection() {
|
||||
}
|
||||
|
||||
export function trace_floor_circuit(level, start_cell, start_edge, on_wire, on_dead_end) {
|
||||
let is_first = true;
|
||||
@ -120,28 +181,3 @@ export function find_matching_wire_tunnel(level, x, y, direction) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TODO make this guy work generically for orange, red, brown buttons? others...?
|
||||
export function find_implicit_connection() {
|
||||
}
|
||||
|
||||
// Iterates over a grid in a diamond pattern, spreading out from the given start cell (but not
|
||||
// including it). Only used for connecting orange buttons.
|
||||
export function* iter_cells_in_diamond(levelish, x0, y0) {
|
||||
let max_search_radius = Math.max(levelish.size_x, levelish.size_y) + 1;
|
||||
for (let dist = 1; dist <= max_search_radius; dist++) {
|
||||
// Start east and move counterclockwise
|
||||
let sx = x0 + dist;
|
||||
let sy = y0;
|
||||
for (let direction of [[-1, -1], [-1, 1], [1, 1], [1, -1]]) {
|
||||
for (let i = 0; i < dist; i++) {
|
||||
let cell = levelish.cell(sx, sy);
|
||||
if (cell) {
|
||||
yield cell;
|
||||
}
|
||||
sx += direction[0];
|
||||
sy += direction[1];
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -4,12 +4,15 @@ import { BitVector, mk, mk_svg } from '../util.js';
|
||||
|
||||
export class SVGConnection {
|
||||
constructor(sx, sy, dx, dy) {
|
||||
this.source = mk_svg('circle.-source', {r: 0.5});
|
||||
this.source = mk_svg('circle.-source', {r: 0.5, cx: sx + 0.5, cy: sy + 0.5});
|
||||
this.line = mk_svg('line.-arrow', {});
|
||||
this.dest = mk_svg('rect.-dest', {width: 1, height: 1});
|
||||
this.dest = mk_svg('rect.-dest', {x: dx, y: dy, width: 1, height: 1});
|
||||
this.element = mk_svg('g.overlay-connection', this.source, this.line, this.dest);
|
||||
this.set_source(sx, sy);
|
||||
this.set_dest(dx, dy);
|
||||
this.sx = sx;
|
||||
this.sy = sy;
|
||||
this.dx = dx;
|
||||
this.dy = dy;
|
||||
this._update_line_endpoints();
|
||||
}
|
||||
|
||||
set_source(sx, sy) {
|
||||
@ -17,17 +20,35 @@ export class SVGConnection {
|
||||
this.sy = sy;
|
||||
this.source.setAttribute('cx', sx + 0.5);
|
||||
this.source.setAttribute('cy', sy + 0.5);
|
||||
this.line.setAttribute('x1', sx + 0.5);
|
||||
this.line.setAttribute('y1', sy + 0.5);
|
||||
this._update_line_endpoints();
|
||||
}
|
||||
|
||||
set_dest(dx, dy) {
|
||||
this.dx = dx;
|
||||
this.dy = dy;
|
||||
this.line.setAttribute('x2', dx + 0.5);
|
||||
this.line.setAttribute('y2', dy + 0.5);
|
||||
this.dest.setAttribute('x', dx);
|
||||
this.dest.setAttribute('y', dy);
|
||||
this._update_line_endpoints();
|
||||
}
|
||||
|
||||
_update_line_endpoints() {
|
||||
// Start the line at the edge of the circle, so, add 0.5 in the direction of the line
|
||||
let vx = this.dx - this.sx;
|
||||
let vy = this.dy - this.sy;
|
||||
let line_length = Math.sqrt(vx*vx + vy*vy);
|
||||
let trim_x = 0;
|
||||
let trim_y = 0;
|
||||
if (line_length >= 1) {
|
||||
trim_x = 0.5 * vx / line_length;
|
||||
trim_y = 0.5 * vy / line_length;
|
||||
}
|
||||
this.line.setAttribute('x1', this.sx + 0.5 + trim_x);
|
||||
this.line.setAttribute('y1', this.sy + 0.5 + trim_y);
|
||||
// Technically this isn't quite right, since the ending is a square and the arrowhead will
|
||||
// poke into it a bit from angles near 45°, but that requires a bit more trig than seems
|
||||
// worth it
|
||||
this.line.setAttribute('x2', this.dx + 0.5 - trim_x);
|
||||
this.line.setAttribute('y2', this.dy + 0.5 - trim_y);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -1,5 +1,6 @@
|
||||
import * as fflate from '../vendor/fflate.js';
|
||||
|
||||
import * as algorithms from '../algorithms.js';
|
||||
import { DIRECTIONS, LAYERS } from '../defs.js';
|
||||
import * as format_base from '../format-base.js';
|
||||
import * as c2g from '../format-c2g.js';
|
||||
@ -81,7 +82,7 @@ export class Editor extends PrimaryView {
|
||||
this.renderer.show_facing = true;
|
||||
|
||||
// FIXME need this in load_level which is called even if we haven't been setup yet
|
||||
this.connections_g = mk_svg('g');
|
||||
this.connections_g = mk_svg('g', {'data-name': 'connections'});
|
||||
// This SVG draws vectors on top of the editor, like monster paths and button connections
|
||||
this.svg_overlay = mk_svg('svg.level-editor-overlay', {viewBox: '0 0 32 32'},
|
||||
mk_svg('defs',
|
||||
@ -89,9 +90,13 @@ export class Editor extends PrimaryView {
|
||||
mk_svg('polygon', {points: '0 0, 4 2, 0 4'}),
|
||||
),
|
||||
mk_svg('filter', {id: 'overlay-filter-outline'},
|
||||
mk_svg('feMorphology', {'in': 'SourceAlpha', result: 'dilated', operator: 'dilate', radius: 0.03125}),
|
||||
mk_svg('feFlood', {'flood-color': '#0009', result: 'fill'}),
|
||||
mk_svg('feComposite', {'in': 'fill', in2: 'dilated', operator: 'in'}),
|
||||
this._filter_morphology_element = mk_svg('feMorphology', {'in': 'SourceAlpha', result: 'dilated', operator: 'dilate', radius: 0.03125}),
|
||||
this._filter_morphology_element2 = mk_svg('feMorphology', {'in': 'SourceAlpha', result: 'dilated2', operator: 'dilate', radius: 0.0625}),
|
||||
mk_svg('feFlood', {'flood-color': '#000'}),
|
||||
mk_svg('feComposite', {in2: 'dilated', operator: 'in', result: 'fill'}),
|
||||
mk_svg('feFlood', {'flood-color': '#fffc'}),
|
||||
mk_svg('feComposite', {in2: 'dilated2', operator: 'in', result: 'fill2'}),
|
||||
mk_svg('feComposite', {'in': 'fill', in2: 'fill2'}),
|
||||
mk_svg('feComposite', {'in': 'SourceGraphic'}),
|
||||
),
|
||||
),
|
||||
@ -1043,16 +1048,16 @@ export class Editor extends PrimaryView {
|
||||
|
||||
// Load connections
|
||||
// TODO what if the source tile is not connectable?
|
||||
// TODO there's a has_custom_connections flag, is that important here or is it just because
|
||||
// i can't test an object as a bool
|
||||
this.connections_g.textContent = '';
|
||||
this.connections_elements = {};
|
||||
for (let [src, dest] of Object.entries(this.stored_level.custom_connections)) {
|
||||
this.connections_arrows = {};
|
||||
for (let [src, dest] of this.stored_level.custom_connections) {
|
||||
let [sx, sy] = this.stored_level.scalar_to_coords(src);
|
||||
let [dx, dy] = this.stored_level.scalar_to_coords(dest);
|
||||
let el = new SVGConnection(sx, sy, dx, dy).element;
|
||||
this.connections_elements[src] = el;
|
||||
this.connections_g.append(el);
|
||||
let arrow = new SVGConnection(sx, sy, dx, dy);
|
||||
this.connections_arrows[src] = arrow;
|
||||
arrow.element.setAttribute(
|
||||
'data-source', this.stored_level.linear_cells[src][LAYERS.terrain].type.name);
|
||||
this.connections_g.append(arrow.element);
|
||||
}
|
||||
// TODO why are these in connections_g lol
|
||||
for (let [i, region] of this.stored_level.camera_regions.entries()) {
|
||||
@ -1060,6 +1065,9 @@ export class Editor extends PrimaryView {
|
||||
this.connections_g.append(el);
|
||||
}
|
||||
|
||||
// Load *implicit* connections
|
||||
this.recreate_implicit_connections();
|
||||
|
||||
this.renderer.set_level(stored_level);
|
||||
if (this.active) {
|
||||
this.redraw_entire_level();
|
||||
@ -1112,6 +1120,9 @@ export class Editor extends PrimaryView {
|
||||
this.svg_overlay.style.setProperty('--scale', this.zoom);
|
||||
this.actual_viewport_el.classList.toggle('--crispy', this.zoom >= 1);
|
||||
this.statusbar_zoom.textContent = `${this.zoom * 100}%`;
|
||||
// The arrow outline isn't CSS, so we have to fix it manually
|
||||
this._filter_morphology_element.setAttribute('radius', 0.03125 / this.zoom);
|
||||
this._filter_morphology_element2.setAttribute('radius', 0.0625 / this.zoom);
|
||||
|
||||
let index = ZOOM_LEVELS.findIndex(el => el >= this.zoom);
|
||||
if (index < 0) {
|
||||
@ -1737,33 +1748,94 @@ export class Editor extends PrimaryView {
|
||||
);
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------------------------------------
|
||||
// Connections (buttons to things they control)
|
||||
|
||||
// Create a connection between two cells and update the UI accordingly. If dest is null or
|
||||
// undefined, delete any existing connection instead.
|
||||
set_custom_connection(src, dest) {
|
||||
let prev = this.stored_level.custom_connections[src];
|
||||
let prev = this.stored_level.custom_connections.get(src);
|
||||
this._do(
|
||||
() => this._set_custom_connection(src, dest),
|
||||
() => this._set_custom_connection(src, prev),
|
||||
);
|
||||
}
|
||||
_set_custom_connection(src, dest) {
|
||||
if (this.connections_elements[src]) {
|
||||
this.connections_elements[src].remove();
|
||||
}
|
||||
|
||||
if ((dest ?? null) === null) {
|
||||
delete this.stored_level.custom_connections[src];
|
||||
delete this.connections_elements[src];
|
||||
if (this.connections_arrows[src]) {
|
||||
this.connections_arrows[src].element.remove();
|
||||
}
|
||||
this.stored_level.custom_connections.delete(src)
|
||||
delete this.connections_arrows[src];
|
||||
}
|
||||
else {
|
||||
this.stored_level.custom_connections[src] = dest;
|
||||
let el = new SVGConnection(
|
||||
...this.stored_level.scalar_to_coords(src),
|
||||
...this.stored_level.scalar_to_coords(dest),
|
||||
).element;
|
||||
this.connections_elements[src] = el;
|
||||
this.connections_g.append(el);
|
||||
this.stored_level.custom_connections.set(src, dest);
|
||||
|
||||
if (this.connections_arrows[src]) {
|
||||
this.connections_arrows[src].set_dest(
|
||||
...this.stored_level.scalar_to_coords(dest));
|
||||
}
|
||||
else {
|
||||
let arrow = new SVGConnection(
|
||||
...this.stored_level.scalar_to_coords(src),
|
||||
...this.stored_level.scalar_to_coords(dest));
|
||||
this.connections_arrows[src] = arrow;
|
||||
this.connections_g.append(arrow.element);
|
||||
}
|
||||
this.connections_arrows[src].element.setAttribute(
|
||||
'data-source', this.stored_level.linear_cells[src][LAYERS.terrain].type.name);
|
||||
}
|
||||
}
|
||||
|
||||
// TODO also use this to indicate traps or flame jets that are initially toggled (trickier with
|
||||
// flame jets since that can make them look like they're the wrong tile...)
|
||||
recreate_implicit_connections() {
|
||||
this.implicit_connections = new Map;
|
||||
|
||||
for (let el of this.connections_g.querySelectorAll(':scope > .--implicit')) {
|
||||
el.remove();
|
||||
}
|
||||
|
||||
for (let [n, cell] of this.stored_level.linear_cells.entries()) {
|
||||
if (this.stored_level.custom_connections.has(n))
|
||||
continue;
|
||||
|
||||
let terrain = cell[LAYERS.terrain];
|
||||
if (! terrain.type.connects_to)
|
||||
continue;
|
||||
|
||||
let find_func;
|
||||
if (terrain.type.connect_order === 'forward') {
|
||||
find_func = algorithms.find_terrain_linear;
|
||||
}
|
||||
else if (terrain.type.connect_order === 'diamond') {
|
||||
find_func = algorithms.find_terrain_diamond;
|
||||
}
|
||||
|
||||
let target_cell = null;
|
||||
for (let [found_tile, found_cell] of find_func(this.stored_level, cell, terrain.type.connects_to)) {
|
||||
target_cell = found_cell;
|
||||
break;
|
||||
}
|
||||
if (target_cell === null)
|
||||
continue;
|
||||
|
||||
let svg = new SVGConnection(
|
||||
...this.stored_level.scalar_to_coords(n),
|
||||
target_cell.x, target_cell.y);
|
||||
this.implicit_connections.set(n, {
|
||||
index: this.stored_level.coords_to_scalar(target_cell.x, target_cell.y),
|
||||
svg_connection: svg,
|
||||
});
|
||||
svg.element.classList.add('--implicit');
|
||||
svg.element.setAttribute('data-source', terrain.type.name);
|
||||
this.connections_g.append(svg.element);
|
||||
}
|
||||
}
|
||||
|
||||
update_implicit_connection(cell, tile, previous_type) {
|
||||
// TODO actually this should also update explicit ones, if the source/dest types are changed
|
||||
// in such a way as to make the connection invalid
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------------------------------------
|
||||
|
||||
@ -17,6 +17,7 @@ import { TILES_WITH_PROPS } from './tile-overlays.js';
|
||||
// - no ice drawing tool
|
||||
// - cursor box shows with selection tool which seems inappropriate
|
||||
// - controls do not exactly stand out and are just plain text
|
||||
// - set trap as initially open? feels like a weird hack. but it does appear in cc2lp1
|
||||
const MOUSE_BUTTON_MASKS = [1, 4, 2]; // MouseEvent.button/buttons are ordered differently
|
||||
export class MouseOperation {
|
||||
constructor(editor, physical_button) {
|
||||
@ -57,7 +58,7 @@ export class MouseOperation {
|
||||
// appropriate, and its position will be updated to match the position of the cursor
|
||||
set_cursor_element(el) {
|
||||
this.cursor_element = el;
|
||||
el.setAttribute('data-mouseop', this.constructor.name);
|
||||
el.setAttribute('data-name', this.constructor.name);
|
||||
if (! this.is_hover_visible) {
|
||||
el.style.display = 'none';
|
||||
}
|
||||
@ -834,14 +835,40 @@ export class TrackOperation extends MouseOperation {
|
||||
}
|
||||
|
||||
export class ConnectOperation extends MouseOperation {
|
||||
constructor(...args) {
|
||||
super(...args);
|
||||
|
||||
// This is the SVGConnection structure but with only the source circle
|
||||
this.connectable_circle = mk_svg('circle.-source', {r: 0.5});
|
||||
this.connectable_cursor = mk_svg('g.overlay-connection', this.connectable_circle);
|
||||
this.connectable_cursor.style.display = 'none';
|
||||
// TODO how do i distinguish from existing ones
|
||||
this.connectable_cursor.style.stroke = 'lime';
|
||||
this.editor.svg_overlay.append(this.connectable_cursor);
|
||||
}
|
||||
|
||||
handle_hover(client_x, client_y, frac_cell_x, frac_cell_y, cell_x, cell_y) {
|
||||
let cell = this.cell(cell_x, cell_y);
|
||||
let terrain = cell[LAYERS.terrain];
|
||||
if (terrain.type.connects_to) {
|
||||
this.connectable_cursor.style.display = '';
|
||||
this.connectable_circle.setAttribute('cx', cell_x + 0.5);
|
||||
this.connectable_circle.setAttribute('cy', cell_y + 0.5);
|
||||
}
|
||||
else {
|
||||
this.connectable_cursor.style.display = 'none';
|
||||
}
|
||||
}
|
||||
|
||||
handle_press(x, y) {
|
||||
// TODO restrict to button/cloner unless holding shift
|
||||
// TODO what do i do when you erase a button/cloner? can i detect if you're picking it up?
|
||||
let src = this.editor.stored_level.coords_to_scalar(x, y);
|
||||
if (this.alt_mode) {
|
||||
// Auto connect using Lynx rules
|
||||
let cell = this.cell(x, y);
|
||||
let terrain = cell[LAYERS.terrain];
|
||||
if (this.alt_mode) {
|
||||
// Auto connect using Lynx rules
|
||||
// TODO just use the editor's existing implicits for this?
|
||||
let other = null;
|
||||
let swap = false;
|
||||
if (terrain.type.name === 'button_red') {
|
||||
@ -870,8 +897,17 @@ export class ConnectOperation extends MouseOperation {
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// Otherwise, this is the start of a drag
|
||||
if (! terrain.type.connects_to)
|
||||
return;
|
||||
|
||||
this.pending_cxn = new SVGConnection(x, y, x, y);
|
||||
this.pending_source = src;
|
||||
this.pending_type = terrain.type.name;
|
||||
this.editor.svg_overlay.append(this.pending_cxn.element);
|
||||
// Hide the normal cursor for the duration
|
||||
this.connectable_cursor.style.display = 'none';
|
||||
}
|
||||
// FIXME this is hella the sort of thing that should be on Editor, or in algorithms
|
||||
search_for(i0, name, dir) {
|
||||
@ -896,14 +932,46 @@ export class ConnectOperation extends MouseOperation {
|
||||
}
|
||||
}
|
||||
handle_drag(client_x, client_y, frac_cell_x, frac_cell_y, cell_x, cell_y) {
|
||||
if (! this.pending_cxn)
|
||||
return;
|
||||
|
||||
this.pending_cxn.set_dest(cell_x, cell_y);
|
||||
|
||||
let cell = this.cell(cell_x, cell_y);
|
||||
if (TILE_TYPES[this.pending_type].connects_to.has(cell[LAYERS.terrain].type.name)) {
|
||||
this.pending_target = this.editor.stored_level.coords_to_scalar(cell_x, cell_y);
|
||||
this.pending_cxn.element.style.opacity = 0.5;
|
||||
}
|
||||
else {
|
||||
this.pending_target = null;
|
||||
this.pending_cxn.element.style.opacity = '';
|
||||
}
|
||||
}
|
||||
commit_press() {
|
||||
// TODO
|
||||
if (! this.pending_cxn)
|
||||
return;
|
||||
|
||||
if (this.pending_target !== null) {
|
||||
this.editor.set_custom_connection(this.pending_source, this.pending_target);
|
||||
}
|
||||
|
||||
this.pending_cxn.element.remove();
|
||||
this.pending_cxn = null;
|
||||
}
|
||||
abort_press() {
|
||||
if (this.pending_cxn) {
|
||||
this.pending_cxn.element.remove();
|
||||
this.pending_cxn = null;
|
||||
}
|
||||
}
|
||||
cleanup_press() {
|
||||
}
|
||||
|
||||
do_destroy() {
|
||||
this.connectable_cursor.remove();
|
||||
super.do_destroy();
|
||||
}
|
||||
}
|
||||
export class WireOperation extends MouseOperation {
|
||||
handle_press() {
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
import { LAYERS } from './defs.js';
|
||||
import { DIRECTIONS, LAYERS } from './defs.js';
|
||||
import * as util from './util.js';
|
||||
|
||||
export class StoredCell extends Array {
|
||||
@ -89,6 +89,11 @@ export class LevelInterface {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
get_neighboring_cell(cell, direction) {
|
||||
let move = DIRECTIONS[direction].movement;
|
||||
return this.cell(cell.x + move[0], cell.y + move[1]);
|
||||
}
|
||||
}
|
||||
|
||||
export class StoredLevel extends LevelInterface {
|
||||
@ -128,8 +133,10 @@ export class StoredLevel extends LevelInterface {
|
||||
this.linear_cells = [];
|
||||
|
||||
// Maps of button positions to trap/cloner positions, as scalars
|
||||
this.has_custom_connections = false;
|
||||
this.custom_connections = {};
|
||||
// Not supported by Steam CC2, but supported by Tile World even in Lynx mode
|
||||
this.custom_connections = new Map;
|
||||
// If true, Lynx-style implicit connections don't work at all
|
||||
this.only_custom_connections = false;
|
||||
|
||||
// New LL feature: custom camera regions, as lists of {x, y, width, height}
|
||||
this.camera_regions = [];
|
||||
|
||||
@ -1332,12 +1332,11 @@ export function parse_level(buf, number = 1) {
|
||||
if (bytes.length % 4 !== 0)
|
||||
throw new Error(`Expected LXCX chunk to be a multiple of 4 bytes; got ${bytes.length}`);
|
||||
|
||||
level.has_custom_connections = true;
|
||||
let p = 0;
|
||||
while (p < bytes.length) {
|
||||
let src = view.getUint16(p, true);
|
||||
let dest = view.getUint16(p + 2, true);
|
||||
level.custom_connections[src] = dest;
|
||||
level.custom_connections.set(src, dest);
|
||||
p += 4;
|
||||
}
|
||||
}
|
||||
@ -1572,12 +1571,12 @@ export function synthesize_level(stored_level) {
|
||||
|
||||
// Store MSCC-like custom connections
|
||||
// TODO LL feature, should be distinguished somehow
|
||||
let num_connections = Object.keys(stored_level.custom_connections).length;
|
||||
let num_connections = stored_level.custom_connections.size;
|
||||
if (num_connections > 0) {
|
||||
let buf = new ArrayBuffer(4 * num_connections);
|
||||
let view = new DataView(buf);
|
||||
let p = 0;
|
||||
for (let [src, dest] of Object.entries(stored_level.custom_connections)) {
|
||||
for (let [src, dest] of stored_level.custom_connections) {
|
||||
view.setUint16(p + 0, src, true);
|
||||
view.setUint16(p + 2, dest, true);
|
||||
p += 4;
|
||||
|
||||
@ -209,7 +209,7 @@ export function parse_level_metadata(bytes) {
|
||||
|
||||
function parse_level(bytes, number) {
|
||||
let level = new format_base.StoredLevel(number);
|
||||
level.has_custom_connections = true;
|
||||
level.only_custom_connections = true;
|
||||
level.format = 'ccl';
|
||||
level.uses_ll_extensions = false;
|
||||
level.chips_required = 0;
|
||||
@ -353,7 +353,7 @@ function parse_level(bytes, number) {
|
||||
let s = level.coords_to_scalar(button_x, button_y);
|
||||
let d = level.coords_to_scalar(trap_x, trap_y);
|
||||
if (level.linear_cells[s][LAYERS.terrain].type.name === 'button_brown') {
|
||||
level.custom_connections[s] = d;
|
||||
level.custom_connections.set(s, d);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -372,7 +372,7 @@ function parse_level(bytes, number) {
|
||||
let s = level.coords_to_scalar(button_x, button_y);
|
||||
let d = level.coords_to_scalar(cloner_x, cloner_y);
|
||||
if (level.linear_cells[s][LAYERS.terrain].type.name === 'button_red') {
|
||||
level.custom_connections[s] = d;
|
||||
level.custom_connections.set(s, d);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -561,9 +561,10 @@ export function synthesize_level(stored_level) {
|
||||
else if (other.type.name === 'button_brown') {
|
||||
cxn_target = 'trap';
|
||||
}
|
||||
if (cxn_target && i in stored_level.custom_connections) {
|
||||
let dest = stored_level.custom_connections[i];
|
||||
if (cxn_target && stored_level.custom_connections.has(i)) {
|
||||
let dest = stored_level.custom_connections.get(i);
|
||||
let dest_cell = stored_level.linear_cells[dest];
|
||||
// FIXME these need to be sorted by destination actually
|
||||
if (dest_cell && dest_cell[LAYERS.terrain].type.name === cxn_target) {
|
||||
if (other.type.name === 'button_red') {
|
||||
cloner_cxns.push(x, y, ...stored_level.scalar_to_coords(dest));
|
||||
@ -620,7 +621,7 @@ export function synthesize_level(stored_level) {
|
||||
// TODO do something with not-ascii; does TW support utf8 or latin1 or anything?
|
||||
add_block(3, util.bytestring_to_buffer(stored_level.title.substring(0, 63) + "\0"));
|
||||
// Trap and cloner connections
|
||||
function to_words(cxns) {
|
||||
function encode_connections(cxns) {
|
||||
let words = new ArrayBuffer(cxns.length * 2);
|
||||
let view = new DataView(words);
|
||||
for (let [i, val] of cxns.entries()) {
|
||||
@ -629,10 +630,10 @@ export function synthesize_level(stored_level) {
|
||||
return words;
|
||||
}
|
||||
if (trap_cxns.length > 0) {
|
||||
add_block(4, to_words(trap_cxns));
|
||||
add_block(4, encode_connections(trap_cxns));
|
||||
}
|
||||
if (cloner_cxns.length > 0) {
|
||||
add_block(5, to_words(cloner_cxns));
|
||||
add_block(5, encode_connections(cloner_cxns));
|
||||
}
|
||||
// Password
|
||||
// TODO support this for real lol
|
||||
|
||||
93
js/game.js
93
js/game.js
@ -476,53 +476,39 @@ export class Level extends LevelInterface {
|
||||
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;
|
||||
|
||||
// Check for custom wiring, for MSCC .DAT levels
|
||||
// TODO would be neat if this applied to orange buttons too
|
||||
// TODO RAINBOW TELEPORTER, ARBITRARY TILE TARGET HAHA
|
||||
if (this.stored_level.has_custom_connections) {
|
||||
if (this.stored_level.custom_connections.size > 0) {
|
||||
let n = this.stored_level.coords_to_scalar(x, y);
|
||||
let target_cell_n = null;
|
||||
if (connectable.type.name === 'button_brown' || connectable.type.name === 'button_red') {
|
||||
target_cell_n = this.stored_level.custom_connections[n] ?? null;
|
||||
}
|
||||
if (target_cell_n && target_cell_n < this.width * this.height) {
|
||||
let target_cell_n = this.stored_level.custom_connections.get(n) ?? null;
|
||||
if (target_cell_n !== null && 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) {
|
||||
if (tile && goals.has(tile.type.name)) {
|
||||
connectable.connection = tile;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (this.stored_level.only_custom_connections)
|
||||
return;
|
||||
}
|
||||
|
||||
// Orange buttons do a really weird diamond search
|
||||
if (connectable.type.connect_order === 'diamond') {
|
||||
for (let cell of algorithms.iter_cells_in_diamond(
|
||||
this, connectable.cell.x, connectable.cell.y))
|
||||
{
|
||||
let target = null;
|
||||
for (let tile of cell) {
|
||||
if (tile && goals.has(tile.type.name)) {
|
||||
target = tile;
|
||||
for (let [tile, _cell] of algorithms.find_terrain_diamond(this, cell, goals)) {
|
||||
connectable.connection = 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)) {
|
||||
for (let [tile, _cell] of algorithms.find_terrain_linear(this, cell, goals)) {
|
||||
// TODO ideally this should be a weak connection somehow, since dynamite can destroy
|
||||
// empty cloners and probably traps too
|
||||
connectable.connection = tile;
|
||||
@ -2366,67 +2352,6 @@ export class Level extends LevelInterface {
|
||||
|
||||
// Level inspection -------------------------------------------------------------------------------
|
||||
|
||||
get_neighboring_cell(cell, direction) {
|
||||
let move = DIRECTIONS[direction].movement;
|
||||
return this.cell(cell.x + move[0], cell.y + move[1]);
|
||||
}
|
||||
|
||||
// Iterates over the grid in (reverse?) reading order and yields all tiles with the given name.
|
||||
// The starting cell is iterated last.
|
||||
*iter_tiles_in_reading_order(start_cell, name, reverse = false) {
|
||||
let i = this.coords_to_scalar(start_cell.x, start_cell.y);
|
||||
let index = TILE_TYPES[name].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 && tile.type.name === name) {
|
||||
yield tile;
|
||||
}
|
||||
|
||||
if (cell === start_cell)
|
||||
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];
|
||||
// FIXME probably uh do a lookup here
|
||||
if (tile && names.indexOf(tile.type.name) >= 0) {
|
||||
yield tile;
|
||||
}
|
||||
|
||||
if (cell === start_cell)
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// FIXME require_stub should really just care whether we ourselves /can/ contain wire, and also
|
||||
// we should check that on our neighbor
|
||||
is_tile_wired(tile, require_stub = true) {
|
||||
|
||||
@ -1,3 +1,4 @@
|
||||
import { find_terrain_linear } from './algorithms.js';
|
||||
import { COLLISION, DIRECTIONS, DIRECTION_ORDER, LAYERS, TICS_PER_SECOND, PICKUP_PRIORITIES } from './defs.js';
|
||||
|
||||
// TODO factor out some repeated stuff: common monster bits, common item bits, repeated collision
|
||||
@ -731,6 +732,8 @@ const TILE_TYPES = {
|
||||
speed_factor: 0.5,
|
||||
},
|
||||
grass: {
|
||||
// TODO should bugs leave if they have no other option...? that seems real hard.
|
||||
// TODO teeth move at full speed? that also seems hard.
|
||||
layer: LAYERS.terrain,
|
||||
blocks_collision: COLLISION.block_cc1 | COLLISION.block_cc2,
|
||||
blocks(me, level, other) {
|
||||
@ -1778,7 +1781,9 @@ 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_multiple(me.cell, ['teleport_blue', 'teleport_blue_exit'], true)) {
|
||||
for (let [dest, cell] of find_terrain_linear(
|
||||
level, me.cell, new Set(['teleport_blue', 'teleport_blue_exit']), true))
|
||||
{
|
||||
if (! dest.wire_directions) {
|
||||
yield [dest, exit_direction];
|
||||
}
|
||||
@ -1871,13 +1876,13 @@ const TILE_TYPES = {
|
||||
// wires are directly connected to another neighboring wire.
|
||||
let iterable;
|
||||
if (me.is_active) {
|
||||
iterable = level.iter_tiles_in_reading_order(me.cell, 'teleport_red');
|
||||
iterable = find_terrain_linear(level, me.cell, new Set(['teleport_red']));
|
||||
}
|
||||
else {
|
||||
iterable = [me];
|
||||
iterable = [[me, me.cell]];
|
||||
}
|
||||
let exit_direction = other.direction;
|
||||
for (let tile of iterable) {
|
||||
for (let [tile, cell] of iterable) {
|
||||
// Red teleporters allow exiting in any direction, searching clockwise, except for
|
||||
// the teleporter you entered
|
||||
if (tile === me) {
|
||||
@ -1926,7 +1931,7 @@ const TILE_TYPES = {
|
||||
|
||||
// This iterator starts on the /next/ teleporter, so we appear last, and we can index
|
||||
// from zero to the second-to-last element.
|
||||
let all = Array.from(level.iter_tiles_in_reading_order(me.cell, 'teleport_green'));
|
||||
let all = Array.from(find_terrain_linear(level, me.cell, new Set(['teleport_green'])));
|
||||
if (all.length <= 1) {
|
||||
// If this is the only teleporter, just walk out the other side — and, crucially, do
|
||||
// NOT advance the PRNG
|
||||
@ -1938,6 +1943,7 @@ const TILE_TYPES = {
|
||||
let exit_direction = DIRECTION_ORDER[level.prng() % 4];
|
||||
|
||||
let candidates;
|
||||
all = all.map(([tile, cell]) => tile);
|
||||
if (level.compat.green_teleports_can_fail) {
|
||||
// CC2 bug emulation: only look through "unclogged" exits
|
||||
candidates = all.filter(tile => tile === me || ! tile.cell.get_actor());
|
||||
@ -1982,7 +1988,7 @@ const TILE_TYPES = {
|
||||
allow_player_override: true,
|
||||
*teleport_dest_order(me, level, other) {
|
||||
let exit_direction = other.direction;
|
||||
for (let dest of level.iter_tiles_in_reading_order(me.cell, 'teleport_yellow', true)) {
|
||||
for (let [dest, cell] of find_terrain_linear(level, me.cell, new Set(['teleport_yellow']), true)) {
|
||||
yield [dest, exit_direction];
|
||||
}
|
||||
},
|
||||
@ -2113,7 +2119,7 @@ const TILE_TYPES = {
|
||||
},
|
||||
button_brown: {
|
||||
layer: LAYERS.terrain,
|
||||
connects_to: 'trap',
|
||||
connects_to: new Set(['trap']),
|
||||
connect_order: 'forward',
|
||||
on_ready(me, level) {
|
||||
// Inform the trap of any actors that start out holding us down
|
||||
@ -2145,7 +2151,7 @@ const TILE_TYPES = {
|
||||
},
|
||||
button_red: {
|
||||
layer: LAYERS.terrain,
|
||||
connects_to: 'cloner',
|
||||
connects_to: new Set(['cloner']),
|
||||
connect_order: 'forward',
|
||||
on_arrive(me, level, other) {
|
||||
level.sfx.play_once('button-press', me.cell);
|
||||
|
||||
21
style.css
21
style.css
@ -1514,7 +1514,7 @@ body.--debug .player-overlay-message {
|
||||
background: radial-gradient(#0004, hsla(330, 10%, 10%, 0.5) 40%, hsl(330, 20%, 10%));
|
||||
}
|
||||
.player-overlay-message[data-reason=success] {
|
||||
background: radial-gradient(hsla(var(--main-hue), 60%, 5%, 0.75), 60%, hsla(var(--main-hue), 60%, 25%, 0.75));
|
||||
background: radial-gradient(hsla(30, 80%, 10%, 0.75), 60%, hsla(40, 100%, 30%, 0.75));
|
||||
}
|
||||
.player-overlay-message[data-reason=ended] {
|
||||
/* Rearrange this entirely, to fit the ending image in */
|
||||
@ -2219,7 +2219,7 @@ svg.level-editor-overlay {
|
||||
* consistent size at any zoom level */
|
||||
--scale: 1;
|
||||
/* default svg properties */
|
||||
stroke-width: calc(0.0625px / var(--scale));
|
||||
stroke-width: calc(0.125px / var(--scale));
|
||||
fill: none;
|
||||
}
|
||||
svg.level-editor-overlay .overlay-transient {
|
||||
@ -2228,7 +2228,7 @@ svg.level-editor-overlay .overlay-transient {
|
||||
svg.level-editor-overlay .overlay-transient.--visible {
|
||||
display: initial;
|
||||
}
|
||||
svg.level-editor-overlay rect.overlay-cursor {
|
||||
svg.level-editor-overlay rect.overlay-pencil-cursor {
|
||||
stroke: hsla(var(--main-hue), 100%, 90%, 0.75);
|
||||
fill: hsla(var(--main-hue), 100%, 75%, 0.25);
|
||||
}
|
||||
@ -2264,11 +2264,24 @@ svg.level-editor-overlay path.overlay-selection.--floating {
|
||||
}
|
||||
#overlay-arrowhead {
|
||||
fill: white;
|
||||
fill: context-stroke;
|
||||
}
|
||||
svg.level-editor-overlay g.overlay-connection {
|
||||
stroke: white;
|
||||
stroke: #e4e4e4;
|
||||
filter: url(#overlay-filter-outline);
|
||||
}
|
||||
svg.level-editor-overlay g.overlay-connection[data-source=button_red] {
|
||||
stroke: hsl(0, 90%, 60%);
|
||||
}
|
||||
svg.level-editor-overlay g.overlay-connection[data-source=button_brown] {
|
||||
stroke: hsl(20, 60%, 60%);
|
||||
}
|
||||
svg.level-editor-overlay g.overlay-connection[data-source=button_orange] {
|
||||
stroke: hsl(30, 90%, 60%);
|
||||
}
|
||||
svg.level-editor-overlay g.overlay-connection.--implicit line.-arrow {
|
||||
stroke-dasharray: calc(0.25px / var(--scale)), calc(0.25px / var(--scale));
|
||||
}
|
||||
svg.level-editor-overlay g.overlay-connection line.-arrow {
|
||||
marker-end: url(#overlay-arrowhead);
|
||||
}
|
||||
|
||||
Loading…
Reference in New Issue
Block a user