Add support for rotating or flipping a level or selection
This commit is contained in:
parent
7ed3d38489
commit
53ed2f0948
@ -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
|
||||||
|
|||||||
@ -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'},
|
||||||
|
);
|
||||||
|
|||||||
@ -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)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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) {
|
||||||
|
|||||||
@ -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()) {
|
||||||
|
|||||||
@ -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 && ! (
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user