Editor: Teach the adjust tool to edit individual tiles
This commit is contained in:
parent
560a89cfd3
commit
0d376e003e
@ -198,6 +198,9 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<nav class="controls">
|
<nav class="controls">
|
||||||
|
<div id="editor-tile">
|
||||||
|
<!-- tools go here -->
|
||||||
|
</div>
|
||||||
<div id="editor-toolbar">
|
<div id="editor-toolbar">
|
||||||
<!-- tools go here -->
|
<!-- tools go here -->
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -54,6 +54,8 @@ export class Overlay {
|
|||||||
overlay.addEventListener('click', ev => {
|
overlay.addEventListener('click', ev => {
|
||||||
this.close();
|
this.close();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
return overlay;
|
||||||
}
|
}
|
||||||
|
|
||||||
close() {
|
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
|
// Overlay styled like a dialog box
|
||||||
export class DialogOverlay extends Overlay {
|
export class DialogOverlay extends Overlay {
|
||||||
constructor(conductor) {
|
constructor(conductor) {
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
import { DIRECTIONS, TICS_PER_SECOND } from './defs.js';
|
import { DIRECTIONS, TICS_PER_SECOND } from './defs.js';
|
||||||
import * as c2g from './format-c2g.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 CanvasRenderer from './renderer-canvas.js';
|
||||||
import TILE_TYPES from './tiletypes.js';
|
import TILE_TYPES from './tiletypes.js';
|
||||||
import { SVG_NS, mk, mk_svg, walk_grid } from './util.js';
|
import { SVG_NS, mk, mk_svg, walk_grid } from './util.js';
|
||||||
@ -266,7 +266,17 @@ const ADJUST_TOGGLES = {
|
|||||||
};
|
};
|
||||||
class AdjustOperation extends MouseOperation {
|
class AdjustOperation extends MouseOperation {
|
||||||
start() {
|
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) {
|
for (let tile of cell) {
|
||||||
// Toggle tiles that go in obvious pairs
|
// Toggle tiles that go in obvious pairs
|
||||||
let other = ADJUST_TOGGLES[tile.type.name];
|
let other = ADJUST_TOGGLES[tile.type.name];
|
||||||
@ -282,6 +292,7 @@ class AdjustOperation extends MouseOperation {
|
|||||||
}
|
}
|
||||||
// Adjust tool doesn't support dragging
|
// Adjust tool doesn't support dragging
|
||||||
// TODO should it?
|
// 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
|
// FIXME currently allows creating outside the map bounds and moving beyond the right/bottom, sigh
|
||||||
@ -472,9 +483,10 @@ const EDITOR_TOOLS = {
|
|||||||
pencil: {
|
pencil: {
|
||||||
icon: 'icons/tool-pencil.png',
|
icon: 'icons/tool-pencil.png',
|
||||||
name: "Pencil",
|
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,
|
op1: PencilOperation,
|
||||||
//op2: EraseOperation,
|
//op2: EraseOperation,
|
||||||
|
//hover: show current selection under cursor
|
||||||
},
|
},
|
||||||
line: {
|
line: {
|
||||||
// TODO not implemented
|
// TODO not implemented
|
||||||
@ -503,7 +515,7 @@ const EDITOR_TOOLS = {
|
|||||||
adjust: {
|
adjust: {
|
||||||
icon: 'icons/tool-adjust.png',
|
icon: 'icons/tool-adjust.png',
|
||||||
name: "Adjust",
|
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,
|
op1: AdjustOperation,
|
||||||
},
|
},
|
||||||
connect: {
|
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 {
|
export class Editor extends PrimaryView {
|
||||||
constructor(conductor) {
|
constructor(conductor) {
|
||||||
super(conductor, document.body.querySelector('main#editor'));
|
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);
|
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() {
|
cancel_mouse_operation() {
|
||||||
if (this.mouse_op) {
|
if (this.mouse_op) {
|
||||||
this.mouse_op.do_abort();
|
this.mouse_op.do_abort();
|
||||||
|
|||||||
@ -328,6 +328,7 @@ const TILE_TYPES = {
|
|||||||
on_ready(me) {
|
on_ready(me) {
|
||||||
// If there's already an actor on top of us, assume it entered the way it's already
|
// 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)
|
// 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();
|
let actor = me.cell.get_actor();
|
||||||
if (actor) {
|
if (actor) {
|
||||||
me.entered_direction = actor.direction;
|
me.entered_direction = actor.direction;
|
||||||
|
|||||||
@ -24,7 +24,9 @@ function _mk(el, children) {
|
|||||||
export function mk(tag_selector, ...children) {
|
export function mk(tag_selector, ...children) {
|
||||||
let [tag, ...classes] = tag_selector.split('.');
|
let [tag, ...classes] = tag_selector.split('.');
|
||||||
let el = document.createElement(tag);
|
let el = document.createElement(tag);
|
||||||
|
if (classes.length > 0) {
|
||||||
el.classList = classes.join(' ');
|
el.classList = classes.join(' ');
|
||||||
|
}
|
||||||
return _mk(el, children);
|
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) {
|
export function mk_svg(tag_selector, ...children) {
|
||||||
let [tag, ...classes] = tag_selector.split('.');
|
let [tag, ...classes] = tag_selector.split('.');
|
||||||
let el = document.createElementNS(SVG_NS, tag);
|
let el = document.createElementNS(SVG_NS, tag);
|
||||||
|
if (classes.length > 0) {
|
||||||
el.classList = classes.join(' ');
|
el.classList = classes.join(' ');
|
||||||
|
}
|
||||||
return _mk(el, children);
|
return _mk(el, children);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
62
style.css
62
style.css
@ -143,6 +143,11 @@ svg.svg-icon {
|
|||||||
right: 0;
|
right: 0;
|
||||||
background: #fff4;
|
background: #fff4;
|
||||||
}
|
}
|
||||||
|
.overlay.--transient {
|
||||||
|
align-items: start;
|
||||||
|
justify-content: start;
|
||||||
|
background: none;
|
||||||
|
}
|
||||||
.dialog {
|
.dialog {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
@ -1011,3 +1016,60 @@ main.--has-demo .demo-controls {
|
|||||||
z-index: 1;
|
z-index: 1;
|
||||||
box-shadow: 0 0 0 1px black, 0 0 0 3px white;
|
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%);
|
||||||
|
}
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user