Merge trap/cloner connections; round-trip them through C2M; stub out connect tool

This commit is contained in:
Eevee (Evelyn Woods) 2021-04-28 22:05:01 -06:00
parent 7f90ee5f7d
commit eff62a9765
7 changed files with 278 additions and 65 deletions

View File

@ -127,12 +127,9 @@ export class StoredLevel extends LevelInterface {
this.size_y = 0; this.size_y = 0;
this.linear_cells = []; this.linear_cells = [];
// Maps of button positions to trap/cloner positions, as scalar indexes // Maps of button positions to trap/cloner positions, as scalars
// in the linear cell list
// TODO merge these imo
this.has_custom_connections = false; this.has_custom_connections = false;
this.custom_trap_wiring = {}; this.custom_connections = {};
this.custom_cloner_wiring = {};
// New LL feature: custom camera regions, as lists of {x, y, width, height} // New LL feature: custom camera regions, as lists of {x, y, width, height}
this.camera_regions = []; this.camera_regions = [];

View File

@ -1307,6 +1307,20 @@ export function parse_level(buf, number = 1) {
p += 4; p += 4;
} }
} }
else if (type === 'LXCX') {
// Custom connections, like MSCC (but more! maybe)
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;
p += 4;
}
}
else { else {
console.warn(`Unrecognized section type '${type}' at offset ${bytes.byteOffset}`, view); console.warn(`Unrecognized section type '${type}' at offset ${bytes.byteOffset}`, view);
// TODO save it, persist when editing level // TODO save it, persist when editing level
@ -1536,6 +1550,21 @@ export function synthesize_level(stored_level) {
c2m.add_section('LXCM', bytes.buffer); c2m.add_section('LXCM', bytes.buffer);
} }
// Store MSCC-like custom connections
// TODO LL feature, should be distinguished somehow
let num_connections = Object.keys(stored_level.custom_connections).length;
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)) {
view.setUint16(p + 0, src, true);
view.setUint16(p + 2, dest, true);
p += 4;
}
c2m.add_section('LXCX', buf);
}
let map_bytes = new Uint8Array(1024); let map_bytes = new Uint8Array(1024);
let map_view = new DataView(map_bytes.buffer); let map_view = new DataView(map_bytes.buffer);
map_bytes[0] = stored_level.size_x; map_bytes[0] = stored_level.size_x;

View File

