Editor: Teach the adjust tool to edit individual tiles

This commit is contained in:
Eevee (Evelyn Woods) 2020-12-02 09:05:20 -07:00
parent 560a89cfd3
commit 0d376e003e
6 changed files with 178 additions and 6 deletions

View File

@ -198,6 +198,9 @@
</div>
</div>
<nav class="controls">
<div id="editor-tile">
<!-- tools go here -->
</div>
<div id="editor-toolbar">
<!-- tools go here -->
</div>

View File

@ -54,6 +54,8 @@ export class Overlay {
overlay.addEventListener('click', ev => {
this.close();
});
return overlay;
}
close() {
@ -61,6 +63,16 @@ export class Overlay {
}
}
// Overlay styled like a popup of some sort
export class TransientOverlay extends Overlay {
open() {
// TODO i don't like how vaguely arbitrary this feels.
let overlay = super.open();
overlay.classList.add('--transient');
return overlay;
}
}
// Overlay styled like a dialog box
export class DialogOverlay extends Overlay {
constructor(conductor) {

View File

@ -1,6 +1,6 @@
import { DIRECTIONS, TICS_PER_SECOND } from './defs.js';
import * as c2g from './format-c2g.js';
import { PrimaryView, DialogOverlay } from './main-base.js';
import { PrimaryView, TransientOverlay, DialogOverlay } from './main-base.js';
import CanvasRenderer from './renderer-canvas.js';
import TILE_TYPES from './tiletypes.js';
import { SVG_NS, mk, mk_svg, walk_grid } from './util.js';
@ -266,7 +266,17 @@ const ADJUST_TOGGLES = {
};
class AdjustOperation extends MouseOperation {
start() {
let cell = this.editor.stored_level.cells[this.gy1][this.gx1];
let cell = this.cell(this.gx1, this.gy1);
if (this.modifier === 'ctrl') {
for (let tile of cell) {
if (tile.type.name === 'floor_letter') {
// TODO use the tile's bbox, not the mouse position
this.editor.open_tile_prop_overlay(tile, this.mx0, this.my0);
break;
}
}
return;
}
for (let tile of cell) {
// Toggle tiles that go in obvious pairs
let other = ADJUST_TOGGLES[tile.type.name];
@ -282,6 +292,7 @@ class AdjustOperation extends MouseOperation {
}
// Adjust tool doesn't support dragging
// TODO should it?
// TODO if it does then it should end as soon as you spawn a popup
}
// FIXME currently allows creating outside the map bounds and moving beyond the right/bottom, sigh
@ -472,9 +483,10 @@ const EDITOR_TOOLS = {
pencil: {
icon: 'icons/tool-pencil.png',
name: "Pencil",
desc: "Draw individual tiles",
desc: "Click to draw. Right click to erase. Hold Shift to affect all layers. Ctrl-click to eyedrop.",
op1: PencilOperation,
//op2: EraseOperation,
//hover: show current selection under cursor
},
line: {
// TODO not implemented
@ -503,7 +515,7 @@ const EDITOR_TOOLS = {
adjust: {
icon: 'icons/tool-adjust.png',
name: "Adjust",
desc: "Toggle blocks and rotate actors",
desc: "Click to rotate actor or toggle terrain. Right click to rotate in reverse. Hold Shift to always affect terrain. Ctrl-click to edit properties of complex tiles (wires, railroads, hints, etc.)",
op1: AdjustOperation,
},
connect: {
@ -602,6 +614,49 @@ const EDITOR_PALETTE = [{
],
}];
// FIXME could very much stand to have a little animation when appearing
class LetterTileEditor extends TransientOverlay {
constructor(conductor) {
let root = mk('form.editor-popup-tile-editor');
root.append(mk('span.popup-chevron'));
super(conductor, root);
this.tile = null;
let list = mk('ol.editor-letter-tile-picker');
root.append(list);
this.glyph_elements = {};
for (let c = 32; c < 128; c++) {
let glyph = String.fromCharCode(c);
let input = mk('input', {type: 'radio', name: 'glyph', value: glyph});
this.glyph_elements[glyph] = input;
let item = mk('li', mk('label', input, mk('span.-glyph', glyph)));
list.append(item);
}
list.addEventListener('change', ev => {
let glyph = this.root.elements['glyph'].value;
if (this.tile) {
this.tile.ascii_code = glyph.charCodeAt(0);
// FIXME should be able to mark tiles as dirty, also this is sure a mouthful
this.conductor.editor.renderer.draw();
}
});
}
edit_tile(tile) {
this.tile = tile;
this.root.elements['glyph'].value = String.fromCharCode(tile.ascii_code);
}
static configure_tile_defaults(tile) {
tile.ascii_code = 32;
}
}
const EDITOR_TILES_WITH_PROPS = {
floor_letter: LetterTileEditor,
};
export class Editor extends PrimaryView {
constructor(conductor) {
super(conductor, document.body.querySelector('main#editor'));
@ -874,6 +929,41 @@ export class Editor extends PrimaryView {
cell.sort((a, b) => a.type.draw_layer - b.type.draw_layer);
}
open_tile_prop_overlay(tile, x0, y0) {
this.cancel_mouse_operation();
let overlay_class = EDITOR_TILES_WITH_PROPS[tile.type.name];
let overlay = new overlay_class(this.conductor);
overlay.edit_tile(tile);
overlay.open();
// FIXME move this into TransientOverlay or some other base class
let root = overlay.root;
// Vertical position: either above or below, preferring the side that has more space
if (y0 > document.body.clientHeight / 2) {
// Above
root.classList.add('--above');
root.style.top = `${y0 - root.offsetHeight}px`;
}
else {
// Below
root.classList.remove('--above');
root.style.top = `${y0}px`;
}
// Horizontal position: centered, but kept within the screen
let left;
let margin = 8; // prefer to not quite touch the edges
let halfwidth = root.offsetWidth / 2 + margin;
if (document.body.clientWidth / 2 < halfwidth) {
// It doesn't fit on the screen at all, so there's nothing we can do; just center it
left = (document.body.clientWidth - root.offsetWidth) / 2;
}
else {
left = Math.max(margin, Math.min(document.body.clientWidth - halfwidth, x0 - halfwidth));
}
root.style.left = `${left}px`;
root.style.setProperty('--chevron-offset', `${x0 - left}px`);
}
cancel_mouse_operation() {
if (this.mouse_op) {
this.mouse_op.do_abort();

View File

@ -328,6 +328,7 @@ const TILE_TYPES = {
on_ready(me) {
// If there's already an actor on top of us, assume it entered the way it's already
// facing (which may be illegal, in which case it can't leave)
// FIXME wrong! > Yeah, so in the high byte, the low nibble encodes the active track. What's missing is that the high nibble encodes a direction value, which is required when a mob starts out on top of the track: it represents the direction the mob was going when it entered the tile, which controls which way it can go on the track.
let actor = me.cell.get_actor();
if (actor) {
me.entered_direction = actor.direction;

View File

@ -24,7 +24,9 @@ function _mk(el, children) {
export function mk(tag_selector, ...children) {
let [tag, ...classes] = tag_selector.split('.');
let el = document.createElement(tag);
if (classes.length > 0) {
el.classList = classes.join(' ');
}
return _mk(el, children);
}
@ -32,7 +34,9 @@ export const SVG_NS = 'http://www.w3.org/2000/svg';
export function mk_svg(tag_selector, ...children) {
let [tag, ...classes] = tag_selector.split('.');
let el = document.createElementNS(SVG_NS, tag);
if (classes.length > 0) {
el.classList = classes.join(' ');
}
return _mk(el, children);
}

View File

@ -143,6 +143,11 @@ svg.svg-icon {
right: 0;
background: #fff4;
}
.overlay.--transient {
align-items: start;
justify-content: start;
background: none;
}
.dialog {
display: flex;
flex-direction: column;
@ -1011,3 +1016,60 @@ main.--has-demo .demo-controls {
z-index: 1;
box-shadow: 0 0 0 1px black, 0 0 0 3px white;
}
/* Mini editors for specific tiles with complex properties */
/* FIXME should this stuff be on an overlay container class? */
form.editor-popup-tile-editor {
position: relative;
padding: 0.5em;
color: black;
background: white;
border: 1px solid black;
box-shadow: 0 2px 4px #0004;
margin-top: 1em;
--chevron-offset: 0px;
}
form.editor-popup-tile-editor.--above {
margin-top: -1em;
}
.popup-chevron {
position: absolute;
border: 1em solid transparent;
left: var(--chevron-offset);
margin-left: -1em;
top: -1em;
border-top: none;
border-bottom-color: white;
filter: drop-shadow(0 -1px 0 black);
}
form.editor-popup-tile-editor.--above .popup-chevron {
top: auto;
bottom: -1em;
border: 1em solid transparent;
border-bottom: none;
border-top-color: white;
filter: drop-shadow(0 1px 0 black);
}
/* Letter floor tiles, which let you pick the character to use; show them as a grid. Note the
* characters are preceded by radio buttons, which we hide for simplicity */
ol.editor-letter-tile-picker {
display: grid;
grid: auto-flow 1.5em / repeat(16, 1.5em);
text-align: center;
font-family: monospace;
}
ol.editor-letter-tile-picker label,
ol.editor-letter-tile-picker .-glyph {
display: block;
height: 100%;
}
ol.editor-letter-tile-picker input[type=radio] {
display: none;
}
ol.editor-letter-tile-picker input[type=radio]:checked + .-glyph {
background: hsl(225, 75%, 90%);
outline: 2px solid hsl(225, 75%, 80%);
}