From ed58cbac6baf56967284a51b5d7da280e66f6e40 Mon Sep 17 00:00:00 2001 From: "Eevee (Evelyn Woods)" Date: Tue, 8 Dec 2020 18:56:49 -0700 Subject: [PATCH] Editor: Add a (glitchy) wire tool; add right-click support to existing tools (hurrah) --- js/editor-tile-overlays.js | 4 +- js/main-editor.js | 256 ++++++++++++++++++++++++++++++++++--- js/tiletypes.js | 3 + 3 files changed, 244 insertions(+), 19 deletions(-) diff --git a/js/editor-tile-overlays.js b/js/editor-tile-overlays.js index 2277f19..92ffc4a 100644 --- a/js/editor-tile-overlays.js +++ b/js/editor-tile-overlays.js @@ -246,10 +246,8 @@ export const TILES_WITH_PROPS = { hint: HintTileEditor, directional_block: DirectionalBlockTileEditor, railroad: RailroadTileEditor, - // TODO various wireable tiles + // TODO various wireable tiles (hmm not sure how that ui works) // TODO initial value of counter // TODO cloner arrows - // TODO railroad parts // TODO later, custom floor/wall selection - // TODO directional blocks }; diff --git a/js/main-editor.js b/js/main-editor.js index b4cd796..5602e50 100644 --- a/js/main-editor.js +++ b/js/main-editor.js @@ -258,7 +258,6 @@ class EditorLevelBrowserOverlay extends DialogOverlay { } } - class EditorShareOverlay extends DialogOverlay { constructor(conductor, url) { super(conductor); @@ -280,6 +279,9 @@ class EditorShareOverlay extends DialogOverlay { } } +//////////////////////////////////////////////////////////////////////////////////////////////////// +// Mouse handling + // Stores and controls what the mouse is doing during a movement, mostly by dispatching to functions // defined for the individual tools const MOUSE_BUTTON_MASKS = [1, 4, 2]; // MouseEvent.button/buttons are ordered differently @@ -288,6 +290,7 @@ class MouseOperation { this.editor = editor; this.target = target; this.button_mask = MOUSE_BUTTON_MASKS[ev.button]; + this.alt_mode = ev.button > 0; this.modifier = null; // or 'shift' or 'ctrl' (ctrl takes precedent) this._update_modifier(ev); @@ -406,20 +409,47 @@ class PencilOperation extends DrawOperation { if (cell) { this.editor.select_palette(cell[cell.length - 1]); } + return; } - else if (this.modifier === 'shift') { - // Aggressive mode: erase whatever's already in the cell + + let template = this.editor.palette_selection; + if (this.alt_mode) { + // Erase let cell = this.cell(x, y); - cell.length = 0; - let type = this.editor.palette_selection.type; - if (type.draw_layer !== 0) { + if (this.modifier === 'shift') { + // Aggressive mode: erase the entire cell + cell.length = 0; cell.push({type: TILE_TYPES.floor}); } - this.editor.place_in_cell(x, y, this.editor.palette_selection); + else if (template) { + // Erase whatever's on the same layer + // TODO this seems like the wrong place for this + for (let i = cell.length - 1; i >= 0; i--) { + if (cell[i].type.draw_layer === template.type.draw_layer) { + cell.splice(i, 1); + } + } + this.editor.mark_cell_dirty(cell); + } } else { - // Default operation: only erase whatever's on the same layer - this.editor.place_in_cell(x, y, this.editor.palette_selection); + // Draw + if (! template) + return; + if (this.modifier === 'shift') { + // Aggressive mode: erase whatever's already in the cell + let cell = this.cell(x, y); + cell.length = 0; + let type = this.editor.palette_selection.type; + if (type.draw_layer !== 0) { + cell.push({type: TILE_TYPES.floor}); + } + this.editor.place_in_cell(x, y, template); + } + else { + // Default operation: only erase whatever's on the same layer + this.editor.place_in_cell(x, y, template); + } } } } @@ -491,6 +521,8 @@ class ForceFloorOperation extends DrawOperation { // TODO entered cell should get blank railroad? // TODO maybe place a straight track in the new cell so it looks like we're doing something, then // fix it if it wasn't there? +// TODO gonna need an ice tool too, so maybe i can merge all three with some base thing that tracks +// the directions the mouse is moving? or is FF tool too different? class TrackOperation extends DrawOperation { start() { // Do nothing to start; we only lay track when the mouse leaves a cell @@ -549,9 +581,18 @@ class TrackOperation extends DrawOperation { let cell = this.cell(prevx, prevy); let terrain = cell[0]; if (terrain.type.name === 'railroad') { - terrain.tracks |= bit; + if (this.alt_mode) { + // Erase + // TODO fix track switch? + // TODO if this leaves tracks === 0, replace with floor? + terrain.tracks &= ~bit; + } + else { + // Draw + terrain.tracks |= bit; + } } - else { + else if (! this.alt_mode) { terrain = { type: TILE_TYPES['railroad'] }; terrain.type.populate_defaults(terrain); terrain.tracks |= bit; @@ -565,6 +606,171 @@ class TrackOperation extends DrawOperation { } } +class WireOperation extends DrawOperation { + start() { + if (this.modifier === 'ctrl') { + // Place or remove wire tunnels + let cell = this.cell(this.gx0f, this.gy0f); + if (! cell) + return; + + let direction; + // Use the offset from the center to figure out which edge of the tile to affect + let xoff = this.gx0f % 1 - 0.5; + let yoff = this.gy0f % 1 - 0.5; + if (Math.abs(xoff) > Math.abs(yoff)) { + if (xoff > 0) { + direction = 'east'; + } + else { + direction = 'west'; + } + } + else { + if (yoff > 0) { + direction = 'south'; + } + else { + direction = 'north'; + } + } + let bit = DIRECTIONS[direction].bit; + + for (let tile of cell) { + if (tile.type.name !== 'floor') + continue; + if (this.alt_mode) { + tile.wire_tunnel_directions &= ~bit; + } + else { + tile.wire_tunnel_directions |= bit; + } + } + return; + } + } + step(mx, my, gxf, gyf) { + if (this.modifier === 'ctrl') { + // Wire tunnels don't support dragging + // TODO but maybe they should?? makes erasing a lot of them easier at least + return; + } + + // Wire is interesting. Consider this diagram. + // +-------+ + // | . A . | + // |...A...| + // | . A . | + // |BBB+CCC| + // | . D . | + // |...D...| + // | . D . | + // +-------+ + // In order to know which of the four wire pieces in a cell (A, B, C, D) someone is trying + // to draw over, we use a quarter-size grid, indicated by the dots. Then any mouse movement + // that crosses the first horizontal grid line means we should draw wire A. + // (Note that crossing either a tile boundary or the middle of a cell doesn't mean anything; + // for example, dragging the mouse horizontally across the A wire is meaningless.) + // TODO maybe i should just have a walk_grid variant that yields line crossings, christ + let prevqx = null, prevqy = null; + for (let [qx, qy] of walk_grid( + this.gx1f * 4, this.gy1f * 4, gxf * 4, gyf * 4, + // See comment in iter_touched_cells + -1, -1, this.editor.stored_level.size_x * 2, this.editor.stored_level.size_y * 2)) + { + if (prevqx === null || prevqy === null) { + prevqx = qx; + prevqy = qy; + continue; + } + + // Figure out which grid line we've crossed; direction doesn't matter, so we just get + // the index of the line, which matches the coordinate of the cell to the right/bottom + // FIXME 'continue' means we skip the update of prevs, solution is really annoying + // FIXME if you trace around just the outside of a tile, you'll get absolute nonsense: + // +---+---+ + // | | | + // | |.+ | + // | |.| | + // +---+.--+ + // | .... | + // | +-| | + // | | | + // +---+---+ + let wire_direction; + let x, y; + if (qx === prevqx) { + // Vertical + let line = Math.max(qy, prevqy); + // Even crossings don't correspond to a wire + if (line % 2 === 0) { + prevqx = qx; + prevqy = qy; + continue; + } + + // Convert to real coordinates + x = Math.floor(qx / 4); + y = Math.floor(line / 4); + + if (line % 4 === 1) { + // Consult the diagram! + wire_direction = 'north'; + } + else { + wire_direction = 'south'; + } + } + else { + // Horizontal; same as above + let line = Math.max(qx, prevqx); + if (line % 2 === 0) { + prevqx = qx; + prevqy = qy; + continue; + } + + x = Math.floor(line / 4); + y = Math.floor(qy / 4); + + if (line % 4 === 1) { + wire_direction = 'west'; + } + else { + wire_direction = 'east'; + } + } + + if (! this.editor.is_in_bounds(x, y)) { + prevqx = qx; + prevqy = qy; + continue; + } + + let cell = this.cell(x, y); + for (let tile of cell) { + // TODO probably a better way to do this + if (['floor', 'steel', 'button_pink', 'button_black', 'teleport_blue', 'teleport_red', 'light_switch_on', 'light_switch_off'].indexOf(tile.type.name) < 0) + continue; + + tile.wire_directions = tile.wire_directions ?? 0; + if (this.alt_mode) { + // Erase + tile.wire_directions &= ~DIRECTIONS[wire_direction].bit; + } + else { + // Draw + tile.wire_directions |= DIRECTIONS[wire_direction].bit; + } + // TODO this.editor.mark_tile_dirty(tile); + } + + prevqx = qx; + prevqy = qy; + } + } +} + // Tiles the "adjust" tool will turn into each other const ADJUST_TOGGLES_CW = {}; const ADJUST_TOGGLES_CCW = {}; @@ -590,6 +796,7 @@ const ADJUST_TOGGLES_CCW = {}; ['water', 'turtle'], ['no_player1_sign', 'no_player2_sign'], ['flame_jet_off', 'flame_jet_on'], + ['light_switch_off', 'light_switch_on'], ]) { for (let [i, tile] of cycle.entries()) { @@ -616,13 +823,21 @@ class AdjustOperation extends MouseOperation { for (let i = cell.length - 1; i >= 0; i--) { let tile = cell[i]; - if (this.editor.rotate_tile_right(tile)) { + let rotated; + 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.mark_tile_dirty(tile); break; } // Toggle tiles that go in obvious pairs - let other = ADJUST_TOGGLES_CW[tile.type.name]; + let other = (this.alt_mode ? ADJUST_TOGGLES_CCW : ADJUST_TOGGLES_CW)[tile.type.name]; if (other) { tile.type = TILE_TYPES[other]; this.editor.mark_tile_dirty(tile); @@ -826,7 +1041,7 @@ const EDITOR_TOOLS = { desc: "Place, erase, and select tiles.\nLeft click: draw\nRight click: erase\nShift: Replace all layers\nCtrl-click: eyedrop", uses_palette: true, op1: PencilOperation, - //op2: EraseOperation, + op2: PencilOperation, //hover: show current selection under cursor }, line: { @@ -868,6 +1083,7 @@ const EDITOR_TOOLS = { name: "Adjust", desc: "Edit existing tiles.\nLeft click: rotate actor or toggle terrain\nRight click: rotate or toggle in reverse\nShift: always target terrain\nCtrl-click: edit properties of complex tiles\n(wires, railroads, hints, etc.)", op1: AdjustOperation, + op2: AdjustOperation, }, connect: { // TODO not implemented @@ -879,7 +1095,9 @@ const EDITOR_TOOLS = { // TODO not implemented icon: 'icons/tool-wire.png', name: "Wire", - desc: "Draw CC2 wiring", + desc: "Edit CC2 wiring.\nLeft click: draw wires\nRight click: erase wires\nCtrl-click: toggle tunnels (floor only)", + op1: WireOperation, + op2: WireOperation, }, camera: { icon: 'icons/tool-camera.png', @@ -894,7 +1112,7 @@ const EDITOR_TOOLS = { // slade when you have some selected? // TODO ah, railroads... }; -const EDITOR_TOOL_ORDER = ['pencil', 'adjust', 'force-floors', 'tracks', 'camera']; +const EDITOR_TOOL_ORDER = ['pencil', 'adjust', 'force-floors', 'tracks', 'wire', 'camera']; // TODO this MUST use a LL tileset! const EDITOR_PALETTE = [{ @@ -1022,6 +1240,8 @@ const EDITOR_PALETTE = [{ 'logic_gate/counter', 'button_pink', 'button_black', + 'light_switch_off', + 'light_switch_on', 'purple_floor', 'purple_wall', 'button_gray', @@ -1761,6 +1981,10 @@ export class Editor extends PrimaryView { } } + mark_cell_dirty(cell) { + this.renderer.draw(); + } + is_in_bounds(x, y) { return 0 <= x && x < this.stored_level.size_x && 0 <= y && y < this.stored_level.size_y; } diff --git a/js/tiletypes.js b/js/tiletypes.js index 6ae3d36..35bdc35 100644 --- a/js/tiletypes.js +++ b/js/tiletypes.js @@ -1514,6 +1514,8 @@ const TILE_TYPES = { return 0; }, on_arrive(me, level, other) { + // TODO distinct sfx? more clicky? + level.sfx.play_once('button-press', me.cell); level.transmute_tile(me, 'light_switch_on'); }, }, @@ -1524,6 +1526,7 @@ const TILE_TYPES = { return me.wire_directions; }, on_arrive(me, level, other) { + level.sfx.play_once('button-press', me.cell); level.transmute_tile(me, 'light_switch_off'); }, },