From 3a9e7c1cd8ea2c33ac562bed7dded3bc1f54f3be Mon Sep 17 00:00:00 2001 From: "Eevee (Evelyn Woods)" Date: Mon, 22 Apr 2024 00:24:07 -0600 Subject: [PATCH] Split the adjust tool into rotate/adjust It was trying to do too many things. Also, the adjust tool is now free to operate on actors, and can toggle the form of a number of them. - Rearranged the palette to put colored tiles in canonical key order, finally - Expanded the size of the SVG overlay slightly so hover effects don't get cut off at the level border - Fixed some MouseOperation nonsense by simply using the same object when the same operation is bound to both mouse buttons - Added a verb and preview to the adjust tool, in the hopes of making it slightly more clear what it might do - Enhanced the adjust tool to place individual thin walls and frame arrows --- icons/editor-icons.aseprite | Bin 2921 -> 3276 bytes icons/rotate-left.png | Bin 235 -> 478 bytes icons/rotate-right.png | Bin 235 -> 478 bytes icons/tool-adjust.png | Bin 440 -> 506 bytes icons/tool-rotate.png | Bin 0 -> 439 bytes icons/tool-text.png | Bin 0 -> 452 bytes js/editor/editordefs.js | 32 +- js/editor/main.js | 77 +++-- js/editor/mouseops.js | 563 +++++++++++++++++++++++++++++------- js/tiletypes.js | 1 + style.css | 43 ++- 11 files changed, 571 insertions(+), 145 deletions(-) create mode 100644 icons/tool-rotate.png create mode 100644 icons/tool-text.png diff --git a/icons/editor-icons.aseprite b/icons/editor-icons.aseprite index b783bbecebd0e728ebb1b5db155cb85a9ff7f36d..26f5d89dd68314d1585967bf8a2148e0d455fa0d 100644 GIT binary patch delta 561 zcmaDUc1Duv49`TSI$j?h28LfB)frM47#P$y_G&Y;l%!UaOkTj;&s&sVl30=|2b5q@ zVBRdp@`bU!7RX^$U}a!oU}N~t1mu8$1cNX`#hj@Zz4;C(aJUAFr9PUw_wV~vthr03 zndlZav}@Fz>3Xp@==IvEt!EogO+3Q?{m|Kuy|r`XY}$UZrZ&5My^@f_F=>`QCE-wtw~Y zIAsz0=5qFgb1MTDh_om_l}igX-YvbnV>Y|kL<5^-K_xC@7K68frF_oo>e zbzU}jrt_=X)+O$9{l{a9_bN;R>$v0`x2^y?mS=J<$8tvA$)cQ78Tlr!=B#JrpRCR0 zz$h>|hfA)We`(3(C-?t{xoU+@35}d7-qRd)O2aE`iO<;?vkX>Q&(3`#Cb4pPQF;Y$ z$8T%SBPl&|Wiquo`%qtDOq>kG=FI?r~96?bh9jHylXz1hRL-8p-%?GB&Z l&w;ibFaJrcnHzuZskcqTO|sf+JFE6 delta 245 zcmX>j`BIE2lY1gl9q&sn28LfBl^IeP7#Nf{_G&XvzQSC-Ig{lvBfBq9h*g1g@&z^@ zMz+ba>|TuQlQYc#?1hUq%uK=s#ntXwCIV1Pv3a+V)Jd^pj>lt|` z_i;Ng@=ZR^EmxnPy2SV2-~7-eL9UTgr&b=cSh2KcQHtxSD?KYY7w_79^H^Tv#44@w zw(AW$_D^>3Id{9_a`y^UO4~& diff --git a/icons/rotate-left.png b/icons/rotate-left.png index 15313213a0a85628e9193286244e9ae046d03fce..cd67eb3612dbed9df37ce695534de432b6046766 100644 GIT binary patch literal 478 zcmeAS@N?(olHy`uVBq!ia0vp^5+KaM3?#3wJbMaAF%}28J29*~C-V}>IS}9z;`*N< z^XSk26JP%iIPq0)?R&+>$3mw4!cw_LTHA~4o^C4s|MKwvmka(s%Km=J^Y}cS3F}n@ z-$qXO)v)>B`jh{!-TD7?%fBUgKjPD$&MdxE7 zRbc&f&gcUU}oXkrghd;n4#1%+Osaohd274CPp{lE!wx(w22TZ26gRy8eEbTV$AWs$z#$`I52fN}PMy!<}Z86n4OWLRF!dn9x-TdvglX#U@oavU?* z4sr_A>1^8aLnv-aM&I7}v?B%cW@j4KHu!V1&QtO+^*414-M?DECp7f2{EA-7lq{7} RPM}K|JYD@<);T3K0RRNaO;G>< diff --git a/icons/rotate-right.png b/icons/rotate-right.png index 1c54b80bec0e11fcc1a8af37e1e464c8bb3fb93b..c04830959cd0a722870371378bff76aaae21fe88 100644 GIT binary patch literal 478 zcmeAS@N?(olHy`uVBq!ia0vp^5+KaM3?#3wJbMaAF%}28J29*~C-V}>IS}9z;`*N< z^XSk26JP%iIPq0)?R&+>$3mw4!cw_LTHA~4o^C4s|MKwvmka(s%Km=J^Y}cS3F}n@ z-$qXO)v)>B`jh{!-TD7?%fBUgKjPD$&MdxE7 zRbc&f&gcUrf zuVY!k%@}@lI?qp)kl2IEo^M`mJMVJBp_RMWCtRQ1=rQ41yQqhk%0!hn3+C1_3RfsH zluVHOepT|}y`~3jzi##DAN-aygXMNw?o7D@+rEEe3@^MheaVvNmL;B^n@;f0Uv1@n Tu*6OX=oSV~S3j3^P6U}oXkrghd;n4#1%+Osaohd274CPp{lS!wx*G22X_}!kBi;)cf!@-&)V`$dfTr z;Q$-sCWQ&9&J(gvI~By<;MmDOq(PKe#xyd$n(g9X}`-yN~w2_;%}e$`O;_78!=}1%BM6%}PFZeM}v%R<9E92@QQL?^SA%^8fIb RCqS1lc)I$ztaD0e0svxyPdESo diff --git a/icons/tool-adjust.png b/icons/tool-adjust.png index df3bdacc6389d6567382c163d255f8269a71bee1..75ef15d0c26d9d13eb1fc7d805b179a9b2be61f2 100644 GIT binary patch delta 298 zcmdnN{EK;lA>)CGMr-Q1Il0XB%)RV9QX-PtYg)Fi+kF4-z5oCJw<;XWU|?WyC<*cl zW&rXj0m9nz41s3N@pN$vu{eG9vMpbO0S8-vxcSt>^(6|7N$=~m8`YwMFC2Aj%lfqU zg8W;PzpF32(o7SZI3?rFQkCWadqoqb|uPeraFpKGjF7B21e8 v@f`i>{&dm&cibQ4IAR{$mi=+I)`lT)g5%cs&x)^rE@kj^^>bP0l+XkK6Pl*t delta 231 zcmV zS^!Gj01tv`C)M#o+x8j*QdMm>aGL?Bs;Z*-_@LeJYyofpZ7$QH`v6;re=fcQAn*b3 z4vGcs19+H=2$pesPphh=8Pu}v-bYy_JzFf>M;0YXDS&lHP?A!anXl}3fT<@JM^}L0 hwR5-ox^3-G=?@P|G~5-$Sa<*c002ovPDHLkV1gZzWUl}K diff --git a/icons/tool-rotate.png b/icons/tool-rotate.png new file mode 100644 index 0000000000000000000000000000000000000000..38627c2c97a8cb74e63466dc7040b3f6304a45df GIT binary patch literal 439 zcmV;o0Z9IdP)Px#s8CE)MgIV4#rgk{>;F*5^)9XNA%Wu(HIEY-Z89piaz5phMgR7D=>Lej`ALE6CRoN56qE@EXCnbVA57ebC%<Mzf@B#1_ z6b0=Ac$-^mQO4~(t<02WQDxh`uQF4*MwIO%i{)3yNJHX;| hbL-S~s%Pmh=mif-G~B=qCy@XE002ovPDHLkV1j4G!hQe% literal 0 HcmV?d00001 diff --git a/icons/tool-text.png b/icons/tool-text.png new file mode 100644 index 0000000000000000000000000000000000000000..da5c201febd39b12ae31d0c453c937160a5010f5 GIT binary patch literal 452 zcmeAS@N?(olHy`uVBq!ia0vp^3LwnE3?yBabR7dyjKx9jP7LeL$-D$|4g~mwxc+Cz zJo@wh#Ml1=PJGo{`(Cl}v5;xMuvD&**7hR1r<;oZzdZc^<%0i@vcI45JU&ln!g|%f zw~-TmHEjO3{^b8_cm6-!@^4ArkNEVbGm9@3#vKaqSZQO^XJC`BWZ^5UWx=ic#>nA+ zt;7E<&j0Va{{L3};(y2PpPr4c)gq3GicH~TPgiENllQsPuD-uqU|tPNTIR(6`vaa| z6xEsxFdeC`Fbk-^i|&t;ucLK6Vei|J4R literal 0 HcmV?d00001 diff --git a/js/editor/editordefs.js b/js/editor/editordefs.js index 1f43f5c..73b5106 100644 --- a/js/editor/editordefs.js +++ b/js/editor/editordefs.js @@ -59,10 +59,18 @@ export const TOOLS = { op1: mouseops.TrackOperation, op2: mouseops.TrackOperation, }, + rotate: { + icon: 'icons/tool-rotate.png', + name: "Rotate", + desc: "Rotate existing tiles.\nAffects the top-most tile by default.\n\n[mouse1] Rotate clockwise\n[mouse2] Rotate counter-clockwise\n[ctrl] Target terrain\n[shift] Target actor", // TODO? \n[ctrl] [shift] Affect actor without rotating + op1: mouseops.RotateOperation, + op2: mouseops.RotateOperation, + shortcut: 'r', + }, adjust: { icon: 'icons/tool-adjust.png', name: "Adjust", - desc: "Inspect and edit existing tiles in a variety of ways. Give it a try!\n\n[mouse1] Rotate actor\n[mouse1] Rotate or change terrain\n[mouse1] Press button\n[mouse2] Rotate/toggle in the other direction\n[shift] Always target terrain\n\n[ctrl] [mouse1] Edit properties of complex tiles\n(wires, railroads, hints, etc.)", + desc: "Inspect and alter miscellaneous tiles in a variety of ways.\nGive it a try! Affects the top-most tile by default.\n\n[mouse1] Toggle tile type\n[mouse1] Press button\n[mouse2] Edit properties of complex tiles\n(wires, railroads, hints, etc.)\n[ctrl] Target terrain\n[shift] Target actor\n[ctrl] [shift] Target item", op1: mouseops.AdjustOperation, op2: mouseops.AdjustOperation, shortcut: 'a', @@ -102,7 +110,7 @@ export const TOOLS = { // slade when you have some selected? // TODO ah, railroads... }; -export const TOOL_ORDER = ['pencil', 'select_box', 'fill', 'adjust', 'force-floors', 'tracks', 'connect', 'wire', 'camera']; +export const TOOL_ORDER = ['pencil', 'select_box', 'fill', 'rotate', 'adjust', 'force-floors', 'tracks', 'connect', 'wire', 'camera']; export const TOOL_SHORTCUTS = {}; for (let [tool, tooldef] of Object.entries(TOOLS)) { if (tooldef.shortcut) { @@ -139,10 +147,10 @@ export const PALETTE = [{ 'no_player1_sign', 'no_player2_sign', - 'floor_custom_green', 'floor_custom_pink', 'floor_custom_yellow', 'floor_custom_blue', - 'wall_custom_green', 'wall_custom_pink', 'wall_custom_yellow', 'wall_custom_blue', + 'floor_custom_pink', 'floor_custom_blue', 'floor_custom_yellow', 'floor_custom_green', + 'wall_custom_pink', 'wall_custom_blue', 'wall_custom_yellow', 'wall_custom_green', - 'door_blue', 'door_red', 'door_yellow', 'door_green', + 'door_red', 'door_blue', 'door_yellow', 'door_green', 'swivel_nw', 'railroad/straight', 'railroad/curve', @@ -156,8 +164,8 @@ export const PALETTE = [{ }, { title: "Items", tiles: [ - 'key_blue', 'key_red', 'key_yellow', 'key_green', - 'flippers', 'fire_boots', 'cleats', 'suction_boots', + 'key_red', 'key_blue', 'key_yellow', 'key_green', + 'cleats', 'suction_boots', 'fire_boots', 'flippers', 'hiking_boots', 'speed_boots', 'lightning_bolt', 'railroad_sign', 'helmet', 'foil', 'hook', 'xray_eye', 'bribe', 'bowling_ball', 'dynamite', 'no_sign', @@ -210,10 +218,10 @@ export const PALETTE = [{ 'button_orange', 'flame_jet_off', 'flame_jet_on', 'transmogrifier', - 'teleport_blue', 'teleport_red', - 'teleport_green', + 'teleport_blue', 'teleport_yellow', + 'teleport_green', 'stopwatch_bonus', 'stopwatch_penalty', 'stopwatch_toggle', @@ -248,17 +256,17 @@ export const PALETTE = [{ tiles: [ 'sokoban_block/red', 'sokoban_block/blue', - 'sokoban_block/green', 'sokoban_block/yellow', + 'sokoban_block/green', 'sokoban_button/red', 'sokoban_button/blue', - 'sokoban_button/green', 'sokoban_button/yellow', + 'sokoban_button/green', 'sokoban_wall/red', 'sokoban_wall/blue', - 'sokoban_wall/green', 'sokoban_wall/yellow', + 'sokoban_wall/green', 'gate_red', 'gate_blue', 'gate_yellow', diff --git a/js/editor/main.js b/js/editor/main.js index 1002477..9028229 100644 --- a/js/editor/main.js +++ b/js/editor/main.js @@ -80,11 +80,12 @@ export class Editor extends PrimaryView { this.renderer = new CanvasRenderer(this.conductor.tilesets['ll'], 32); this.renderer.perception = 'editor'; this.renderer.show_facing = true; + this.renderer.canvas.classList.add('editor-renderer-canvas'); // FIXME need this in load_level which is called even if we haven't been setup yet 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'}, + this.svg_overlay = mk_svg('svg.level-editor-overlay', {viewBox: '-1 -1 34 34'}, 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'}), @@ -306,6 +307,8 @@ export class Editor extends PrimaryView { ev.stopPropagation(); ev.preventDefault(); + // TODO Alt: Scroll through palette + let index = ZOOM_LEVELS.findIndex(el => el >= this.zoom); if (index < 0) { index = ZOOM_LEVELS.length - 1; @@ -1082,6 +1085,9 @@ export class Editor extends PrimaryView { // Load *implicit* connections this.recreate_implicit_connections(); + // Trace out circuitry + this.update_circuits(); + this.renderer.set_level(stored_level); if (this.active) { this.redraw_entire_level(); @@ -1103,7 +1109,9 @@ export class Editor extends PrimaryView { update_viewport_size() { this.renderer.set_viewport_size(this.stored_level.size_x, this.stored_level.size_y); - this.svg_overlay.setAttribute('viewBox', `0 0 ${this.stored_level.size_x} ${this.stored_level.size_y}`); + this.svg_overlay.setAttribute('viewBox', `-1 -1 ${this.stored_level.size_x + 2} ${this.stored_level.size_y + 2}`); + this.svg_overlay.style.setProperty('--tile-width', `${this.renderer.tileset.size_x}px`); + this.svg_overlay.style.setProperty('--tile-height', `${this.renderer.tileset.size_y}px`); } update_after_size_change() { @@ -1188,26 +1196,38 @@ export class Editor extends PrimaryView { this.tool_button_els[this.current_tool].classList.add('-selected'); // Left button: activate tool - this._init_mouse_op(0, this.current_tool && TOOLS[this.current_tool].op1); // Right button: activate tool's alt mode - this._init_mouse_op(2, this.current_tool && TOOLS[this.current_tool].op2); + let op_type1 = this.current_tool && TOOLS[this.current_tool].op1; + let op_type2 = this.current_tool && TOOLS[this.current_tool].op2; + // Destroy the old operations. Be careful since they might be the same object + if (this.mouse_ops[0]) { + this.mouse_ops[0].do_destroy(); + } + if (this.mouse_ops[2] && this.mouse_ops[2] !== this.mouse_ops[0]) { + this.mouse_ops[2].do_destroy(); + } + // Create new ones + if (op_type1) { + this.mouse_ops[0] = new op_type1(this); + } + else { + this.mouse_ops[0] = null; + } + if (op_type2) { + if (op_type1 === op_type2) { + // Use the same operation for both buttons, to simplify handling of hovering + this.mouse_ops[2] = this.mouse_ops[0]; + } + else { + this.mouse_ops[2] = new op_type2(this); + } + } + else { + this.mouse_ops[2] = null; + } this.set_mouse_button(0); } - _init_mouse_op(button, op_type) { - if (this.mouse_ops[button] && op_type && this.mouse_ops[button] instanceof op_type) - // Don't recreate the same type of mouse operation - return; - - if (this.mouse_ops[button]) { - this.mouse_ops[button].do_destroy(); - this.mouse_ops[button] = null; - } - - if (op_type) { - this.mouse_ops[button] = new op_type(this, button); - } - } set_mouse_button(button) { this.mouse_op = this.mouse_ops[button]; @@ -1396,7 +1416,7 @@ export class Editor extends PrimaryView { ctx.clearRect(0, 0, this.fg_tile_el.width, this.fg_tile_el.height); this.renderer.draw_single_tile_type( this.fg_tile.type.name, this.fg_tile, this.fg_tile_el); - for (let mouse_op of this.mouse_ops) { + for (let mouse_op of new Set(this.mouse_ops)) { if (mouse_op) { mouse_op.handle_tile_updated(); } @@ -1408,7 +1428,7 @@ export class Editor extends PrimaryView { ctx.clearRect(0, 0, this.bg_tile_el.width, this.bg_tile_el.height); this.renderer.draw_single_tile_type( this.bg_tile.type.name, this.bg_tile, this.bg_tile_el); - for (let mouse_op of this.mouse_ops) { + for (let mouse_op of new Set(this.mouse_ops)) { if (mouse_op) { mouse_op.handle_tile_updated(true); } @@ -1871,6 +1891,7 @@ export class Editor extends PrimaryView { } } + // TODO handle old_tile or new_tile being null (won't connect anyway) // 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 @@ -1882,6 +1903,7 @@ export class Editor extends PrimaryView { // - 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 + // TODO all this stuff needs to apply to transforms as well, oopsie _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))) @@ -1889,7 +1911,7 @@ export class Editor extends PrimaryView { // Nothing to do return; } - if (old_tile.type.name === new_tile.type.name) + if (old_tile && new_tile && old_tile.type.name === new_tile.type.name) return; // TODO actually this should also update explicit ones, if the source/dest types are changed @@ -1898,11 +1920,11 @@ export class Editor extends PrimaryView { let n = this.cell_to_scalar(cell); // Remove an old outgoing connection - if (old_tile.type.connects_to) { + if (old_tile && old_tile.type.connects_to) { this.__delete_implicit_connection(n); } // Remove an old incoming connection - if (old_tile.type.connects_from) { + if (old_tile && 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 @@ -1917,11 +1939,11 @@ export class Editor extends PrimaryView { } // Add a new outgoing connection - if (new_tile.type.connects_to) { + if (new_tile && 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) { + if (new_tile && 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 @@ -1944,7 +1966,7 @@ export class Editor extends PrimaryView { // 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(); + let terrain = source_cell[LAYERS.terrain]; if (terrain.type !== source_type) continue; @@ -1990,6 +2012,9 @@ export class Editor extends PrimaryView { } } + update_circuits() { + } + // ------------------------------------------------------------------------------------------------ // Undo/redo diff --git a/js/editor/mouseops.js b/js/editor/mouseops.js index 4d6ba3c..c3c5dd6 100644 --- a/js/editor/mouseops.js +++ b/js/editor/mouseops.js @@ -5,6 +5,7 @@ import { DIRECTIONS, LAYERS } from '../defs.js'; import TILE_TYPES from '../tiletypes.js'; import { mk, mk_svg, walk_grid } from '../util.js'; +import { SPECIAL_TILE_BEHAVIOR } from './editordefs.js'; import { SVGConnection } from './helpers.js'; import { TILES_WITH_PROPS } from './tile-overlays.js'; @@ -20,11 +21,10 @@ import { TILES_WITH_PROPS } from './tile-overlays.js'; // - 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) { + constructor(editor) { this.editor = editor; - this.is_held = false; - this.physical_button = physical_button; - this.alt_mode = physical_button !== 0; + this.held_button = null; + this.alt_mode = false; this.ctrl = false; this.shift = false; @@ -81,8 +81,30 @@ export class MouseOperation { return this.editor.cell(Math.floor(x), Math.floor(y)); } + get_tile_edge() { + let frac_x = this.prev_frac_cell_x - this.prev_cell_x; + let frac_y = this.prev_frac_cell_y - this.prev_cell_y; + if (frac_x >= frac_y) { + if (frac_x >= 1 - frac_y) { + return 'east'; + } + else { + return 'north'; + } + } + else { + if (frac_x <= 1 - frac_y) { + return 'west'; + } + else { + return 'south'; + } + } + } + do_press(ev) { - this.is_held = true; + this.held_button = ev.button; + this.alt_mode = (ev.button === 2); this._update_modifiers(ev); this.client_x = ev.clientX; @@ -107,7 +129,7 @@ export class MouseOperation { let cell_x = Math.floor(frac_cell_x); let cell_y = Math.floor(frac_cell_y); - if (this.is_held && (ev.buttons & MOUSE_BUTTON_MASKS[this.physical_button]) === 0) { + if (this.held_button !== null && (ev.buttons & MOUSE_BUTTON_MASKS[this.held_button]) === 0) { this.do_abort(); } @@ -115,7 +137,7 @@ export class MouseOperation { this.cursor_element.setAttribute('transform', `translate(${cell_x} ${cell_y})`); } - if (this.is_held) { + if (this.held_button !== null) { // Continue a drag even if the mouse goes outside the viewport this.handle_drag(ev.clientX, ev.clientY, frac_cell_x, frac_cell_y, cell_x, cell_y); } @@ -191,21 +213,23 @@ export class MouseOperation { } do_commit() { - if (! this.is_held) + if (this.held_button === null) return; this.commit_press(); this.cleanup_press(); - this.is_held = false; + this.alt_mode = false; + this.held_button = null; } do_abort() { - if (! this.is_held) + if (this.held_button === null) return; this.abort_press(); this.cleanup_press(); - this.is_held = false; + this.alt_mode = false; + this.held_button = null; } do_destroy() { @@ -568,6 +592,7 @@ export class FillOperation extends MouseOperation { // TODO also, delete? there's no delete?? // FIXME don't show the overlay text until has_moved // TODO cursor: 'cell' by default...? +// FIXME possible to start dragging from outside the level bounds, augh export class SelectOperation extends MouseOperation { handle_press() { if (this.shift) { @@ -631,7 +656,11 @@ export class SelectOperation extends MouseOperation { } update_pending_selection() { - this.pending_selection.set_extrema(this.click_cell_x, this.click_cell_y, this.prev_cell_x, this.prev_cell_y); + this.pending_selection.set_extrema( + Math.max(0, Math.min(this.editor.stored_level.size_x - 1, this.click_cell_x)), + Math.max(0, Math.min(this.editor.stored_level.size_y - 1, this.click_cell_y)), + Math.max(0, Math.min(this.editor.stored_level.size_x - 1, this.prev_cell_x)), + Math.max(0, Math.min(this.editor.stored_level.size_y - 1, this.prev_cell_y))); } commit_press() { @@ -1164,40 +1193,189 @@ export class WireOperation extends MouseOperation { } } +// TODO hmm there's no way to rotate the wires on a circuit block without rotating the block itself +// TODO this highlights blocks even though they don't usually show their direction... +// maybe put a pencil-like preview tile on here that highlights the tile being targeted, and also +// forces showing the arrow on blocks? +export class RotateOperation extends MouseOperation { + constructor(...args) { + super(...args); + this.hovered_layer = null; + + this.set_cursor_element(mk_svg('circle.overlay-transient.overlay-adjust-cursor', { + cx: 0.5, + cy: 0.5, + r: 0.75, + })); + } + + _find_target_tile(cell) { + let top_layer = LAYERS.MAX - 1; + let bottom_layer = 0; + if (this.ctrl) { + // ctrl: explicitly target terrain + top_layer = LAYERS.terrain; + bottom_layer = LAYERS.terrain; + } + else if (this.shift) { + // shift: explicitly target actor + top_layer = LAYERS.actor; + bottom_layer = LAYERS.actor; + } + for (let layer = top_layer; layer >= bottom_layer; layer--) { + let tile = cell[layer]; + if (! tile) + continue; + + // Detecting if a tile is rotatable is, uhh, a little, complicated + if (tile.type.is_actor) { + return layer; + } + // The counter doesn't actually rotate + if (tile.type.name === 'logic_gate' && tile.gate_type === 'counter') { + continue; + } + let behavior = SPECIAL_TILE_BEHAVIOR[tile.type.name]; + if (behavior && behavior.rotate_left) { + return layer; + } + + if (tile.wire_directions || tile.wire_tunnel_directions) { + return layer; + } + } + + return null; + } + + handle_hover(client_x, client_y, frac_cell_x, frac_cell_y, cell_x, cell_y) { + // TODO hrmm if we undo without moving the mouse then this becomes wrong (even without the + // stuff here) + // TODO uhhh that's true for all kinds of kb shortcuts actually, even for pressing/releasing + // ctrl or shift to change the target. dang + + let cell = this.cell(cell_x, cell_y); + let layer = this._find_target_tile(cell); + this.hovered_layer = layer; + + if (layer === null) { + this.cursor_element.classList.remove('--visible'); + return; + } + + this.cursor_element.classList.add('--visible'); + if (layer === LAYERS.terrain) { + this.cursor_element.setAttribute('data-layer', 'terrain'); + } + else if (layer === LAYERS.item) { + this.cursor_element.setAttribute('data-layer', 'item'); + } + else if (layer === LAYERS.actor) { + this.cursor_element.setAttribute('data-layer', 'actor'); + } + else if (layer === LAYERS.thin_wall) { + this.cursor_element.setAttribute('data-layer', 'thin-wall'); + } + } + + handle_press() { + let cell = this.cell(this.prev_cell_x, this.prev_cell_y); + if (this.hovered_layer === null) + return; + let tile = cell[this.hovered_layer]; + if (! tile) + return; + + let rotated; + tile = {...tile}; // TODO little inefficient + if (this.alt_mode) { + // Reverse, go counterclockwise + rotated = this.editor.rotate_tile_left(tile); + } + else { + rotated = this.editor.rotate_tile_right(tile); + } + if (rotated) { + this.editor.place_in_cell(cell, tile); + this.editor.commit_undo(); + return; + } + } + + // Rotate tool doesn't support dragging + // TODO should it? +} + // Tiles the "adjust" tool will turn into each other -const ADJUST_TOGGLES_CW = {}; -const ADJUST_TOGGLES_CCW = {}; +const ADJUST_TILE_TYPES = {}; +const ADJUST_GATE_TYPES = {}; { - for (let cycle of [ - ['chip', 'chip_extra'], - // TODO shouldn't this convert regular walls into regular floors then? - ['floor_custom_green', 'wall_custom_green'], - ['floor_custom_pink', 'wall_custom_pink'], - ['floor_custom_yellow', 'wall_custom_yellow'], - ['floor_custom_blue', 'wall_custom_blue'], - ['fake_floor', 'fake_wall'], - ['popdown_floor', 'popdown_wall'], - ['wall_invisible', 'wall_appearing'], - ['green_floor', 'green_wall'], - ['green_bomb', 'green_chip'], - ['purple_floor', 'purple_wall'], - ['thief_keys', 'thief_tools'], + // Try to make these intuitive, the kind of things someone would naturally want to alter in a + // very small way. The "other one". + for (let [verb, ...cycle] of [ + ["Swap", 'player', 'player2'], + ["Swap", 'chip', 'chip_extra'], + // TODO shouldn't this convert regular walls into regular floors then? or... steel, if it + // has wires in it...? + // TODO annoying that there are two obvious kinds of change to make here + ["Recolor", 'floor_custom_pink', 'floor_custom_blue', 'floor_custom_yellow', 'floor_custom_green'], + ["Recolor", 'wall_custom_pink', 'wall_custom_blue', 'wall_custom_yellow', 'wall_custom_green'], + ["Recolor", 'door_red', 'door_blue', 'door_yellow', 'door_green'], + ["Recolor", 'key_red', 'key_blue', 'key_yellow', 'key_green'], + ["Recolor", 'teleport_red', 'teleport_blue', 'teleport_yellow', 'teleport_green'], + ["Recolor", 'gate_red', 'gate_blue', 'gate_yellow', 'gate_green'], + ["Toggle", 'green_floor', 'green_wall'], + ["Toggle", 'green_bomb', 'green_chip'], + ["Toggle", 'purple_floor', 'purple_wall'], + ["Swap", 'fake_floor', 'fake_wall'], + ["Swap", 'popdown_floor', 'popdown_wall'], + ["Swap", 'wall_invisible', 'wall_appearing'], + ["Swap", 'thief_keys', 'thief_tools'], + /* ['swivel_nw', 'swivel_ne', 'swivel_se', 'swivel_sw'], ['ice_nw', 'ice_ne', 'ice_se', 'ice_sw'], ['force_floor_n', 'force_floor_e', 'force_floor_s', 'force_floor_w'], - ['ice', 'force_floor_all'], - ['water', 'turtle'], - ['no_player1_sign', 'no_player2_sign'], - ['flame_jet_off', 'flame_jet_on'], - ['light_switch_off', 'light_switch_on'], - ['stopwatch_bonus', 'stopwatch_penalty'], - ['turntable_cw', 'turntable_ccw'], + */ + ["Flip", 'force_floor_n', 'force_floor_s'], + ["Flip", 'force_floor_e', 'force_floor_w'], + ["Swap", 'ice', 'force_floor_all'], + ["Swap", 'water', 'turtle'], + ["Swap", 'no_player1_sign', 'no_player2_sign'], + ["Toggle", 'flame_jet_off', 'flame_jet_on'], + ["Flip", 'light_switch_off', 'light_switch_on'], + ["Swap", 'stopwatch_bonus', 'stopwatch_penalty'], + ["Swap", 'turntable_cw', 'turntable_ccw'], + ["Swap", 'score_10', 'score_100', 'score_1000'], + ["Swap", 'dirt_block', 'ice_block'], + + ["Swap", 'doppelganger1', 'doppelganger2'], + ["Swap", 'ball', 'tank_blue'], + ["Swap", 'fireball', 'glider'], + ["Swap", 'bug', 'paramecium'], + ["Swap", 'walker', 'blob'], + ["Swap", 'teeth', 'teeth_timid'], ]) { - for (let [i, tile] of cycle.entries()) { - let other = cycle[(i + 1) % cycle.length]; - ADJUST_TOGGLES_CW[tile] = other; - ADJUST_TOGGLES_CCW[other] = tile; + for (let [i, type] of cycle.entries()) { + ADJUST_TILE_TYPES[type] = { + verb, + next: cycle[(i + 1) % cycle.length], + prev: cycle[(i - 1 + cycle.length) % cycle.length], + }; + } + } + + for (let cycle of [ + ['not', 'diode'], + ['and', 'or', 'xor', 'nand'], + ['latch-cw', 'latch-ccw'], + ]) + { + for (let [i, type] of cycle.entries()) { + ADJUST_GATE_TYPES[type] = { + next: cycle[(i + 1) % cycle.length], + prev: cycle[(i - 1 + cycle.length) % cycle.length], + }; } } } @@ -1259,6 +1437,7 @@ const ADJUST_SPECIAL = { }, button_gray(editor, tile, cell) { // Toggle gray objects... er... objects affected by gray buttons + // TODO right-click should allow toggling backwards! for (let dy = -2; dy <= 2; dy++) { for (let dx = -2; dx <= 2; dx++) { if (dx === 0 && dy === 0) @@ -1276,57 +1455,223 @@ const ADJUST_SPECIAL = { } }, }; -// TODO maybe better visual feedback of what will happen when you click? -// - rotate terrain (cw, ccw) -// - change terrain -// - rotate actor (cw, ccw) -// - press button +// FIXME the preview is not very good because the hover effect becomes stale, pressing ctrl/shift +// leaves it stale, etc +// FIXME it might be nice to actually preview what we intend to do, which would require just, uh, +// doing it to a temporary tile, but actually that does sound a lot better than all this export class AdjustOperation extends MouseOperation { constructor(...args) { super(...args); this.gray_button_preview = mk_svg('g.overlay-transient', {'data-source': 'AdjustOperation'}); - this.gray_button_preview.append(mk_svg('rect.overlay-adjust-gray-button-radius', { + this.gray_button_bounds_rect = mk_svg('rect.overlay-adjust-gray-button-radius', { x: -2, y: -2, width: 5, height: 5, - })); + }); + this.gray_button_preview.append(this.gray_button_bounds_rect); this.editor.svg_overlay.append(this.gray_button_preview); + // Cool octagon + /* + this.set_cursor_element(mk_svg('path.overlay-transient.overlay-adjust-cursor', { + //d: 'M -0.25,-0.25 L 0.5,-0.5 L 1.25,-0.25 L 1.5,0.5' + + // 'L 1.25,1.25 L 0.5,1.5 L -0.25,1.25 L -0.5,0.5 z', + //d: 'M 0.5,0.5 m 0.75,-0.75 l 0.75,-0.25 l 0.75,0.25 l 0.25,0.75' + + // 'l -0.25,0.75 l -0.75,0.25 l -0.75,-0.25 l -0.25,-0.75 z', + d: 'M 0.5,0.5 m -0.5,-0.5 l 0.5,-0.125 l 0.5,0.125 l 0.125,0.5' + + 'l -0.125,0.5 l -0.5,0.125 l -0.5,-0.125 l -0.125,-0.5 z', + })); + */ + // The cursor is the tile being targeted, drawn with high opacity atop the rest of the cell, + // to hopefully make it clear which layer we're looking at + let renderer = this.editor.renderer; + this.canvas = mk('canvas', { + width: renderer.tileset.size_x, + height: renderer.tileset.size_y, + }); + // Need an extra here so the translate transform doesn't clobber the scale on the + // foreignObject + this.set_cursor_element(mk_svg('g.overlay-transient', + mk_svg('foreignObject', { + x: 0, + y: 0, + width: this.canvas.width, + height: this.canvas.height, + transform: `scale(${1/renderer.tileset.size_x} ${1/renderer.tileset.size_y})`, + opacity: 0.75, + }, this.canvas), + )); + + this.click_hint = mk_svg('text.overlay-adjust-hint.overlay-transient'); + this.editor.svg_overlay.append(this.click_hint); + + this.hovered_layer = null; + } + + _find_target_tile(cell) { + let top_layer = LAYERS.MAX - 1; + let bottom_layer = 0; + if (this.ctrl) { + // ctrl: explicitly target terrain + top_layer = LAYERS.terrain; + bottom_layer = LAYERS.terrain; + } + else if (this.shift) { + // shift: explicitly target actor + top_layer = LAYERS.actor; + bottom_layer = LAYERS.actor; + } + for (let layer = top_layer; layer >= bottom_layer; layer--) { + let tile = cell[layer]; + if (! tile) + continue; + + // This is kind of like documentation for everything the adjust tool can do I guess + if (TILE_TYPES['transmogrifier']._mogrifications[tile.type.name]) { + // Toggle between related tile types + return [layer, "Mogrify"]; + } + if (ADJUST_TILE_TYPES[tile.type.name]) { + // Toggle between related tile types + return [layer, ADJUST_TILE_TYPES[tile.type.name].verb]; + } + if (tile.type.name === 'logic_gate' && ADJUST_GATE_TYPES[tile.gate_type]) { + // Also toggle between related logic gate types + return [layer, "Change"]; + } + if (tile.type.name === 'logic_gate' && tile.gate_type === 'counter') { + // Adjust the starting number on a logic gate + return [layer, "Count"]; + } + if (layer === LAYERS.thin_wall) { + // Place or delete individual thin walls + return [layer, "Place"]; + } + if (tile.type.name === 'frame block') { + // Place or delete individual frame block arrows + return [layer, "Place"]; + } + + // These are + // TODO need a single-click thing to do for + let behavior = SPECIAL_TILE_BEHAVIOR[tile.type.name]; + if (behavior && behavior.adjust_forward) { + // + return [layer, "Adjust"]; + } + + if (TILES_WITH_PROPS[tile.type.name]) { + // Open special tile editors + return [layer, "Edit"]; + } + + if (ADJUST_SPECIAL[tile.type.name]) { + return [layer, "Press"]; + } + } + + return [null, null]; } handle_hover(client_x, client_y, frac_cell_x, frac_cell_y, cell_x, cell_y) { + // TODO hrmm if we undo without moving the mouse then this becomes wrong (even without the + // stuff here) + // TODO uhhh that's true for all kinds of kb shortcuts actually, even for pressing/releasing + // ctrl or shift to change the target. dang + if (cell_x === this.prev_cell_x && cell_y === this.prev_cell_y) + return; + let cell = this.cell(cell_x, cell_y); - let terrain = cell[LAYERS.terrain]; - if (terrain.type.name === 'button_gray') { + let [layer, hint] = this._find_target_tile(cell); + this.hovered_layer = layer; + if (hint === null) { + this.click_hint.classList.remove('--visible'); + } + else { + this.click_hint.classList.add('--visible'); + this.click_hint.setAttribute('x', cell_x + 0.5); + this.click_hint.setAttribute('y', cell_y - 0.125); + this.click_hint.textContent = hint; + } + + if (layer === null) { + this.cursor_element.classList.remove('--visible'); + this.gray_button_preview.classList.remove('--visible'); + return; + } + let tile = cell[layer]; + + /* + this.cursor_element.classList.add('--visible'); + if (layer === LAYERS.terrain) { + this.cursor_element.setAttribute('data-layer', 'terrain'); + } + else if (layer === LAYERS.item) { + this.cursor_element.setAttribute('data-layer', 'item'); + } + else if (layer === LAYERS.actor) { + this.cursor_element.setAttribute('data-layer', 'actor'); + } + else if (layer === LAYERS.thin_wall) { + this.cursor_element.setAttribute('data-layer', 'thin-wall'); + } + */ + + if (cell.filter(t => t).length <= 1) { + // Only one tile, so the canvas is pointless + this.cursor_element.classList.remove('--visible'); + } + else { + // Draw the targeted tile on top of everything else + this.cursor_element.classList.add('--visible'); + let ctx = this.canvas.getContext('2d'); + ctx.clearRect(0, 0, this.canvas.width, this.canvas.height); + if (layer !== LAYERS.terrain) { + this.editor.renderer.draw_single_tile_type('floor', null, this.canvas); + } + this.editor.renderer.draw_single_tile_type(tile.type.name, tile, this.canvas); + } + + // Special previewing behavior + if (tile.type.name === 'button_gray') { + this.cursor_element.classList.remove('--visible'); this.gray_button_preview.classList.add('--visible'); - this.gray_button_preview.setAttribute('transform', `translate(${cell_x} ${cell_y})`); + let gx0 = Math.max(0, cell_x - 2); + let gy0 = Math.max(0, cell_y - 2); + let gx1 = Math.min(this.editor.stored_level.size_x - 1, cell_x + 2); + let gy1 = Math.min(this.editor.stored_level.size_y - 1, cell_y + 2); + + this.gray_button_bounds_rect.setAttribute('x', gx0); + this.gray_button_bounds_rect.setAttribute('y', gy0); + this.gray_button_bounds_rect.setAttribute('width', gx1 - gx0 + 1); + this.gray_button_bounds_rect.setAttribute('height', gy1 - gy0 + 1); for (let el of this.gray_button_preview.querySelectorAll('rect.overlay-adjust-gray-button-shroud')) { el.remove(); } // The easiest way I can find to preview this is to slap an overlay on everything NOT // affected by the button. Try to consolidate some of the resulting rectangles though - for (let dy = -2; dy <= 2; dy++) { - let last_rect, last_dx; - for (let dx = -2; dx <= 2; dx++) { - let target = this.cell(cell_x + dx, cell_y + dy); + for (let y = gy0; y <= gy1; y++) { + let last_rect, last_x; + for (let x = gx0; x <= gx1; x++) { + let target = this.cell(x, y); if (target && target !== cell && target[LAYERS.terrain].type.on_gray_button) continue; - if (last_rect && last_dx === dx - 1) { + if (last_rect && last_x === x - 1) { last_rect.setAttribute('width', 1 + parseInt(last_rect.getAttribute('width'), 10)); } else { last_rect = mk_svg('rect.overlay-adjust-gray-button-shroud', { - x: dx, - y: dy, + x: x, + y: y, width: 1, height: 1, }); this.gray_button_preview.append(last_rect); } - last_dx = dx; + last_x = x; } } } @@ -1337,53 +1682,72 @@ export class AdjustOperation extends MouseOperation { handle_press() { let cell = this.cell(this.prev_cell_x, this.prev_cell_y); - if (this.ctrl) { - for (let tile of cell) { - if (tile && TILES_WITH_PROPS[tile.type.name] !== undefined) { - this.editor.open_tile_prop_overlay( - tile, cell, this.editor.renderer.get_cell_rect(cell.x, cell.y)); - break; - } - } + let tile = cell[this.hovered_layer]; + if (! tile) return; - } - let start_layer = this.shift ? 0 : LAYERS.MAX - 1; - for (let layer = start_layer; layer >= 0; layer--) { - let tile = cell[layer]; - if (! tile) - continue; + let behavior = SPECIAL_TILE_BEHAVIOR[tile.type.name]; - let rotated; - tile = {...tile}; // TODO little inefficient - if (this.alt_mode) { - // Reverse, go counterclockwise - rotated = this.editor.rotate_tile_left(tile); + // Same order as _find_target_tile + if (TILE_TYPES['transmogrifier']._mogrifications[tile.type.name]) { + // Toggle between related tile types + tile.type = TILE_TYPES[TILE_TYPES['transmogrifier']._mogrifications[tile.type.name]]; + this.editor.place_in_cell(cell, tile); + this.editor.commit_undo(); + } + else if (ADJUST_TILE_TYPES[tile.type.name]) { + // Toggle between related tile types + // TODO can you go backwards any more, or no? + let toggled = ADJUST_TILE_TYPES[tile.type.name].next; + tile.type = TILE_TYPES[toggled]; + this.editor.place_in_cell(cell, tile); + this.editor.commit_undo(); + } + else if (tile.type.name === 'logic_gate' && ADJUST_GATE_TYPES[tile.gate_type]) { + // Also toggle between related logic gate types + let toggled = ADJUST_GATE_TYPES[tile.gate_type].next; + tile.gate_type = toggled; + this.editor.place_in_cell(cell, tile); + this.editor.commit_undo(); + } + else if (tile.type.name === 'logic_gate' && tile.gate_type === 'counter') { + // Adjust the starting number on a logic gate + // TODO is this in adjust_forward or...? + } + else if (this.hovered_layer === LAYERS.thin_wall) { + // Place or delete individual thin walls + // XXX don't allow deleting ALL the thin walls...?? + let bit = DIRECTIONS[this.get_tile_edge()].bit; + tile.edges ^= bit; + this.editor.place_in_cell(cell, tile); + this.editor.commit_undo(); + } + else if (tile.type.name === 'frame_block') { + // Place or delete individual frame block arrows + let edge = this.get_tile_edge(); + tile.arrows = new Set(tile.arrows); + if (tile.arrows.has(edge)) { + tile.arrows.delete(edge); } else { - rotated = this.editor.rotate_tile_right(tile); - } - if (rotated) { - this.editor.place_in_cell(cell, tile); - this.editor.commit_undo(); - break; - } - - // Toggle tiles that go in obvious pairs - let toggled = (this.alt_mode ? ADJUST_TOGGLES_CCW : ADJUST_TOGGLES_CW)[tile.type.name]; - if (toggled) { - tile.type = TILE_TYPES[toggled]; - this.editor.place_in_cell(cell, tile); - this.editor.commit_undo(); - break; - } - - // Other special tile behavior - let special = ADJUST_SPECIAL[tile.type.name]; - if (special) { - special(this.editor, tile, cell); - this.editor.commit_undo(); - break; + tile.arrows.add(edge); } + this.editor.place_in_cell(cell, tile); + this.editor.commit_undo(); + } + else if (behavior && behavior.adjust_forward) { + behavior.adjust_forward(tile); + this.editor.place_in_cell(cell, tile); + this.editor.commit_undo(); + } + else if (ADJUST_SPECIAL[tile.type.name]) { + ADJUST_SPECIAL[tile.type.name](this.editor, tile, cell); + this.editor.commit_undo(); + } + else if (TILES_WITH_PROPS[tile.type.name]) { + // Open special tile editors -- this is a last resort, which is why right-click does it + // explicitly + this.editor.open_tile_prop_overlay( + tile, cell, this.editor.renderer.get_cell_rect(cell.x, cell.y)); } } // Adjust tool doesn't support dragging @@ -1391,6 +1755,7 @@ export class AdjustOperation extends MouseOperation { // TODO if it does then it should end as soon as you spawn a popup do_destroy() { this.gray_button_preview.remove(); + this.click_hint.remove(); super.do_destroy(); } } diff --git a/js/tiletypes.js b/js/tiletypes.js index d18d061..c4e51ee 100644 --- a/js/tiletypes.js +++ b/js/tiletypes.js @@ -1222,6 +1222,7 @@ const TILE_TYPES = { can_reveal_walls: true, can_reverse_on_railroad: true, movement_speed: 4, + // TODO why does this have a Set where most things have a bitmask allows_push(me, direction) { return me.arrows && me.arrows.has(direction); }, diff --git a/style.css b/style.css index a1f4b6b..c7d32fd 100644 --- a/style.css +++ b/style.css @@ -1511,10 +1511,10 @@ body.--debug .player-overlay-message { } .player-overlay-message[data-reason=failure] { background: hsla(330, 20%, 10%, 0.5); - background: radial-gradient(#0004, hsla(330, 10%, 10%, 0.5) 40%, hsl(330, 20%, 10%)); + background: radial-gradient(hsla(330, 10%, 10%, 0.75) 40%, hsl(330, 20%, 10%)); } .player-overlay-message[data-reason=success] { - background: radial-gradient(hsla(30, 80%, 10%, 0.75), 60%, hsla(40, 100%, 30%, 0.75)); + background: radial-gradient(hsla(40, 80%, 10%, 0.75), hsla(40, 80%, 20%, 0.875) 80%, hsla(40, 80%, 30%, 0.875)); } .player-overlay-message[data-reason=ended] { /* Rearrange this entirely, to fit the ending image in */ @@ -2198,7 +2198,7 @@ body.--debug #player-debug { width: -moz-fit-content; width: fit-content; } -#editor .editor-canvas canvas { +#editor .editor-canvas canvas.editor-renderer-canvas { display: block; width: calc(var(--viewport-width) * var(--tile-width) * var(--scale)); --viewport-width: 9; @@ -2208,10 +2208,7 @@ body.--debug #player-debug { /* SVG overlays */ svg.level-editor-overlay { position: absolute; - top: 0; - bottom: 0; - left: 0; - right: 0; + inset: calc(-1 * var(--tile-width) * var(--scale)) calc(-1 * var(--tile-height) * var(--scale)); /* allow clicks to go through us! */ pointer-events: none; @@ -2279,7 +2276,7 @@ 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%); + stroke: hsl(50, 90%, 50%); } svg.level-editor-overlay g.overlay-connection[data-source=button_orange] { stroke: hsl(30, 90%, 60%); @@ -2290,6 +2287,27 @@ svg.level-editor-overlay g.overlay-connection.--implicit line.-arrow { svg.level-editor-overlay g.overlay-connection line.-arrow { marker-end: url(#overlay-arrowhead); } +svg.level-editor-overlay .overlay-adjust-cursor { + /* shared between rotate+adjust tools, though they use different elements/shapes */ + stroke: #444; + fill: #fff4; +} +svg.level-editor-overlay .overlay-adjust-cursor[data-layer=terrain] { + stroke: hsl(150deg, 80%, 20%, 0.8); + fill: hsl(150deg, 80%, 60%, 0.4); +} +svg.level-editor-overlay .overlay-adjust-cursor[data-layer=item] { + stroke: hsl(50deg, 80%, 20%, 0.8); + fill: hsl(50deg, 80%, 60%, 0.4); +} +svg.level-editor-overlay .overlay-adjust-cursor[data-layer=actor] { + stroke: hsl(215deg, 80%, 20%, 0.8); + fill: hsl(215deg, 80%, 60%, 0.4); +} +svg.level-editor-overlay .overlay-adjust-cursor[data-layer=thin-wall] { + stroke: hsl(330deg, 80%, 20%, 0.8); + fill: hsl(330deg, 80%, 60%, 0.4); +} svg.level-editor-overlay .overlay-adjust-gray-button-radius { stroke: #f4f4f4; fill: hsla(10, 10%, 80%, 0.125); @@ -2314,6 +2332,15 @@ svg.level-editor-overlay text.overlay-edit-tip { text-anchor: middle; dominant-baseline: middle; } +svg.level-editor-overlay text.overlay-adjust-hint { + font-size: calc(0.5px / var(--scale)); + font-weight: bold; + stroke: black; + fill: white; + paint-order: stroke; + text-anchor: middle; + dominant-baseline: auto; +} .editor-big-tooltip { /* shared between toolbar and palette tooltips */