Add some more complex tiles to the palette; add ,/. shortcuts

This commit is contained in:
Eevee (Evelyn Woods) 2020-12-07 12:45:21 -07:00
parent c4bb1f3df1
commit f462ae3394
4 changed files with 423 additions and 69 deletions

View File

@ -85,6 +85,82 @@ class HintTileEditor extends TileEditorOverlay {
}
}
class DirectionalBlockTileEditor extends TileEditorOverlay {
constructor(conductor) {
super(conductor);
let svg_icons = [];
for (let center of [[16, 0], [16, 16], [0, 16], [0, 0]]) {
let symbol = mk_svg('svg', {viewBox: '0 0 16 16'},
mk_svg('circle', {cx: center[0], cy: center[1], r: 3}),
mk_svg('circle', {cx: center[0], cy: center[1], r: 13}),
);
svg_icons.push(symbol);
}
svg_icons.push(mk_svg('svg', {viewBox: '0 0 16 16'},
mk_svg('rect', {x: -2, y: 3, width: 20, height: 10}),
));
svg_icons.push(mk_svg('svg', {viewBox: '0 0 16 16'},
mk_svg('rect', {x: 3, y: -2, width: 10, height: 20}),
));
this.root.append(mk('h3', "Arrows"));
let arrow_list = mk('ol.editor-directional-block-tile-arrows.editor-tile-editor-svg-parts');
// Arrange the arrows in a grid
for (let [direction, icon] of [
[null, mk_svg('path', {d: 'M 8,16 v -8 h 8'})],
['north', mk_svg('path', {d: 'M 0,12 h 16 l -8,-8 z'})],
[null, mk_svg('path', {d: 'M 0,8 h 8 v 8'})],
['west', mk_svg('path', {d: 'M 12,16 v -16 l -8,8 z'})],
[null, null],
['east', mk_svg('path', {d: 'M 4,0 v 16 l 8,-8 z'})],
[null, mk_svg('path', {d: 'M 16,8 h -8 v -8'})],
['south', mk_svg('path', {d: 'M 16,4 h -16 l 8,8 z'})],
[null, mk_svg('path', {d: 'M 8,0 v 8 h -8'})],
]) {
let li = mk('li');
let svg;
if (icon) {
svg = mk_svg('svg', {viewBox: '0 0 16 16'}, icon);
}
if (direction === null) {
if (svg) {
li.append(svg);
}
}
else {
let input = mk('input', {type: 'checkbox', name: 'direction', value: direction});
li.append(mk('label', input, svg));
}
arrow_list.append(li);
}
arrow_list.addEventListener('change', ev => {
if (! this.tile)
return;
if (ev.target.checked) {
this.tile.arrows.add(ev.target.value);
}
else {
this.tile.arrows.delete(ev.target.value);
}
this.editor.mark_tile_dirty(this.tile);
});
this.root.append(arrow_list);
}
edit_tile(tile) {
super.edit_tile(tile);
for (let input of this.root.elements['direction']) {
input.checked = tile.arrows.has(input.value);
}
}
static configure_tile_defaults(tile) {
}
}
class RailroadTileEditor extends TileEditorOverlay {
constructor(conductor) {
super(conductor);
@ -105,7 +181,7 @@ class RailroadTileEditor extends TileEditorOverlay {
));
this.root.append(mk('h3', "Tracks"));
let track_list = mk('ul.editor-railroad-tile-tracks');
let track_list = mk('ul.editor-railroad-tile-tracks.editor-tile-editor-svg-parts');
// Shown as two rows, this puts the straight parts first and the rest in a circle
let track_order = [4, 1, 2, 5, 0, 3];
for (let i of track_order) {
@ -168,6 +244,7 @@ class RailroadTileEditor extends TileEditorOverlay {
export const TILES_WITH_PROPS = {
floor_letter: LetterTileEditor,
hint: HintTileEditor,
directional_block: DirectionalBlockTileEditor,
railroad: RailroadTileEditor,
// TODO various wireable tiles
// TODO initial value of counter

View File

@ -612,36 +612,21 @@ class AdjustOperation extends MouseOperation {
}
return;
}
for (let tile of cell) {
// Rotate railroads, which are a bit complicated
if (tile.type.name === 'railroad') {
let new_tracks = 0;
let rotated_tracks = [1, 2, 3, 0, 5, 4];
for (let [i, new_bit] of rotated_tracks.entries()) {
if (tile.tracks & (1 << i)) {
new_tracks |= (1 << new_bit);
}
}
tile.tracks = new_tracks;
// FIXME implement shift to always target floor, or maybe start from bottom
for (let i = cell.length - 1; i >= 0; i--) {
let tile = cell[i];
if (tile.switch_track !== null) {
tile.switch_track = rotated_tracks[tile.switch_track];
}
tile.entered_direction = DIRECTIONS[tile.entered_direction].right;
if (this.editor.rotate_tile_right(tile)) {
this.editor.mark_tile_dirty(tile);
break;
}
// TODO also directional blocks
// Toggle tiles that go in obvious pairs
let other = ADJUST_TOGGLES_CW[tile.type.name];
if (other) {
tile.type = TILE_TYPES[other];
continue;
}
// Rotate actors
if (TILE_TYPES[tile.type.name].is_actor) {
tile.direction = DIRECTIONS[tile.direction ?? 'south'].right;
this.editor.mark_tile_dirty(tile);
break;
}
}
}
@ -839,6 +824,7 @@ const EDITOR_TOOLS = {
icon: 'icons/tool-pencil.png',
name: "Pencil",
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,
//hover: show current selection under cursor
@ -848,18 +834,21 @@ const EDITOR_TOOLS = {
icon: 'icons/tool-line.png',
name: "Line",
desc: "Draw straight lines",
uses_palette: true,
},
box: {
// TODO not implemented
icon: 'icons/tool-box.png',
name: "Box",
desc: "Fill a rectangular area with tiles",
uses_palette: true,
},
fill: {
// TODO not implemented
icon: 'icons/tool-fill.png',
name: "Fill",
desc: "Flood-fill an area with tiles",
uses_palette: true,
},
'force-floors': {
icon: 'icons/tool-force-floors.png',
@ -941,8 +930,8 @@ const EDITOR_PALETTE = [{
'door_blue', 'door_red', 'door_yellow', 'door_green',
'water', 'turtle', 'fire',
'ice', 'ice_nw', 'ice_ne', 'ice_sw', 'ice_se',
'force_floor_n', 'force_floor_s', 'force_floor_w', 'force_floor_e', 'force_floor_all',
'ice', 'ice_nw', // 'ice_ne', 'ice_sw', 'ice_se',
'force_floor_n', /* 'force_floor_s', 'force_floor_w', 'force_floor_e', */ 'force_floor_all',
],
}, {
title: "Items",
@ -980,39 +969,193 @@ const EDITOR_PALETTE = [{
tiles: [
'dirt_block',
'ice_block',
/*
* FIXME this won't work for all kinds of reasons
{ name: 'directional_block', arrows: new Set },
{ name: 'directional_block', arrows: new Set(['north']) },
{ name: 'directional_block', arrows: new Set(['north', 'east']) },
{ name: 'directional_block', arrows: new Set(['north', 'south']) },
{ name: 'directional_block', arrows: new Set(['north', 'east', 'south']) },
{ name: 'directional_block', arrows: new Set(['north', 'east', 'south', 'west']) },
*/
'bomb',
'button_gray',
'button_green',
'directional_block/0',
'directional_block/1',
'directional_block/2a',
'directional_block/2o',
'directional_block/3',
'directional_block/4',
'green_floor',
'green_wall',
'green_chip',
'green_bomb',
'button_yellow',
'button_green',
'button_blue',
'button_yellow',
'bomb',
'button_red', 'cloner',
'button_brown', 'trap',
'button_orange', 'flame_jet_off', 'flame_jet_on',
'button_pink',
'button_black',
'purple_floor',
'purple_wall',
'transmogrifier',
'teleport_blue',
'teleport_red',
'teleport_green',
'teleport_yellow',
'transmogrifier',
'railroad/straight',
'railroad/curve',
'railroad/switch',
],
// TODO missing:
// - wires, wire tunnels probably a dedicated tool, placing tunnels like a tile makes no sense
// - stopwatches normal tiles
// - swivel special rotate logic, like ice corners
// - canopy normal tile; layering problem
// - thin walls special rotate logic, like force floors; layering problem
// TODO should tiles that respond to wiring and/or gray buttons be highlighted, highlightable?
}, {
title: "Logic",
tiles: [
'logic_gate/not',
'logic_gate/and',
'logic_gate/or',
'logic_gate/xor',
'logic_gate/nand',
'logic_gate/latch-cw',
'logic_gate/latch-ccw',
'logic_gate/counter',
'button_pink',
'button_black',
'purple_floor',
'purple_wall',
'button_gray',
],
}];
const SPECIAL_PALETTE_ENTRIES = {
'directional_block/0': { name: 'directional_block', arrows: new Set },
'directional_block/1': { name: 'directional_block', arrows: new Set(['north']) },
'directional_block/2a': { name: 'directional_block', arrows: new Set(['north', 'east']) },
'directional_block/2o': { name: 'directional_block', arrows: new Set(['north', 'south']) },
'directional_block/3': { name: 'directional_block', arrows: new Set(['north', 'east', 'south']) },
'directional_block/4': { name: 'directional_block', arrows: new Set(['north', 'east', 'south', 'west']) },
// FIXME these should be additive/subtractive, but a track picked up from the level should replace
'railroad/straight': { name: 'railroad', tracks: 1 << 5, track_switch: null, entered_direction: 'north' },
'railroad/curve': { name: 'railroad', tracks: 1 << 0, track_switch: null, entered_direction: 'north' },
'railroad/switch': { name: 'railroad', tracks: 0, track_switch: 0, entered_direction: 'north' },
'logic_gate/not': { name: 'logic_gate', direction: 'north', gate_type: 'not' },
'logic_gate/and': { name: 'logic_gate', direction: 'north', gate_type: 'and' },
'logic_gate/or': { name: 'logic_gate', direction: 'north', gate_type: 'or' },
'logic_gate/xor': { name: 'logic_gate', direction: 'north', gate_type: 'xor' },
'logic_gate/nand': { name: 'logic_gate', direction: 'north', gate_type: 'nand' },
'logic_gate/latch-cw': { name: 'logic_gate', direction: 'north', gate_type: 'latch-cw' },
'logic_gate/latch-ccw': { name: 'logic_gate', direction: 'north', gate_type: 'latch-ccw' },
'logic_gate/counter': { name: 'logic_gate', direction: 'north', gate_type: 'counter' },
};
const _RAILROAD_ROTATED_LEFT = [3, 0, 1, 2, 5, 4];
const _RAILROAD_ROTATED_RIGHT = [1, 2, 3, 0, 5, 4];
const SPECIAL_PALETTE_BEHAVIOR = {
directional_block: {
pick_palette_entry(tile) {
if (tile.arrows.size === 2) {
let [a, b] = tile.arrows.keys();
if (a === DIRECTIONS[b].opposite) {
return 'directional_block/2o';
}
else {
return 'directional_block/2a';
}
}
else {
return `directional_block/${tile.arrows.size}`;
}
},
rotate_left(tile) {
tile.arrows = new Set(Array.from(tile.arrows, arrow => DIRECTIONS[arrow].left));
},
rotate_right(tile) {
tile.arrows = new Set(Array.from(tile.arrows, arrow => DIRECTIONS[arrow].right));
},
},
logic_gate: {
pick_palette_entry(tile) {
return `logic_gate/${tile.gate_type}`;
},
rotate_left(tile) {
if (tile.gate_type !== 'counter') {
tile.direction = DIRECTIONS[tile.direction].left;
}
},
rotate_right(tile) {
if (tile.gate_type !== 'counter') {
tile.direction = DIRECTIONS[tile.direction].right;
}
},
},
railroad: {
pick_palette_entry(tile) {
// This is a little fuzzy, since railroads are compound, but we just go with the first
// one that matches and fall back to the switch if it's empty
if (tile.tracks & 0x30) {
return 'railroad/straight';
}
if (tile.tracks) {
return 'railroad/curve';
}
return 'railroad/switch';
},
rotate_left(tile) {
let new_tracks = 0;
for (let i = 0; i < 6; i++) {
if (tile.tracks & (1 << i)) {
new_tracks |= 1 << _RAILROAD_ROTATED_LEFT[i];
}
}
tile.tracks = new_tracks;
if (tile.track_switch !== null) {
tile.track_switch = _RAILROAD_ROTATED_LEFT[tile.track_switch];
}
if (tile.entered_direction) {
tile.entered_direction = DIRECTIONS[tile.entered_direction].left;
}
},
rotate_right(tile) {
let new_tracks = 0;
for (let i = 0; i < 6; i++) {
if (tile.tracks & (1 << i)) {
new_tracks |= 1 << _RAILROAD_ROTATED_RIGHT[i];
}
}
tile.tracks = new_tracks;
if (tile.track_switch !== null) {
tile.track_switch = _RAILROAD_ROTATED_RIGHT[tile.track_switch];
}
if (tile.entered_direction) {
tile.entered_direction = DIRECTIONS[tile.entered_direction].right;
}
},
},
};
// Fill in some special behavior that boils down to rotating tiles which happen to be encoded as
// different tile types
for (let cycle of [
['force_floor_n', 'force_floor_e', 'force_floor_s', 'force_floor_w'],
['ice_nw', 'ice_ne', 'ice_se', 'ice_sw'],
['swivel_nw', 'swivel_ne', 'swivel_se', 'swivel_sw'],
]) {
for (let [i, name] of cycle.entries()) {
let left = cycle[(i - 1 + cycle.length) % cycle.length];
let right = cycle[(i + 1) % cycle.length];
SPECIAL_PALETTE_BEHAVIOR[name] = {
pick_palette_entry(tile) {
return name;
},
rotate_left(tile) {
tile.type = TILE_TYPES[left];
},
rotate_right(tile) {
tile.type = TILE_TYPES[right];
},
};
}
}
export class Editor extends PrimaryView {
constructor(conductor) {
@ -1047,6 +1190,28 @@ export class Editor extends PrimaryView {
}
setup() {
// Keyboard shortcuts
window.addEventListener('keydown', ev => {
if (! this.active)
return;
if (ev.key === ',') {
if (ev.shiftKey) {
this.rotate_palette_left();
}
else if (this.palette_selection) {
this.rotate_tile_left(this.palette_selection);
}
}
else if (ev.key === '.') {
if (ev.shiftKey) {
this.rotate_palette_right();
}
else if (this.palette_selection) {
this.rotate_tile_right(this.palette_selection);
}
}
});
// Level canvas and mouse handling
this.mouse_op = null;
this.viewport_el.addEventListener('mousedown', ev => {
@ -1234,11 +1399,18 @@ export class Editor extends PrimaryView {
for (let sectiondef of EDITOR_PALETTE) {
let section_el = mk('section');
palette_el.append(mk('h2', sectiondef.title), section_el);
for (let name of sectiondef.tiles) {
let entry = this.renderer.create_tile_type_canvas(name);
entry.setAttribute('data-tile-name', name);
for (let key of sectiondef.tiles) {
let entry;
if (SPECIAL_PALETTE_ENTRIES[key]) {
let tile = SPECIAL_PALETTE_ENTRIES[key];
entry = this.renderer.create_tile_type_canvas(tile.name, tile);
}
else {
entry = this.renderer.create_tile_type_canvas(key);
}
entry.setAttribute('data-palette-key', key);
entry.classList = 'palette-entry';
this.palette[name] = entry;
this.palette[key] = entry;
section_el.append(entry);
}
}
@ -1247,7 +1419,18 @@ export class Editor extends PrimaryView {
if (! entry)
return;
this.select_palette(entry.getAttribute('data-tile-name'));
let key = entry.getAttribute('data-palette-key');
if (SPECIAL_PALETTE_ENTRIES[key]) {
// Tile with preconfigured stuff on it
let tile = Object.assign({}, SPECIAL_PALETTE_ENTRIES[key]);
tile.type = TILE_TYPES[tile.name];
delete tile.name;
this.select_palette(tile);
}
else {
// Regular tile name
this.select_palette(key);
}
});
this.palette_selection = null;
this.select_palette('floor');
@ -1479,10 +1662,8 @@ export class Editor extends PrimaryView {
let name, tile;
if (typeof name_or_tile === 'string') {
name = name_or_tile;
if (this.palette_selection && name === this.palette_selection.type.name)
return;
tile = { type: TILE_TYPES[name] };
if (tile.type.is_actor) {
tile.direction = 'south';
}
@ -1495,26 +1676,63 @@ export class Editor extends PrimaryView {
name = tile.type.name;
}
if (this.palette_selection) {
let entry = this.palette[this.palette_selection.type.name];
if (entry) {
entry.classList.remove('--selected');
}
// Deselect any previous selection
if (this.palette_selection_el) {
this.palette_selection_el.classList.remove('--selected');
}
// Store the tile
this.palette_selection = tile;
if (this.palette[name]) {
this.palette[name].classList.add('--selected');
// Select it in the palette, if possible
let key = name;
if (SPECIAL_PALETTE_BEHAVIOR[name]) {
key = SPECIAL_PALETTE_BEHAVIOR[name].pick_palette_entry(tile);
}
this.palette_selection_el = this.palette[key] ?? null;
if (this.palette_selection_el) {
this.palette_selection_el.classList.add('--selected');
}
this.mark_tile_dirty(tile);
// Some tools obviously don't work with a palette selection, in which case changing tiles
// should default you back to the pencil
if (this.current_tool === 'adjust') {
if (! EDITOR_TOOLS[this.current_tool].uses_palette) {
this.select_tool('pencil');
}
}
rotate_tile_left(tile) {
if (SPECIAL_PALETTE_BEHAVIOR[tile.type.name]) {
SPECIAL_PALETTE_BEHAVIOR[tile.type.name].rotate_left(tile);
}
else if (TILE_TYPES[tile.type.name].is_actor) {
tile.direction = DIRECTIONS[tile.direction ?? 'south'].left;
}
else {
return false;
}
this.mark_tile_dirty(tile);
return true;
}
rotate_tile_right(tile) {
if (SPECIAL_PALETTE_BEHAVIOR[tile.type.name]) {
SPECIAL_PALETTE_BEHAVIOR[tile.type.name].rotate_right(tile);
}
else if (TILE_TYPES[tile.type.name].is_actor) {
tile.direction = DIRECTIONS[tile.direction ?? 'south'].right;
}
else {
return false;
}
this.mark_tile_dirty(tile);
return true;
}
rotate_palette_left() {
this.palette_rotation_index += 1;
this.palette_rotation_index %= 4;

View File

@ -391,6 +391,7 @@ const TILE_TYPES = {
],
populate_defaults(me) {
me.tracks = 0; // bitmask of bits 0-5, corresponding to track order above
// FIXME it's possible to have a switch but no tracks...
me.track_switch = null; // null, or 0-5 indicating the active switched track
// If there's already an actor on us, it's treated as though it entered the tile moving
// in this direction, which is given in the save file and defaults to zero i.e. north

View File

@ -1196,6 +1196,57 @@ main.--has-demo .demo-controls {
box-shadow: 0 0 0 1px black, 0 0 0 3px white;
}
.editor-level-browser {
display: grid;
grid: auto-flow auto / repeat(auto-fill, minmax(13em, 1fr)); /* 12em preview wdith + padding */
gap: 0.5em;
width: 70vw;
/* seems to go into the parent's right padding fsr, i guess because the scrollbar is there */
margin-right: 1em;
list-style: none;
}
.editor-level-browser li {
display: grid;
grid:
"preview preview"
"number title"
/ min-content 1fr
;
gap: 0.25em;
padding: 0.5em;
}
.editor-level-browser li:hover {
background: hsl(225, 60%, 85%);
}
.editor-level-browser li > .-preview {
grid-area: preview;
display: flex;
align-items: center;
justify-content: center;
width: 12em;
height: 12em;
margin: auto;
}
.editor-level-browser li > .-preview:empty::before {
content: '···';
display: block;
font-size: 5em;
color: #c0c0c0;
}
.editor-level-browser li > .-preview canvas {
display: block;
max-width: 100%;
max-height: 100%;
}
.editor-level-browser li > .-number {
grid-area: number;
font-size: 2em;
}
.editor-level-browser li > .-title {
grid-area: title;
align-self: center;
}
/* Mini editors for specific tiles with complex properties */
/* FIXME should this stuff be on an overlay container class? */
form.editor-popup-tile-editor {
@ -1271,25 +1322,32 @@ textarea.editor-hint-tile-text {
border: none;
font-family: serif;
}
/* Railroad tracks are... complicated */
ul.editor-railroad-tile-tracks {
display: grid;
grid: auto-flow 3em / repeat(3, 3em);
gap: 0.25em;
}
ul.editor-railroad-tile-tracks input {
/* Class for a list that uses hidden inputs with svg icons, shared by directional blocks and
* railroad tracks */
.editor-tile-editor-svg-parts input {
display: none;
}
ul.editor-railroad-tile-tracks svg {
.editor-tile-editor-svg-parts svg {
display: block;
width: 3em;
fill: none;
stroke: #c0c0c0;
stroke-width: 2;
}
ul.editor-railroad-tile-tracks input:checked + svg {
.editor-tile-editor-svg-parts input:checked + svg {
stroke: hsl(225, 90%, 50%);
}
/* Directional blocks have arrows */
ol.editor-directional-block-tile-arrows {
display: grid;
grid: auto-flow 3em / repeat(3, 3em);
}
/* Railroad tracks are... complicated */
ul.editor-railroad-tile-tracks {
display: grid;
grid: auto-flow 3em / repeat(3, 3em);
gap: 0.25em;
}
ul.editor-railroad-tile-tracks.--switch input:checked + svg {
stroke: hsl(15, 90%, 50%);
}