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 {
|
||||
constructor(conductor, url) {
|
||||
super(conductor);
|
||||
@ -904,6 +1009,7 @@ const EDITOR_PALETTE = [{
|
||||
'teleport_red',
|
||||
'teleport_green',
|
||||
'teleport_yellow',
|
||||
'transmogrifier',
|
||||
],
|
||||
}];
|
||||
|
||||
@ -1051,6 +1157,15 @@ export class Editor extends PrimaryView {
|
||||
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.
|
||||
let button_container = mk('div.-buttons');
|
||||
this.root.querySelector('.controls').append(button_container);
|
||||
@ -1251,17 +1366,57 @@ export class Editor extends PrimaryView {
|
||||
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) {
|
||||
}
|
||||
|
||||
_xxx_update_stored_level_cells() {
|
||||
// XXX need this for renderer compat, not used otherwise
|
||||
this.stored_level.cells = [];
|
||||
_xxx_update_stored_level_cells(stored_level) {
|
||||
// XXX need this for renderer compat, not used otherwise, PLEASE delete
|
||||
stored_level.cells = [];
|
||||
let row;
|
||||
for (let [i, cell] of this.stored_level.linear_cells.entries()) {
|
||||
if (i % this.stored_level.size_x === 0) {
|
||||
for (let [i, cell] of stored_level.linear_cells.entries()) {
|
||||
if (i % stored_level.size_x === 0) {
|
||||
row = [];
|
||||
this.stored_level.cells.push(row);
|
||||
stored_level.cells.push(row);
|
||||
}
|
||||
row.push(cell);
|
||||
}
|
||||
@ -1272,7 +1427,7 @@ export class Editor extends PrimaryView {
|
||||
this.stored_level = stored_level;
|
||||
this.update_viewport_size();
|
||||
|
||||
this._xxx_update_stored_level_cells();
|
||||
this._xxx_update_stored_level_cells(this.stored_level);
|
||||
|
||||
// Load connections
|
||||
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}`);
|
||||
}
|
||||
|
||||
open_level_browser() {
|
||||
new EditorLevelBrowserOverlay(this.conductor).open();
|
||||
}
|
||||
|
||||
select_tool(tool) {
|
||||
if (tool === this.current_tool)
|
||||
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) {
|
||||
// TODO partial redraws! until then, redraw everything
|
||||
if (tile === this.palette_selection) {
|
||||
@ -1453,7 +1618,7 @@ export class Editor extends PrimaryView {
|
||||
this.stored_level.linear_cells = new_cells;
|
||||
this.stored_level.size_x = size_x;
|
||||
this.stored_level.size_y = size_y;
|
||||
this._xxx_update_stored_level_cells();
|
||||
this._xxx_update_stored_level_cells(this.stored_level);
|
||||
this.update_viewport_size();
|
||||
this.renderer.draw();
|
||||
}
|
||||
|
||||
15
js/main.js
15
js/main.js
@ -836,6 +836,10 @@ class Player extends PrimaryView {
|
||||
this._redraw();
|
||||
}
|
||||
|
||||
open_level_browser() {
|
||||
new LevelBrowserOverlay(this.conductor).open();
|
||||
}
|
||||
|
||||
play_demo() {
|
||||
this.restart_level();
|
||||
let demo = this.level.stored_level.demo;
|
||||
@ -1609,7 +1613,7 @@ class Splash extends PrimaryView {
|
||||
// Add buttons for any existing packs
|
||||
let packs = this.conductor.editor.stash.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_list = editor_section;
|
||||
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 {
|
||||
constructor(conductor) {
|
||||
super(conductor);
|
||||
@ -2019,9 +2023,10 @@ class Conductor {
|
||||
ev.target.blur();
|
||||
});
|
||||
this.nav_choose_level_button.addEventListener('click', ev => {
|
||||
if (this.stored_game) {
|
||||
new LevelBrowserOverlay(this).open();
|
||||
}
|
||||
if (! this.stored_game)
|
||||
return;
|
||||
|
||||
this.current.open_level_browser();
|
||||
ev.target.blur();
|
||||
});
|
||||
document.querySelector('#main-change-pack').addEventListener('click', ev => {
|
||||
|
||||
Loading…
Reference in New Issue
Block a user