Split out mouse operations; add camera regions, our first custom feature
This commit is contained in:
parent
8711d87a36
commit
432bb881e6
10
index.html
10
index.html
@ -153,8 +153,14 @@
|
||||
-->
|
||||
</header>
|
||||
<div class="level"><!-- level canvas and any overlays go here --></div>
|
||||
<div class="controls">
|
||||
<nav class="controls">
|
||||
<div id="editor-toolbar">
|
||||
<!-- tools go here -->
|
||||
</div>
|
||||
<button id="editor-share-url" type="button">Share?</button>
|
||||
<div id="editor-tool-help">
|
||||
<strong>Pencil</strong> — <span>Select a tile and draw with the left mouse button. Erase with the right mouse button.</span>
|
||||
</div>
|
||||
<!--
|
||||
<p style>
|
||||
Tip: Right click to color drop.<br>
|
||||
@ -179,7 +185,7 @@
|
||||
map size
|
||||
</pre>
|
||||
-->
|
||||
</div>
|
||||
</nav>
|
||||
<div class="palette"></div>
|
||||
<!-- TODO:
|
||||
controls
|
||||
|
||||
@ -23,8 +23,12 @@ export class StoredLevel {
|
||||
|
||||
// Maps of button positions to trap/cloner positions, as scalar indexes
|
||||
// in the linear cell list
|
||||
// TODO merge these imo
|
||||
this.custom_trap_wiring = {};
|
||||
this.custom_cloner_wiring = {};
|
||||
|
||||
// New LL feature: custom camera regions, as lists of {x, y, width, height}
|
||||
this.camera_regions = [];
|
||||
}
|
||||
|
||||
scalar_to_coords(n) {
|
||||
|
||||
@ -25,57 +25,151 @@ class EditorShareOverlay extends DialogOverlay {
|
||||
}
|
||||
}
|
||||
|
||||
const EDITOR_TOOLS = [{
|
||||
mode: 'pencil',
|
||||
icon: 'icons/tool-pencil.png',
|
||||
name: "Pencil",
|
||||
desc: "Draw individual tiles",
|
||||
/* TODO not implemented
|
||||
}, {
|
||||
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",
|
||||
/* TODO not implemented
|
||||
}, {
|
||||
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; ice tool; map generator?; subtools for select tool (copy, paste, crop)
|
||||
// TODO interesting option: rotate an actor as you draw it by dragging? or hold a key like in
|
||||
// slade when you have some selected?
|
||||
// TODO ah, railroads...
|
||||
*/
|
||||
}];
|
||||
// Stores and controls what the mouse is doing during a movement, mostly by dispatching to functions
|
||||
// defined for the individual tools
|
||||
const MOUSE_BUTTON_MASKS = [1, 4, 2]; // MouseEvent.button/buttons are ordered differently
|
||||
class MouseOperation {
|
||||
constructor(editor, ev, target = null) {
|
||||
this.editor = editor;
|
||||
this.target = target;
|
||||
this.button_mask = MOUSE_BUTTON_MASKS[ev.button];
|
||||
|
||||
// Client coordinates of the initial click
|
||||
this.mx0 = ev.clientX;
|
||||
this.my0 = ev.clientY;
|
||||
|
||||
// Client coordinates of the previous mouse position
|
||||
this.mx1 = ev.clientX;
|
||||
this.my1 = ev.clientY;
|
||||
// Cell coordinates of the previous mouse position
|
||||
[this.gx1, this.gy1] = this.editor.renderer.cell_coords_from_event(ev);
|
||||
// Real cell coordinates (i.e. including fractional position within a cell) of etc
|
||||
[this.gx1f, this.gy1f] = this.editor.renderer.real_cell_coords_from_event(ev);
|
||||
|
||||
this.start();
|
||||
}
|
||||
|
||||
cell(gx, gy) {
|
||||
return this.editor.stored_level.cells[Math.floor(gy)][Math.floor(gx)];
|
||||
}
|
||||
|
||||
do_mousemove(ev) {
|
||||
let [gx1f, gy1f] = this.editor.renderer.real_cell_coords_from_event(ev);
|
||||
|
||||
this.step(ev.clientX, ev.clientY, gx1f, gy1f);
|
||||
|
||||
// Client coordinates of the previous mouse position
|
||||
this.mx1 = ev.clientX;
|
||||
this.my1 = ev.clientY;
|
||||
// Cell coordinates of the previous mouse position
|
||||
[this.gx1, this.gy1] = this.editor.renderer.cell_coords_from_event(ev);
|
||||
// Real cell coordinates (i.e. including fractional position within a cell) of etc
|
||||
this.gx1f = gx1f;
|
||||
this.gy1f = gy1f;
|
||||
}
|
||||
|
||||
do_commit() {
|
||||
this.commit();
|
||||
}
|
||||
|
||||
do_abort() {
|
||||
this.abort();
|
||||
}
|
||||
|
||||
// Implement these
|
||||
start() {}
|
||||
step(x, y) {}
|
||||
commit() {}
|
||||
abort() {}
|
||||
}
|
||||
|
||||
class PanOperation extends MouseOperation {
|
||||
step(mx, my) {
|
||||
this.editor.viewport_el.scrollLeft -= mx - this.mx1;
|
||||
this.editor.viewport_el.scrollTop -= my - this.my1;
|
||||
}
|
||||
}
|
||||
|
||||
class DrawOperation extends MouseOperation {
|
||||
}
|
||||
|
||||
class PencilOperation extends DrawOperation {
|
||||
start() {
|
||||
this.editor.place_in_cell(this.gx1, this.gy1, this.editor.palette_selection);
|
||||
}
|
||||
step(mx, my, gx, gy) {
|
||||
for (let [x, y] of walk_grid(this.gx1f, this.gy1f, gx, gy)) {
|
||||
this.editor.place_in_cell(x, y, this.editor.palette_selection);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class ForceFloorOperation extends DrawOperation {
|
||||
start() {
|
||||
// Begin by placing an all-way force floor under the mouse
|
||||
this.editor.place_in_cell(x, y, 'force_floor_all');
|
||||
}
|
||||
step(mx, my, gx, gy) {
|
||||
// Walk the mouse movement and change each we touch to match the direction we
|
||||
// crossed the border
|
||||
// FIXME occasionally i draw a tetris S kinda shape and both middle parts point
|
||||
// the same direction, but shouldn't
|
||||
let i = 0;
|
||||
let prevx, prevy;
|
||||
for (let [x, y] of walk_grid(this.gx1f, this.gy1f, gx, gy)) {
|
||||
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 = x;
|
||||
prevy = y;
|
||||
continue;
|
||||
}
|
||||
let name;
|
||||
if (x === prevx) {
|
||||
if (y > prevy) {
|
||||
name = 'force_floor_s';
|
||||
}
|
||||
else {
|
||||
name = 'force_floor_n';
|
||||
}
|
||||
}
|
||||
else {
|
||||
if (x > prevx) {
|
||||
name = 'force_floor_e';
|
||||
}
|
||||
else {
|
||||
name = 'force_floor_w';
|
||||
}
|
||||
}
|
||||
|
||||
// The second cell tells us the direction to use for the first, assuming it
|
||||
// had some kind of force floor
|
||||
if (i === 2) {
|
||||
let prevcell = this.editor.stored_level.cells[prevy][prevx];
|
||||
if (prevcell[0].type.name.startsWith('force_floor_')) {
|
||||
prevcell[0].type = TILE_TYPES[name];
|
||||
}
|
||||
}
|
||||
|
||||
// Drawing a loop with force floors creates ice (but not in the previous
|
||||
// cell, obviously)
|
||||
let cell = this.editor.stored_level.cells[y][x];
|
||||
if (cell[0].type.name.startsWith('force_floor_') &&
|
||||
cell[0].type.name !== name)
|
||||
{
|
||||
name = 'ice';
|
||||
}
|
||||
this.editor.place_in_cell(x, y, name);
|
||||
|
||||
prevx = x;
|
||||
prevy = y;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Tiles the "adjust" tool will turn into each other
|
||||
const EDITOR_ADJUST_TOGGLES = {
|
||||
const ADJUST_TOGGLES = {
|
||||
floor_custom_green: 'wall_custom_green',
|
||||
floor_custom_pink: 'wall_custom_pink',
|
||||
floor_custom_yellow: 'wall_custom_yellow',
|
||||
@ -97,7 +191,137 @@ const EDITOR_ADJUST_TOGGLES = {
|
||||
thief_keys: 'thief_tools',
|
||||
thief_tools: 'thief_keys',
|
||||
};
|
||||
// TODO this MUST use a cc2 tileset!
|
||||
class AdjustOperation extends MouseOperation {
|
||||
start() {
|
||||
let cell = this.editor.stored_level.cells[this.gy1][this.gx1];
|
||||
for (let tile of cell) {
|
||||
// Toggle tiles that go in obvious pairs
|
||||
let other = ADJUST_TOGGLES[tile.type.name];
|
||||
if (other) {
|
||||
tile.type = TILE_TYPES[other];
|
||||
}
|
||||
|
||||
// Rotate actors
|
||||
if (TILE_TYPES[tile.type.name].is_actor) {
|
||||
tile.direction = DIRECTIONS[tile.direction ?? 'south'].right;
|
||||
}
|
||||
}
|
||||
}
|
||||
// Adjust tool doesn't support dragging
|
||||
// TODO should it?
|
||||
}
|
||||
|
||||
class CameraOperation extends MouseOperation {
|
||||
start() {
|
||||
this.region = this.editor.stored_level.camera_regions[0];
|
||||
|
||||
// TODO allow resizing it too
|
||||
let rect = this.target.getBoundingClientRect();
|
||||
if (this.mx0 < rect.left + 16 || this.mx0 > rect.right - 16) {
|
||||
this.mode = 'resize';
|
||||
}
|
||||
else if (this.my0 < rect.top + 16 || this.my0 > rect.bottom - 16) {
|
||||
this.mode = 'resize';
|
||||
}
|
||||
else {
|
||||
this.mode = 'move';
|
||||
}
|
||||
|
||||
this.offset_x = 0;
|
||||
this.offset_y = 0;
|
||||
}
|
||||
step(mx, my) {
|
||||
let dx = (mx - this.mx0) / this.editor.conductor.tileset.size_x;
|
||||
let dy = (my - this.my0) / this.editor.conductor.tileset.size_y;
|
||||
this.offset_x = Math.floor(dx + 0.5);
|
||||
this.offset_y = Math.floor(dy + 0.5);
|
||||
|
||||
// Keep it within the map!
|
||||
let stored_level = this.editor.stored_level;
|
||||
this.offset_x = Math.max(- this.region.x, Math.min(stored_level.size_x - this.region.width, this.offset_x));
|
||||
this.offset_y = Math.max(- this.region.y, Math.min(stored_level.size_y - this.region.height, this.offset_y));
|
||||
|
||||
this.target.setAttribute('x', this.region.x + this.offset_x);
|
||||
this.target.setAttribute('y', this.region.y + this.offset_y);
|
||||
}
|
||||
commit() {
|
||||
// Actually edit the underlying region
|
||||
this.region.x += this.offset_x;
|
||||
this.region.y += this.offset_y;
|
||||
}
|
||||
abort() {
|
||||
// Move the element back to its original location
|
||||
this.target.setAttribute('x', this.region.x);
|
||||
this.target.setAttribute('y', this.region.y);
|
||||
}
|
||||
}
|
||||
CameraOperation.TARGET_SELECTOR = '.overlay-camera';
|
||||
|
||||
const EDITOR_TOOLS = {
|
||||
pencil: {
|
||||
icon: 'icons/tool-pencil.png',
|
||||
name: "Pencil",
|
||||
desc: "Draw individual tiles",
|
||||
op1: PencilOperation,
|
||||
//op2: EraseOperation,
|
||||
},
|
||||
line: {
|
||||
// TODO not implemented
|
||||
icon: 'icons/tool-line.png',
|
||||
name: "Line",
|
||||
desc: "Draw straight lines",
|
||||
},
|
||||
box: {
|
||||
// TODO not implemented
|
||||
icon: 'icons/tool-box.png',
|
||||
name: "Box",
|
||||
desc: "Fill a rectangular area with tiles",
|
||||
},
|
||||
fill: {
|
||||
// TODO not implemented
|
||||
icon: 'icons/tool-fill.png',
|
||||
name: "Fill",
|
||||
desc: "Flood-fill an area with tiles",
|
||||
},
|
||||
'force-floors': {
|
||||
icon: 'icons/tool-force-floors.png',
|
||||
name: "Force floors",
|
||||
desc: "Draw force floors in the direction you draw",
|
||||
op1: ForceFloorOperation,
|
||||
},
|
||||
adjust: {
|
||||
icon: 'icons/tool-adjust.png',
|
||||
name: "Adjust",
|
||||
desc: "Toggle blocks and rotate actors",
|
||||
op1: AdjustOperation,
|
||||
},
|
||||
connect: {
|
||||
// TODO not implemented
|
||||
icon: 'icons/tool-connect.png',
|
||||
name: "Connect",
|
||||
desc: "Set up CC1 clone and trap connections",
|
||||
},
|
||||
wire: {
|
||||
// TODO not implemented
|
||||
icon: 'icons/tool-wire.png',
|
||||
name: "Wire",
|
||||
desc: "Draw CC2 wiring",
|
||||
},
|
||||
camera: {
|
||||
icon: 'icons/tool-camera.png',
|
||||
name: "Camera",
|
||||
desc: "Draw and edit custom camera bounds",
|
||||
help: "Draw and edit camera bounds. When the player is within a camera region, the camera will avoid showing anything outside that region. LL only.",
|
||||
op1: CameraOperation,
|
||||
},
|
||||
// TODO text tool; thin walls tool; ice tool; map generator?; subtools for select tool (copy, paste, crop)
|
||||
// TODO interesting option: rotate an actor as you draw it by dragging? or hold a key like in
|
||||
// slade when you have some selected?
|
||||
// TODO ah, railroads...
|
||||
};
|
||||
const EDITOR_TOOL_ORDER = ['pencil', 'force-floors', 'adjust', 'camera'];
|
||||
|
||||
// TODO this MUST use a LL tileset!
|
||||
const EDITOR_PALETTE = [{
|
||||
title: "Basics",
|
||||
tiles: [
|
||||
@ -152,10 +376,13 @@ const EDITOR_PALETTE = [{
|
||||
'teleport_yellow',
|
||||
],
|
||||
}];
|
||||
|
||||
export class Editor extends PrimaryView {
|
||||
constructor(conductor) {
|
||||
super(conductor, document.body.querySelector('main#editor'));
|
||||
|
||||
this.viewport_el = this.root.querySelector('.level');
|
||||
|
||||
// FIXME don't hardcode size here, convey this to renderer some other way
|
||||
this.renderer = new CanvasRenderer(this.conductor.tileset, 32);
|
||||
|
||||
@ -168,154 +395,58 @@ export class Editor extends PrimaryView {
|
||||
// This SVG draws vectors on top of the editor, like monster paths and button connections
|
||||
// FIXME change viewBox in load_level, can't right now because order of ops
|
||||
this.svg_overlay = mk_svg('svg.level-editor-overlay', {viewBox: '0 0 32 32'}, this.connections_g);
|
||||
this.root.querySelector('.level').append(
|
||||
this.renderer.canvas,
|
||||
this.svg_overlay);
|
||||
this.mouse_mode = null;
|
||||
this.mouse_button = null;
|
||||
this.mouse_cell = null;
|
||||
this.renderer.canvas.addEventListener('mousedown', ev => {
|
||||
this.viewport_el.append(this.renderer.canvas, this.svg_overlay);
|
||||
this.mouse_op = null;
|
||||
this.viewport_el.addEventListener('mousedown', ev => {
|
||||
this.cancel_mouse_operation();
|
||||
|
||||
if (ev.button === 0) {
|
||||
// Left button: draw
|
||||
this.mouse_mode = 'draw';
|
||||
this.mouse_button_mask = 1;
|
||||
this.mouse_coords = [ev.clientX, ev.clientY];
|
||||
// Left button: activate tool
|
||||
let op_type = EDITOR_TOOLS[this.current_tool].op1;
|
||||
if (! op_type)
|
||||
return;
|
||||
|
||||
let [x, y] = this.renderer.cell_coords_from_event(ev);
|
||||
this.mouse_cell = [x, y];
|
||||
let target;
|
||||
if (op_type.TARGET_SELECTOR) {
|
||||
target = ev.target.closest(op_type.TARGET_SELECTOR);
|
||||
if (! target)
|
||||
return;
|
||||
}
|
||||
this.mouse_op = new op_type(this, ev, target);
|
||||
ev.preventDefault();
|
||||
ev.stopPropagation();
|
||||
|
||||
if (this.current_tool === 'pencil') {
|
||||
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.type.name];
|
||||
if (other) {
|
||||
tile.type = TILE_TYPES[other];
|
||||
}
|
||||
|
||||
// Rotate actors
|
||||
if (TILE_TYPES[tile.type.name].is_actor) {
|
||||
tile.direction = DIRECTIONS[tile.direction ?? 'south'].right;
|
||||
}
|
||||
}
|
||||
}
|
||||
this.renderer.draw();
|
||||
}
|
||||
else if (ev.button === 1) {
|
||||
// Middle button: pan
|
||||
this.mouse_mode = 'pan';
|
||||
this.mouse_button_mask = 4;
|
||||
this.mouse_coords = [ev.clientX, ev.clientY];
|
||||
// Middle button: always pan
|
||||
this.mouse_op = new PanOperation(this, ev);
|
||||
|
||||
ev.preventDefault();
|
||||
ev.stopPropagation();
|
||||
}
|
||||
});
|
||||
this.renderer.canvas.addEventListener('mousemove', ev => {
|
||||
if (this.mouse_mode === null)
|
||||
this.viewport_el.addEventListener('mousemove', ev => {
|
||||
if (! this.mouse_op)
|
||||
return;
|
||||
// TODO check for the specific button we're holding
|
||||
if ((ev.buttons & this.mouse_button_mask) === 0) {
|
||||
this.mouse_mode = null;
|
||||
if ((ev.buttons & this.mouse_op.button_mask) === 0) {
|
||||
this.cancel_mouse_operation();
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.mouse_mode === 'draw') {
|
||||
// FIXME also fill in a trail between previous cell and here, mousemove is not fired continuously
|
||||
let [x, y] = this.renderer.cell_coords_from_event(ev);
|
||||
if (x === this.mouse_cell[0] && y === this.mouse_cell[1])
|
||||
return;
|
||||
this.mouse_op.do_mousemove(ev);
|
||||
|
||||
// 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)) {
|
||||
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
|
||||
// FIXME occasionally i draw a tetris S kinda shape and both middle parts point
|
||||
// the same direction, but shouldn't
|
||||
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;
|
||||
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';
|
||||
}
|
||||
}
|
||||
|
||||
// The second cell tells us the direction to use for the first, assuming it
|
||||
// had some kind of force floor
|
||||
if (i === 2) {
|
||||
let prevcell = this.stored_level.cells[prevy][prevx];
|
||||
if (prevcell[0].type.name.startsWith('force_floor_')) {
|
||||
prevcell[0].type = TILE_TYPES[name];
|
||||
}
|
||||
}
|
||||
|
||||
// Drawing a loop with force floors creates ice (but not in the previous
|
||||
// cell, obviously)
|
||||
let cell = this.stored_level.cells[cy][cx];
|
||||
if (cell[0].type.name.startsWith('force_floor_') &&
|
||||
cell[0].type.name !== name)
|
||||
{
|
||||
name = 'ice';
|
||||
}
|
||||
this.place_in_cell(cx, cy, name);
|
||||
|
||||
prevx = cx;
|
||||
prevy = cy;
|
||||
}
|
||||
}
|
||||
else if (this.current_tool === 'adjust') {
|
||||
// Adjust tool doesn't support dragging
|
||||
// TODO should it
|
||||
}
|
||||
this.renderer.draw();
|
||||
|
||||
this.mouse_cell = [x, y];
|
||||
}
|
||||
else if (this.mouse_mode === 'pan') {
|
||||
let dx = ev.clientX - this.mouse_coords[0];
|
||||
let dy = ev.clientY - this.mouse_coords[1];
|
||||
this.renderer.canvas.parentNode.scrollLeft -= dx;
|
||||
this.renderer.canvas.parentNode.scrollTop -= dy;
|
||||
this.mouse_coords = [ev.clientX, ev.clientY];
|
||||
}
|
||||
this.renderer.draw();
|
||||
});
|
||||
this.renderer.canvas.addEventListener('mouseup', ev => {
|
||||
this.mouse_mode = null;
|
||||
// TODO should this happen for a mouseup anywhere?
|
||||
this.viewport_el.addEventListener('mouseup', ev => {
|
||||
if (this.mouse_op) {
|
||||
this.mouse_op.do_commit();
|
||||
this.mouse_op = null;
|
||||
}
|
||||
});
|
||||
window.addEventListener('blur', ev => {
|
||||
// Unbind the mouse if the page loses focus
|
||||
this.mouse_mode = null;
|
||||
this.cancel_mouse_operation();
|
||||
});
|
||||
|
||||
// Toolbar buttons
|
||||
@ -336,11 +467,12 @@ export class Editor extends PrimaryView {
|
||||
let toolbox = mk('div.icon-button-set')
|
||||
this.root.querySelector('.controls').append(toolbox);
|
||||
this.tool_button_els = {};
|
||||
for (let tooldef of EDITOR_TOOLS) {
|
||||
for (let toolname of EDITOR_TOOL_ORDER) {
|
||||
let tooldef = EDITOR_TOOLS[toolname];
|
||||
let button = mk(
|
||||
'button', {
|
||||
type: 'button',
|
||||
'data-tool': tooldef.mode,
|
||||
'data-tool': toolname,
|
||||
},
|
||||
mk('img', {
|
||||
src: tooldef.icon,
|
||||
@ -348,7 +480,7 @@ export class Editor extends PrimaryView {
|
||||
title: `${tooldef.name}: ${tooldef.desc}`,
|
||||
}),
|
||||
);
|
||||
this.tool_button_els[tooldef.mode] = button;
|
||||
this.tool_button_els[toolname] = button;
|
||||
toolbox.append(button);
|
||||
}
|
||||
this.current_tool = 'pencil';
|
||||
@ -419,6 +551,11 @@ export class Editor extends PrimaryView {
|
||||
mk_svg('line.overlay-cxn', {x1: sx + 0.5, y1: sy + 0.5, x2: dx + 0.5, y2: dy + 0.5}),
|
||||
);
|
||||
}
|
||||
this.stored_level.camera_regions.push(new DOMRect(0, 0, 10, 10));
|
||||
for (let [i, region] of this.stored_level.camera_regions.entries()) {
|
||||
let el = mk_svg('rect.overlay-camera', {x: region.x, y: region.y, width: region.width, height: region.height});
|
||||
this.connections_g.append(el);
|
||||
}
|
||||
|
||||
this.renderer.set_level(stored_level);
|
||||
if (this.active) {
|
||||
@ -481,6 +618,13 @@ export class Editor extends PrimaryView {
|
||||
cell.sort((a, b) => a.type.draw_layer - b.type.draw_layer);
|
||||
}
|
||||
}
|
||||
|
||||
cancel_mouse_operation() {
|
||||
if (this.mouse_op) {
|
||||
this.mouse_op.do_abort();
|
||||
this.mouse_op = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
@ -42,6 +42,15 @@ export class CanvasRenderer {
|
||||
return [x, y];
|
||||
}
|
||||
|
||||
real_cell_coords_from_event(ev) {
|
||||
let rect = this.canvas.getBoundingClientRect();
|
||||
let scale_x = rect.width / this.canvas.width;
|
||||
let scale_y = rect.height / this.canvas.height;
|
||||
let x = (ev.clientX - rect.x) / scale_x / this.tileset.size_x + this.viewport_x;
|
||||
let y = (ev.clientY - rect.y) / scale_y / this.tileset.size_y + this.viewport_y;
|
||||
return [x, y];
|
||||
}
|
||||
|
||||
// Draw to a canvas using tile coordinates
|
||||
blit(ctx, sx, sy, dx, dy, w = 1, h = w) {
|
||||
let tw = this.tileset.size_x;
|
||||
@ -76,8 +85,24 @@ export class CanvasRenderer {
|
||||
[px, py] = [0, 0];
|
||||
}
|
||||
// Figure out where to start drawing
|
||||
let x0 = Math.max(0, Math.min(this.level.size_x - this.viewport_size_x, px - xmargin));
|
||||
let y0 = Math.max(0, Math.min(this.level.size_y - this.viewport_size_y, py - ymargin));
|
||||
// TODO support overlapping regions better
|
||||
let x0 = px - xmargin;
|
||||
let y0 = py - ymargin;
|
||||
// FIXME editor vs player again ugh, which is goofy since none of this is even relevant;
|
||||
// maybe need to have a separate positioning method
|
||||
if (this.level.stored_level) {
|
||||
for (let region of this.level.stored_level.camera_regions) {
|
||||
if (px >= region.left && px < region.right &&
|
||||
py >= region.top && py < region.bottom)
|
||||
{
|
||||
x0 = Math.max(region.left, Math.min(region.right - this.viewport_size_x, x0));
|
||||
y0 = Math.max(region.top, Math.min(region.bottom - this.viewport_size_y, y0));
|
||||
}
|
||||
}
|
||||
}
|
||||
// Always keep us within the map bounds
|
||||
x0 = Math.max(0, Math.min(this.level.size_x - this.viewport_size_x, x0));
|
||||
y0 = Math.max(0, Math.min(this.level.size_y - this.viewport_size_y, y0));
|
||||
// Round to the pixel grid
|
||||
x0 = Math.floor(x0 * tw + 0.5) / tw;
|
||||
y0 = Math.floor(y0 * th + 0.5) / th;
|
||||
|
||||
27
style.css
27
style.css
@ -792,9 +792,9 @@ main.--has-demo .demo-controls {
|
||||
flex: 1 1 auto;
|
||||
display: grid;
|
||||
grid:
|
||||
"controls level" min-content
|
||||
"palette level" 1fr
|
||||
/ minmax(25%, auto) auto
|
||||
"controls controls" min-content
|
||||
"palette level" 1fr
|
||||
/ minmax(25%, auto) 1fr
|
||||
;
|
||||
gap: 0.5em;
|
||||
|
||||
@ -807,7 +807,12 @@ main.--has-demo .demo-controls {
|
||||
grid-area: level;
|
||||
position: relative;
|
||||
overflow: auto;
|
||||
/* Padding and background make it easier to tell when we're at the edge of the map */
|
||||
/* TODO padding should be half a cell, and svg should respect it too */
|
||||
/* padding: 1em; */
|
||||
background: #202020;
|
||||
}
|
||||
/* SVG overlays */
|
||||
#editor svg.level-editor-overlay {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
@ -817,16 +822,22 @@ main.--has-demo .demo-controls {
|
||||
height: 1024px;
|
||||
/* allow clicks to go through us! */
|
||||
pointer-events: none;
|
||||
}
|
||||
#editor .level-editor-overlay rect.overlay-cxn {
|
||||
|
||||
/* default svg properties */
|
||||
stroke-width: 0.0625;
|
||||
stroke: red;
|
||||
fill: none;
|
||||
}
|
||||
#editor .level-editor-overlay line.overlay-cxn {
|
||||
stroke-width: 0.0625;
|
||||
#editor .level-editor-overlay rect.overlay-cxn {
|
||||
stroke: red;
|
||||
}
|
||||
#editor .level-editor-overlay line.overlay-cxn {
|
||||
stroke: red;
|
||||
}
|
||||
#editor .level-editor-overlay rect.overlay-camera {
|
||||
stroke: #808080;
|
||||
fill: #80808040;
|
||||
pointer-events: auto;
|
||||
}
|
||||
|
||||
#editor .controls {
|
||||
grid-area: controls;
|
||||
|
||||
Loading…
Reference in New Issue
Block a user