Start on editor UI; introduce some tools; implement adjust and force floors

This commit is contained in:
Eevee (Evelyn Woods) 2020-09-10 20:24:20 -06:00
parent 5da3a0f473
commit 7c9bc92627
17 changed files with 329 additions and 38 deletions

View File

Before

Width:  |  Height:  |  Size: 332 B

After

Width:  |  Height:  |  Size: 332 B

View File

Before

Width:  |  Height:  |  Size: 319 B

After

Width:  |  Height:  |  Size: 319 B

BIN
icons/compat-notcc2.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 716 B

BIN
icons/help.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 266 B

BIN
icons/tool-adjust.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 299 B

BIN
icons/tool-bg-selected.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 115 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 158 B

BIN
icons/tool-box.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 155 B

BIN
icons/tool-connect.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 276 B

BIN
icons/tool-fill.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 207 B

BIN
icons/tool-force-floors.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 226 B

BIN
icons/tool-line.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 154 B

BIN
icons/tool-pencil.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 184 B

BIN
icons/tool-wire.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 260 B

View File

@ -106,6 +106,30 @@
</header> </header>
<div class="level"><!-- level canvas and any overlays go here --></div> <div class="level"><!-- level canvas and any overlays go here --></div>
<div class="controls"> <div class="controls">
<!--
<p style>
Tip: Right click to color drop.<br>
Tip: Ctrl-click with terrain to replace only the current tile's terrain, rather than overwriting the whole tile.
</p>
<p>Layer: [all/auto] [terrain] [item] [actor] [overlay]</p>
<p>Actor direction: [north] [south] [east] [west]</p>
<p>[ ] Show connections</p>
<p>[ ] Toggle green objects</p>
<p>[ ] Show monster pathing</p>
<p>[ ] Show circuits???</p>
<pre>
Metadata:
xxx / yyy chips required
Time limit: [____]
Title: [__________]
Author: [__________]
map size
</pre>
-->
</div> </div>
<div class="palette"></div> <div class="palette"></div>
<!-- TODO: <!-- TODO:

View File

