Editor: Add a dedicated level browser with previews, and a button to add a new level
This commit is contained in:
parent
e754e483ec
commit
c4bb1f3df1
@ -154,6 +154,111 @@ class EditorLevelMetaOverlay extends DialogOverlay {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// List of levels, used in the player
|
||||||
|
class EditorLevelBrowserOverlay extends DialogOverlay {
|
||||||
|
constructor(conductor) {
|
||||||
|
super(conductor);
|
||||||
|
this.set_title("choose a level");
|
||||||
|
|
||||||
|
// Set up some infrastructure to lazily display level renders
|
||||||
|
this.renderer = new CanvasRenderer(this.conductor.tileset, 32);
|
||||||
|
this.awaiting_renders = [];
|
||||||
|
this.observer = new IntersectionObserver((entries, observer) => {
|
||||||
|
let any_new = false;
|
||||||
|
let to_remove = new Set;
|
||||||
|
for (let entry of entries) {
|
||||||
|
if (entry.target.classList.contains('--rendered'))
|
||||||
|
continue;
|
||||||
|
|
||||||
|
let index = parseInt(entry.target.getAttribute('data-index'), 10);
|
||||||
|
if (entry.isIntersecting) {
|
||||||
|
this.awaiting_renders.push(index);
|
||||||
|
any_new = true;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
to_remove.add(index);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
this.awaiting_renders = this.awaiting_renders.filter(index => ! to_remove.has(index));
|
||||||
|
if (any_new) {
|
||||||
|
this.schedule_level_render();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{ root: this.main },
|
||||||
|
);
|
||||||
|
this.list = mk('ol.editor-level-browser');
|
||||||
|
for (let [i, meta] of conductor.stored_game.level_metadata.entries()) {
|
||||||
|
let title = meta.title;
|
||||||
|
let li = mk('li',
|
||||||
|
{'data-index': i},
|
||||||
|
mk('div.-preview'),
|
||||||
|
mk('div.-number', {}, meta.number),
|
||||||
|
mk('div.-title', {}, meta.error ? "(error!)" : meta.title),
|
||||||
|
);
|
||||||
|
|
||||||
|
this.list.append(li);
|
||||||
|
|
||||||
|
if (meta.error) {
|
||||||
|
li.classList.add('--error');
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
this.observer.observe(li);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
this.main.append(this.list);
|
||||||
|
|
||||||
|
this.list.addEventListener('click', ev => {
|
||||||
|
let li = ev.target.closest('li');
|
||||||
|
if (! li)
|
||||||
|
return;
|
||||||
|
|
||||||
|
let index = parseInt(li.getAttribute('data-index'), 10);
|
||||||
|
if (this.conductor.change_level(index)) {
|
||||||
|
this.close();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
this.add_button("new level", ev => {
|
||||||
|
this.conductor.editor.append_new_level();
|
||||||
|
this.close();
|
||||||
|
});
|
||||||
|
this.add_button("nevermind", ev => {
|
||||||
|
this.close();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
schedule_level_render() {
|
||||||
|
if (this._handle)
|
||||||
|
return;
|
||||||
|
this._handle = setTimeout(() => { this.render_level() }, 100);
|
||||||
|
}
|
||||||
|
|
||||||
|
render_level() {
|
||||||
|
this._handle = null;
|
||||||
|
if (this.awaiting_renders.length === 0)
|
||||||
|
return;
|
||||||
|
|
||||||
|
let index = this.awaiting_renders.shift();
|
||||||
|
let element = this.list.childNodes[index];
|
||||||
|
let stored_level = this.conductor.stored_game.load_level(index);
|
||||||
|
this.conductor.editor._xxx_update_stored_level_cells(stored_level);
|
||||||
|
this.renderer.set_level(stored_level);
|
||||||
|
this.renderer.set_viewport_size(stored_level.size_x, stored_level.size_y);
|
||||||
|
this.renderer.draw();
|
||||||
|
let canvas = mk('canvas', {
|
||||||
|
width: stored_level.size_x * this.conductor.tileset.size_x / 4,
|
||||||
|
height: stored_level.size_y * this.conductor.tileset.size_y / 4,
|
||||||
|
});
|
||||||
|
canvas.getContext('2d').drawImage(this.renderer.canvas, 0, 0, canvas.width, canvas.height);
|
||||||
|
element.querySelector('.-preview').append(canvas);
|
||||||
|
element.classList.add('--rendered');
|
||||||
|
|
||||||
|
this.schedule_level_render();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
class EditorShareOverlay extends DialogOverlay {
|
class EditorShareOverlay extends DialogOverlay {
|
||||||
constructor(conductor, url) {
|
constructor(conductor, url) {
|
||||||
super(conductor);
|
super(conductor);
|
||||||
@ -904,6 +1009,7 @@ const EDITOR_PALETTE = [{
|
|||||||
'teleport_red',
|
'teleport_red',
|
||||||
'teleport_green',
|
'teleport_green',
|
||||||
'teleport_yellow',
|
'teleport_yellow',
|
||||||
|
'transmogrifier',
|
||||||
],
|
],
|
||||||
}];
|
}];
|
||||||
|
|
||||||
@ -1051,6 +1157,15 @@ export class Editor extends PrimaryView {
|
|||||||
this.select_tool(button.getAttribute('data-tool'));
|
this.select_tool(button.getAttribute('data-tool'));
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Rotation buttons, which affect both the palette tile and the entire palette
|
||||||
|
this.palette_rotation_index = 0;
|
||||||
|
this.palette_actor_direction = 'south';
|
||||||
|
let rotate_left_button = mk('button.--image', {type: 'button'}, mk('img', {src: '/icons/rotate-left.png'}));
|
||||||
|
rotate_left_button.addEventListener('click', ev => {
|
||||||
|
this.rotate_palette_left();
|
||||||
|
});
|
||||||
|
// TODO finish this up: this.root.querySelector('.controls').append(rotate_left_button);
|
||||||
|
|
||||||
// Toolbar buttons for saving, exporting, etc.
|
// Toolbar buttons for saving, exporting, etc.
|
||||||
let button_container = mk('div.-buttons');
|
let button_container = mk('div.-buttons');
|
||||||
this.root.querySelector('.controls').append(button_container);
|
this.root.querySelector('.controls').append(button_container);
|
||||||
@ -1251,17 +1366,57 @@ export class Editor extends PrimaryView {
|
|||||||
this.conductor.switch_to_editor();
|
this.conductor.switch_to_editor();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
append_new_level() {
|
||||||
|
let stored_pack = this.conductor.stored_game;
|
||||||
|
let index = stored_pack.level_metadata.length;
|
||||||
|
let number = index + 1;
|
||||||
|
let stored_level = this._make_empty_level(number, 32, 32);
|
||||||
|
let level_key = `LLL-${Date.now()}`;
|
||||||
|
stored_level.editor_metadata = {
|
||||||
|
key: level_key,
|
||||||
|
};
|
||||||
|
// FIXME should convert this to the storage-backed version when switching levels, rather
|
||||||
|
// than keeping it around?
|
||||||
|
stored_pack.level_metadata.push({
|
||||||
|
stored_level: stored_level,
|
||||||
|
key: level_key,
|
||||||
|
title: stored_level.title,
|
||||||
|
index: index,
|
||||||
|
number: number,
|
||||||
|
});
|
||||||
|
|
||||||
|
let pack_key = stored_pack.editor_metadata.key;
|
||||||
|
let stash_pack_entry = this.stash.packs[pack_key];
|
||||||
|
stash_pack_entry.level_count = number;
|
||||||
|
stash_pack_entry.last_modified = Date.now();
|
||||||
|
save_json_to_storage("Lexy's Labyrinth editor", this.stash);
|
||||||
|
|
||||||
|
let pack_stash = load_json_from_storage(pack_key);
|
||||||
|
pack_stash.levels.push({
|
||||||
|
key: level_key,
|
||||||
|
title: stored_level.title,
|
||||||
|
last_modified: Date.now(),
|
||||||
|
});
|
||||||
|
save_json_to_storage(pack_key, pack_stash);
|
||||||
|
|
||||||
|
let buf = c2g.synthesize_level(stored_level);
|
||||||
|
let stringy_buf = string_from_buffer_ascii(buf);
|
||||||
|
window.localStorage.setItem(level_key, stringy_buf);
|
||||||
|
|
||||||
|
this.conductor.change_level(index);
|
||||||
|
}
|
||||||
|
|
||||||
load_game(stored_game) {
|
load_game(stored_game) {
|
||||||
}
|
}
|
||||||
|
|
||||||
_xxx_update_stored_level_cells() {
|
_xxx_update_stored_level_cells(stored_level) {
|
||||||
// XXX need this for renderer compat, not used otherwise
|
// XXX need this for renderer compat, not used otherwise, PLEASE delete
|
||||||
this.stored_level.cells = [];
|
stored_level.cells = [];
|
||||||
let row;
|
let row;
|
||||||
for (let [i, cell] of this.stored_level.linear_cells.entries()) {
|
for (let [i, cell] of stored_level.linear_cells.entries()) {
|
||||||
if (i % this.stored_level.size_x === 0) {
|
if (i % stored_level.size_x === 0) {
|
||||||
row = [];
|
row = [];
|
||||||
this.stored_level.cells.push(row);
|
stored_level.cells.push(row);
|
||||||
}
|
}
|
||||||
row.push(cell);
|
row.push(cell);
|
||||||
}
|
}
|
||||||
@ -1272,7 +1427,7 @@ export class Editor extends PrimaryView {
|
|||||||
this.stored_level = stored_level;
|
this.stored_level = stored_level;
|
||||||
this.update_viewport_size();
|
this.update_viewport_size();
|
||||||
|
|
||||||
this._xxx_update_stored_level_cells();
|
this._xxx_update_stored_level_cells(this.stored_level);
|
||||||
|
|
||||||
// Load connections
|
// Load connections
|
||||||
this.connections_g.textContent = '';
|
this.connections_g.textContent = '';
|
||||||
@ -1305,6 +1460,10 @@ 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}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
open_level_browser() {
|
||||||
|
new EditorLevelBrowserOverlay(this.conductor).open();
|
||||||
|
}
|
||||||
|
|
||||||
select_tool(tool) {
|
select_tool(tool) {
|
||||||
if (tool === this.current_tool)
|
if (tool === this.current_tool)
|
||||||
return;
|
return;
|
||||||
@ -1356,6 +1515,12 @@ export class Editor extends PrimaryView {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
rotate_palette_left() {
|
||||||
|
this.palette_rotation_index += 1;
|
||||||
|
this.palette_rotation_index %= 4;
|
||||||
|
this.palette_actor_direction = DIRECTIONS[this.palette_actor_direction].left;
|
||||||
|
}
|
||||||
|
|
||||||
mark_tile_dirty(tile) {
|
mark_tile_dirty(tile) {
|
||||||
// TODO partial redraws! until then, redraw everything
|
// TODO partial redraws! until then, redraw everything
|
||||||
if (tile === this.palette_selection) {
|
if (tile === this.palette_selection) {
|
||||||
@ -1453,7 +1618,7 @@ 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._xxx_update_stored_level_cells();
|
this._xxx_update_stored_level_cells(this.stored_level);
|
||||||
this.update_viewport_size();
|
this.update_viewport_size();
|
||||||
this.renderer.draw();
|
this.renderer.draw();
|
||||||
}
|
}
|
||||||
|
|||||||
15
js/main.js
15
js/main.js
@ -836,6 +836,10 @@ class Player extends PrimaryView {
|
|||||||
this._redraw();
|
this._redraw();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
open_level_browser() {
|
||||||
|
new LevelBrowserOverlay(this.conductor).open();
|
||||||
|
}
|
||||||
|
|
||||||
play_demo() {
|
play_demo() {
|
||||||
this.restart_level();
|
this.restart_level();
|
||||||
let demo = this.level.stored_level.demo;
|
let demo = this.level.stored_level.demo;
|
||||||
@ -1609,7 +1613,7 @@ class Splash extends PrimaryView {
|
|||||||
// Add buttons for any existing packs
|
// Add buttons for any existing packs
|
||||||
let packs = this.conductor.editor.stash.packs;
|
let packs = this.conductor.editor.stash.packs;
|
||||||
let pack_keys = Object.keys(packs);
|
let pack_keys = Object.keys(packs);
|
||||||
pack_keys.sort((a, b) => packs[a].last_modified - packs[b].last_modified);
|
pack_keys.sort((a, b) => packs[b].last_modified - packs[a].last_modified);
|
||||||
let editor_section = this.root.querySelector('#splash-your-levels');
|
let editor_section = this.root.querySelector('#splash-your-levels');
|
||||||
let editor_list = editor_section;
|
let editor_list = editor_section;
|
||||||
for (let key of pack_keys) {
|
for (let key of pack_keys) {
|
||||||
@ -1862,7 +1866,7 @@ class OptionsOverlay extends DialogOverlay {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// List of levels
|
// List of levels, used in the player
|
||||||
class LevelBrowserOverlay extends DialogOverlay {
|
class LevelBrowserOverlay extends DialogOverlay {
|
||||||
constructor(conductor) {
|
constructor(conductor) {
|
||||||
super(conductor);
|
super(conductor);
|
||||||
@ -2019,9 +2023,10 @@ class Conductor {
|
|||||||
ev.target.blur();
|
ev.target.blur();
|
||||||
});
|
});
|
||||||
this.nav_choose_level_button.addEventListener('click', ev => {
|
this.nav_choose_level_button.addEventListener('click', ev => {
|
||||||
if (this.stored_game) {
|
if (! this.stored_game)
|
||||||
new LevelBrowserOverlay(this).open();
|
return;
|
||||||
}
|
|
||||||
|
this.current.open_level_browser();
|
||||||
ev.target.blur();
|
ev.target.blur();
|
||||||
});
|
});
|
||||||
document.querySelector('#main-change-pack').addEventListener('click', ev => {
|
document.querySelector('#main-change-pack').addEventListener('click', ev => {
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user