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', left: 'west',
right: 'east', right: 'east',
opposite: 'south', opposite: 'south',
mirrored: 'north',
flipped: 'south',
}, },
south: { south: {
movement: [0, 1], movement: [0, 1],
@ -20,6 +22,8 @@ export const DIRECTIONS = {
left: 'east', left: 'east',
right: 'west', right: 'west',
opposite: 'north', opposite: 'north',
mirrored: 'south',
flipped: 'north',
}, },
west: { west: {
movement: [-1, 0], movement: [-1, 0],
@ -30,6 +34,8 @@ export const DIRECTIONS = {
left: 'south', left: 'south',
right: 'north', right: 'north',
opposite: 'east', opposite: 'east',
mirrored: 'east',
flipped: 'west',
}, },
east: { east: {
movement: [1, 0], movement: [1, 0],
@ -40,6 +46,8 @@ export const DIRECTIONS = {
left: 'north', left: 'north',
right: 'south', right: 'south',
opposite: 'west', opposite: 'west',
mirrored: 'west',
flipped: 'east',
}, },
}; };
// Should match the bit ordering above, and CC2's order // 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 = { export const TILE_DESCRIPTIONS = {
// Basics // Basics
player: { player: {
@ -296,6 +336,7 @@ export const TILE_DESCRIPTIONS = {
name: "Cerise", name: "Cerise",
cc2_name: "Melinda", cc2_name: "Melinda",
desc: "The player, a gel rabbat who enjoys Lexy. Walks on ice. Stopped by dirt and gravel. Reuses yellow keys.", desc: "The player, a gel rabbat who enjoys Lexy. Walks on ice. Stopped by dirt and gravel. Reuses yellow keys.",
min_version: 2,
}, },
hint: { hint: {
name: "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 }, export function transform_direction_bitmask(bits, dirprop) {
'frame_block/0': { name: 'frame_block', direction: 'south', arrows: new Set }, let new_bits = 0;
'frame_block/1': { name: 'frame_block', direction: 'north', arrows: new Set(['north']) }, for (let dirinfo of Object.values(DIRECTIONS)) {
'frame_block/2a': { name: 'frame_block', direction: 'north', arrows: new Set(['north', 'east']) }, if (bits & dirinfo.bit) {
'frame_block/2o': { name: 'frame_block', direction: 'south', arrows: new Set(['north', 'south']) }, new_bits |= DIRECTIONS[dirinfo[dirprop]].bit;
'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 return new_bits;
'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' }, // Editor-specific tile properties.
'logic_gate/not': { name: 'logic_gate', direction: 'north', gate_type: 'not' }, // - pick_palette_entry: given a tile, return the palette key to select when it's eyedropped
'logic_gate/diode': { name: 'logic_gate', direction: 'north', gate_type: 'diode' }, // - adjust_forward, adjust_backward: alterations that can be made with the adjust tool or ,/. keys,
'logic_gate/and': { name: 'logic_gate', direction: 'north', gate_type: 'and' }, // but that aren't real rotations (and thus aren't used for rotate/flip/etc)
'logic_gate/or': { name: 'logic_gate', direction: 'north', gate_type: 'or' }, // - rotate_left, rotate_right, flip, mirror: transform a tile
'logic_gate/xor': { name: 'logic_gate', direction: 'north', gate_type: 'xor' }, // - combine_draw, combine_erase: special handling for composite tiles, when drawing or erasing
'logic_gate/nand': { name: 'logic_gate', direction: 'north', gate_type: 'nand' }, // using a 'pristine' tile chosen from the palette
'logic_gate/latch-cw': { name: 'logic_gate', direction: 'north', gate_type: 'latch-cw' }, // All the tile modification functions edit in-place with no undo support; that's up to the caller.
'logic_gate/latch-ccw': { name: 'logic_gate', direction: 'north', gate_type: 'latch-ccw' }, export const SPECIAL_TILE_BEHAVIOR = {
'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 = {
floor_letter: { floor_letter: {
pick_palette_entry() {
return 'floor_letter';
},
_arrows: ["⬆", "➡", "⬇", "⬅"], _arrows: ["⬆", "➡", "⬇", "⬅"],
rotate_left(tile) { adjust_backward(tile) {
// Rotate through arrows and ASCII separately // Rotate through arrows and ASCII separately
let arrow_index = this._arrows.indexOf(tile.overlaid_glyph); let arrow_index = this._arrows.indexOf(tile.overlaid_glyph);
if (arrow_index >= 0) { if (arrow_index >= 0) {
@ -985,7 +1003,7 @@ export const SPECIAL_PALETTE_BEHAVIOR = {
} }
tile.overlaid_glyph = String.fromCharCode(cp); tile.overlaid_glyph = String.fromCharCode(cp);
}, },
rotate_right(tile) { adjust_forward(tile) {
let arrow_index = this._arrows.indexOf(tile.overlaid_glyph); let arrow_index = this._arrows.indexOf(tile.overlaid_glyph);
if (arrow_index >= 0) { if (arrow_index >= 0) {
tile.overlaid_glyph = this._arrows[(arrow_index + 1) % 4]; tile.overlaid_glyph = this._arrows[(arrow_index + 1) % 4];
@ -999,22 +1017,23 @@ export const SPECIAL_PALETTE_BEHAVIOR = {
} }
tile.overlaid_glyph = String.fromCharCode(cp); tile.overlaid_glyph = String.fromCharCode(cp);
}, },
// TODO rotate arrows at least
}, },
thin_walls: { thin_walls: {
pick_palette_entry() { pick_palette_entry() {
return 'thin_walls/south'; return 'thin_walls/south';
}, },
rotate_left(tile) { rotate_left(tile) {
if (tile.edges & 0x01) { tile.edges = transform_direction_bitmask(tile.edges, 'left');
tile.edges |= 0x10;
}
tile.edges >>= 1;
}, },
rotate_right(tile) { rotate_right(tile) {
tile.edges <<= 1; tile.edges = transform_direction_bitmask(tile.edges, 'right');
if (tile.edges & 0x10) { },
tile.edges = (tile.edges & ~0x10) | 0x01; 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) { combine_draw(palette_tile, existing_tile) {
existing_tile.edges |= palette_tile.edges; existing_tile.edges |= palette_tile.edges;
@ -1040,20 +1059,28 @@ export const SPECIAL_PALETTE_BEHAVIOR = {
return `frame_block/${tile.arrows.size}`; 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) { rotate_left(tile) {
tile.direction = DIRECTIONS[tile.direction].left; this._transform(tile, 'left');
tile.arrows = new Set(Array.from(tile.arrows, arrow => DIRECTIONS[arrow].left));
}, },
rotate_right(tile) { rotate_right(tile) {
tile.direction = DIRECTIONS[tile.direction].right; this._transform(tile, 'right');
tile.arrows = new Set(Array.from(tile.arrows, arrow => DIRECTIONS[arrow].right)); },
mirror(tile) {
this._transform(tile, 'mirrored');
},
flip(tile) {
this._transform(tile, 'flipped');
}, },
}, },
logic_gate: { logic_gate: {
pick_palette_entry(tile) { pick_palette_entry(tile) {
return `logic_gate/${tile.gate_type}`; return `logic_gate/${tile.gate_type}`;
}, },
rotate_left(tile) { adjust_backward(tile) {
if (tile.gate_type === 'counter') { if (tile.gate_type === 'counter') {
tile.memory = (tile.memory + 9) % 10; tile.memory = (tile.memory + 9) % 10;
} }
@ -1061,7 +1088,7 @@ export const SPECIAL_PALETTE_BEHAVIOR = {
tile.direction = DIRECTIONS[tile.direction].left; tile.direction = DIRECTIONS[tile.direction].left;
} }
}, },
rotate_right(tile) { adjust_forward(tile) {
if (tile.gate_type === 'counter') { if (tile.gate_type === 'counter') {
tile.memory = (tile.memory + 1) % 10; tile.memory = (tile.memory + 1) % 10;
} }
@ -1069,6 +1096,43 @@ export const SPECIAL_PALETTE_BEHAVIOR = {
tile.direction = DIRECTIONS[tile.direction].right; 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: { railroad: {
pick_palette_entry(tile) { pick_palette_entry(tile) {
@ -1082,40 +1146,52 @@ export const SPECIAL_PALETTE_BEHAVIOR = {
} }
return 'railroad/switch'; 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; let new_tracks = 0;
for (let i = 0; i < 6; i++) { for (let i = 0; i < 6; i++) {
if (tile.tracks & (1 << i)) { if (tile.tracks & (1 << i)) {
new_tracks |= 1 << _RAILROAD_ROTATED_LEFT[i]; new_tracks |= 1 << track_mapping[i];
} }
} }
tile.tracks = new_tracks; tile.tracks = new_tracks;
if (tile.track_switch !== null) { 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) { if (tile.entered_direction) {
tile.entered_direction = DIRECTIONS[tile.entered_direction].left; tile.entered_direction = DIRECTIONS[tile.entered_direction].left;
} }
}, },
rotate_right(tile) { rotate_right(tile) {
let new_tracks = 0; this._transform_tracks(tile, this._tracks_right);
for (let i = 0; i < 6; i++) {
if (tile.tracks & (1 << i)) {
new_tracks |= 1 << _RAILROAD_ROTATED_RIGHT[i];
}
}
tile.tracks = new_tracks;
if (tile.track_switch !== null) {
tile.track_switch = _RAILROAD_ROTATED_RIGHT[tile.track_switch];
}
if (tile.entered_direction) { if (tile.entered_direction) {
tile.entered_direction = DIRECTIONS[tile.entered_direction].right; 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) { combine_draw(palette_tile, existing_tile) {
existing_tile.tracks |= palette_tile.tracks; existing_tile.tracks |= palette_tile.tracks;
// If we have a switch already, the just-placed track becomes the current one // 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_TILE_BEHAVIOR['one_way_walls'] = {
...SPECIAL_PALETTE_BEHAVIOR['thin_walls'], ...SPECIAL_TILE_BEHAVIOR['thin_walls'],
...SPECIAL_PALETTE_BEHAVIOR['one_way_walls'], ...SPECIAL_TILE_BEHAVIOR['one_way_walls'],
}; };
// Fill in some special behavior that boils down to rotating tiles which happen to be encoded as // Fill in some special behavior that boils down to rotating tiles which happen to be encoded as
// different tile types // different tile types
for (let cycle of [ function add_special_tile_cycle(rotation_order, mirror_mapping, flip_mapping) {
['force_floor_n', 'force_floor_e', 'force_floor_s', 'force_floor_w'], let names = new Set(rotation_order);
['ice_nw', 'ice_ne', 'ice_se', 'ice_sw'],
['swivel_nw', 'swivel_ne', 'swivel_se', 'swivel_sw'], // Make the flip and mirror mappings symmetrical
['terraformer_n', 'terraformer_e', 'terraformer_s', 'terraformer_w'], for (let map of [mirror_mapping, flip_mapping]) {
['turntable_cw', 'turntable_ccw'], for (let [key, value] of Object.entries(map)) {
]) { names.add(key);
for (let [i, name] of cycle.entries()) { names.add(value);
let left = cycle[(i - 1 + cycle.length) % cycle.length]; if (! (value in map)) {
let right = cycle[(i + 1) % cycle.length]; map[value] = key;
SPECIAL_PALETTE_BEHAVIOR[name] = { }
pick_palette_entry() { }
return name; }
},
rotate_left(tile) { 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]; tile.type = TILE_TYPES[left];
}, };
rotate_right(tile) { behavior.rotate_right = function rotate_right(tile) {
tile.type = TILE_TYPES[right]; 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_cells = null;
this.floated_element = null; this.floated_element = null;
this.floated_canvas = null;
} }
get is_empty() { get is_empty() {
@ -117,6 +118,15 @@ export class Selection {
this.element.setAttribute('y', this.rect.y); this.element.setAttribute('y', this.rect.y);
this.element.setAttribute('width', this.rect.width); this.element.setAttribute('width', this.rect.width);
this.element.setAttribute('height', this.rect.height); 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) { move_by(dx, dy) {
@ -157,8 +167,8 @@ export class Selection {
return; return;
let stored_level = this.editor.stored_level; 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); let n = stored_level.coords_to_scalar(x, y);
yield [x, y, n]; yield [x, y, n];
} }
@ -202,6 +212,7 @@ export class Selection {
// it forever // it forever
this.editor._do( this.editor._do(
() => { () => {
this.floated_canvas = canvas;
this.floated_element = floated_element; this.floated_element = floated_element;
this.floated_cells = floated_cells; this.floated_cells = floated_cells;
this.svg_group.append(floated_element); this.svg_group.append(floated_element);
@ -235,11 +246,13 @@ export class Selection {
this.stamp_float(); this.stamp_float();
let element = this.floated_element; let element = this.floated_element;
let canvas = this.floated_canvas;
let cells = this.floated_cells; let cells = this.floated_cells;
this.editor._do( this.editor._do(
() => this._defloat(), () => this._defloat(),
() => { () => {
this.floated_cells = cells; this.floated_cells = cells;
this.floated_canvas = canvas;
this.floated_element = element; this.floated_element = element;
this.svg_group.append(element); this.svg_group.append(element);
}, },
@ -250,9 +263,27 @@ export class Selection {
_defloat() { _defloat() {
this.floated_element.remove(); this.floated_element.remove();
this.floated_element = null; this.floated_element = null;
this.floated_canvas = null;
this.floated_cells = 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 allow floating/dragging, ctrl-dragging to copy, anchoring...
// TODO make more stuff respect this (more things should go through Editor for undo reasons anyway) // 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 util from '../util.js';
import * as dialogs from './dialogs.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 { SVGConnection, Selection } from './helpers.js';
import * as mouseops from './mouseops.js'; import * as mouseops from './mouseops.js';
import { TILES_WITH_PROPS } from './tile-overlays.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_button = _make_button("Redo", () => {
this.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...", () => { _make_button("Pack properties...", () => {
new dialogs.EditorPackMetaOverlay(this.conductor, this.conductor.stored_game).open(); 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}`); 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) { 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 // Select it in the palette, if possible
let key = name; let key = name;
if (SPECIAL_PALETTE_BEHAVIOR[name]) { let behavior = SPECIAL_TILE_BEHAVIOR[name];
key = SPECIAL_PALETTE_BEHAVIOR[name].pick_palette_entry(tile); if (behavior && behavior.pick_palette_entry) {
key = SPECIAL_TILE_BEHAVIOR[name].pick_palette_entry(tile);
} }
this.palette_fg_selected_el = this.palette[key] ?? null; this.palette_fg_selected_el = this.palette[key] ?? null;
if (this.palette_fg_selected_el) { if (this.palette_fg_selected_el) {
@ -1235,32 +1269,51 @@ export class Editor extends PrimaryView {
this.redraw_background_tile(); this.redraw_background_tile();
} }
rotate_tile_left(tile) { // Transform an individual tile in various ways. No undo handling (as the tile may or may not
if (SPECIAL_PALETTE_BEHAVIOR[tile.type.name]) { // even be part of the level).
SPECIAL_PALETTE_BEHAVIOR[tile.type.name].rotate_left(tile); _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) { 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 { 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_left(tile, include_faux_adjustments = true) {
rotate_tile_right(tile) { return this._transform_tile(
if (SPECIAL_PALETTE_BEHAVIOR[tile.type.name]) { tile, include_faux_adjustments ? 'adjust_backward' : null, 'rotate_left', 'left');
SPECIAL_PALETTE_BEHAVIOR[tile.type.name].rotate_right(tile); }
} rotate_tile_right(tile, include_faux_adjustments = true) {
else if (TILE_TYPES[tile.type.name].is_actor) { return this._transform_tile(
tile.direction = DIRECTIONS[tile.direction ?? 'south'].right; tile, include_faux_adjustments ? 'adjust_forward' : null, 'rotate_right', 'right');
} }
else { mirror_tile(tile) {
return false; return this._transform_tile(tile, null, 'mirror', 'mirrored');
} }
flip_tile(tile) {
return true; return this._transform_tile(tile, null, 'flip', 'flipped');
} }
rotate_palette_left() { rotate_palette_left() {
@ -1371,12 +1424,12 @@ export class Editor extends PrimaryView {
if (existing_tile && existing_tile.type === tile.type && if (existing_tile && existing_tile.type === tile.type &&
// FIXME this is hacky garbage // FIXME this is hacky garbage
tile === this.fg_tile && this.fg_tile_from_palette && tile === this.fg_tile && this.fg_tile_from_palette &&
SPECIAL_PALETTE_BEHAVIOR[tile.type.name] && SPECIAL_TILE_BEHAVIOR[tile.type.name] &&
SPECIAL_PALETTE_BEHAVIOR[tile.type.name].combine_draw) SPECIAL_TILE_BEHAVIOR[tile.type.name].combine_draw)
{ {
let old_tile = {...existing_tile}; let old_tile = {...existing_tile};
let new_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); this._assign_tile(cell, layer, new_tile, old_tile);
return; return;
} }
@ -1418,12 +1471,12 @@ export class Editor extends PrimaryView {
if (existing_tile && existing_tile.type === tile.type && if (existing_tile && existing_tile.type === tile.type &&
// FIXME this is hacky garbage // FIXME this is hacky garbage
tile === this.fg_tile && this.fg_tile_from_palette && tile === this.fg_tile && this.fg_tile_from_palette &&
SPECIAL_PALETTE_BEHAVIOR[tile.type.name] && SPECIAL_TILE_BEHAVIOR[tile.type.name] &&
SPECIAL_PALETTE_BEHAVIOR[tile.type.name].combine_erase) SPECIAL_TILE_BEHAVIOR[tile.type.name].combine_erase)
{ {
let old_tile = {...existing_tile}; let old_tile = {...existing_tile};
let new_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) { if (! remove) {
this._assign_tile(cell, tile.type.layer, new_tile, old_tile); this._assign_tile(cell, tile.type.layer, new_tile, old_tile);
return; return;
@ -1474,20 +1527,160 @@ export class Editor extends PrimaryView {
this.stored_level.linear_cells = new_cells; this.stored_level.linear_cells = new_cells;
this.stored_level.size_x = size_x; this.stored_level.size_x = size_x;
this.stored_level.size_y = size_y; this.stored_level.size_y = size_y;
this.update_viewport_size(); this.update_after_size_change();
this.update_cell_coordinates();
this.redraw_entire_level();
}, () => { }, () => {
this.stored_level.linear_cells = original_cells; this.stored_level.linear_cells = original_cells;
this.stored_level.size_x = original_size_x; this.stored_level.size_x = original_size_x;
this.stored_level.size_y = original_size_y; this.stored_level.size_y = original_size_y;
this.update_viewport_size(); this.update_after_size_change();
this.update_cell_coordinates();
this.redraw_entire_level();
}); });
this.commit_undo(); 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 // Create a connection between two cells and update the UI accordingly. If dest is null or
// undefined, delete any existing connection instead. // undefined, delete any existing connection instead.
set_custom_connection(src, dest) { set_custom_connection(src, dest) {

View File

@ -1031,6 +1031,7 @@ const ADJUST_TOGGLES_CCW = {};
['flame_jet_off', 'flame_jet_on'], ['flame_jet_off', 'flame_jet_on'],
['light_switch_off', 'light_switch_on'], ['light_switch_off', 'light_switch_on'],
['stopwatch_bonus', 'stopwatch_penalty'], ['stopwatch_bonus', 'stopwatch_penalty'],
['turntable_cw', 'turntable_ccw'],
]) ])
{ {
for (let [i, tile] of cycle.entries()) { 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), // Used by the editor and map previews. Draws a region of the level (probably a StoredLevel),
// assuming nothing is moving. // assuming nothing is moving.
draw_static_region(x0, y0, x1, y1, destx = x0, desty = y0) { 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 x = x0; x <= x1; x++) {
for (let y = y0; y <= y1; y++) { for (let y = y0; y <= y1; y++) {
let cell = this.level.cell(x, y); let cell = cells[y * width + x];
if (! cell) if (! cell)
continue; continue;
@ -350,11 +365,11 @@ export class CanvasRenderer {
// For actors (i.e., blocks), perception only applies if there's something // For actors (i.e., blocks), perception only applies if there's something
// of potential interest underneath // 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'; packet.perception = 'normal';
} }
else { else {
packet.perception = this.perception; packet.perception = perception;
} }
if (tile.type.layer < LAYERS.actor && ! ( if (tile.type.layer < LAYERS.actor && ! (