@ -1,6 +1,6 @@
// TODO bugs and quirks i'm aware of: // TODO bugs and quirks i'm aware of:
// - steam: if a player character starts on a force floor they won't be able to make any voluntary movements until they are no longer on a force floor // - steam: if a player character starts on a force floor they won't be able to make any voluntary movements until they are no longer on a force floor
import { TICS_PER_SECOND } from './defs.js'; import { DIRECTIONS, TICS_PER_SECOND } from './defs.js';
import * as c2m from './format-c2m.js'; import * as c2m from './format-c2m.js';
import * as dat from './format-dat.js'; import * as dat from './format-dat.js';
import * as format_util from './format-util.js'; import * as format_util from './format-util.js';
@ -639,6 +639,119 @@ class Player extends PrimaryView {
} }
const EDITOR_TOOLS = [{
mode: 'pencil',
icon: 'icons/tool-pencil.png',
name: "Pencil",
desc: "Draw individual tiles",
}, {
mode: 'line',
icon: 'icons/tool-line.png',
name: "Line",
desc: "Draw straight lines",
}, {
mode: 'box',
icon: 'icons/tool-box.png',
name: "Box",
desc: "Fill a rectangular area with tiles",
}, {
mode: 'fill',
icon: 'icons/tool-fill.png',
name: "Fill",
desc: "Flood-fill an area with tiles",
}, {
mode: 'force-floors',
icon: 'icons/tool-force-floors.png',
name: "Force floors",
desc: "Draw force floors in the direction you draw",
}, {
mode: 'adjust',
icon: 'icons/tool-adjust.png',
name: "Adjust",
desc: "Toggle blocks and rotate actors",
}, {
mode: 'connect',
icon: 'icons/tool-connect.png',
name: "Connect",
desc: "Set up CC1 clone and trap connections",
}, {
mode: 'wire',
icon: 'icons/tool-wire.png',
name: "Wire",
desc: "Draw CC2 wiring",
// TODO text tool; thin walls tool; map generator?; subtools for select tool (copy, paste, crop)
}];
// Tiles the "adjust" tool will turn into each other
const EDITOR_ADJUST_TOGGLES = {
floor: 'wall',
wall: 'floor',
floor_custom_green: 'wall_custom_green',
floor_custom_pink: 'wall_custom_pink',
floor_custom_yellow: 'wall_custom_yellow',
floor_custom_blue: 'wall_custom_blue',
wall_custom_green: 'floor_custom_green',
wall_custom_pink: 'floor_custom_pink',
wall_custom_yellow: 'floor_custom_yellow',
wall_custom_blue: 'floor_custom_blue',
fake_floor: 'fake_wall',
fake_wall: 'fake_floor',
wall_invisible: 'wall_appearing',
wall_appearing: 'wall_invisible',
green_floor: 'green_wall',
green_wall: 'green_floor',
green_bomb: 'green_chip',
green_chip: 'green_bomb',
purple_floor: 'purple_wall',
purple_wall: 'purple_floor',
thief_keys: 'thief_tools',
thief_tools: 'thief_keys',
};
// TODO this MUST use a cc2 tileset!
const EDITOR_PALETTE = [{
title: "Our hero",
tiles: ['player'],
}, {
title: "Terrain",
tiles: [
'floor', 'wall', 'hint', 'socket', 'exit',
'popwall',
'fake_floor', 'fake_wall',
'gravel',
'dirt',
'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',
],
}, {
title: "Items",
tiles: [
'chip', 'chip_extra',
'key_blue', 'key_red', 'key_yellow', 'key_green',
'flippers', 'fire_boots', 'cleats', 'suction_boots',
],
}, {
title: "Creatures",
tiles: [
'tank_blue',
'ball',
'fireball',
'glider',
'bug',
'paramecium',
'walker',
'teeth',
'blob',
],
}, {
title: "Mechanisms",
tiles: [
'bomb',
'dirt_block',
'button_red',
'cloner',
'trap',
],
}];
class Editor extends PrimaryView { class Editor extends PrimaryView {
constructor(conductor) { constructor(conductor) {
super(conductor, document.body.querySelector('main#editor')); super(conductor, document.body.querySelector('main#editor'));
@ -655,12 +768,38 @@ class Editor extends PrimaryView {
let [x, y] = this.renderer.cell_coords_from_event(ev); let [x, y] = this.renderer.cell_coords_from_event(ev);
this.mouse_cell = [x, y]; this.mouse_cell = [x, y];
if (this.current_tool === 'pencil') {
this.place_in_cell(x, y, this.palette_selection); this.place_in_cell(x, y, this.palette_selection);
}
else if (this.current_tool === 'force-floors') {
// Begin by placing an all-way force floor under the mouse
this.place_in_cell(x, y, 'force_floor_all');
}
else if (this.current_tool === 'adjust') {
let cell = this.stored_level.cells[y][x];
for (let tile of cell) {
// Toggle tiles that go in obvious pairs
let other = EDITOR_ADJUST_TOGGLES[tile.name];
if (other) {
tile.name = other;
}
// Rotate actors
if (TILE_TYPES[tile.name].is_actor) {
tile.direction = DIRECTIONS[tile.direction].right;
}
}
}
this.renderer.draw(); this.renderer.draw();
}); });
this.renderer.canvas.addEventListener('mousemove', ev => { this.renderer.canvas.addEventListener('mousemove', ev => {
if (this.mouse_mode === null) if (this.mouse_mode === null)
return; return;
// TODO check for the specific button we're holding
if (ev.buttons === 0) {
this.mouse_mode = null;
return;
}
if (this.mouse_mode === 'draw') { if (this.mouse_mode === 'draw') {
// FIXME also fill in a trail between previous cell and here, mousemove is not fired continuously // FIXME also fill in a trail between previous cell and here, mousemove is not fired continuously
@ -669,9 +808,65 @@ class Editor extends PrimaryView {
return; return;
// TODO do a pixel-perfect draw too // TODO do a pixel-perfect draw too
if (this.current_tool === 'pencil') {
for (let [cx, cy] of walk_grid(this.mouse_cell[0], this.mouse_cell[1], x, y)) { for (let [cx, cy] of walk_grid(this.mouse_cell[0], this.mouse_cell[1], x, y)) {
this.place_in_cell(cx, cy, this.palette_selection); this.place_in_cell(cx, cy, this.palette_selection);
} }
}
else if (this.current_tool === 'force-floors') {
// Walk the mouse movement and change each we touch to match the direction we
// crossed the border
let i = 0;
let prevx, prevy;
for (let [cx, cy] of walk_grid(this.mouse_cell[0], this.mouse_cell[1], x, y)) {
i++;
// The very first cell is the one the mouse was already in, and we don't
// have a movement direction yet, so leave that alone
if (i === 1) {
prevx = cx;
prevy = cy;
continue;
}
let name;
let cell = this.stored_level.cells[cy][cx];
if (cell[0].name.startsWith('force_floor_')) {
// Drawing a loop with force floors creates ice
name = 'ice';
}
else if (cx === prevx) {
if (cy > prevy) {
name = 'force_floor_s';
}
else {
name = 'force_floor_n';
}
}
else {
if (cx > prevx) {
name = 'force_floor_e';
}
else {
name = 'force_floor_w';
}
}
this.place_in_cell(cx, cy, name);
// The second cell tells us the direction to use for the first, assuming it
// had an RFF marking it
if (i === 2) {
let prevcell = this.stored_level.cells[prevy][prevx];
if (prevcell[0].name === 'force_floor_all') {
prevcell[0].name = name;
}
}
prevx = cx;
prevy = cy;
}
}
else if (this.current_tool === 'adjust') {
// Adjust tool doesn't support dragging
// TODO should it
}
this.renderer.draw(); this.renderer.draw();
this.mouse_cell = [x, y]; this.mouse_cell = [x, y];
@ -685,30 +880,42 @@ class Editor extends PrimaryView {
this.mouse_mode = null; this.mouse_mode = null;
}); });
// Toolbox
let toolbox = mk('div.icon-button-set')
this.root.querySelector('.controls').append(toolbox);
this.tool_button_els = {};
for (let tooldef of EDITOR_TOOLS) {
let button = mk(
'button', {
type: 'button',
'data-tool': tooldef.mode,
},
mk('img', {
src: tooldef.icon,
alt: tooldef.name,
title: `${tooldef.name}: ${tooldef.desc}`,
}),
);
this.tool_button_els[tooldef.mode] = button;
toolbox.append(button);
}
this.current_tool = 'pencil';
this.tool_button_els['pencil'].classList.add('-selected');
toolbox.addEventListener('click', ev => {
let button = ev.target.closest('.icon-button-set button');
if (! button)
return;
this.select_tool(button.getAttribute('data-tool'));
});
// Tile palette // Tile palette
let palette_el = this.root.querySelector('.palette'); let palette_el = this.root.querySelector('.palette');
this.palette = {}; // name => element this.palette = {}; // name => element
for (let name of [ for (let sectiondef of EDITOR_PALETTE) {
// Terrain let section_el = mk('section');
'floor', 'wall', 'hint', 'socket', 'exit', palette_el.append(mk('h2', sectiondef.title), section_el);
'popwall', for (let name of sectiondef.tiles) {
'fake_floor', 'fake_wall',
'water', 'fire', 'ice', 'force_floor_all',
// TODO ice curves
// Items
'chip', // 'chip_extra', -- XXX doesn't exist in TW tileset!
'key_blue', 'key_red', 'key_yellow', 'key_green',
// Creatures
'ball', 'player',
'dirt_block',
'clone_block',
'cloner',
'bomb',
'button_red',
'tank_blue',
'turtle',
])
{
let entry = mk('canvas.palette-entry', { let entry = mk('canvas.palette-entry', {
width: this.conductor.tileset.size_x, width: this.conductor.tileset.size_x,
height: this.conductor.tileset.size_y, height: this.conductor.tileset.size_y,
@ -717,7 +924,8 @@ class Editor extends PrimaryView {
let ctx = entry.getContext('2d'); let ctx = entry.getContext('2d');
this.conductor.tileset.draw_type(name, null, null, ctx, 0, 0); this.conductor.tileset.draw_type(name, null, null, ctx, 0, 0);
this.palette[name] = entry; this.palette[name] = entry;
palette_el.append(entry); section_el.append(entry);
}
} }
palette_el.addEventListener('click', ev => { palette_el.addEventListener('click', ev => {
let entry = ev.target.closest('canvas.palette-entry'); let entry = ev.target.closest('canvas.palette-entry');
@ -759,6 +967,17 @@ class Editor extends PrimaryView {
} }
} }
select_tool(tool) {
if (tool === this.current_tool)
return;
if (! this.tool_button_els[tool])
return;
this.tool_button_els[this.current_tool].classList.remove('-selected');
this.current_tool = tool;
this.tool_button_els[this.current_tool].classList.add('-selected');
}
select_palette(name) { select_palette(name) {
if (name === this.palette_selection) if (name === this.palette_selection)
return; return;
@ -770,6 +989,12 @@ class Editor extends PrimaryView {
if (this.palette_selection) { if (this.palette_selection) {
this.palette[this.palette_selection].classList.add('--selected'); this.palette[this.palette_selection].classList.add('--selected');
} }
// 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') {
this.select_tool('pencil');
}
} }
place_in_cell(x, y, name) { place_in_cell(x, y, name) {
@ -1012,11 +1237,11 @@ class OptionsOverlay extends DialogOverlay {
label.append(mk('input', {type: 'checkbox', name: optdef.key})); label.append(mk('input', {type: 'checkbox', name: optdef.key}));
if (optdef.impls) { if (optdef.impls) {
for (let impl of optdef.impls) { for (let impl of optdef.impls) {
label.append(mk('img.compat-icon', {src: `compat-${impl}.png`})); label.append(mk('img.compat-icon', {src: `icons/compat-${impl}.png`}));
} }
} }
label.append(mk('span.option-label', optdef.label)); label.append(mk('span.option-label', optdef.label));
let help_icon = mk('img.-help', {src: 'help.png'}); let help_icon = mk('img.-help', {src: 'icons/help.png'});
label.append(help_icon); label.append(help_icon);
let help_text = mk('p.option-help', optdef.note); let help_text = mk('p.option-help', optdef.note);
li.append(label); li.append(label);

View File

@ -469,7 +469,7 @@ dl.score-chart .-sum {
.inventory img { .inventory img {
width: calc(var(--tile-width) * var(--scale)); width: calc(var(--tile-width) * var(--scale));
} }
.controls { #player .controls {
grid-area: controls; grid-area: controls;
display: flex; display: flex;
} }
@ -528,8 +528,11 @@ main.--has-demo .demo-controls {
flex: 1 1 auto; flex: 1 1 auto;
display: grid; display: grid;
grid: grid:
"level palette" "controls level"
"palette level"
/ minmax(20%, 1fr) auto
; ;
gap: 1em;
min-height: 0; min-height: 0;
margin: auto; margin: auto;
@ -541,9 +544,48 @@ main.--has-demo .demo-controls {
overflow: auto; overflow: auto;
} }
#editor .controls {
grid-area: controls;
}
#editor .controls p img {
background: url(icons/tool-bg-unselected.png);
}
.icon-button-set {
display: flex;
flex-wrap: wrap;
}
.icon-button-set button {
width: auto;
height: auto;
padding: 0;
margin: 0;
line-height: 1;
background: url(icons/tool-bg-unselected.png) no-repeat;
border: none;
}
.icon-button-set button.-selected {
background-image: url(icons/tool-bg-selected.png);
}
.icon-button-set button img {
display: block;
}
#editor .palette { #editor .palette {
grid-area: palette; grid-area: palette;
} }
#editor .palette h2 {
font-size: 1em;
margin-top: 1em;
border-bottom: 1px solid currentColor;
color: #909090;
}
#editor .palette h2:first-child {
margin-top: 0;
}
#editor .palette section {
display: grid;
grid: auto-flow 32px / repeat(auto-fit, 32px);
gap: 3px;
}
.palette-entry { .palette-entry {
margin: 0.25em; margin: 0.25em;
} }