Add support for rotating or flipping a level or selection

This commit is contained in:
Eevee (Evelyn Woods) 2021-05-16 17:52:31 -06:00
parent 7ed3d38489
commit 53ed2f0948
6 changed files with 505 additions and 137 deletions

View File

@ -10,6 +10,8 @@ export const DIRECTIONS = {
left: 'west',
right: 'east',
opposite: 'south',
mirrored: 'north',
flipped: 'south',
},
south: {
movement: [0, 1],
@ -20,6 +22,8 @@ export const DIRECTIONS = {
left: 'east',
right: 'west',
opposite: 'north',
mirrored: 'south',
flipped: 'north',
},
west: {
movement: [-1, 0],
@ -30,6 +34,8 @@ export const DIRECTIONS = {
left: 'south',
right: 'north',
opposite: 'east',
mirrored: 'east',
flipped: 'west',
},
east: {
movement: [1, 0],
@ -40,6 +46,8 @@ export const DIRECTIONS = {
left: 'north',
right: 'south',
opposite: 'west',
mirrored: 'west',
flipped: 'east',
},
};
// Should match the bit ordering above, and CC2's order

View File

@ -284,7 +284,47 @@ export const PALETTE = [{
],
}];
// TODO loading this from json might actually be faster
// Palette entries that aren't names of real tiles, but pre-configured ones. The faux tile names
// listed here should generally be returned from the real tile's pick_palette_entry()
export const SPECIAL_PALETTE_ENTRIES = {
'thin_walls/south': { name: 'thin_walls', edges: DIRECTIONS['south'].bit },
'frame_block/0': { name: 'frame_block', direction: 'south', arrows: new Set },
'frame_block/1': { name: 'frame_block', direction: 'north', arrows: new Set(['north']) },
'frame_block/2a': { name: 'frame_block', direction: 'north', arrows: new Set(['north', 'east']) },
'frame_block/2o': { name: 'frame_block', direction: 'south', arrows: new Set(['north', 'south']) },
'frame_block/3': { name: 'frame_block', direction: 'south', arrows: new Set(['north', 'east', 'south']) },
'frame_block/4': { name: 'frame_block', direction: 'south', arrows: new Set(['north', 'east', 'south', 'west']) },
// FIXME need to handle entered_direction intelligently, but also allow setting it explicitly
'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/diode': { name: 'logic_gate', direction: 'north', gate_type: 'diode' },
'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', memory: 0 },
'circuit_block/xxx': { name: 'circuit_block', direction: 'south', wire_directions: 0xf },
'sokoban_block/red': { name: 'sokoban_block', color: 'red' },
'sokoban_button/red': { name: 'sokoban_button', color: 'red' },
'sokoban_wall/red': { name: 'sokoban_wall', color: 'red' },
'sokoban_block/blue': { name: 'sokoban_block', color: 'blue' },
'sokoban_button/blue': { name: 'sokoban_button', color: 'blue' },
'sokoban_wall/blue': { name: 'sokoban_wall', color: 'blue' },
'sokoban_block/yellow': { name: 'sokoban_block', color: 'yellow' },
'sokoban_button/yellow':{ name: 'sokoban_button', color: 'yellow' },
'sokoban_wall/yellow': { name: 'sokoban_wall', color: 'yellow' },
'sokoban_block/green': { name: 'sokoban_block', color: 'green' },
'sokoban_button/green': { name: 'sokoban_button', color: 'green' },
'sokoban_wall/green': { name: 'sokoban_wall', color: 'green' },
'one_way_walls/south': { name: 'one_way_walls', edges: DIRECTIONS['south'].bit },
};
// Editor-specific tile properties. Every tile has a help entry, but some complex tiles have extra
// editor behavior as well
export const TILE_DESCRIPTIONS = {
// Basics
player: {
@ -296,6 +336,7 @@ export const TILE_DESCRIPTIONS = {
name: "Cerise",
cc2_name: "Melinda",
desc: "The player, a gel rabbat who enjoys Lexy. Walks on ice. Stopped by dirt and gravel. Reuses yellow keys.",
min_version: 2,
},
hint: {
name: "Hint",
@ -925,52 +966,29 @@ export const TILE_DESCRIPTIONS = {
},
};
export const SPECIAL_PALETTE_ENTRIES = {
'thin_walls/south': { name: 'thin_walls', edges: DIRECTIONS['south'].bit },
'frame_block/0': { name: 'frame_block', direction: 'south', arrows: new Set },
'frame_block/1': { name: 'frame_block', direction: 'north', arrows: new Set(['north']) },
'frame_block/2a': { name: 'frame_block', direction: 'north', arrows: new Set(['north', 'east']) },
'frame_block/2o': { name: 'frame_block', direction: 'south', arrows: new Set(['north', 'south']) },
'frame_block/3': { name: 'frame_block', direction: 'south', arrows: new Set(['north', 'east', 'south']) },
'frame_block/4': { name: 'frame_block', direction: 'south', arrows: new Set(['north', 'east', 'south', 'west']) },
// FIXME need to handle entered_direction intelligently, but also allow setting it explicitly
'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/diode': { name: 'logic_gate', direction: 'north', gate_type: 'diode' },
'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', memory: 0 },
'circuit_block/xxx': { name: 'circuit_block', direction: 'south', wire_directions: 0xf },
'sokoban_block/red': { name: 'sokoban_block', color: 'red' },
'sokoban_button/red': { name: 'sokoban_button', color: 'red' },
'sokoban_wall/red': { name: 'sokoban_wall', color: 'red' },
'sokoban_block/blue': { name: 'sokoban_block', color: 'blue' },
'sokoban_button/blue': { name: 'sokoban_button', color: 'blue' },
'sokoban_wall/blue': { name: 'sokoban_wall', color: 'blue' },
'sokoban_block/yellow': { name: 'sokoban_block', color: 'yellow' },
'sokoban_button/yellow':{ name: 'sokoban_button', color: 'yellow' },
'sokoban_wall/yellow': { name: 'sokoban_wall', color: 'yellow' },
'sokoban_block/green': { name: 'sokoban_block', color: 'green' },
'sokoban_button/green': { name: 'sokoban_button', color: 'green' },
'sokoban_wall/green': { name: 'sokoban_wall', color: 'green' },
'one_way_walls/south': { name: 'one_way_walls', edges: DIRECTIONS['south'].bit },
};
const _RAILROAD_ROTATED_LEFT = [3, 0, 1, 2, 5, 4];
const _RAILROAD_ROTATED_RIGHT = [1, 2, 3, 0, 5, 4];
// TODO merge this with the editor descriptions into one big dict of tile stuff only relevant to the editor
export const SPECIAL_PALETTE_BEHAVIOR = {
export function transform_direction_bitmask(bits, dirprop) {
let new_bits = 0;
for (let dirinfo of Object.values(DIRECTIONS)) {
if (bits & dirinfo.bit) {
new_bits |= DIRECTIONS[dirinfo[dirprop]].bit;
}
}
return new_bits;
}
// Editor-specific tile properties.
// - pick_palette_entry: given a tile, return the palette key to select when it's eyedropped
// - adjust_forward, adjust_backward: alterations that can be made with the adjust tool or ,/. keys,
// but that aren't real rotations (and thus aren't used for rotate/flip/etc)
// - rotate_left, rotate_right, flip, mirror: transform a tile
// - combine_draw, combine_erase: special handling for composite tiles, when drawing or erasing
// using a 'pristine' tile chosen from the palette
// All the tile modification functions edit in-place with no undo support; that's up to the caller.
export const SPECIAL_TILE_BEHAVIOR = {
floor_letter: {
pick_palette_entry() {
return 'floor_letter';
},
_arrows: ["⬆", "➡", "⬇", "⬅"],
rotate_left(tile) {
adjust_backward(tile) {
// Rotate through arrows and ASCII separately
let arrow_index = this._arrows.indexOf(tile.overlaid_glyph);
if (arrow_index >= 0) {
@ -985,7 +1003,7 @@ export const SPECIAL_PALETTE_BEHAVIOR = {
}
tile.overlaid_glyph = String.fromCharCode(cp);
},
rotate_right(tile) {
adjust_forward(tile) {
let arrow_index = this._arrows.indexOf(tile.overlaid_glyph);
if (arrow_index >= 0) {
tile.overlaid_glyph = this._arrows[(arrow_index + 1) % 4];
@ -999,22 +1017,23 @@ export const SPECIAL_PALETTE_BEHAVIOR = {
}
tile.overlaid_glyph = String.fromCharCode(cp);
},
// TODO rotate arrows at least
},
thin_walls: {
pick_palette_entry() {
return 'thin_walls/south';
},
rotate_left(tile) {
if (tile.edges & 0x01) {
tile.edges |= 0x10;
}
tile.edges >>= 1;
tile.edges = transform_direction_bitmask(tile.edges, 'left');
},
rotate_right(tile) {
tile.edges <<= 1;
if (tile.edges & 0x10) {
tile.edges = (tile.edges & ~0x10) | 0x01;
}
tile.edges = transform_direction_bitmask(tile.edges, 'right');
},
mirror(tile) {
tile.edges = transform_direction_bitmask(tile.edges, 'mirrored');
},
flip(tile) {
tile.edges = transform_direction_bitmask(tile.edges, 'flipped');
},
combine_draw(palette_tile, existing_tile) {
existing_tile.edges |= palette_tile.edges;
@ -1040,20 +1059,28 @@ export const SPECIAL_PALETTE_BEHAVIOR = {
return `frame_block/${tile.arrows.size}`;
}
},
_transform(tile, dirprop) {
tile.direction = DIRECTIONS[tile.direction][dirprop];
tile.arrows = new Set(Array.from(tile.arrows, arrow => DIRECTIONS[arrow][dirprop]));
},
rotate_left(tile) {
tile.direction = DIRECTIONS[tile.direction].left;
tile.arrows = new Set(Array.from(tile.arrows, arrow => DIRECTIONS[arrow].left));
this._transform(tile, 'left');
},
rotate_right(tile) {
tile.direction = DIRECTIONS[tile.direction].right;
tile.arrows = new Set(Array.from(tile.arrows, arrow => DIRECTIONS[arrow].right));
this._transform(tile, 'right');
},
mirror(tile) {
this._transform(tile, 'mirrored');
},
flip(tile) {
this._transform(tile, 'flipped');
},
},
logic_gate: {
pick_palette_entry(tile) {
return `logic_gate/${tile.gate_type}`;
},
rotate_left(tile) {
adjust_backward(tile) {
if (tile.gate_type === 'counter') {
tile.memory = (tile.memory + 9) % 10;
}
@ -1061,7 +1088,7 @@ export const SPECIAL_PALETTE_BEHAVIOR = {
tile.direction = DIRECTIONS[tile.direction].left;
}
},
rotate_right(tile) {
adjust_forward(tile) {
if (tile.gate_type === 'counter') {
tile.memory = (tile.memory + 1) % 10;
}
@ -1069,6 +1096,43 @@ export const SPECIAL_PALETTE_BEHAVIOR = {
tile.direction = DIRECTIONS[tile.direction].right;
}
},
// Note that the counter gate can neither rotate nor flip
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;
}
},
mirror(tile) {
if (tile.gate_type === 'counter')
return;
if (tile.gate_type === 'latch_cw') {
tile.gate_type = 'latch_ccw';
}
else if (tile.gate_type === 'latch_ccw') {
tile.gate_type = 'latch_cw';
}
tile.direction = DIRECTIONS[tile.direction].mirrored;
},
flip(tile) {
if (tile.gate_type === 'counter')
return;
if (tile.gate_type === 'latch_cw') {
tile.gate_type = 'latch_ccw';
}
else if (tile.gate_type === 'latch_ccw') {
tile.gate_type = 'latch_cw';
}
tile.direction = DIRECTIONS[tile.direction].flipped;
},
},
railroad: {
pick_palette_entry(tile) {
@ -1082,40 +1146,52 @@ export const SPECIAL_PALETTE_BEHAVIOR = {
}
return 'railroad/switch';
},
rotate_left(tile) {
// track order: 0 NE, 1 SE, 2 SW, 3 NW, 4 EW, 5 NS
_tracks_left: [3, 0, 1, 2, 5, 4],
_tracks_right: [1, 2, 3, 0, 5, 4],
_tracks_mirror: [3, 2, 1, 0, 4, 5],
_tracks_flip: [1, 0, 3, 2, 4, 5],
_transform_tracks(tile, track_mapping) {
let new_tracks = 0;
for (let i = 0; i < 6; i++) {
if (tile.tracks & (1 << i)) {
new_tracks |= 1 << _RAILROAD_ROTATED_LEFT[i];
new_tracks |= 1 << track_mapping[i];
}
}
tile.tracks = new_tracks;
if (tile.track_switch !== null) {
tile.track_switch = _RAILROAD_ROTATED_LEFT[tile.track_switch];
tile.track_switch = track_mapping[tile.track_switch];
}
},
rotate_left(tile) {
this._transform_tracks(tile, this._tracks_left);
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];
}
this._transform_tracks(tile, this._tracks_right);
if (tile.entered_direction) {
tile.entered_direction = DIRECTIONS[tile.entered_direction].right;
}
},
mirror(tile) {
this._transform_tracks(tile, this._tracks_mirror);
if (tile.entered_direction) {
tile.entered_direction = DIRECTIONS[tile.entered_direction].mirrored;
}
},
flip(tile) {
this._transform_tracks(tile, this._tracks_flip);
if (tile.entered_direction) {
tile.entered_direction = DIRECTIONS[tile.entered_direction].flipped;
}
},
combine_draw(palette_tile, existing_tile) {
existing_tile.tracks |= palette_tile.tracks;
// If we have a switch already, the just-placed track becomes the current one
@ -1198,32 +1274,76 @@ export const SPECIAL_PALETTE_BEHAVIOR = {
},
},
};
SPECIAL_PALETTE_BEHAVIOR['one_way_walls'] = {
...SPECIAL_PALETTE_BEHAVIOR['thin_walls'],
...SPECIAL_PALETTE_BEHAVIOR['one_way_walls'],
SPECIAL_TILE_BEHAVIOR['one_way_walls'] = {
...SPECIAL_TILE_BEHAVIOR['thin_walls'],
...SPECIAL_TILE_BEHAVIOR['one_way_walls'],
};
// 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'],
['terraformer_n', 'terraformer_e', 'terraformer_s', 'terraformer_w'],
['turntable_cw', 'turntable_ccw'],
]) {
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() {
return name;
},
rotate_left(tile) {
function add_special_tile_cycle(rotation_order, mirror_mapping, flip_mapping) {
let names = new Set(rotation_order);
// Make the flip and mirror mappings symmetrical
for (let map of [mirror_mapping, flip_mapping]) {
for (let [key, value] of Object.entries(map)) {
names.add(key);
names.add(value);
if (! (value in map)) {
map[value] = key;
}
}
}
for (let name of names) {
let behavior = {};
let i = rotation_order.indexOf(name);
if (i >= 0) {
let left = rotation_order[(i - 1 + rotation_order.length) % rotation_order.length];
let right = rotation_order[(i + 1) % rotation_order.length];
behavior.rotate_left = function rotate_left(tile) {
tile.type = TILE_TYPES[left];
},
rotate_right(tile) {
};
behavior.rotate_right = function rotate_right(tile) {
tile.type = TILE_TYPES[right];
},
};
};
}
if (name in mirror_mapping) {
let mirror = mirror_mapping[name];
behavior.mirror = function mirror(tile) {
tile.type = TILE_TYPES[mirror];
};
}
if (name in flip_mapping) {
let flip = flip_mapping[name];
behavior.flip = function flip(tile) {
tile.type = TILE_TYPES[flip];
};
}
SPECIAL_TILE_BEHAVIOR[name] = behavior;
}
}
add_special_tile_cycle(
['force_floor_n', 'force_floor_e', 'force_floor_s', 'force_floor_w'],
{force_floor_e: 'force_floor_w'},
{force_floor_n: 'force_floor_s'},
);
add_special_tile_cycle(
['ice_nw', 'ice_ne', 'ice_se', 'ice_sw'],
{ice_nw: 'ice_ne', ice_sw: 'ice_se'},
{ice_nw: 'ice_sw', ice_ne: 'ice_se'},
);
add_special_tile_cycle(
['swivel_nw', 'swivel_ne', 'swivel_se', 'swivel_sw'],
{swivel_nw: 'swivel_ne', swivel_sw: 'swivel_se'},
{swivel_nw: 'swivel_sw', swivel_ne: 'swivel_se'},
);
add_special_tile_cycle(
[], // turntables don't rotate, but they do flip/mirror
{turntable_cw: 'turntable_ccw'},
{turntable_cw: 'turntable_ccw'},
);

View File

@ -72,6 +72,7 @@ export class Selection {
this.floated_cells = null;
this.floated_element = null;
this.floated_canvas = null;
}
get is_empty() {
@ -117,6 +118,15 @@ export class Selection {
this.element.setAttribute('y', this.rect.y);
this.element.setAttribute('width', this.rect.width);
this.element.setAttribute('height', this.rect.height);
if (this.floated_element) {
let tileset = this.editor.renderer.tileset;
this.floated_canvas.width = rect.width * tileset.size_x;
this.floated_canvas.height = rect.height * tileset.size_y;
let foreign_obj = this.floated_element.querySelector('foreignObject');
foreign_obj.setAttribute('width', this.floated_canvas.width);
foreign_obj.setAttribute('height', this.floated_canvas.height);
}
}
move_by(dx, dy) {
@ -157,8 +167,8 @@ export class Selection {
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++) {
for (let y = this.rect.top; y < this.rect.bottom; y++) {
for (let x = this.rect.left; x < this.rect.right; x++) {
let n = stored_level.coords_to_scalar(x, y);
yield [x, y, n];
}
@ -202,6 +212,7 @@ export class Selection {
// it forever
this.editor._do(
() => {
this.floated_canvas = canvas;
this.floated_element = floated_element;
this.floated_cells = floated_cells;
this.svg_group.append(floated_element);
@ -235,11 +246,13 @@ export class Selection {
this.stamp_float();
let element = this.floated_element;
let canvas = this.floated_canvas;
let cells = this.floated_cells;
this.editor._do(
() => this._defloat(),
() => {
this.floated_cells = cells;
this.floated_canvas = canvas;
this.floated_element = element;
this.svg_group.append(element);
},
@ -250,9 +263,27 @@ export class Selection {
_defloat() {
this.floated_element.remove();
this.floated_element = null;
this.floated_canvas = null;
this.floated_cells = null;
}
// Redraw the selection canvas from scratch
redraw() {
if (! this.floated_canvas)
return;
// FIXME uhoh, how do i actually do this? we have no renderer of our own, we have a
// separate canvas, and all the renderer stuff expects to get ahold of a level. i guess
// refactor it to draw a block of cells?
this.editor.renderer.draw_static_generic({
x0: 0, y0: 0,
x1: this.rect.width, y1: this.rect.height,
cells: this.floated_cells,
width: this.rect.width,
ctx: this.floated_canvas.getContext('2d'),
});
}
// TODO allow floating/dragging, ctrl-dragging to copy, anchoring...
// TODO make more stuff respect this (more things should go through Editor for undo reasons anyway)
}

View File

@ -11,7 +11,7 @@ import { mk, mk_svg, string_from_buffer_ascii, bytestring_to_buffer } from '../u
import * as util from '../util.js';
import * as dialogs from './dialogs.js';
import { TOOLS, TOOL_ORDER, TOOL_SHORTCUTS, PALETTE, SPECIAL_PALETTE_ENTRIES, SPECIAL_PALETTE_BEHAVIOR, TILE_DESCRIPTIONS } from './editordefs.js';
import { TOOLS, TOOL_ORDER, TOOL_SHORTCUTS, PALETTE, SPECIAL_PALETTE_ENTRIES, SPECIAL_TILE_BEHAVIOR, TILE_DESCRIPTIONS, transform_direction_bitmask } from './editordefs.js';
import { SVGConnection, Selection } from './helpers.js';
import * as mouseops from './mouseops.js';
import { TILES_WITH_PROPS } from './tile-overlays.js';
@ -448,6 +448,33 @@ export class Editor extends PrimaryView {
this.redo_button = _make_button("Redo", () => {
this.redo();
});
let edit_items = [
["Rotate CCW", () => {
this.rotate_level_left();
}],
["Rotate CW", () => {
this.rotate_level_right();
}],
["Mirror", () => {
this.mirror_level();
}],
["Flip", () => {
this.flip_level();
}],
];
this.edit_menu = new MenuOverlay(
this.conductor,
edit_items,
item => item[0],
item => item[1](),
);
let edit_menu_button = _make_button("Edit ", ev => {
this.edit_menu.open(ev.currentTarget);
});
edit_menu_button.append(
mk_svg('svg.svg-icon', {viewBox: '0 0 16 16'},
mk_svg('use', {href: `#svg-icon-menu-chevron`})),
);
_make_button("Pack properties...", () => {
new dialogs.EditorPackMetaOverlay(this.conductor, this.conductor.stored_game).open();
});
@ -1064,6 +1091,12 @@ export class Editor extends PrimaryView {
this.svg_overlay.setAttribute('viewBox', `0 0 ${this.stored_level.size_x} ${this.stored_level.size_y}`);
}
update_after_size_change() {
this.update_viewport_size();
this.update_cell_coordinates();
this.redraw_entire_level();
}
// ------------------------------------------------------------------------------------------------
set_canvas_zoom(zoom, origin_x = null, origin_y = null) {
@ -1210,8 +1243,9 @@ export class Editor extends PrimaryView {
// Select it in the palette, if possible
let key = name;
if (SPECIAL_PALETTE_BEHAVIOR[name]) {
key = SPECIAL_PALETTE_BEHAVIOR[name].pick_palette_entry(tile);
let behavior = SPECIAL_TILE_BEHAVIOR[name];
if (behavior && behavior.pick_palette_entry) {
key = SPECIAL_TILE_BEHAVIOR[name].pick_palette_entry(tile);
}
this.palette_fg_selected_el = this.palette[key] ?? null;
if (this.palette_fg_selected_el) {
@ -1235,32 +1269,51 @@ export class Editor extends PrimaryView {
this.redraw_background_tile();
}
rotate_tile_left(tile) {
if (SPECIAL_PALETTE_BEHAVIOR[tile.type.name]) {
SPECIAL_PALETTE_BEHAVIOR[tile.type.name].rotate_left(tile);
// Transform an individual tile in various ways. No undo handling (as the tile may or may not
// even be part of the level).
_transform_tile(tile, adjust_method, transform_method, direction_property) {
let did_anything = true;
let behavior = SPECIAL_TILE_BEHAVIOR[tile.type.name];
if (adjust_method && behavior && behavior[adjust_method]) {
behavior[adjust_method](tile);
}
else if (behavior && behavior[transform_method]) {
behavior[transform_method](tile);
}
else if (TILE_TYPES[tile.type.name].is_actor) {
tile.direction = DIRECTIONS[tile.direction ?? 'south'].left;
tile.direction = DIRECTIONS[tile.direction ?? 'south'][direction_property];
}
else {
return false;
did_anything = false;
}
return true;
if (tile.wire_directions) {
tile.wire_directions = transform_direction_bitmask(
tile.wire_directions, direction_property);
did_anything = true;
}
if (tile.wire_tunnel_directions) {
tile.wire_tunnel_directions = transform_direction_bitmask(
tile.wire_tunnel_directions, direction_property);
did_anything = true;
}
return did_anything;
}
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;
}
return true;
rotate_tile_left(tile, include_faux_adjustments = true) {
return this._transform_tile(
tile, include_faux_adjustments ? 'adjust_backward' : null, 'rotate_left', 'left');
}
rotate_tile_right(tile, include_faux_adjustments = true) {
return this._transform_tile(
tile, include_faux_adjustments ? 'adjust_forward' : null, 'rotate_right', 'right');
}
mirror_tile(tile) {
return this._transform_tile(tile, null, 'mirror', 'mirrored');
}
flip_tile(tile) {
return this._transform_tile(tile, null, 'flip', 'flipped');
}
rotate_palette_left() {
@ -1371,12 +1424,12 @@ export class Editor extends PrimaryView {
if (existing_tile && existing_tile.type === tile.type &&
// FIXME this is hacky garbage
tile === this.fg_tile && this.fg_tile_from_palette &&
SPECIAL_PALETTE_BEHAVIOR[tile.type.name] &&
SPECIAL_PALETTE_BEHAVIOR[tile.type.name].combine_draw)
SPECIAL_TILE_BEHAVIOR[tile.type.name] &&
SPECIAL_TILE_BEHAVIOR[tile.type.name].combine_draw)
{
let old_tile = {...existing_tile};
let new_tile = existing_tile;
SPECIAL_PALETTE_BEHAVIOR[tile.type.name].combine_draw(tile, new_tile);
SPECIAL_TILE_BEHAVIOR[tile.type.name].combine_draw(tile, new_tile);
this._assign_tile(cell, layer, new_tile, old_tile);
return;
}
@ -1418,12 +1471,12 @@ export class Editor extends PrimaryView {
if (existing_tile && existing_tile.type === tile.type &&
// FIXME this is hacky garbage
tile === this.fg_tile && this.fg_tile_from_palette &&
SPECIAL_PALETTE_BEHAVIOR[tile.type.name] &&
SPECIAL_PALETTE_BEHAVIOR[tile.type.name].combine_erase)
SPECIAL_TILE_BEHAVIOR[tile.type.name] &&
SPECIAL_TILE_BEHAVIOR[tile.type.name].combine_erase)
{
let old_tile = {...existing_tile};
let new_tile = existing_tile;
let remove = SPECIAL_PALETTE_BEHAVIOR[tile.type.name].combine_erase(tile, new_tile);
let remove = SPECIAL_TILE_BEHAVIOR[tile.type.name].combine_erase(tile, new_tile);
if (! remove) {
this._assign_tile(cell, tile.type.layer, new_tile, old_tile);
return;
@ -1474,20 +1527,160 @@ export class Editor extends PrimaryView {
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.update_after_size_change();
}, () => {
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.update_after_size_change();
});
this.commit_undo();
}
// Rearranges cells in the current selection or whole level, based on a few callbacks.
// DOES NOT commit.
// (These don't save undo entries for individual tiles, either, because they're expected to be
// completely reversible, and undo is done by performing the opposite transform rather than
// reloading a copy of a previous state.)
_rearrange_cells(swap_dimensions, downgrade_coords, upgrade_tile) {
let old_cells, old_w;
let w, h;
let new_cells = [];
if (this.selection.is_empty) {
// Do it to the whole level
w = this.stored_level.size_x;
h = this.stored_level.size_y;
old_w = w;
if (swap_dimensions) {
[w, h] = [h, w];
this.stored_level.size_x = w;
this.stored_level.size_y = h;
}
old_cells = this.stored_level.linear_cells;
this.stored_level.linear_cells = new_cells;
}
else {
// Do it to the selection
w = this.selection.rect.width;
h = this.selection.rect.height;
old_w = w;
if (swap_dimensions) {
[w, h] = [h, w];
this.selection._set_from_rect(new DOMRect(
this.selection.rect.x, this.selection.rect.y, w, h));
}
old_cells = this.selection.floated_cells;
this.selection.floated_cells = new_cells;
}
for (let y = 0; y < h; y++) {
for (let x = 0; x < w; x++) {
let [old_x, old_y] = downgrade_coords(x, y, w, h);
let cell = old_cells[old_y * old_w + old_x];
for (let tile of cell) {
if (tile) {
upgrade_tile(tile);
}
}
new_cells.push(cell);
}
}
}
rotate_level_right() {
this._do_transform(
true,
() => this._rotate_level_right(),
() => this._rotate_level_left(),
);
}
rotate_level_left() {
this._do_transform(
true,
() => this._rotate_level_left(),
() => this._rotate_level_right(),
);
}
rotate_level_180() {
}
mirror_level() {
this._do_transform(
false,
() => this._mirror_level(),
() => this._mirror_level(),
);
}
flip_level() {
this._do_transform(
false,
() => this._flip_level(),
() => this._flip_level(),
);
}
_do_transform(affects_size, redo, undo) {
// FIXME apply transform to connections if appropriate, somehow, ?? i don't even know how
// those interact with floating selection yet :S
if (! this.selection.is_empty && ! this.selection.is_floating) {
this.selection.enfloat();
}
this._do(
() => {
redo();
this._post_transform_cleanup(affects_size);
},
() => {
undo();
this._post_transform_cleanup(affects_size);
},
);
this.commit_undo();
}
_post_transform_cleanup(affects_size) {
if (this.selection.is_empty) {
if (affects_size) {
this.update_after_size_change();
}
else {
this.redraw_entire_level();
}
}
else {
// FIXME what if it affects size?
this.selection.redraw();
}
}
// TODO mirror diagonally?
// Internal-use versions of the above. These DO NOT create undo entries.
_rotate_level_left() {
this._rearrange_cells(
true,
(x, y, w, h) => [h - 1 - y, x],
tile => this.rotate_tile_left(tile, false),
);
}
_rotate_level_right() {
this._rearrange_cells(
true,
(x, y, w, h) => [y, w - 1 - x],
tile => this.rotate_tile_right(tile, false),
);
}
_mirror_level() {
this._rearrange_cells(
false,
(x, y, w, h) => [w - 1 - x, y],
tile => this.mirror_tile(tile),
);
}
_flip_level() {
this._rearrange_cells(
false,
(x, y, w, h) => [x, h - 1 - y],
tile => this.flip_tile(tile),
);
}
// Create a connection between two cells and update the UI accordingly. If dest is null or
// undefined, delete any existing connection instead.
set_custom_connection(src, dest) {

View File

@ -1031,6 +1031,7 @@ const ADJUST_TOGGLES_CCW = {};
['flame_jet_off', 'flame_jet_on'],
['light_switch_off', 'light_switch_on'],
['stopwatch_bonus', 'stopwatch_penalty'],
['turntable_cw', 'turntable_ccw'],
])
{
for (let [i, tile] of cycle.entries()) {

View File

@ -334,12 +334,27 @@ export class CanvasRenderer {
// Used by the editor and map previews. Draws a region of the level (probably a StoredLevel),
// assuming nothing is moving.
draw_static_region(x0, y0, x1, y1, destx = x0, desty = y0) {
this._adjust_viewport_if_dirty();
this.draw_static_generic({x0, y0, x1, y1, destx, desty});
}
let packet = new CanvasRendererDrawPacket(this, this.ctx, this.perception);
// Most generic possible form of drawing a static region; mainly useful if you want to use a
// different canvas or draw a custom block of cells
// TODO does this actually need any state at all? could it just be, dare i ask, a function?
draw_static_generic({
x0, y0, x1, y1, destx = x0, desty = y0, cells = null, width = null,
ctx = this.ctx, perception = this.perception, show_facing = this.show_facing,
}) {
if (ctx === this.ctx) {
this._adjust_viewport_if_dirty();
}
width = width ?? this.level.size_x;
cells = cells ?? this.level.linear_cells;
let packet = new CanvasRendererDrawPacket(this, ctx, perception);
for (let x = x0; x <= x1; x++) {
for (let y = y0; y <= y1; y++) {
let cell = this.level.cell(x, y);
let cell = cells[y * width + x];
if (! cell)
continue;
@ -350,11 +365,11 @@ export class CanvasRenderer {
// For actors (i.e., blocks), perception only applies if there's something
// of potential interest underneath
if (this.perception !== 'normal' && tile.type.is_block && ! seen_anything_interesting) {
if (perception !== 'normal' && tile.type.is_block && ! seen_anything_interesting) {
packet.perception = 'normal';
}
else {
packet.perception = this.perception;
packet.perception = perception;
}
if (tile.type.layer < LAYERS.actor && ! (