@ -348,7 +348,13 @@ function parse_level(bytes, number) {
let trap_y = field_view.getUint16(q + 6, true); let trap_y = field_view.getUint16(q + 6, true);
// Fifth u16 is always zero, possibly live game state // Fifth u16 is always zero, possibly live game state
q += 10; q += 10;
level.custom_trap_wiring[button_x + button_y * level.size_x] = trap_x + trap_y * level.size_x; // Connections are ignored if they're on the wrong tiles anyway, and we use a single
// mapping that's a bit more flexible, so only store valid connections
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;
}
} }
} }
else if (field_type === 0x05) { else if (field_type === 0x05) {
@ -361,7 +367,13 @@ function parse_level(bytes, number) {
let cloner_x = field_view.getUint16(q + 4, true); let cloner_x = field_view.getUint16(q + 4, true);
let cloner_y = field_view.getUint16(q + 6, true); let cloner_y = field_view.getUint16(q + 6, true);
q += 8; q += 8;
level.custom_cloner_wiring[button_x + button_y * level.size_x] = cloner_x + cloner_y * level.size_x; // Connections are ignored if they're on the wrong tiles anyway, and we use a single
// mapping that's a bit more flexible, so only store valid connections
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;
}
} }
} }
else if (field_type === 0x06) { else if (field_type === 0x06) {
@ -460,9 +472,11 @@ export function synthesize_level(stored_level) {
let top_layer = []; let top_layer = [];
let bottom_layer = []; let bottom_layer = [];
let hint_text = null; let hint_text = null;
let monster_coords = [];
let error_found_wires = false; let error_found_wires = false;
// TODO i could be a little kinder and support, say, items on terrain; do those work in mscc? tw lynx? // TODO i could be a little kinder and support, say, items on terrain; do those work in mscc? tw lynx?
for (let [i, cell] of stored_level.linear_cells.entries()) { for (let [i, cell] of stored_level.linear_cells.entries()) {
let [x, y] = stored_level.scalar_to_coords(i);
let actor = null; let actor = null;
let other = null; let other = null;
for (let tile of cell) { for (let tile of cell) {
@ -480,12 +494,15 @@ export function synthesize_level(stored_level) {
continue; continue;
} }
else if (other) { else if (other) {
let [x, y] = stored_level.scalar_to_coords(i);
errors.push(`A cell can only contain one static tile, but cell (${x}, ${y}) has both ${other.type.name} and ${tile.type.name}`); errors.push(`A cell can only contain one static tile, but cell (${x}, ${y}) has both ${other.type.name} and ${tile.type.name}`);
} }
else { else {
other = tile; other = tile;
} }
if (tile.type.is_monster) {
monster_coords.push(x, y);
}
} }
let actor_byte = null; let actor_byte = null;
@ -584,9 +601,14 @@ export function synthesize_level(stored_level) {
if (hint_text !== null) { if (hint_text !== null) {
add_block(7, util.bytestring_to_buffer(hint_text.substring(0, 127) + "\0")); add_block(7, util.bytestring_to_buffer(hint_text.substring(0, 127) + "\0"));
} }
// Monster positions // Monster positions (dumb as hell and only used in MS mode)
// TODO this is dumb as hell but do it too if (monster_coords.length > 0) {
add_block(10, new ArrayBuffer); if (monster_coords.length > 256) {
errors.push(`Level has ${monster_coords.length >> 1} monsters, but MS only supports up to 128`);
monster_coords.length = 256;
}
add_block(10, new Uint8Array(monster_coords).buffer);
}
if (errors.length > 0) { if (errors.length > 0) {
throw new CCLEncodingErrors(errors); throw new CCLEncodingErrors(errors);

View File

@ -637,14 +637,12 @@ export class Level extends LevelInterface {
// Check for custom wiring, for MSCC .DAT levels // Check for custom wiring, for MSCC .DAT levels
// TODO would be neat if this applied to orange buttons too // 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.has_custom_connections) {
let n = this.stored_level.coords_to_scalar(x, y); let n = this.stored_level.coords_to_scalar(x, y);
let target_cell_n = null; let target_cell_n = null;
if (connectable.type.name === 'button_brown') { if (connectable.type.name === 'button_brown' || connectable.type.name === 'button_red') {
target_cell_n = this.stored_level.custom_trap_wiring[n] ?? null; target_cell_n = this.stored_level.custom_connections[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) { if (target_cell_n && target_cell_n < this.width * this.height) {
let [tx, ty] = this.stored_level.scalar_to_coords(target_cell_n); let [tx, ty] = this.stored_level.scalar_to_coords(target_cell_n);

View File

@ -1313,6 +1313,103 @@ class TrackOperation extends MouseOperation {
} }
} }
class SVGConnection {
constructor(sx, sy, dx, dy) {
this.source = mk_svg('rect.-source', {width: 1, height: 1});
this.line = mk_svg('line.-arrow', {});
this.element = mk_svg('g.overlay-connection', this.source, this.line);
this.set_source(sx, sy);
this.set_dest(dx, dy);
}
set_source(sx, sy) {
this.sx = sx;
this.sy = sy;
this.source.setAttribute('x', sx);
this.source.setAttribute('y', sy);
this.line.setAttribute('x1', sx + 0.5);
this.line.setAttribute('y1', sy + 0.5);
}
set_dest(dx, dy) {
this.dx = dx;
this.dy = dy;
this.line.setAttribute('x2', dx + 0.5);
this.line.setAttribute('y2', dy + 0.5);
}
}
class ConnectOperation extends MouseOperation {
handle_press(x, y, ev) {
// 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];
let other = null;
let swap = false;
if (terrain.type.name === 'button_red') {
other = this.search_for(src, 'cloner', 1);
}
else if (terrain.type.name === 'cloner') {
other = this.search_for(src, 'button_red', -1);
swap = true;
}
else if (terrain.type.name === 'button_brown') {
other = this.search_for(src, 'trap', 1);
}
else if (terrain.type.name === 'trap') {
other = this.search_for(src, 'button_brown', -1);
swap = true;
}
if (other !== null) {
if (swap) {
this.editor.set_custom_connection(other, src);
}
else {
this.editor.set_custom_connection(src, other);
}
this.editor.commit_undo();
}
return;
}
this.pending_cxn = new SVGConnection(x, y, x, y);
this.editor.svg_overlay.append(this.pending_cxn.element);
}
// FIXME this is hella the sort of thing that should be on Editor, or in algorithms
search_for(i0, name, dir) {
let l = this.editor.stored_level.linear_cells.length;
let i = i0;
while (true) {
i += dir;
if (i < 0) {
i += l;
}
else if (i >= l) {
i -= l;
}
if (i === i0)
return null;
let cell = this.editor.stored_level.linear_cells[i];
let tile = cell[LAYERS.terrain];
if (tile.type.name === name) {
return i;
}
}
}
handle_drag(client_x, client_y, frac_cell_x, frac_cell_y, cell_x, cell_y) {
}
commit_press() {
}
abort_press() {
this.pending_cxn.element.remove();
}
cleanup_press() {
}
}
class WireOperation extends MouseOperation { class WireOperation extends MouseOperation {
handle_press(x, y) { handle_press(x, y) {
if (this.alt_mode) { if (this.alt_mode) {
@ -1825,13 +1922,16 @@ const EDITOR_TOOLS = {
shortcut: 'a', shortcut: 'a',
}, },
connect: { connect: {
// TODO not implemented
icon: 'icons/tool-connect.png', icon: 'icons/tool-connect.png',
name: "Connect", name: "Connect",
desc: "Set up CC1 clone and trap connections", // XXX shouldn't you be able to drag the destination?
// TODO mod + right click for RRO or diamond alg? ah but we only have ctrl available
desc: "Set up CC1-style clone and trap connections.\n(WIP)\nNOTE: Not supported in CC2!\nRight click: auto link using Lynx rules",
//desc: "Set up CC1-style clone and trap connections.\nNOTE: Not supported in CC2!\nLeft drag: link button with valid target\nCtrl-click: erase link\nRight click: auto link using Lynx rules",
op1: ConnectOperation,
op2: ConnectOperation,
}, },
wire: { wire: {
// TODO not implemented
icon: 'icons/tool-wire.png', icon: 'icons/tool-wire.png',
name: "Wire", name: "Wire",
desc: "Edit CC2 wiring.\nLeft click: draw wires\nCtrl-click: erase wires\nRight click: toggle tunnels (floor only)", desc: "Edit CC2 wiring.\nLeft click: draw wires\nCtrl-click: erase wires\nRight click: toggle tunnels (floor only)",
@ -1851,7 +1951,7 @@ const EDITOR_TOOLS = {
// slade when you have some selected? // slade when you have some selected?
// TODO ah, railroads... // TODO ah, railroads...
}; };
const EDITOR_TOOL_ORDER = ['pencil', 'select_box', 'fill', 'adjust', 'force-floors', 'tracks', 'wire', 'camera']; const EDITOR_TOOL_ORDER = ['pencil', 'select_box', 'fill', 'adjust', 'force-floors', 'tracks', 'connect', 'wire', 'camera'];
const EDITOR_TOOL_SHORTCUTS = {}; const EDITOR_TOOL_SHORTCUTS = {};
for (let [tool, tooldef] of Object.entries(EDITOR_TOOLS)) { for (let [tool, tooldef] of Object.entries(EDITOR_TOOLS)) {
if (tooldef.shortcut) { if (tooldef.shortcut) {
@ -3256,7 +3356,20 @@ export class Editor extends PrimaryView {
// FIXME need this in load_level which is called even if we haven't been setup yet // 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');
// This SVG draws vectors on top of the editor, like monster paths and button 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'}, this.connections_g); this.svg_overlay = mk_svg('svg.level-editor-overlay', {viewBox: '0 0 32 32'},
mk_svg('defs',
mk_svg('marker', {id: 'overlay-arrowhead', markerWidth: 4, markerHeight: 4, refX: 3, refY: 2, orient: 'auto'},
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'}),
mk_svg('feComposite', {'in': 'SourceGraphic'}),
),
this.connections_g,
);
this.viewport_el.append(this.renderer.canvas, this.svg_overlay); this.viewport_el.append(this.renderer.canvas, this.svg_overlay);
// This is done more correctly in setup(), but we need a sensible default so levels can be // This is done more correctly in setup(), but we need a sensible default so levels can be
@ -3436,12 +3549,20 @@ export class Editor extends PrimaryView {
this.mouse_coords = [ev.clientX, ev.clientY]; this.mouse_coords = [ev.clientX, ev.clientY];
// TODO move this into MouseOperation // TODO move this into MouseOperation
let [x, y] = this.renderer.cell_coords_from_event(ev); let [x, y] = this.renderer.cell_coords_from_event(ev);
if (this.is_in_bounds(x, y)) { // TODO only do this stuff if the cell coords changed
let cell = this.cell(x, y);
if (cell) {
this.svg_cursor.classList.add('--visible'); this.svg_cursor.classList.add('--visible');
this.svg_cursor.setAttribute('x', x); this.svg_cursor.setAttribute('x', x);
this.svg_cursor.setAttribute('y', y); this.svg_cursor.setAttribute('y', y);
this.statusbar_cursor.textContent = `(${x}, ${y})`; this.statusbar_cursor.textContent = `(${x}, ${y})`;
// TODO don't /always/ do this. maybe make it optionally always visible, and have
// an inspection tool that does it on point
let terrain = cell[LAYERS.terrain];
if (terrain.type.name === 'button_gray') {
}
} }
else { else {
this.svg_cursor.classList.remove('--visible'); this.svg_cursor.classList.remove('--visible');
@ -3633,27 +3754,12 @@ export class Editor extends PrimaryView {
url.searchParams.append('level', data); url.searchParams.append('level', data);
new EditorShareOverlay(this.conductor, url.toString()).open(); new EditorShareOverlay(this.conductor, url.toString()).open();
}], }],
["Download level as C2M", () => { ["Download level as C2M (new CC2 format)", () => {
// TODO support getting warnings + errors out of synthesis // TODO support getting warnings + errors out of synthesis
let buf = c2g.synthesize_level(this.stored_level); let buf = c2g.synthesize_level(this.stored_level);
util.trigger_local_download((this.stored_level.title || 'untitled') + '.c2m', new Blob([buf])); util.trigger_local_download((this.stored_level.title || 'untitled') + '.c2m', new Blob([buf]));
}], }],
["Download level as MSCC DAT/CCL", () => { ["Download pack as C2G (new CC2 format)", () => {
// TODO support getting warnings out of synthesis?
let buf;
try {
buf = dat.synthesize_level(this.stored_level);
}
catch (errs) {
if (errs instanceof dat.CCLEncodingErrors) {
new EditorExportFailedOverlay(this.conductor, errs.errors).open();
return;
}
throw errs;
}
util.trigger_local_download((this.stored_level.title || 'untitled') + '.ccl', new Blob([buf]));
}],
["Download pack as C2G", () => {
let stored_pack = this.conductor.stored_game; let stored_pack = this.conductor.stored_game;
// This is pretty heckin' best-effort for now; TODO move into format-c2g? // This is pretty heckin' best-effort for now; TODO move into format-c2g?
@ -3699,6 +3805,21 @@ export class Editor extends PrimaryView {
// TODO support getting warnings + errors out of synthesis // TODO support getting warnings + errors out of synthesis
util.trigger_local_download((stored_pack.title || 'untitled') + '.zip', new Blob([u8array])); util.trigger_local_download((stored_pack.title || 'untitled') + '.zip', new Blob([u8array]));
}], }],
["Download level as CCL (old CC1 format)", () => {
// TODO support getting warnings out of synthesis?
let buf;
try {
buf = dat.synthesize_level(this.stored_level);
}
catch (errs) {
if (errs instanceof dat.CCLEncodingErrors) {
new EditorExportFailedOverlay(this.conductor, errs.errors).open();
return;
}
throw errs;
}
util.trigger_local_download((this.stored_level.title || 'untitled') + '.ccl', new Blob([buf]));
}],
]; ];
this.export_menu = new MenuOverlay( this.export_menu = new MenuOverlay(
this.conductor, this.conductor,
@ -4186,15 +4307,17 @@ export class Editor extends PrimaryView {
} }
// Load connections // Load connections
// TODO cloners too // 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_g.textContent = '';
for (let [src, dest] of Object.entries(this.stored_level.custom_trap_wiring)) { this.connections_elements = {};
for (let [src, dest] of Object.entries(this.stored_level.custom_connections)) {
let [sx, sy] = this.stored_level.scalar_to_coords(src); let [sx, sy] = this.stored_level.scalar_to_coords(src);
let [dx, dy] = this.stored_level.scalar_to_coords(dest); let [dx, dy] = this.stored_level.scalar_to_coords(dest);
this.connections_g.append( let el = new SVGConnection(sx, sy, dx, dy).element;
mk_svg('rect.overlay-cxn', {x: sx, y: sy, width: 1, height: 1}), this.connections_elements[src] = el;
mk_svg('line.overlay-cxn', {x1: sx + 0.5, y1: sy + 0.5, x2: dx + 0.5, y2: dy + 0.5}), this.connections_g.append(el);
);
} }
// TODO why are these in connections_g lol // TODO why are these in connections_g lol
for (let [i, region] of this.stored_level.camera_regions.entries()) { for (let [i, region] of this.stored_level.camera_regions.entries()) {
@ -4505,16 +4628,11 @@ export class Editor extends PrimaryView {
// Utility/inspection // Utility/inspection
is_in_bounds(x, y) { is_in_bounds(x, y) {
return 0 <= x && x < this.stored_level.size_x && 0 <= y && y < this.stored_level.size_y; return this.stored_level.is_point_within_bounds(x, y);
} }
cell(x, y) { cell(x, y) {
if (this.is_in_bounds(x, y)) { return this.stored_level.cell(x, y);
return this.stored_level.linear_cells[this.stored_level.coords_to_scalar(x, y)];
}
else {
return null;
}
} }
// ------------------------------------------------------------------------------------------------ // ------------------------------------------------------------------------------------------------
@ -4655,6 +4773,35 @@ export class Editor extends PrimaryView {
this.commit_undo(); this.commit_undo();
} }
// 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];
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];
}
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);
}
}
// ------------------------------------------------------------------------------------------------ // ------------------------------------------------------------------------------------------------
// Undo/redo // Undo/redo

View File

@ -48,6 +48,22 @@ export function mk_svg(tag_selector, ...children) {
return _mk(el, children); return _mk(el, children);
} }
export function trigger_local_download(filename, blob) {
let url = URL.createObjectURL(blob);
// To download a file, um, make an <a> and click it. Not kidding
let a = mk('a', {
href: url,
download: filename,
});
document.body.append(a);
a.click();
// Absolutely no idea when I'm allowed to revoke this, but surely a minute is safe
window.setTimeout(() => {
a.remove();
URL.revokeObjectURL(url);
}, 60 * 1000);
}
export function handle_drop(element, options) { export function handle_drop(element, options) {
let dropzone_class = options.dropzone_class ?? null; let dropzone_class = options.dropzone_class ?? null;
let on_drop = options.on_drop; let on_drop = options.on_drop;

View File

@ -1839,7 +1839,7 @@ body.--debug #player-debug {
--scale: 1; --scale: 1;
} }
/* SVG overlays */ /* SVG overlays */
#editor svg.level-editor-overlay { svg.level-editor-overlay {
position: absolute; position: absolute;
top: 0; top: 0;
bottom: 0; bottom: 0;
@ -1852,21 +1852,21 @@ body.--debug #player-debug {
stroke-width: 0.0625; stroke-width: 0.0625;
fill: none; fill: none;
} }
#editor .level-editor-overlay .overlay-transient { svg.level-editor-overlay .overlay-transient {
display: none; display: none;
} }
#editor .level-editor-overlay .overlay-transient.--visible { svg.level-editor-overlay .overlay-transient.--visible {
display: initial; display: initial;
} }
#editor .level-editor-overlay rect.overlay-cursor { svg.level-editor-overlay rect.overlay-cursor {
x-stroke: hsla(225, 100%, 60%, 0.5); x-stroke: hsla(225, 100%, 60%, 0.5);
fill: hsla(225, 100%, 75%, 0.25); fill: hsla(225, 100%, 75%, 0.25);
} }
#editor .level-editor-overlay rect.overlay-pending-selection { svg.level-editor-overlay rect.overlay-pending-selection {
stroke: hsla(225, 100%, 60%, 0.5); stroke: hsla(225, 100%, 60%, 0.5);
fill: hsla(225, 100%, 75%, 0.25); fill: hsla(225, 100%, 75%, 0.25);
} }
#editor .level-editor-overlay rect.overlay-selection { svg.level-editor-overlay rect.overlay-selection {
stroke: #000c; stroke: #000c;
fill: hsla(225, 0%, 75%, 0.25); fill: hsla(225, 0%, 75%, 0.25);
stroke-dasharray: 0.125, 0.125; stroke-dasharray: 0.125, 0.125;
@ -1882,22 +1882,26 @@ body.--debug #player-debug {
stroke-dashoffset: 0; stroke-dashoffset: 0;
} }
} }
#editor .level-editor-overlay rect.overlay-cxn { #overlay-arrowhead {
stroke: red; fill: hsl(345, 75%, 75%);
} }
#editor .level-editor-overlay line.overlay-cxn { svg.level-editor-overlay g.overlay-connection {
stroke: red; stroke: hsl(345, 75%, 75%);
filter: url(#overlay-filter-outline);
} }
#editor .level-editor-overlay rect.overlay-camera { svg.level-editor-overlay g.overlay-connection line.-arrow {
marker-end: url(#overlay-arrowhead);
}
svg.level-editor-overlay rect.overlay-camera {
stroke: #808080; stroke: #808080;
fill: #80808040; fill: #80808040;
pointer-events: auto; pointer-events: auto;
} }
#editor .level-editor-overlay text { svg.level-editor-overlay text {
/* Each cell is one "pixel", so text needs to be real small */ /* Each cell is one "pixel", so text needs to be real small */
font-size: 1px; font-size: 1px;
} }
#editor .level-editor-overlay text.overlay-edit-tip { svg.level-editor-overlay text.overlay-edit-tip {
stroke: none; stroke: none;
fill: black; fill: black;
} }