Update implicit button connections when editing, I hope

This commit is contained in:
Eevee (Evelyn Woods) 2024-04-21 02:30:34 -06:00
parent 04d6b3dddb
commit 39f0f20dc6
6 changed files with 230 additions and 53 deletions

View File

@ -57,9 +57,11 @@ export function* find_terrain_diamond(levelish, start_cell, type_names) {
}
}
// TODO make this guy work generically for orange, red, brown buttons? others...?
export function find_implicit_connection() {
}
export const CONNECTION_FUNCTIONS = {
forward: find_terrain_linear,
diamond: find_terrain_diamond,
};
export class Circuit {
constructor() {

View File

@ -7,7 +7,7 @@ export const TOOLS = {
pencil: {
icon: 'icons/tool-pencil.png',
name: "Pencil",
desc: "Place, erase, and select tiles.\n\n[mouse1] Draw\n[ctrl] [mouse1] Erase\n[shift] [mouse1] Draw, replacing everything\n\n[mouse2] Pick foreground tile\n[ctrl] [mouse2] Pick background tile",
desc: "Place, erase, and select tiles.\n\n[mouse1] Draw\n[shift] [mouse1] Draw, replacing entire cell\n[ctrl] [mouse1] Erase (terrain becomes background)\n[ctrl] [shift] [mouse1] Erase entire cell\n\n[mouse2] Pick foreground tile\n[ctrl] [mouse2] Pick background tile",
uses_palette: true,
op1: mouseops.PencilOperation,
op2: mouseops.EyedropOperation,

View File

@ -624,23 +624,23 @@ export class Editor extends PrimaryView {
tile.type = TILE_TYPES[tile.name];
delete tile.name;
if (fg) {
this.select_foreground_tile(tile, true);
this.select_foreground_tile(tile, 'palette');
}
else {
if (tile.type.layer !== LAYERS.terrain)
return;
this.select_background_tile(tile, true);
this.select_background_tile(tile, 'palette');
}
}
else {
// Regular tile name
if (fg) {
this.select_foreground_tile(key, true);
this.select_foreground_tile(key, 'palette');
}
else {
if (TILE_TYPES[key].layer !== LAYERS.terrain)
return;
this.select_background_tile(key, true);
this.select_background_tile(key, 'palette');
}
}
});
@ -668,10 +668,12 @@ export class Editor extends PrimaryView {
this.fg_tile = null; // used for most drawing
this.fg_tile_from_palette = false;
this.fg_tile_source_cell = null;
this.palette_fg_selected_el = null;
this.select_foreground_tile('wall', true);
this.bg_tile = null; // used to populate new/cleared cells
this.select_background_tile('floor', true);
this.bg_tile_source_cell = null;
this.select_foreground_tile('wall', 'palette');
this.select_background_tile('floor', 'palette');
this.selection = new Selection(this);
@ -1048,13 +1050,23 @@ export class Editor extends PrimaryView {
save_json_to_storage(pack_key, pack_stash);
}
this.connectable_types = new Set;
for (let [name, type] of Object.entries(TILE_TYPES)) {
if (type.connects_to) {
this.connectable_types.add(name);
for (let to_name of type.connects_to) {
this.connectable_types.add(to_name);
}
}
}
// Load connections
// TODO what if the source tile is not connectable?
this.connections_g.textContent = '';
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 [sx, sy] = this.scalar_to_coords(src);
let [dx, dy] = this.scalar_to_coords(dest);
let arrow = new SVGConnection(sx, sy, dx, dy);
this.connections_arrows[src] = arrow;
arrow.element.setAttribute(
@ -1085,7 +1097,7 @@ export class Editor extends PrimaryView {
update_cell_coordinates() {
// We rely on each StoredCell having .x and .y for partial redrawing
for (let [i, cell] of this.stored_level.linear_cells.entries()) {
[cell.x, cell.y] = this.stored_level.scalar_to_coords(i);
[cell.x, cell.y] = this.scalar_to_coords(i);
}
}
@ -1250,7 +1262,10 @@ export class Editor extends PrimaryView {
return [name, tile];
}
select_foreground_tile(name_or_tile, from_palette = false) {
// Sets the current tile used by the pencil.
// source: either 'palette' for a palette-sourced tile (which may have combining behavior), or a
// cell index (used to track explicit connections).
select_foreground_tile(name_or_tile, source) {
let [name, tile] = this._name_or_tile_to_name_and_tile(name_or_tile);
// Deselect any previous selection
@ -1260,7 +1275,14 @@ export class Editor extends PrimaryView {
// Store the tile
this.fg_tile = tile;
this.fg_tile_from_palette = from_palette;
if (source === 'palette') {
this.fg_tile_from_palette = true;
this.fg_tile_source_cell = null;
}
else {
this.fg_tile_from_palette = false;
this.fg_tile_source_cell = source;
}
// Select it in the palette, if possible
let key = name;
@ -1282,10 +1304,16 @@ export class Editor extends PrimaryView {
}
}
select_background_tile(name_or_tile) {
select_background_tile(name_or_tile, source) {
let [_name, tile] = this._name_or_tile_to_name_and_tile(name_or_tile);
this.bg_tile = tile;
if (source === 'palette') {
this.bg_tile_source_cell = null;
}
else {
this.bg_tile_source_cell = source;
}
this.redraw_background_tile();
}
@ -1437,6 +1465,18 @@ export class Editor extends PrimaryView {
// ------------------------------------------------------------------------------------------------
// Utility/inspection
scalar_to_coords(n) {
return this.stored_level.scalar_to_coords(n);
}
coords_to_scalar(x, y) {
return this.stored_level.coords_to_scalar(x, y);
}
cell_to_scalar(cell) {
return this.stored_level.cell_to_scalar(cell);
}
is_in_bounds(x, y) {
return this.stored_level.is_point_within_bounds(x, y);
}
@ -1500,6 +1540,7 @@ export class Editor extends PrimaryView {
}
erase_tile(cell, tile = null) {
// TODO this is only used in one place, by pencil, and doesn't pass 'tile'
// TODO respect selection
if (tile === null) {
@ -1536,19 +1577,21 @@ export class Editor extends PrimaryView {
replace_cell(cell, new_cell) {
// Save the coordinates so it doesn't matter what they are when undoing
let x = cell.x, y = cell.y;
let n = this.stored_level.coords_to_scalar(x, y);
let n = this.coords_to_scalar(x, y);
this._do(
() => {
this.stored_level.linear_cells[n] = new_cell;
new_cell.x = x;
new_cell.y = y;
this.mark_cell_dirty(new_cell);
this._update_connections(new_cell, cell[LAYERS.terrain], new_cell[LAYERS.terrain]);
},
() => {
this.stored_level.linear_cells[n] = cell;
cell.x = x;
cell.y = y;
this.mark_cell_dirty(cell);
this._update_connections(cell, new_cell[LAYERS.terrain], cell[LAYERS.terrain]);
},
);
}
@ -1612,7 +1655,7 @@ export class Editor extends PrimaryView {
upgrade_tile(tile);
}
}
let n2 = this.stored_level.coords_to_scalar(x2, y2);
let n2 = this.coords_to_scalar(x2, y2);
if (new_cells[n2]) {
console.error("Tile transformation overwriting the same cell twice:", x2, y2);
}
@ -1775,12 +1818,12 @@ export class Editor extends PrimaryView {
if (this.connections_arrows[src]) {
this.connections_arrows[src].set_dest(
...this.stored_level.scalar_to_coords(dest));
...this.scalar_to_coords(dest));
}
else {
let arrow = new SVGConnection(
...this.stored_level.scalar_to_coords(src),
...this.stored_level.scalar_to_coords(dest));
...this.scalar_to_coords(src),
...this.scalar_to_coords(dest));
this.connections_arrows[src] = arrow;
this.connections_g.append(arrow.element);
}
@ -1792,7 +1835,9 @@ export class Editor extends PrimaryView {
// 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() {
let t0 = performance.now();
this.implicit_connections = new Map;
this.reverse_implicit_connections = new Map;
for (let el of this.connections_g.querySelectorAll(':scope > .--implicit')) {
el.remove();
@ -1803,41 +1848,146 @@ export class Editor extends PrimaryView {
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;
if (terrain.type.connects_to){
this._implicit_connect_tile(terrain, cell, n);
}
}
console.log("implicit connections time", performance.now() - t0);
}
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;
_implicit_connect_tile(tile, cell, n) {
if (this.stored_level.custom_connections.has(n))
return;
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);
let find_func = algorithms.CONNECTION_FUNCTIONS[tile.type.connect_order];
let target_cell = null;
for (let [found_tile, found_cell] of find_func(this.stored_level, cell, tile.type.connects_to)) {
target_cell = found_cell;
break;
}
if (target_cell) {
this.__add_implicit_connection(
n, this.coords_to_scalar(target_cell.x, target_cell.y), tile.type.name);
}
}
update_implicit_connection(cell, tile, previous_type) {
// TODO explicit connection stuff left:
// - adding an explicit connection should delete all the implicit ones from the source
// - deleting an explicit connection should add an auto implicit connection
// - altering the src/dest of an explicit connection should... delete it idk
// - eyedropping an explicit src and then penciling it elsewhere should create a new explicit
// connection
// - stamping an explicit connection...
// - if src and dest are in the selection, create a new connection
// - if only src, copy original dest
// - if only dest, then stamping should only do it if it doesn't already exist?
// also arrow should follow the selection
_update_connections(cell, old_tile, new_tile) {
if (! (old_tile && ! this.connectable_types.has(old_tile.type.name)) &&
! (new_tile && ! this.connectable_types.has(new_tile.type.name)))
{
// Nothing to do
return;
}
if (old_tile.type.name === new_tile.type.name)
return;
// 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
let n = this.cell_to_scalar(cell);
// Remove an old outgoing connection
if (old_tile.type.connects_to) {
this.__delete_implicit_connection(n);
}
// Remove an old incoming connection
if (old_tile.type.connects_from) {
let sources = this.reverse_implicit_connections.get(n);
if (sources) {
// All the buttons pointing at us are now dangling. We could be a little clever
// here (e.g., red/brown buttons only need to start searching from us, not from
// themselves), but frankly, fuck it, just rescan everyone.
for (let src of sources) {
this.__delete_implicit_connection(src);
let source_cell = this.stored_level.linear_cells[src];
this._implicit_connect_tile(source_cell[LAYERS.terrain], source_cell, src);
}
}
}
// Add a new outgoing connection
if (new_tile.type.connects_to) {
this._implicit_connect_tile(new_tile, cell, n);
}
// Add a new incoming connection, which is a bit more complicated
if (new_tile.type.connects_from) {
for (let source_type_name of new_tile.type.connects_from) {
let source_type = TILE_TYPES[source_type_name];
// For a trap or cloner, we can search backwards until we see another trap or
// cloner, and we know we're the target of every button we see in the meantime
if (source_type.connect_order === 'forward') {
for (let [other_tile, other_cell] of algorithms.find_terrain_linear(
this.stored_level, cell, new Set([new_tile.type.name, source_type_name]), true))
{
if (other_tile.type.name === new_tile.type.name)
break;
if (other_tile.type.name === source_type_name) {
let src = this.cell_to_scalar(other_cell);
if (! this.stored_level.custom_connections.has(src)) {
this.__add_implicit_connection(src, n, source_type_name);
}
}
}
}
// For flame jets... I don't think there's any way to be sure except to re-check
// every orange button in the level!
else if (source_type.connect_order === 'diamond') {
for (let source_cell of this.stored_level.linear_cells) {
let terrain = source_cell.get_terrain();
if (terrain.type !== source_type)
continue;
this._implicit_connect_tile(terrain, source_cell, this.cell_to_scalar(source_cell));
}
}
}
}
}
__add_implicit_connection(src, dest, type_name) {
let [x0, y0] = this.scalar_to_coords(src);
let [x1, y1] = this.scalar_to_coords(dest);
let cxn;
if (this.implicit_connections.has(src)) {
cxn = this.implicit_connections.get(src);
this.reverse_implicit_connections.get(cxn.index).delete(src);
cxn.svg_connection.set_dest(x1, y1);
}
else {
let svg = new SVGConnection(x0, y0, x1, y1);
cxn = {
index: dest,
svg_connection: svg,
};
this.implicit_connections.set(src, cxn);
}
util.setdefault(this.reverse_implicit_connections, dest, () => new Set).add(src);
cxn.svg_connection.element.classList.add('--implicit');
cxn.svg_connection.element.setAttribute('data-source', type_name);
this.connections_g.append(cxn.svg_connection.element);
}
__delete_implicit_connection(src) {
let cxn = this.implicit_connections.get(src);
if (cxn) {
cxn.svg_connection.element.remove();
this.implicit_connections.delete(src);
this.reverse_implicit_connections.get(cxn.index).delete(src);
}
}
// ------------------------------------------------------------------------------------------------
@ -1861,10 +2011,12 @@ export class Editor extends PrimaryView {
() => {
cell[layer] = new_tile;
this.mark_cell_dirty(cell);
this._update_connections(cell, old_tile, new_tile);
},
() => {
cell[layer] = old_tile;
this.mark_cell_dirty(cell);
this._update_connections(cell, new_tile, old_tile);
},
);
}

View File

@ -284,10 +284,11 @@ export class EyedropOperation extends MouseOperation {
this.last_eyedropped_coords = null;
return;
}
let n = this.editor.coords_to_scalar(x, y);
// If we're picking the background, we always use the terrain
if (this.ctrl) {
this.editor.select_background_tile(cell[LAYERS.terrain]);
this.editor.select_background_tile(cell[LAYERS.terrain], n);
return;
}
@ -306,7 +307,7 @@ export class EyedropOperation extends MouseOperation {
if (! tile)
continue;
this.editor.select_foreground_tile(tile);
this.editor.select_foreground_tile(tile, n);
this.last_eyedropped_coords = [x, y];
this.last_layer = layer;
return;
@ -436,7 +437,7 @@ export class FillOperation extends MouseOperation {
this._floodfill_from(cell_x, cell_y);
}
_floodfill_from(x0, y0) {
let i0 = this.editor.stored_level.coords_to_scalar(x0, y0);
let i0 = this.editor.coords_to_scalar(x0, y0);
if (this.fill_state && this.fill_state[i0]) {
// This cell is already part of the pending fill, so there's nothing to do
return;
@ -877,7 +878,7 @@ export class ConnectOperation extends MouseOperation {
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);
let src = this.editor.coords_to_scalar(x, y);
let cell = this.cell(x, y);
let terrain = cell[LAYERS.terrain];
if (this.alt_mode) {
@ -953,7 +954,7 @@ export class ConnectOperation extends MouseOperation {
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_target = this.editor.coords_to_scalar(cell_x, cell_y);
this.pending_cxn.element.style.opacity = 0.5;
}
else {

View File

@ -3374,6 +3374,17 @@ for (let [name, type] of Object.entries(TILE_TYPES)) {
if (type.item_priority === undefined)
console.error(`Tile type ${name} is an item but has no item priority`);
}
// Add a .connects_from, used only by the editor
if (type.connects_to) {
for (let other_name of type.connects_to) {
let other = TILE_TYPES[other_name];
if (! other.connects_from) {
other.connects_from = new Set;
}
other.connects_from.add(name);
}
}
}
export default TILE_TYPES;

View File

@ -8,6 +8,17 @@ export function random_choice(list) {
return list[Math.floor(Math.random() * list.length)];
}
export function setdefault(map, key, defaulter) {
if (map.has(key)) {
return map.get(key);
}
else {
let value = defaulter();
map.set(key, value);
return value;
}
}
// DOM stuff
function _mk(el, children) {