Add undo/redo support to the editor
This commit is contained in:
parent
884d6d9164
commit
acfad66974
@ -13,19 +13,41 @@ class TileEditorOverlay extends TransientOverlay {
|
||||
edit_tile(tile, cell) {
|
||||
this.tile = tile;
|
||||
this.cell = cell;
|
||||
|
||||
this.needs_undo_entry = false;
|
||||
}
|
||||
|
||||
// Please call this BEFORE actually modifying the tile; it's important for undo!
|
||||
mark_dirty() {
|
||||
if (this.cell) {
|
||||
if (! this.needs_undo_entry) {
|
||||
// We are ABOUT to mutate this tile for the first time; swap it out with a clone in
|
||||
// preparation for making an undo entry when this overlay closes
|
||||
this.pristine_tile = this.tile;
|
||||
this.tile = {...this.tile};
|
||||
this.cell[this.tile.type.layer] = this.tile;
|
||||
this.needs_undo_entry = true;
|
||||
}
|
||||
this.editor.mark_cell_dirty(this.cell);
|
||||
}
|
||||
else {
|
||||
// TODO i guess i'm just kind of assuming it's the palette selection, but it doesn't
|
||||
// necessarily have to be, if i... do something later
|
||||
this.editor.redraw_palette_selection();
|
||||
// The change hasn't happened yet! Don't redraw until we return to the event loop
|
||||
setTimeout(() => this.editor.redraw_palette_selection(), 0);
|
||||
}
|
||||
}
|
||||
|
||||
close() {
|
||||
if (this.needs_undo_entry) {
|
||||
// This will be a no-op the first time since the tile was already swapped, but it's
|
||||
// important for redo
|
||||
this.editor._assign_tile(this.cell, this.tile.type.layer, this.tile, this.pristine_tile);
|
||||
this.editor.commit_undo();
|
||||
}
|
||||
super.close();
|
||||
}
|
||||
|
||||
static configure_tile_defaults(tile) {
|
||||
// FIXME maybe this should be on the tile type, so it functions as documentation there?
|
||||
}
|
||||
@ -57,8 +79,8 @@ class LetterTileEditor extends TileEditorOverlay {
|
||||
|
||||
list.addEventListener('change', ev => {
|
||||
if (this.tile) {
|
||||
this.tile.overlaid_glyph = this.root.elements['glyph'].value;
|
||||
this.mark_dirty();
|
||||
this.tile.overlaid_glyph = this.root.elements['glyph'].value;
|
||||
}
|
||||
});
|
||||
}
|
||||
@ -150,13 +172,13 @@ class FrameBlockTileEditor extends TileEditorOverlay {
|
||||
if (! this.tile)
|
||||
return;
|
||||
|
||||
this.mark_dirty();
|
||||
if (ev.target.checked) {
|
||||
this.tile.arrows.add(ev.target.value);
|
||||
}
|
||||
else {
|
||||
this.tile.arrows.delete(ev.target.value);
|
||||
}
|
||||
this.mark_dirty();
|
||||
});
|
||||
this.root.append(arrow_list);
|
||||
}
|
||||
@ -201,7 +223,10 @@ class RailroadTileEditor extends TileEditorOverlay {
|
||||
track_list.append(mk('li', mk('label', input, svg_icons[i])));
|
||||
}
|
||||
track_list.addEventListener('change', ev => {
|
||||
if (this.tile) {
|
||||
if (! this.tile)
|
||||
return;
|
||||
|
||||
this.mark_dirty();
|
||||
let bit = 1 << ev.target.value;
|
||||
if (ev.target.checked) {
|
||||
this.tile.tracks |= bit;
|
||||
@ -209,8 +234,6 @@ class RailroadTileEditor extends TileEditorOverlay {
|
||||
else {
|
||||
this.tile.tracks &= ~bit;
|
||||
}
|
||||
this.mark_dirty();
|
||||
}
|
||||
});
|
||||
this.root.append(track_list);
|
||||
|
||||
@ -224,8 +247,8 @@ class RailroadTileEditor extends TileEditorOverlay {
|
||||
// TODO if they pick a track that's missing it should add it
|
||||
switch_list.addEventListener('change', ev => {
|
||||
if (this.tile) {
|
||||
this.tile.track_switch = parseInt(ev.target.value, 10);
|
||||
this.mark_dirty();
|
||||
this.tile.track_switch = parseInt(ev.target.value, 10);
|
||||
}
|
||||
});
|
||||
this.root.append(switch_list);
|
||||
@ -260,6 +283,6 @@ export const TILES_WITH_PROPS = {
|
||||
railroad: RailroadTileEditor,
|
||||
// TODO various wireable tiles (hmm not sure how that ui works)
|
||||
// TODO initial value of counter
|
||||
// TODO cloner arrows
|
||||
// TODO cloner arrows (should this be automatic unless you set them explicitly?)
|
||||
// TODO later, custom floor/wall selection
|
||||
};
|
||||
|
||||
@ -270,6 +270,7 @@ class EditorLevelBrowserOverlay extends DialogOverlay {
|
||||
},
|
||||
});
|
||||
|
||||
// FIXME ring buffer?
|
||||
this.undo_stack = [];
|
||||
|
||||
// Left buttons
|
||||
@ -647,11 +648,8 @@ class PencilOperation extends DrawOperation {
|
||||
if (this.alt_mode) {
|
||||
// Erase
|
||||
if (this.modifier === 'shift') {
|
||||
// Aggressive mode: erase the entire cell
|
||||
for (let layer = 0; layer < LAYERS.MAX; layer++) {
|
||||
cell[layer] = null;
|
||||
}
|
||||
cell[LAYERS.terrain] = {type: TILE_TYPES.floor};
|
||||
let new_cell = this.editor.make_blank_cell(x, y);
|
||||
this.editor.replace_cell(cell, new_cell);
|
||||
}
|
||||
else if (template) {
|
||||
// Erase whatever's on the same layer
|
||||
@ -664,24 +662,26 @@ class PencilOperation extends DrawOperation {
|
||||
return;
|
||||
if (this.modifier === 'shift') {
|
||||
// Aggressive mode: erase whatever's already in the cell
|
||||
for (let layer = 0; layer < LAYERS.MAX; layer++) {
|
||||
cell[layer] = null;
|
||||
}
|
||||
let type = this.editor.palette_selection.type;
|
||||
if (type.layer !== LAYERS.terrain) {
|
||||
cell[LAYERS.terrain] = {type: TILE_TYPES.floor};
|
||||
}
|
||||
this.editor.place_in_cell(x, y, template);
|
||||
let new_cell = this.editor.make_blank_cell(x, y);
|
||||
new_cell[template.type.layer] = {...template};
|
||||
this.editor.replace_cell(cell, new_cell);
|
||||
}
|
||||
else {
|
||||
// Default operation: only erase whatever's on the same layer
|
||||
this.editor.place_in_cell(x, y, template);
|
||||
this.editor.place_in_cell(cell, template);
|
||||
}
|
||||
}
|
||||
this.editor.mark_cell_dirty(cell);
|
||||
}
|
||||
|
||||
cleanup() {
|
||||
this.editor.commit_undo();
|
||||
}
|
||||
}
|
||||
|
||||
// TODO also, delete
|
||||
// TODO also, non-rectangular selections
|
||||
// TODO also, better marching ants, hard to see on gravel
|
||||
// TODO press esc to cancel pending selection
|
||||
class SelectOperation extends MouseOperation {
|
||||
start() {
|
||||
if (! this.editor.selection.is_empty && this.editor.selection.contains(this.gx0, this.gy0)) {
|
||||
@ -700,7 +700,11 @@ class SelectOperation extends MouseOperation {
|
||||
}
|
||||
step(mx, my, gxf, gyf, gx, gy) {
|
||||
if (this.mode === 'float') {
|
||||
if (! this.has_moved) {
|
||||
if (this.has_moved) {
|
||||
this.editor.selection.move_by(Math.floor(gx - this.gx1), Math.floor(gy - this.gy1));
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.make_copy) {
|
||||
if (this.editor.selection.is_floating) {
|
||||
// Stamp the floating selection but keep it floating
|
||||
@ -714,12 +718,9 @@ class SelectOperation extends MouseOperation {
|
||||
this.editor.selection.enfloat();
|
||||
}
|
||||
}
|
||||
this.editor.selection.move_by(Math.floor(gx - this.gx1), Math.floor(gy - this.gy1));
|
||||
}
|
||||
else {
|
||||
this.update_pending_selection();
|
||||
}
|
||||
|
||||
this.has_moved = true;
|
||||
}
|
||||
|
||||
@ -729,8 +730,20 @@ class SelectOperation extends MouseOperation {
|
||||
|
||||
commit() {
|
||||
if (this.mode === 'float') {
|
||||
// Make selection move undoable
|
||||
let dx = Math.floor(this.gx1 - this.gx0);
|
||||
let dy = Math.floor(this.gy1 - this.gy0);
|
||||
if (dx || dy) {
|
||||
this.editor._done(
|
||||
() => this.editor.selection.move_by(dx, dy),
|
||||
() => this.editor.selection.move_by(-dx, -dy),
|
||||
);
|
||||
}
|
||||
}
|
||||
else {
|
||||
// If there's an existing floating selection (which isn't what we're operating on),
|
||||
// commit it before doing anything else
|
||||
this.editor.selection.defloat();
|
||||
if (! this.has_moved) {
|
||||
// Plain click clears selection
|
||||
this.pending_selection.discard();
|
||||
@ -740,10 +753,11 @@ class SelectOperation extends MouseOperation {
|
||||
this.pending_selection.commit();
|
||||
}
|
||||
}
|
||||
this.editor.commit_undo();
|
||||
}
|
||||
abort() {
|
||||
if (this.mode === 'float') {
|
||||
// Nothing to do really
|
||||
// FIXME revert the move?
|
||||
}
|
||||
else {
|
||||
this.pending_selection.discard();
|
||||
@ -754,7 +768,7 @@ class SelectOperation extends MouseOperation {
|
||||
class ForceFloorOperation extends DrawOperation {
|
||||
start() {
|
||||
// Begin by placing an all-way force floor under the mouse
|
||||
this.editor.place_in_cell(this.gx0, this.gy0, {type: TILE_TYPES.force_floor_all});
|
||||
this.editor.place_in_cell(this.cell(this.gx0, this.gy0), {type: TILE_TYPES.force_floor_all});
|
||||
}
|
||||
step(mx, my, gxf, gyf) {
|
||||
// Walk the mouse movement and change each we touch to match the direction we
|
||||
@ -795,7 +809,7 @@ class ForceFloorOperation extends DrawOperation {
|
||||
if (i === 2) {
|
||||
let prevcell = this.editor.cell(prevx, prevy);
|
||||
if (prevcell[LAYERS.terrain].type.name.startsWith('force_floor_')) {
|
||||
this.editor.place_in_cell(prevcell.x, prevcell.y, {type: TILE_TYPES[name]});
|
||||
this.editor.place_in_cell(prevcell, {type: TILE_TYPES[name]});
|
||||
}
|
||||
}
|
||||
|
||||
@ -807,12 +821,15 @@ class ForceFloorOperation extends DrawOperation {
|
||||
{
|
||||
name = 'ice';
|
||||
}
|
||||
this.editor.place_in_cell(x, y, {type: TILE_TYPES[name]});
|
||||
this.editor.place_in_cell(cell, {type: TILE_TYPES[name]});
|
||||
|
||||
prevx = x;
|
||||
prevy = y;
|
||||
}
|
||||
}
|
||||
cleanup() {
|
||||
this.editor.commit_undo();
|
||||
}
|
||||
}
|
||||
|
||||
// TODO entered cell should get blank railroad?
|
||||
@ -878,23 +895,24 @@ class TrackOperation extends DrawOperation {
|
||||
let cell = this.cell(prevx, prevy);
|
||||
let terrain = cell[0];
|
||||
if (terrain.type.name === 'railroad') {
|
||||
let new_terrain = {...terrain};
|
||||
if (this.alt_mode) {
|
||||
// Erase
|
||||
// TODO fix track switch?
|
||||
// TODO if this leaves tracks === 0, replace with floor?
|
||||
terrain.tracks &= ~bit;
|
||||
new_terrain.tracks &= ~bit;
|
||||
}
|
||||
else {
|
||||
// Draw
|
||||
terrain.tracks |= bit;
|
||||
new_terrain.tracks |= bit;
|
||||
}
|
||||
this.editor.mark_cell_dirty(cell);
|
||||
this.editor.place_in_cell(cell, new_terrain);
|
||||
}
|
||||
else if (! this.alt_mode) {
|
||||
terrain = { type: TILE_TYPES['railroad'] };
|
||||
terrain.type.populate_defaults(terrain);
|
||||
terrain.tracks |= bit;
|
||||
this.editor.place_in_cell(prevx, prevy, terrain);
|
||||
this.editor.place_in_cell(cell, terrain);
|
||||
}
|
||||
|
||||
prevx = x;
|
||||
@ -902,6 +920,9 @@ class TrackOperation extends DrawOperation {
|
||||
this.entry_direction = DIRECTIONS[exit_direction].opposite;
|
||||
}
|
||||
}
|
||||
cleanup() {
|
||||
this.editor.commit_undo();
|
||||
}
|
||||
}
|
||||
|
||||
class WireOperation extends DrawOperation {
|
||||
@ -936,15 +957,16 @@ class WireOperation extends DrawOperation {
|
||||
|
||||
let terrain = cell[LAYERS.terrain];
|
||||
if (terrain.type.name === 'floor') {
|
||||
terrain = {...terrain};
|
||||
if (this.alt_mode) {
|
||||
terrain.wire_tunnel_directions &= ~bit;
|
||||
}
|
||||
else {
|
||||
terrain.wire_tunnel_directions |= bit;
|
||||
}
|
||||
this.editor.mark_cell_dirty(cell);
|
||||
this.editor.place_in_cell(cell, terrain);
|
||||
this.editor.commit_undo();
|
||||
}
|
||||
return;
|
||||
}
|
||||
}
|
||||
step(mx, my, gxf, gyf) {
|
||||
@ -1053,6 +1075,7 @@ class WireOperation extends DrawOperation {
|
||||
if (['floor', 'steel', 'button_pink', 'button_black', 'teleport_blue', 'teleport_red', 'light_switch_on', 'light_switch_off', 'circuit_block'].indexOf(tile.type.name) < 0)
|
||||
continue;
|
||||
|
||||
tile = {...tile};
|
||||
tile.wire_directions = tile.wire_directions ?? 0;
|
||||
if (this.alt_mode) {
|
||||
// Erase
|
||||
@ -1062,7 +1085,7 @@ class WireOperation extends DrawOperation {
|
||||
// Draw
|
||||
tile.wire_directions |= DIRECTIONS[wire_direction].bit;
|
||||
}
|
||||
this.editor.mark_cell_dirty(cell);
|
||||
this.editor.place_in_cell(cell, tile);
|
||||
break;
|
||||
}
|
||||
|
||||
@ -1070,6 +1093,9 @@ class WireOperation extends DrawOperation {
|
||||
prevqy = qy;
|
||||
}
|
||||
}
|
||||
cleanup() {
|
||||
this.editor.commit_undo();
|
||||
}
|
||||
}
|
||||
|
||||
// Tiles the "adjust" tool will turn into each other
|
||||
@ -1098,6 +1124,7 @@ const ADJUST_TOGGLES_CCW = {};
|
||||
['no_player1_sign', 'no_player2_sign'],
|
||||
['flame_jet_off', 'flame_jet_on'],
|
||||
['light_switch_off', 'light_switch_on'],
|
||||
['stopwatch_bonus', 'stopwatch_penalty'],
|
||||
])
|
||||
{
|
||||
for (let [i, tile] of cycle.entries()) {
|
||||
@ -1127,6 +1154,7 @@ class AdjustOperation extends MouseOperation {
|
||||
continue;
|
||||
|
||||
let rotated;
|
||||
tile = {...tile}; // TODO little inefficient
|
||||
if (this.alt_mode) {
|
||||
// Reverse, go counterclockwise
|
||||
rotated = this.editor.rotate_tile_left(tile);
|
||||
@ -1135,7 +1163,8 @@ class AdjustOperation extends MouseOperation {
|
||||
rotated = this.editor.rotate_tile_right(tile);
|
||||
}
|
||||
if (rotated) {
|
||||
this.editor.mark_cell_dirty(cell);
|
||||
this.editor.place_in_cell(cell, tile);
|
||||
this.editor.commit_undo();
|
||||
break;
|
||||
}
|
||||
|
||||
@ -1143,7 +1172,8 @@ class AdjustOperation extends MouseOperation {
|
||||
let other = (this.alt_mode ? ADJUST_TOGGLES_CCW : ADJUST_TOGGLES_CW)[tile.type.name];
|
||||
if (other) {
|
||||
tile.type = TILE_TYPES[other];
|
||||
this.editor.mark_cell_dirty(cell);
|
||||
this.editor.place_in_cell(cell, tile);
|
||||
this.editor.commit_undo();
|
||||
break;
|
||||
}
|
||||
}
|
||||
@ -1154,6 +1184,8 @@ class AdjustOperation extends MouseOperation {
|
||||
}
|
||||
|
||||
// FIXME currently allows creating outside the map bounds and moving beyond the right/bottom, sigh
|
||||
// FIXME undo
|
||||
// TODO view is not especially visible
|
||||
class CameraOperation extends MouseOperation {
|
||||
start(ev) {
|
||||
this.offset_x = 0;
|
||||
@ -2464,12 +2496,26 @@ class Selection {
|
||||
}
|
||||
|
||||
create_pending() {
|
||||
this.defloat();
|
||||
|
||||
return new PendingSelection(this);
|
||||
}
|
||||
|
||||
set_from_rect(rect) {
|
||||
let old_rect = this.rect;
|
||||
this.editor._do(
|
||||
() => this._set_from_rect(rect),
|
||||
() => {
|
||||
if (old_rect) {
|
||||
this._set_from_rect(old_rect);
|
||||
}
|
||||
else {
|
||||
this._clear();
|
||||
}
|
||||
},
|
||||
false,
|
||||
);
|
||||
}
|
||||
|
||||
_set_from_rect(rect) {
|
||||
this.rect = rect;
|
||||
this.element.classList.add('--visible');
|
||||
this.element.setAttribute('x', this.rect.x);
|
||||
@ -2495,28 +2541,42 @@ class Selection {
|
||||
}
|
||||
|
||||
clear() {
|
||||
this.defloat();
|
||||
let rect = this.rect;
|
||||
if (! rect)
|
||||
return;
|
||||
|
||||
this.editor._do(
|
||||
() => this._clear(),
|
||||
() => this._set_from_rect(rect),
|
||||
false,
|
||||
);
|
||||
}
|
||||
|
||||
_clear() {
|
||||
this.rect = null;
|
||||
this.element.classList.remove('--visible');
|
||||
}
|
||||
|
||||
*iter_cells() {
|
||||
*iter_coords() {
|
||||
if (! this.rect)
|
||||
return;
|
||||
|
||||
let stored_level = this.editor.stored_level;
|
||||
for (let x = this.rect.left; x < this.rect.right; x++) {
|
||||
for (let y = this.rect.top; y < this.rect.bottom; y++) {
|
||||
yield [x, y];
|
||||
let n = stored_level.coords_to_scalar(x, y);
|
||||
yield [x, y, n];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Convert this selection into a floating selection, plucking all the selected cells from the
|
||||
// level and replacing them with blank cells.
|
||||
enfloat(copy = false) {
|
||||
if (this.floated_cells)
|
||||
console.error("Trying to float a selection that's already floating");
|
||||
|
||||
this.floated_cells = [];
|
||||
let floated_cells = [];
|
||||
let tileset = this.editor.renderer.tileset;
|
||||
let stored_level = this.editor.stored_level;
|
||||
let bbox = this.rect;
|
||||
@ -2526,24 +2586,33 @@ class Selection {
|
||||
this.editor.renderer.canvas,
|
||||
bbox.x * tileset.size_x, bbox.y * tileset.size_y, bbox.width * tileset.size_x, bbox.height * tileset.size_y,
|
||||
0, 0, bbox.width * tileset.size_x, bbox.height * tileset.size_y);
|
||||
for (let [x, y] of this.iter_cells()) {
|
||||
let n = stored_level.coords_to_scalar(x, y);
|
||||
for (let [x, y, n] of this.iter_coords()) {
|
||||
let cell = stored_level.linear_cells[n];
|
||||
if (copy) {
|
||||
this.floated_cells.push(stored_level.linear_cells[n].map(tile => tile ? {...tile} : null));
|
||||
floated_cells.push(cell.map(tile => tile ? {...tile} : null));
|
||||
}
|
||||
else {
|
||||
this.floated_cells.push(stored_level.linear_cells[n]);
|
||||
stored_level.linear_cells[n] = this.editor._make_cell(x, y);
|
||||
this.editor.mark_cell_dirty(stored_level.linear_cells[n]);
|
||||
floated_cells.push(cell);
|
||||
this.editor.replace_cell(cell, this.editor.make_blank_cell(x, y));
|
||||
}
|
||||
}
|
||||
this.floated_element = mk_svg('g', mk_svg('foreignObject', {
|
||||
let floated_element = mk_svg('g', mk_svg('foreignObject', {
|
||||
x: 0, y: 0,
|
||||
width: canvas.width, height: canvas.height,
|
||||
transform: `scale(${1/tileset.size_x} ${1/tileset.size_y})`,
|
||||
}, canvas));
|
||||
this.floated_element.setAttribute('transform', `translate(${bbox.x} ${bbox.y})`);
|
||||
this.svg_group.append(this.floated_element);
|
||||
floated_element.setAttribute('transform', `translate(${bbox.x} ${bbox.y})`);
|
||||
|
||||
// FIXME far more memory efficient to recreate the canvas in the redo, rather than hold onto
|
||||
// it forever
|
||||
this.editor._do(
|
||||
() => {
|
||||
this.floated_element = floated_element;
|
||||
this.floated_cells = floated_cells;
|
||||
this.svg_group.append(floated_element);
|
||||
},
|
||||
() => this._defloat(),
|
||||
);
|
||||
}
|
||||
|
||||
stamp_float(copy = false) {
|
||||
@ -2553,16 +2622,14 @@ class Selection {
|
||||
let bbox = this.rect;
|
||||
let stored_level = this.editor.stored_level;
|
||||
let i = 0;
|
||||
for (let [x, y] of this.iter_cells()) {
|
||||
let n = stored_level.coords_to_scalar(x, y);
|
||||
for (let [x, y, n] of this.iter_coords()) {
|
||||
let cell = this.floated_cells[i];
|
||||
if (copy) {
|
||||
cell = cell.map(tile => tile ? {...tile} : null);
|
||||
}
|
||||
cell.x = x;
|
||||
cell.y = y;
|
||||
stored_level.linear_cells[n] = cell;
|
||||
this.editor.mark_cell_dirty(cell);
|
||||
this.editor.replace_cell(stored_level.linear_cells[n], cell);
|
||||
i += 1;
|
||||
}
|
||||
}
|
||||
@ -2572,6 +2639,21 @@ class Selection {
|
||||
return;
|
||||
|
||||
this.stamp_float();
|
||||
|
||||
let element = this.floated_element;
|
||||
let cells = this.floated_cells;
|
||||
this.editor._do(
|
||||
() => this._defloat(),
|
||||
() => {
|
||||
this.floated_cells = cells;
|
||||
this.floated_element = element;
|
||||
this.svg_group.append(element);
|
||||
},
|
||||
false,
|
||||
);
|
||||
}
|
||||
|
||||
_defloat() {
|
||||
this.floated_element.remove();
|
||||
this.floated_element = null;
|
||||
this.floated_cells = null;
|
||||
@ -2801,6 +2883,12 @@ export class Editor extends PrimaryView {
|
||||
button_container.append(button);
|
||||
return button;
|
||||
};
|
||||
this.undo_button = _make_button("Undo", ev => {
|
||||
this.undo();
|
||||
});
|
||||
this.redo_button = _make_button("Redo", ev => {
|
||||
this.redo();
|
||||
});
|
||||
_make_button("Pack properties...", ev => {
|
||||
new EditorPackMetaOverlay(this.conductor, this.conductor.stored_game).open();
|
||||
});
|
||||
@ -2965,6 +3053,8 @@ export class Editor extends PrimaryView {
|
||||
this.select_palette('floor', true);
|
||||
|
||||
this.selection = new Selection(this);
|
||||
|
||||
this.reset_undo();
|
||||
}
|
||||
|
||||
activate() {
|
||||
@ -2981,9 +3071,10 @@ export class Editor extends PrimaryView {
|
||||
super.deactivate();
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------------------------------------
|
||||
// Level creation, management, and saving
|
||||
|
||||
_make_cell(x, y) {
|
||||
make_blank_cell(x, y) {
|
||||
let cell = new format_base.StoredCell;
|
||||
cell.x = x;
|
||||
cell.y = y;
|
||||
@ -2998,7 +3089,7 @@ export class Editor extends PrimaryView {
|
||||
stored_level.size_y = size_y;
|
||||
stored_level.viewport_size = 10;
|
||||
for (let i = 0; i < size_x * size_y; i++) {
|
||||
stored_level.linear_cells.push(this._make_cell(...stored_level.scalar_to_coords(i)));
|
||||
stored_level.linear_cells.push(this.make_blank_cell(...stored_level.scalar_to_coords(i)));
|
||||
}
|
||||
stored_level.linear_cells[0][LAYERS.actor] = {type: TILE_TYPES['player'], direction: 'south'};
|
||||
return stored_level;
|
||||
@ -3240,6 +3331,8 @@ export class Editor extends PrimaryView {
|
||||
{
|
||||
this.conductor.level_index += delta;
|
||||
// Update the current level if it's not stored in the metadata yet
|
||||
// FIXME if you delete the level before the current one, this gets decremented twice?
|
||||
// can't seem to reproduce
|
||||
if (! stored_level) {
|
||||
this.conductor.stored_level.index += delta;
|
||||
this.conductor.stored_level.number += delta;
|
||||
@ -3293,6 +3386,7 @@ export class Editor extends PrimaryView {
|
||||
}
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------------------------------------
|
||||
// Level loading
|
||||
|
||||
load_game(stored_game) {
|
||||
@ -3314,6 +3408,7 @@ export class Editor extends PrimaryView {
|
||||
}
|
||||
|
||||
// Load connections
|
||||
// TODO cloners too
|
||||
this.connections_g.textContent = '';
|
||||
for (let [src, dest] of Object.entries(this.stored_level.custom_trap_wiring)) {
|
||||
let [sx, sy] = this.stored_level.scalar_to_coords(src);
|
||||
@ -3337,6 +3432,11 @@ export class Editor extends PrimaryView {
|
||||
if (this.save_button) {
|
||||
this.save_button.disabled = ! this.conductor.stored_game.editor_metadata;
|
||||
}
|
||||
|
||||
if (this._done_setup) {
|
||||
// XXX this doesn't work yet if setup hasn't run because the undo button won't exist
|
||||
this.reset_undo();
|
||||
}
|
||||
}
|
||||
|
||||
update_cell_coordinates() {
|
||||
@ -3351,6 +3451,8 @@ export class Editor extends PrimaryView {
|
||||
this.svg_overlay.setAttribute('viewBox', `0 0 ${this.stored_level.size_x} ${this.stored_level.size_y}`);
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------------------------------------
|
||||
|
||||
open_level_browser() {
|
||||
if (! this._level_browser) {
|
||||
this._level_browser = new EditorLevelBrowserOverlay(this.conductor);
|
||||
@ -3478,7 +3580,8 @@ export class Editor extends PrimaryView {
|
||||
this.palette_actor_direction = DIRECTIONS[this.palette_actor_direction].left;
|
||||
}
|
||||
|
||||
// -- Drawing --
|
||||
// ------------------------------------------------------------------------------------------------
|
||||
// Drawing
|
||||
|
||||
redraw_palette_selection() {
|
||||
// FIXME should redraw in an existing canvas
|
||||
@ -3488,24 +3591,28 @@ export class Editor extends PrimaryView {
|
||||
}
|
||||
|
||||
mark_cell_dirty(cell) {
|
||||
this.mark_point_dirty(cell.x, cell.y);
|
||||
}
|
||||
|
||||
mark_point_dirty(x, y) {
|
||||
if (! this._dirty_rect) {
|
||||
this._dirty_rect = new DOMRect(cell.x, cell.y, 1, 1);
|
||||
this._dirty_rect = new DOMRect(x, y, 1, 1);
|
||||
}
|
||||
else {
|
||||
let rect = this._dirty_rect;
|
||||
if (cell.x < rect.left) {
|
||||
rect.width = rect.right - cell.x;
|
||||
rect.x = cell.x;
|
||||
if (x < rect.left) {
|
||||
rect.width = rect.right - x;
|
||||
rect.x = x;
|
||||
}
|
||||
else if (cell.x >= rect.right) {
|
||||
rect.width = cell.x - rect.left + 1;
|
||||
else if (x >= rect.right) {
|
||||
rect.width = x - rect.left + 1;
|
||||
}
|
||||
if (cell.y < rect.top) {
|
||||
rect.height = rect.bottom - cell.y;
|
||||
rect.y = cell.y;
|
||||
if (y < rect.top) {
|
||||
rect.height = rect.bottom - y;
|
||||
rect.y = y;
|
||||
}
|
||||
else if (cell.y >= rect.bottom) {
|
||||
rect.height = cell.y - rect.top + 1;
|
||||
else if (y >= rect.bottom) {
|
||||
rect.height = y - rect.top + 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -3530,7 +3637,8 @@ export class Editor extends PrimaryView {
|
||||
this._schedule_redraw_loop();
|
||||
}
|
||||
|
||||
// -- Utility/inspection --
|
||||
// ------------------------------------------------------------------------------------------------
|
||||
// Utility/inspection
|
||||
|
||||
is_in_bounds(x, y) {
|
||||
return 0 <= x && x < this.stored_level.size_x && 0 <= y && y < this.stored_level.size_y;
|
||||
@ -3545,39 +3653,39 @@ export class Editor extends PrimaryView {
|
||||
}
|
||||
}
|
||||
|
||||
// -- Mutation --
|
||||
// ------------------------------------------------------------------------------------------------
|
||||
// Mutation
|
||||
|
||||
place_in_cell(x, y, tile) {
|
||||
// DOES NOT commit the undo entry!
|
||||
place_in_cell(cell, tile) {
|
||||
// TODO weird api?
|
||||
if (! tile)
|
||||
return;
|
||||
|
||||
if (! this.selection.contains(x, y))
|
||||
if (! this.selection.contains(cell.x, cell.y))
|
||||
return;
|
||||
|
||||
let cell = this.cell(x, y);
|
||||
this.mark_cell_dirty(cell);
|
||||
// Replace whatever's on the same layer
|
||||
// TODO should preserve wiring if possible too
|
||||
let existing_tile = cell[tile.type.layer];
|
||||
if (existing_tile) {
|
||||
// If we find a tile of the same type as the one being drawn, see if it has custom
|
||||
// combine behavior (only the case if it came from the palette)
|
||||
if (existing_tile.type === tile.type &&
|
||||
let layer = tile.type.layer;
|
||||
let existing_tile = cell[layer];
|
||||
|
||||
// If we find a tile of the same type as the one being drawn, see if it has custom combine
|
||||
// behavior (only the case if it came from the palette)
|
||||
if (existing_tile && existing_tile.type === tile.type &&
|
||||
// FIXME this is hacky garbage
|
||||
tile === this.palette_selection && this.palette_selection_from_palette &&
|
||||
SPECIAL_PALETTE_BEHAVIOR[tile.type.name] &&
|
||||
SPECIAL_PALETTE_BEHAVIOR[tile.type.name].combine_draw)
|
||||
{
|
||||
SPECIAL_PALETTE_BEHAVIOR[tile.type.name].combine_draw(tile, existing_tile);
|
||||
let old_tile = {...existing_tile};
|
||||
let new_tile = existing_tile;
|
||||
SPECIAL_PALETTE_BEHAVIOR[tile.type.name].combine_draw(tile, new_tile);
|
||||
this._assign_tile(cell, layer, new_tile, old_tile);
|
||||
return;
|
||||
}
|
||||
|
||||
// Otherwise erase it
|
||||
cell[tile.type.layer] = null;
|
||||
}
|
||||
|
||||
cell[tile.type.layer] = {...tile};
|
||||
this._assign_tile(cell, layer, {...tile}, existing_tile);
|
||||
}
|
||||
|
||||
erase_tile(cell, tile = null) {
|
||||
@ -3589,10 +3697,10 @@ export class Editor extends PrimaryView {
|
||||
|
||||
this.mark_cell_dirty(cell);
|
||||
let existing_tile = cell[tile.type.layer];
|
||||
if (existing_tile) {
|
||||
// If we find a tile of the same type as the one being drawn, see if it has custom
|
||||
// combine behavior (only the case if it came from the palette)
|
||||
if (existing_tile.type === tile.type &&
|
||||
|
||||
// If we find a tile of the same type as the one being drawn, see if it has custom combine
|
||||
// behavior (only the case if it came from the palette)
|
||||
if (existing_tile && existing_tile.type === tile.type &&
|
||||
// FIXME this is hacky garbage
|
||||
tile === this.palette_selection && this.palette_selection_from_palette &&
|
||||
SPECIAL_PALETTE_BEHAVIOR[tile.type.name] &&
|
||||
@ -3603,17 +3711,154 @@ export class Editor extends PrimaryView {
|
||||
return;
|
||||
}
|
||||
|
||||
// Otherwise erase it
|
||||
cell[tile.type.layer] = null;
|
||||
}
|
||||
|
||||
// Don't allow erasing the floor entirely
|
||||
let new_tile = null;
|
||||
if (tile.type.layer === LAYERS.terrain) {
|
||||
cell[LAYERS.terrain] = {type: TILE_TYPES.floor};
|
||||
new_tile = {type: TILE_TYPES.floor};
|
||||
}
|
||||
|
||||
this._assign_tile(cell, tile.type.layer, new_tile, existing_tile);
|
||||
}
|
||||
|
||||
replace_cell(cell, new_cell) {
|
||||
// Save the coordinates so it doesn't matter what they are when undoing
|
||||
let x = cell.x, y = cell.y;
|
||||
let n = this.stored_level.coords_to_scalar(x, y);
|
||||
this._do(
|
||||
() => {
|
||||
this.stored_level.linear_cells[n] = new_cell;
|
||||
new_cell.x = x;
|
||||
new_cell.y = y;
|
||||
this.mark_cell_dirty(new_cell);
|
||||
},
|
||||
() => {
|
||||
this.stored_level.linear_cells[n] = cell;
|
||||
cell.x = x;
|
||||
cell.y = y;
|
||||
this.mark_cell_dirty(cell);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
resize_level(size_x, size_y, x0 = 0, y0 = 0) {
|
||||
let new_cells = [];
|
||||
for (let y = y0; y < y0 + size_y; y++) {
|
||||
for (let x = x0; x < x0 + size_x; x++) {
|
||||
new_cells.push(this.cell(x, y) ?? this.make_blank_cell(x, y));
|
||||
}
|
||||
}
|
||||
|
||||
// -- Misc?? --
|
||||
let original_cells = this.stored_level.linear_cells;
|
||||
let original_size_x = this.stored_level.size_x;
|
||||
let original_size_y = this.stored_level.size_y;
|
||||
|
||||
this._do(() => {
|
||||
this.stored_level.linear_cells = new_cells;
|
||||
this.stored_level.size_x = size_x;
|
||||
this.stored_level.size_y = size_y;
|
||||
this.update_viewport_size();
|
||||
this.update_cell_coordinates();
|
||||
this.redraw_entire_level();
|
||||
}, () => {
|
||||
this.stored_level.linear_cells = original_cells;
|
||||
this.stored_level.size_x = original_size_x;
|
||||
this.stored_level.size_y = original_size_y;
|
||||
this.update_viewport_size();
|
||||
this.update_cell_coordinates();
|
||||
this.redraw_entire_level();
|
||||
});
|
||||
this.commit_undo();
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------------------------------------
|
||||
// Undo/redo
|
||||
|
||||
_do(redo, undo, modifies = true) {
|
||||
redo();
|
||||
this._done(redo, undo, modifies);
|
||||
}
|
||||
|
||||
_done(redo, undo, modifies = true) {
|
||||
// TODO parallel arrays would be smaller
|
||||
this.undo_entry.push([undo, redo]);
|
||||
|
||||
if (this.redo_stack.length > 0) {
|
||||
this.redo_stack.length = 0;
|
||||
}
|
||||
}
|
||||
|
||||
_assign_tile(cell, layer, new_tile, old_tile) {
|
||||
this._do(
|
||||
() => {
|
||||
cell[layer] = new_tile;
|
||||
this.mark_cell_dirty(cell);
|
||||
},
|
||||
() => {
|
||||
cell[layer] = old_tile;
|
||||
this.mark_cell_dirty(cell);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
reset_undo() {
|
||||
this.undo_entry = [];
|
||||
this.undo_stack = [];
|
||||
this.redo_stack = [];
|
||||
this._update_undo_redo_enabled();
|
||||
}
|
||||
|
||||
undo() {
|
||||
// We shouldn't really have an uncommitted entry lying around at a time when the user can
|
||||
// click the undo button, but just in case, prefer that to the undo stack
|
||||
let entry;
|
||||
if (this.undo_entry.length > 0) {
|
||||
entry = this.undo_entry;
|
||||
this.undo_entry = [];
|
||||
}
|
||||
else if (this.undo_stack.length > 0) {
|
||||
entry = this.undo_stack.pop();
|
||||
this.redo_stack.push(entry);
|
||||
}
|
||||
else {
|
||||
return;
|
||||
}
|
||||
|
||||
for (let i = entry.length - 1; i >= 0; i--) {
|
||||
entry[i][0]();
|
||||
}
|
||||
|
||||
this._update_undo_redo_enabled();
|
||||
}
|
||||
|
||||
redo() {
|
||||
if (this.redo_stack.length === 0)
|
||||
return;
|
||||
|
||||
this.commit_undo();
|
||||
let entry = this.redo_stack.pop();
|
||||
this.undo_stack.push(entry);
|
||||
for (let [undo, redo] of entry) {
|
||||
redo();
|
||||
}
|
||||
|
||||
this._update_undo_redo_enabled();
|
||||
}
|
||||
|
||||
commit_undo() {
|
||||
if (this.undo_entry.length > 0) {
|
||||
this.undo_stack.push(this.undo_entry);
|
||||
this.undo_entry = [];
|
||||
}
|
||||
|
||||
this._update_undo_redo_enabled();
|
||||
}
|
||||
|
||||
_update_undo_redo_enabled() {
|
||||
this.undo_button.disabled = this.undo_stack.length === 0;
|
||||
this.redo_button.disabled = this.redo_stack.length === 0;
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------------------------------------
|
||||
// Misc UI stuff
|
||||
|
||||
open_tile_prop_overlay(tile, cell, rect) {
|
||||
this.cancel_mouse_operation();
|
||||
@ -3658,20 +3903,4 @@ export class Editor extends PrimaryView {
|
||||
this.mouse_op = null;
|
||||
}
|
||||
}
|
||||
|
||||
resize_level(size_x, size_y, x0 = 0, y0 = 0) {
|
||||
let new_cells = [];
|
||||
for (let y = y0; y < y0 + size_y; y++) {
|
||||
for (let x = x0; x < x0 + size_x; x++) {
|
||||
new_cells.push(this.cell(x, y) ?? this._make_cell(x, y));
|
||||
}
|
||||
}
|
||||
|
||||
this.stored_level.linear_cells = new_cells;
|
||||
this.stored_level.size_x = size_x;
|
||||
this.stored_level.size_y = size_y;
|
||||
this.update_viewport_size();
|
||||
this.update_cell_coordinates();
|
||||
this.redraw_entire_level();
|
||||
}
|
||||
}
|
||||
|
||||
Loading…
Reference in New Issue
Block a user