Add controls for rearranging, duplicating, and deleting levels
This commit is contained in:
parent
f89cccedb2
commit
ac9b702eaa
@ -5,6 +5,8 @@
|
||||
<title>Lexy's Labyrinth</title>
|
||||
<link rel="stylesheet" type="text/css" href="style.css">
|
||||
<link rel="shortcut icon" type="image/png" href="icon.png">
|
||||
<!-- FIXME it would be super swell if i could load this lazily -->
|
||||
<script src="https://cdn.jsdelivr.net/npm/sortablejs@latest/Sortable.min.js"></script>
|
||||
<script type="module" src="js/main.js"></script>
|
||||
<meta name="og:type" content="website">
|
||||
<meta name="og:image" content="https://c.eev.ee/lexys-labyrinth/og-preview.png">
|
||||
|
||||
@ -175,6 +175,11 @@ export class DialogOverlay extends Overlay {
|
||||
let button = mk('button', {type: 'button'}, label);
|
||||
button.addEventListener('click', onclick);
|
||||
this.footer.append(button);
|
||||
return button;
|
||||
}
|
||||
|
||||
add_button_gap() {
|
||||
this.footer.append(mk('div.-spacer'));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
import * as fflate from 'https://unpkg.com/fflate@0.4.8/esm/index.mjs';
|
||||
import * as fflate from 'https://cdn.skypack.dev/fflate?min';
|
||||
|
||||
import { DIRECTIONS, LAYERS, TICS_PER_SECOND } from './defs.js';
|
||||
import { TILES_WITH_PROPS } from './editor-tile-overlays.js';
|
||||
@ -211,7 +211,7 @@ class EditorLevelBrowserOverlay extends DialogOverlay {
|
||||
if (entry.target.classList.contains('--rendered'))
|
||||
continue;
|
||||
|
||||
let index = parseInt(entry.target.getAttribute('data-index'), 10);
|
||||
let index = this._get_index(entry.target);
|
||||
if (entry.isIntersecting) {
|
||||
this.awaiting_renders.push(index);
|
||||
any_new = true;
|
||||
@ -229,55 +229,158 @@ class EditorLevelBrowserOverlay extends DialogOverlay {
|
||||
{ root: this.main },
|
||||
);
|
||||
this.list = mk('ol.editor-level-browser');
|
||||
this.selection = this.conductor.level_index;
|
||||
for (let [i, meta] of conductor.stored_game.level_metadata.entries()) {
|
||||
let title = meta.title;
|
||||
this.list.append(this._make_list_item(i, meta));
|
||||
}
|
||||
this.list.childNodes[this.selection].classList.add('--selected');
|
||||
this.main.append(
|
||||
mk('p', "Drag to rearrange. Changes are immediate!"),
|
||||
this.list,
|
||||
);
|
||||
|
||||
this.list.addEventListener('click', ev => {
|
||||
let index = this._get_index(ev.target);
|
||||
if (index === null)
|
||||
return;
|
||||
this._select(index);
|
||||
});
|
||||
this.list.addEventListener('dblclick', ev => {
|
||||
let index = this._get_index(ev.target);
|
||||
if (index !== null && this.conductor.change_level(index)) {
|
||||
this.close();
|
||||
}
|
||||
});
|
||||
|
||||
this.sortable = new Sortable(this.list, {
|
||||
group: 'editor-levels',
|
||||
onEnd: ev => {
|
||||
if (ev.oldIndex === ev.newIndex)
|
||||
return;
|
||||
|
||||
this._move_level(ev.oldIndex, ev.newIndex);
|
||||
|
||||
this.undo_stack.push(() => {
|
||||
this.list.insertBefore(
|
||||
this.list.childNodes[ev.newIndex],
|
||||
this.list.childNodes[ev.oldIndex + (ev.oldIndex < ev.newIndex ? 0 : 1)]);
|
||||
this._move_level(ev.newIndex, ev.oldIndex);
|
||||
});
|
||||
this.undo_button.disabled = false;
|
||||
},
|
||||
});
|
||||
|
||||
this.undo_stack = [];
|
||||
|
||||
// Left buttons
|
||||
this.undo_button = this.add_button("undo", ev => {
|
||||
if (! this.undo_stack.length)
|
||||
return;
|
||||
|
||||
let undo = this.undo_stack.pop();
|
||||
undo();
|
||||
this.undo_button.disabled = ! this.undo_stack.length;
|
||||
});
|
||||
this.undo_button.disabled = true;
|
||||
this.add_button("create", ev => {
|
||||
let index = this.selection + 1;
|
||||
let stored_level = this.conductor.editor._make_empty_level(index + 1, 32, 32);
|
||||
this.conductor.editor.insert_level(stored_level, index);
|
||||
this._after_insert_level(stored_level, index);
|
||||
|
||||
this.undo_stack.push(() => {
|
||||
this._delete_level(index);
|
||||
});
|
||||
this.undo_button.disabled = false;
|
||||
});
|
||||
this.add_button("duplicate", ev => {
|
||||
let index = this.selection + 1;
|
||||
let stored_level = this.conductor.editor.duplicate_level(this.selection);
|
||||
this._after_insert_level(stored_level, index);
|
||||
|
||||
this.undo_stack.push(() => {
|
||||
this._delete_level(index);
|
||||
});
|
||||
this.undo_button.disabled = false;
|
||||
});
|
||||
this.add_button("delete", ev => {
|
||||
let index = this.selection;
|
||||
if (index === this.conductor.level_index) {
|
||||
// FIXME complain, or disable button
|
||||
return;
|
||||
}
|
||||
|
||||
// Snag a copy of the serialized level for undo purposes
|
||||
// FIXME can't undo deleting a corrupt level
|
||||
let meta = this.conductor.stored_game.level_metadata[index];
|
||||
let serialized_level = window.localStorage.getItem(meta.key);
|
||||
|
||||
this._delete_level(index);
|
||||
|
||||
this.undo_stack.push(() => {
|
||||
let stored_level = meta.stored_level ?? c2g.parse_level(
|
||||
bytestring_to_buffer(serialized_level), index + 1);
|
||||
this.conductor.editor.insert_level(stored_level, index);
|
||||
if (this.selection >= index) {
|
||||
this.selection += 1;
|
||||
}
|
||||
this._after_insert_level(stored_level, index);
|
||||
});
|
||||
this.undo_button.disabled = false;
|
||||
});
|
||||
|
||||
// Right buttons
|
||||
this.add_button_gap();
|
||||
this.add_button("open", ev => {
|
||||
if (this.selection === this.conductor.level_index || this.conductor.change_level(this.selection)) {
|
||||
this.close();
|
||||
}
|
||||
});
|
||||
this.add_button("nevermind", ev => {
|
||||
this.close();
|
||||
});
|
||||
}
|
||||
|
||||
_make_list_item(index, meta) {
|
||||
let li = mk('li',
|
||||
{'data-index': i},
|
||||
{'data-index': index},
|
||||
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');
|
||||
return li;
|
||||
}
|
||||
|
||||
renumber_levels(start_index, end_index = null) {
|
||||
end_index = end_index ?? this.conductor.stored_game.level_metadata.length - 1;
|
||||
for (let i = start_index; i <= end_index; i++) {
|
||||
let li = this.list.childNodes[i];
|
||||
let meta = this.conductor.stored_game.level_metadata[i];
|
||||
li.setAttribute('data-index', i);
|
||||
li.querySelector('.-number').textContent = meta.number;
|
||||
}
|
||||
}
|
||||
|
||||
_get_index(element) {
|
||||
let li = element.closest('li');
|
||||
if (! li)
|
||||
return;
|
||||
return null;
|
||||
|
||||
let index = parseInt(li.getAttribute('data-index'), 10);
|
||||
if (this.conductor.change_level(index)) {
|
||||
this.close();
|
||||
return parseInt(li.getAttribute('data-index'), 10);
|
||||
}
|
||||
});
|
||||
|
||||
// FIXME it would be super swell if i could load this lazily
|
||||
/*
|
||||
<script src="https://cdn.jsdelivr.net/npm/sortablejs@latest/Sortable.min.js"></script>
|
||||
this.sortable = new Sortable(this.list, {
|
||||
group: 'editor-levels',
|
||||
onEnd: ev => {
|
||||
// FIXME rearrange the levels for realsies
|
||||
},
|
||||
});
|
||||
*/
|
||||
|
||||
this.add_button("new level", ev => {
|
||||
this.conductor.editor.append_new_level();
|
||||
this.close();
|
||||
});
|
||||
this.add_button("nevermind", ev => {
|
||||
this.close();
|
||||
});
|
||||
_select(index) {
|
||||
this.list.childNodes[this.selection].classList.remove('--selected');
|
||||
this.selection = index;
|
||||
this.list.childNodes[this.selection].classList.add('--selected');
|
||||
}
|
||||
|
||||
schedule_level_render() {
|
||||
@ -307,6 +410,54 @@ class EditorLevelBrowserOverlay extends DialogOverlay {
|
||||
|
||||
this.schedule_level_render();
|
||||
}
|
||||
|
||||
_after_insert_level(stored_level, index) {
|
||||
this.list.insertBefore(
|
||||
this._make_list_item(index, this.conductor.stored_game.level_metadata[index]),
|
||||
this.list.childNodes[index]);
|
||||
this._select(index);
|
||||
this.renumber_levels(index + 1);
|
||||
}
|
||||
|
||||
_delete_level(index) {
|
||||
let num_levels = this.conductor.stored_game.level_metadata.length;
|
||||
let stored_level = this.conductor.editor.delete_level(index);
|
||||
|
||||
this.list.childNodes[this.selection].classList.remove('--selected');
|
||||
this.list.childNodes[index].remove();
|
||||
if (index === num_levels - 1) {
|
||||
this.selection -= 1;
|
||||
}
|
||||
else {
|
||||
this.renumber_levels(index);
|
||||
}
|
||||
this.list.childNodes[this.selection].classList.add('--selected');
|
||||
}
|
||||
|
||||
_move_level(from_index, to_index) {
|
||||
this.conductor.editor.move_level(from_index, to_index);
|
||||
|
||||
let selection = this.selection;
|
||||
if (from_index < to_index) {
|
||||
this.renumber_levels(from_index, to_index);
|
||||
if (from_index < selection && selection <= to_index) {
|
||||
selection -= 1;
|
||||
}
|
||||
}
|
||||
else {
|
||||
this.renumber_levels(to_index, from_index);
|
||||
if (to_index <= selection && selection < from_index) {
|
||||
selection += 1;
|
||||
}
|
||||
}
|
||||
|
||||
if (this.selection === from_index) {
|
||||
this.selection = to_index;
|
||||
}
|
||||
else {
|
||||
this.selection = selection;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class EditorShareOverlay extends DialogOverlay {
|
||||
@ -2409,6 +2560,27 @@ class Selection {
|
||||
// TODO make more stuff respect this (more things should go through Editor for undo reasons anyway)
|
||||
}
|
||||
|
||||
// Edited levels are stored as follows.
|
||||
// StoredPack and StoredLevel both have an editor_metadata containing:
|
||||
// key
|
||||
// StoredPack's level_metadata contains:
|
||||
// stored_level (optional)
|
||||
// title
|
||||
// key
|
||||
// number
|
||||
// index
|
||||
// The editor's own storage contains:
|
||||
// packs:
|
||||
// key:
|
||||
// title
|
||||
// level_count
|
||||
// last_modified
|
||||
// current_level
|
||||
// And a pack's storage contains:
|
||||
// levels:
|
||||
// - key
|
||||
// title
|
||||
// last_modified
|
||||
export class Editor extends PrimaryView {
|
||||
constructor(conductor) {
|
||||
super(conductor, document.body.querySelector('main#editor'));
|
||||
@ -2420,7 +2592,7 @@ export class Editor extends PrimaryView {
|
||||
this.stash = load_json_from_storage("Lexy's Labyrinth editor");
|
||||
if (! this.stash) {
|
||||
this.stash = {
|
||||
packs: {}, // key: { title, level_count, last_modified }
|
||||
packs: {}, // key: { title, level_count, last_modified, current_level }
|
||||
// More pack data is stored separately under the key, as {
|
||||
// levels: [{key, title}],
|
||||
// }
|
||||
@ -2811,16 +2983,61 @@ export class Editor extends PrimaryView {
|
||||
return stored_level;
|
||||
}
|
||||
|
||||
create_pack() {
|
||||
// TODO get a dialog for asking about level meta first? or is jumping directly into the editor better?
|
||||
_save_pack_to_stash(stored_pack, current_level) {
|
||||
if (! stored_pack.editor_metadata) {
|
||||
console.error("Asked to save a stored pack that's not part of the editor", stored_pack);
|
||||
return;
|
||||
}
|
||||
|
||||
// Reload the stash in case a pack was created in another tab
|
||||
// TODO do this with events
|
||||
this.stash = load_json_from_storage("Lexy's Labyrinth editor") ?? this.stash;
|
||||
|
||||
let pack_key = stored_pack.editor_metadata.key;
|
||||
this.stash.packs[pack_key] = {
|
||||
title: stored_pack.title,
|
||||
level_count: stored_pack.level_metadata.length,
|
||||
// FIXME i want to update current_level on browse but don't want to affect last_modified
|
||||
last_modified: Date.now(),
|
||||
current_level: current_level,
|
||||
};
|
||||
save_json_to_storage("Lexy's Labyrinth editor", this.stash);
|
||||
}
|
||||
|
||||
_save_level_to_storage(stored_level) {
|
||||
if (! stored_level.editor_metadata) {
|
||||
console.error("Asked to save a stored level that's not part of the editor", stored_level);
|
||||
return;
|
||||
}
|
||||
|
||||
let buf = c2g.synthesize_level(stored_level);
|
||||
let stringy_buf = string_from_buffer_ascii(buf);
|
||||
window.localStorage.setItem(stored_level.editor_metadata.key, stringy_buf);
|
||||
}
|
||||
|
||||
create_scratch_level() {
|
||||
let stored_level = this._make_empty_level(1, 32, 32);
|
||||
|
||||
let stored_pack = new format_base.StoredPack(null);
|
||||
stored_pack.title = "scratch pack";
|
||||
stored_pack.level_metadata.push({
|
||||
stored_level: stored_level,
|
||||
});
|
||||
this.conductor.load_game(stored_pack);
|
||||
|
||||
this.conductor.switch_to_editor();
|
||||
}
|
||||
|
||||
create_pack() {
|
||||
let pack_key = `LLP-${Date.now()}`;
|
||||
let level_key = `LLL-${Date.now()}`;
|
||||
let stored_pack = new format_base.StoredPack(pack_key);
|
||||
stored_pack.title = "Untitled pack";
|
||||
stored_pack.editor_metadata = {
|
||||
key: pack_key,
|
||||
};
|
||||
|
||||
let stored_level = this._make_empty_level(1, 32, 32);
|
||||
stored_level.editor_metadata = {
|
||||
key: level_key,
|
||||
};
|
||||
@ -2835,12 +3052,7 @@ export class Editor extends PrimaryView {
|
||||
});
|
||||
this.conductor.load_game(stored_pack);
|
||||
|
||||
this.stash.packs[pack_key] = {
|
||||
title: "Untitled pack",
|
||||
level_count: 1,
|
||||
last_modified: Date.now(),
|
||||
};
|
||||
save_json_to_storage("Lexy's Labyrinth editor", this.stash);
|
||||
this._save_pack_to_stash(stored_pack, 0);
|
||||
|
||||
save_json_to_storage(pack_key, {
|
||||
levels: [{
|
||||
@ -2850,22 +3062,7 @@ export class Editor extends PrimaryView {
|
||||
}],
|
||||
});
|
||||
|
||||
let buf = c2g.synthesize_level(stored_level);
|
||||
let stringy_buf = string_from_buffer_ascii(buf);
|
||||
window.localStorage.setItem(level_key, stringy_buf);
|
||||
|
||||
this.conductor.switch_to_editor();
|
||||
}
|
||||
|
||||
create_scratch_level() {
|
||||
let stored_level = this._make_empty_level(1, 32, 32);
|
||||
|
||||
let stored_pack = new format_base.StoredPack(null);
|
||||
stored_pack.title = "scratch pack";
|
||||
stored_pack.level_metadata.push({
|
||||
stored_level: stored_level,
|
||||
});
|
||||
this.conductor.load_game(stored_pack);
|
||||
this._save_level_to_storage(stored_level);
|
||||
|
||||
this.conductor.switch_to_editor();
|
||||
}
|
||||
@ -2900,79 +3097,189 @@ export class Editor extends PrimaryView {
|
||||
this.conductor.switch_to_editor();
|
||||
}
|
||||
|
||||
append_new_level() {
|
||||
// Move, insert, or delete a level. If dest_index is null, the level will be deleted. If
|
||||
// source is a number, it's an index; otherwise, it's a level, assumed to be newly-created, and
|
||||
// will be given a new key and saved to localStorage. (Passing null and a level will,
|
||||
// of course, do nothing. Passing an out of bounds source index will also do nothing.)
|
||||
_move_level(source, dest_index) {
|
||||
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({
|
||||
if (! stored_pack.editor_metadata) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Get the level, and pull it out of the list if necessary
|
||||
let stored_level, level_metadata, pack_stash_entry, source_index = null;
|
||||
let pack_stash = load_json_from_storage(stored_pack.editor_metadata.key);
|
||||
if (typeof source === 'number') {
|
||||
if (source === dest_index)
|
||||
return;
|
||||
|
||||
source_index = source;
|
||||
if (source_index < 0 || source_index >= stored_pack.level_metadata.length) {
|
||||
console.warn("Asked to move a level with an out-of-bounds source:", source_index);
|
||||
return;
|
||||
}
|
||||
|
||||
[level_metadata] = stored_pack.level_metadata.splice(source_index, 1);
|
||||
[pack_stash_entry] = pack_stash.levels.splice(source_index, 1);
|
||||
|
||||
stored_level = level_metadata.stored_level ?? null;
|
||||
if (stored_level === null && source_index === this.conductor.level_index) {
|
||||
stored_level = this.conductor.stored_level;
|
||||
}
|
||||
}
|
||||
else {
|
||||
// This is a new level
|
||||
if (dest_index === null)
|
||||
// Nothing to do
|
||||
return;
|
||||
|
||||
dest_index = Math.max(0, Math.min(stored_pack.level_metadata.length, dest_index));
|
||||
|
||||
stored_level = source;
|
||||
level_metadata = {
|
||||
stored_level: stored_level,
|
||||
key: level_key,
|
||||
key: `LLL-${Date.now()}`,
|
||||
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,
|
||||
index: dest_index,
|
||||
number: dest_index + 1,
|
||||
};
|
||||
pack_stash_entry = {
|
||||
key: level_metadata.key,
|
||||
title: stored_level.title,
|
||||
last_modified: Date.now(),
|
||||
});
|
||||
save_json_to_storage(pack_key, pack_stash);
|
||||
};
|
||||
stored_level.editor_metadata = {
|
||||
key: level_metadata.key,
|
||||
};
|
||||
this._save_level_to_storage(stored_level);
|
||||
}
|
||||
|
||||
let buf = c2g.synthesize_level(stored_level);
|
||||
let stringy_buf = string_from_buffer_ascii(buf);
|
||||
window.localStorage.setItem(level_key, stringy_buf);
|
||||
if (dest_index === null) {
|
||||
// Erase the level from localStorage
|
||||
window.localStorage.removeItem(level_metadata.key);
|
||||
}
|
||||
else {
|
||||
// Add the level to the appropriate place
|
||||
if (stored_level) {
|
||||
stored_level.index = dest_index;
|
||||
stored_level.number = dest_index + 1;
|
||||
}
|
||||
level_metadata.index = dest_index;
|
||||
level_metadata.number = dest_index + 1;
|
||||
|
||||
this.conductor.change_level(index);
|
||||
stored_pack.level_metadata.splice(dest_index, 0, level_metadata);
|
||||
pack_stash.levels.splice(dest_index, 0, pack_stash_entry);
|
||||
}
|
||||
// This is done with now; the pack stash has no numbering
|
||||
save_json_to_storage(stored_pack.editor_metadata.key, pack_stash);
|
||||
|
||||
// Renumber levels as necessary
|
||||
let delta, start_index, end_index;
|
||||
if (source_index === null) {
|
||||
// A level was inserted, so increment the number of every level after it
|
||||
delta = +1;
|
||||
start_index = dest_index + 1;
|
||||
end_index = stored_pack.level_metadata.length - 1;
|
||||
}
|
||||
else if (dest_index === null) {
|
||||
// A level was deleted, so decrement the number of every level after it
|
||||
delta = -1;
|
||||
start_index = source_index;
|
||||
end_index = stored_pack.level_metadata.length - 1;
|
||||
}
|
||||
else {
|
||||
// A level was moved, so it depends whether it was moved forwards or backwards
|
||||
if (source_index < dest_index) {
|
||||
delta = -1;
|
||||
start_index = source_index;
|
||||
end_index = dest_index - 1;
|
||||
}
|
||||
else {
|
||||
delta = +1;
|
||||
start_index = dest_index + 1;
|
||||
end_index = source_index;
|
||||
}
|
||||
}
|
||||
for (let i = start_index; i <= end_index; i++) {
|
||||
let meta = stored_pack.level_metadata[i];
|
||||
meta.index += delta;
|
||||
meta.number += delta;
|
||||
if (meta.stored_level) {
|
||||
meta.stored_level.index += delta;
|
||||
meta.stored_level.number += delta;
|
||||
}
|
||||
}
|
||||
|
||||
// Update the conductor's index too so it doesn't get confused
|
||||
if (this.conductor.level_index === source_index) {
|
||||
// FIXME refuse to delete the current level
|
||||
this.conductor.level_index = dest_index;
|
||||
}
|
||||
else if (start_index <= this.conductor.level_index && this.conductor.level_index <= end_index) {
|
||||
this.conductor.level_index += delta;
|
||||
// Update the current level if it's not stored in the metadata yet
|
||||
if (! stored_level) {
|
||||
this.conductor.stored_level.index += delta;
|
||||
this.conductor.stored_level.number += delta;
|
||||
}
|
||||
}
|
||||
|
||||
// Update the title and headers, since the level number might have changed
|
||||
this.conductor.update_level_title();
|
||||
this.conductor.update_nav_buttons();
|
||||
|
||||
// Save the pack to the editor stash, and we should be done!
|
||||
this._save_pack_to_stash(stored_pack, this.conductor.level_index);
|
||||
|
||||
return stored_level;
|
||||
}
|
||||
|
||||
insert_level(stored_level, index) {
|
||||
return this._move_level(stored_level, index);
|
||||
}
|
||||
|
||||
move_level(from_index, to_index) {
|
||||
return this._move_level(from_index, to_index);
|
||||
}
|
||||
|
||||
duplicate_level(index) {
|
||||
// The most reliable way to clone a level is to reserialize its current state
|
||||
// TODO with autosave this shouldn't be necessary, just copy the existing serialization
|
||||
let stored_level = c2g.parse_level(c2g.synthesize_level(this.conductor.stored_game.load_level(index)), index + 2);
|
||||
return this._move_level(stored_level, index + 1);
|
||||
}
|
||||
|
||||
delete_level(index) {
|
||||
return this._move_level(index, null);
|
||||
}
|
||||
|
||||
save_level() {
|
||||
// TODO need feedback. or maybe not bc this should be replaced with autosave later
|
||||
// TODO also need to update the pack data's last modified time
|
||||
let stored_game = this.conductor.stored_game;
|
||||
if (! stored_game.editor_metadata)
|
||||
let stored_pack = this.conductor.stored_game;
|
||||
if (! stored_pack.editor_metadata)
|
||||
return;
|
||||
|
||||
// Update the pack index; we need to do this to update the last modified time anyway, so
|
||||
// there's no point in checking whether anything actually changed
|
||||
let pack_key = stored_game.editor_metadata.key;
|
||||
this.stash.packs[pack_key].title = stored_game.title;
|
||||
this.stash.packs[pack_key].last_modified = Date.now();
|
||||
|
||||
// Update the pack itself
|
||||
// TODO maybe should keep this around, but there's a tricky order of operations thing
|
||||
// with it
|
||||
let pack_key = stored_pack.editor_metadata.key;
|
||||
let pack_stash = load_json_from_storage(pack_key);
|
||||
pack_stash.title = stored_game.title;
|
||||
pack_stash.title = stored_pack.title;
|
||||
pack_stash.last_modified = Date.now();
|
||||
pack_stash.levels[this.conductor.level_index].title = this.stored_level.title;
|
||||
pack_stash.levels[this.conductor.level_index].last_modified = Date.now();
|
||||
|
||||
// Serialize the level itself
|
||||
let buf = c2g.synthesize_level(this.stored_level);
|
||||
let stringy_buf = string_from_buffer_ascii(buf);
|
||||
|
||||
// Save everything at once, level first, to minimize chances of an error getting things
|
||||
// out of sync
|
||||
window.localStorage.setItem(this.stored_level.editor_metadata.key, stringy_buf);
|
||||
this._save_level_to_storage(this.stored_level);
|
||||
save_json_to_storage(pack_key, pack_stash);
|
||||
save_json_to_storage("Lexy's Labyrinth editor", this.stash);
|
||||
this._save_pack_to_stash(stored_pack, this.conductor.level_index);
|
||||
}
|
||||
|
||||
// Level loading
|
||||
|
||||
load_game(stored_game) {
|
||||
}
|
||||
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
// TODO bugs and quirks i'm aware of:
|
||||
// - steam: if a player character starts on a force floor they won't be able to make any voluntary movements until they are no longer on a force floor
|
||||
import * as fflate from 'https://unpkg.com/fflate@0.4.8/esm/index.mjs';
|
||||
import * as fflate from 'https://cdn.skypack.dev/fflate?min';
|
||||
|
||||
import { DIRECTIONS, INPUT_BITS, TICS_PER_SECOND } from './defs.js';
|
||||
import * as c2g from './format-c2g.js';
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
import * as fflate from 'https://unpkg.com/fflate@0.4.8/esm/index.mjs';
|
||||
import * as fflate from 'https://cdn.skypack.dev/fflate?min';
|
||||
|
||||
// Base class for custom errors
|
||||
export class LLError extends Error {}
|
||||
|
||||
15
style.css
15
style.css
@ -20,8 +20,9 @@ body {
|
||||
--panel-bg-color: hsl(225, 10%, 15%);
|
||||
--button-bg-color: hsl(225, 10%, 25%);
|
||||
--button-bg-hover-color: hsl(225, 15%, 30%);
|
||||
--generic-bg-hover-on-white: hsl(225, 60%, 85%);
|
||||
--generic-bg-selected-on-white: hsl(225, 60%, 90%);
|
||||
--generic-bg-hover-on-white: hsl(225, 60%, 90%);
|
||||
--generic-bg-selected-on-white: hsl(225, 60%, 85%);
|
||||
--generic-border-selected-on-white: hsl(225, 60%, 75%);
|
||||
}
|
||||
|
||||
/* Generic element styling */
|
||||
@ -266,6 +267,9 @@ svg.svg-icon {
|
||||
padding: 0.5em;
|
||||
background: #d0d0d0;
|
||||
}
|
||||
.dialog > footer > .-spacer {
|
||||
flex: 1;
|
||||
}
|
||||
.dialog > header:empty,
|
||||
.dialog > footer:empty {
|
||||
display: none;
|
||||
@ -351,6 +355,7 @@ table.level-browser button {
|
||||
}
|
||||
table.level-browser tr.--current {
|
||||
background: var(--generic-bg-selected-on-white);
|
||||
outline: 1px solid var(--generic-border-selected-on-white);
|
||||
}
|
||||
table.level-browser tr.--unvisited {
|
||||
color: #606060;
|
||||
@ -1865,8 +1870,12 @@ body.--debug #player-debug {
|
||||
gap: 0.25em;
|
||||
padding: 0.5em;
|
||||
}
|
||||
.editor-level-browser li.--selected {
|
||||
background: var(--generic-bg-selected-on-white);
|
||||
outline: 1px solid var(--generic-border-selected-on-white);
|
||||
}
|
||||
.editor-level-browser li:hover {
|
||||
background: hsl(225, 60%, 85%);
|
||||
background: var(--generic-bg-hover-on-white);
|
||||
}
|
||||
.editor-level-browser li > .-preview {
|
||||
grid-area: preview;
|
||||
|
||||
Loading…
Reference in New Issue
Block a user