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
This commit is contained in:
Eevee (Evelyn Woods) 2024-04-22 00:24:07 -06:00
parent abbda898c7
commit 3a9e7c1cd8
11 changed files with 571 additions and 145 deletions

Binary file not shown.

Binary file not shown.

Before

Width:  |  Height:  |  Size: 235 B

After

Width:  |  Height:  |  Size: 478 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 235 B

After

Width:  |  Height:  |  Size: 478 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 440 B

After

Width:  |  Height:  |  Size: 506 B

BIN
icons/tool-rotate.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 439 B

BIN
icons/tool-text.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 452 B

View File

@ -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',

View File

@ -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

View File

@ -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 <g> 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;
}
}
return;
}
let start_layer = this.shift ? 0 : LAYERS.MAX - 1;
for (let layer = start_layer; layer >= 0; layer--) {
let tile = cell[layer];
let tile = cell[this.hovered_layer];
if (! tile)
continue;
return;
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);
}
else {
rotated = this.editor.rotate_tile_right(tile);
}
if (rotated) {
// 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();
break;
}
// Toggle tiles that go in obvious pairs
let toggled = (this.alt_mode ? ADJUST_TOGGLES_CCW : ADJUST_TOGGLES_CW)[tile.type.name];
if (toggled) {
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();
break;
}
// Other special tile behavior
let special = ADJUST_SPECIAL[tile.type.name];
if (special) {
special(this.editor, tile, cell);
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();
break;
}
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 {
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();
}
}

View File

@ -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);
},

View File

@ -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 */