Editor: Stub out support for actually saving levels
This commit is contained in:
parent
89ae9aa4a3
commit
411005eaa6
@ -82,7 +82,8 @@
|
||||
<section id="splash-your-levels">
|
||||
<h2>Make your own (WIP lol)</h2>
|
||||
<p>Please note that the level editor is <strong>extremely</strong> unfinished, and can't even save yet.</p>
|
||||
<p><button type="button" id="splash-create-level" class="button-big">Create a level</button></p>
|
||||
<p><button type="button" id="splash-create-pack" class="button-big">New pack</button></p>
|
||||
<p><button type="button" id="splash-create-level" class="button-big">New scratch level</button></p>
|
||||
</section>
|
||||
</main>
|
||||
<main id="player" hidden>
|
||||
@ -218,12 +219,6 @@
|
||||
<nav class="controls">
|
||||
<div id="editor-tile">
|
||||
</div>
|
||||
<div class="-buttons">
|
||||
<button id="editor-share-url" type="button">Share?</button>
|
||||
<!--
|
||||
<button id="editor-toggle-green" type="button">Toggle green objects</button>
|
||||
-->
|
||||
</div>
|
||||
<!--
|
||||
<p style>
|
||||
Tip: Right click to color drop.<br>
|
||||
|
||||
@ -48,12 +48,13 @@ export class StoredLevel {
|
||||
}
|
||||
}
|
||||
|
||||
export class StoredGame {
|
||||
export class StoredPack {
|
||||
constructor(identifier, level_loader) {
|
||||
this.identifier = identifier;
|
||||
this.title = "";
|
||||
this._level_loader = level_loader;
|
||||
|
||||
// Simple objects containing keys:
|
||||
// Simple objects containing keys that are usually:
|
||||
// title: level title
|
||||
// index: level index, used internally only
|
||||
// number: level number (may not match index due to C2G shenanigans)
|
||||
@ -76,7 +77,9 @@ export class StoredGame {
|
||||
}
|
||||
else {
|
||||
// Otherwise, attempt to load the level
|
||||
return this._level_loader(meta.bytes, meta.number);
|
||||
return this._level_loader(meta);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export const StoredGame = StoredPack;
|
||||
|
||||
@ -1060,6 +1060,11 @@ export function parse_level(buf, number = 1) {
|
||||
return level;
|
||||
}
|
||||
|
||||
// This thin wrapper is passed to StoredGame as the parser function
|
||||
function _parse_level_from_stored_meta(meta) {
|
||||
return parse_level(meta.bytes, meta.number);
|
||||
}
|
||||
|
||||
|
||||
// Write 1, 2, or 4 bytes to a DataView
|
||||
function write_n_bytes(view, start, n, value) {
|
||||
@ -1212,6 +1217,10 @@ export function synthesize_level(stored_level) {
|
||||
let c2m = new C2M;
|
||||
c2m.add_section('CC2M', '133');
|
||||
|
||||
if (stored_level.title) {
|
||||
c2m.add_section('TITL', stored_level.title);
|
||||
}
|
||||
|
||||
// Store camera regions
|
||||
// TODO LL feature, should be distinguished somehow
|
||||
if (stored_level.camera_regions.length > 0) {
|
||||
@ -1677,7 +1686,7 @@ const MAX_SIMULTANEOUS_REQUESTS = 5;
|
||||
let resolve;
|
||||
let promise = new Promise((res, rej) => { resolve = res });
|
||||
|
||||
let game = new format_base.StoredGame(undefined, parse_level);
|
||||
let game = new format_base.StoredGame(undefined, _parse_level_from_stored_meta);
|
||||
let parser;
|
||||
let active_map_fetches = new Set;
|
||||
let pending_map_fetches = [];
|
||||
@ -1754,7 +1763,7 @@ const MAX_SIMULTANEOUS_REQUESTS = 5;
|
||||
|
||||
// Individual levels don't make sense on their own, but we can wrap them in a dummy one-level game
|
||||
export function wrap_individual_level(buf) {
|
||||
let game = new format_base.StoredGame(undefined, parse_level);
|
||||
let game = new format_base.StoredGame(undefined, _parse_level_from_stored_meta);
|
||||
let meta = {
|
||||
index: 0,
|
||||
number: 1,
|
||||
|
||||
@ -329,8 +329,13 @@ function parse_level(bytes, number) {
|
||||
return level;
|
||||
}
|
||||
|
||||
// This thin wrapper is passed to StoredGame as the parser function
|
||||
function _parse_level_from_stored_meta(meta) {
|
||||
return parse_level(meta.bytes, meta.number);
|
||||
}
|
||||
|
||||
export function parse_game(buf) {
|
||||
let game = new format_base.StoredGame(null, parse_level);
|
||||
let game = new format_base.StoredGame(null, _parse_level_from_stored_meta);
|
||||
|
||||
let full_view = new DataView(buf);
|
||||
let magic = full_view.getUint32(0, true);
|
||||
|
||||
@ -76,7 +76,7 @@ export class TransientOverlay extends Overlay {
|
||||
// Overlay styled like a dialog box
|
||||
export class DialogOverlay extends Overlay {
|
||||
constructor(conductor) {
|
||||
super(conductor, mk('div.dialog'));
|
||||
super(conductor, mk('form.dialog'));
|
||||
|
||||
this.root.append(
|
||||
this.header = mk('header'),
|
||||
@ -115,3 +115,11 @@ export class ConfirmOverlay extends DialogOverlay {
|
||||
this.footer.append(yes, no);
|
||||
}
|
||||
}
|
||||
|
||||
export function load_json_from_storage(key) {
|
||||
return JSON.parse(window.localStorage.getItem(key));
|
||||
}
|
||||
|
||||
export function save_json_to_storage(key, value) {
|
||||
window.localStorage.setItem(key, JSON.stringify(value));
|
||||
}
|
||||
|
||||
@ -1,10 +1,83 @@
|
||||
import { DIRECTIONS, TICS_PER_SECOND } from './defs.js';
|
||||
import { TILES_WITH_PROPS } from './editor-tile-overlays.js';
|
||||
import * as format_base from './format-base.js';
|
||||
import * as c2g from './format-c2g.js';
|
||||
import { PrimaryView, TransientOverlay, DialogOverlay } from './main-base.js';
|
||||
import { PrimaryView, TransientOverlay, DialogOverlay, load_json_from_storage, save_json_to_storage } from './main-base.js';
|
||||
import CanvasRenderer from './renderer-canvas.js';
|
||||
import TILE_TYPES from './tiletypes.js';
|
||||
import { SVG_NS, mk, mk_svg, walk_grid } from './util.js';
|
||||
import { SVG_NS, mk, mk_svg, string_from_buffer_ascii, bytestring_to_buffer, walk_grid } from './util.js';
|
||||
|
||||
class EditorLevelMetaOverlay extends DialogOverlay {
|
||||
constructor(conductor, stored_level) {
|
||||
super(conductor);
|
||||
this.set_title("edit level metadata");
|
||||
let dl = mk('dl.formgrid');
|
||||
this.main.append(dl);
|
||||
|
||||
let time_limit_input = mk('input', {name: 'time_limit', type: 'range', min: 0, max: 999, value: stored_level.time_limit});
|
||||
let time_limit_output = mk('output');
|
||||
let update_time_limit = () => {
|
||||
let time_limit = parseInt(time_limit_input.value, 10);
|
||||
let text;
|
||||
if (time_limit === 0) {
|
||||
text = "No time limit";
|
||||
}
|
||||
else {
|
||||
text = `${time_limit} (${Math.floor(time_limit / 60)}:${('0' + String(time_limit % 60)).slice(-2)})`;
|
||||
}
|
||||
time_limit_output.textContent = text;
|
||||
};
|
||||
update_time_limit();
|
||||
time_limit_input.addEventListener('input', update_time_limit);
|
||||
|
||||
dl.append(
|
||||
mk('dt', "Title"),
|
||||
mk('dd', mk('input', {name: 'title', type: 'text', value: stored_level.title})),
|
||||
mk('dt', "Time limit"),
|
||||
mk('dd', time_limit_input, " ", time_limit_output),
|
||||
mk('dt', "Size"),
|
||||
mk('dd',
|
||||
"Width: ",
|
||||
mk('input', {name: 'size_x', type: 'number', min: 10, max: 100, value: stored_level.size_x}),
|
||||
mk('br'),
|
||||
"Height: ",
|
||||
mk('input', {name: 'size_y', type: 'number', min: 10, max: 100, value: stored_level.size_y}),
|
||||
),
|
||||
);
|
||||
// TODO:
|
||||
// - author
|
||||
// - chips?
|
||||
// - password???
|
||||
// - comment
|
||||
// - viewport size mode
|
||||
// - rng/blob behavior
|
||||
// - use CC1 tools
|
||||
// - hide logic
|
||||
// - "unviewable", "read only"
|
||||
|
||||
let ok = mk('button', {type: 'button'}, "make it so");
|
||||
ok.addEventListener('click', ev => {
|
||||
let els = this.root.elements;
|
||||
|
||||
let title = els.title.value;
|
||||
if (title !== stored_level.title) {
|
||||
stored_level.title = title;
|
||||
this.conductor.update_level_title();
|
||||
}
|
||||
|
||||
stored_level.time_limit = parseInt(els.time_limit.value, 10);
|
||||
|
||||
let size_x = parseInt(els.size_x.value, 10);
|
||||
let size_y = parseInt(els.size_y.value, 10);
|
||||
if (size_x !== stored_level.size_x || size_y !== stored_level.size_y) {
|
||||
this.conductor.editor.resize_level(size_x, size_y);
|
||||
}
|
||||
|
||||
this.close();
|
||||
});
|
||||
this.footer.append(ok);
|
||||
}
|
||||
}
|
||||
|
||||
class EditorShareOverlay extends DialogOverlay {
|
||||
constructor(conductor, url) {
|
||||
@ -217,7 +290,7 @@ class ForceFloorOperation extends DrawOperation {
|
||||
// The second cell tells us the direction to use for the first, assuming it
|
||||
// had some kind of force floor
|
||||
if (i === 2) {
|
||||
let prevcell = this.editor.stored_level.cells[prevy][prevx];
|
||||
let prevcell = this.editor.cell(prevx, prevy);
|
||||
if (prevcell[0].type.name.startsWith('force_floor_')) {
|
||||
prevcell[0].type = TILE_TYPES[name];
|
||||
}
|
||||
@ -225,7 +298,7 @@ class ForceFloorOperation extends DrawOperation {
|
||||
|
||||
// Drawing a loop with force floors creates ice (but not in the previous
|
||||
// cell, obviously)
|
||||
let cell = this.editor.stored_level.cells[y][x];
|
||||
let cell = this.editor.cell(x, y);
|
||||
if (cell[0].type.name.startsWith('force_floor_') &&
|
||||
cell[0].type.name !== name)
|
||||
{
|
||||
@ -736,6 +809,21 @@ export class Editor extends PrimaryView {
|
||||
|
||||
this.viewport_el = this.root.querySelector('.editor-canvas .-container');
|
||||
|
||||
// Load editor state; we may need this before setup() since we create new levels before
|
||||
// actually loading the editor proper
|
||||
this.stash = load_json_from_storage("Lexy's Labyrinth editor");
|
||||
if (! this.stash) {
|
||||
this.stash = {
|
||||
packs: {}, // key: { title, level_count, last_modified }
|
||||
// More pack data is stored separately under the key, as {
|
||||
// levels: [{key, title}],
|
||||
// }
|
||||
// Levels are also stored under separate keys, encoded as C2M.
|
||||
};
|
||||
}
|
||||
this.pack_stash = null;
|
||||
this.level_stash = null;
|
||||
|
||||
// FIXME don't hardcode size here, convey this to renderer some other way
|
||||
this.renderer = new CanvasRenderer(this.conductor.tileset, 32);
|
||||
|
||||
@ -762,6 +850,7 @@ export class Editor extends PrimaryView {
|
||||
ev.preventDefault();
|
||||
ev.stopPropagation();
|
||||
|
||||
// FIXME eventually this should be automatic
|
||||
this.renderer.draw();
|
||||
}
|
||||
else if (ev.button === 1) {
|
||||
@ -797,6 +886,7 @@ export class Editor extends PrimaryView {
|
||||
|
||||
this.mouse_op.do_mousemove(ev);
|
||||
|
||||
// FIXME !!!
|
||||
this.renderer.draw();
|
||||
});
|
||||
// TODO should this happen for a mouseup anywhere?
|
||||
@ -816,20 +906,6 @@ export class Editor extends PrimaryView {
|
||||
this.cancel_mouse_operation();
|
||||
});
|
||||
|
||||
// Toolbar buttons
|
||||
this.root.querySelector('#editor-share-url').addEventListener('click', ev => {
|
||||
let buf = c2g.synthesize_level(this.stored_level);
|
||||
// FIXME Not ideal, but btoa() wants a string rather than any of the myriad binary types
|
||||
let stringy_buf = Array.from(new Uint8Array(buf)).map(n => String.fromCharCode(n)).join('');
|
||||
// Make URL-safe and strip trailing padding
|
||||
let data = btoa(stringy_buf).replace(/[+]/g, '-').replace(/[/]/g, '_').replace(/=+$/, '');
|
||||
let url = new URL(location);
|
||||
url.searchParams.delete('level');
|
||||
url.searchParams.delete('setpath');
|
||||
url.searchParams.append('level', data);
|
||||
new EditorShareOverlay(this.conductor, url.toString()).open();
|
||||
});
|
||||
|
||||
// Toolbox
|
||||
// Selected tile
|
||||
this.selected_tile_el = this.root.querySelector('.controls #editor-tile');
|
||||
@ -869,6 +945,45 @@ export class Editor extends PrimaryView {
|
||||
this.select_tool(button.getAttribute('data-tool'));
|
||||
});
|
||||
|
||||
// Toolbar buttons for saving, exporting, etc.
|
||||
let button_container = mk('div.-buttons');
|
||||
this.root.querySelector('.controls').append(button_container);
|
||||
let _make_button = (label, onclick) => {
|
||||
let button = mk('button', {type: 'button'}, label);
|
||||
button.addEventListener('click', onclick);
|
||||
button_container.append(button);
|
||||
return button;
|
||||
};
|
||||
_make_button("Properties...", ev => {
|
||||
new EditorLevelMetaOverlay(this.conductor, this.stored_level).open();
|
||||
});
|
||||
this.save_button = _make_button("Save", ev => {
|
||||
// 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
|
||||
if (! this.conductor.stored_game.editor_metadata)
|
||||
return;
|
||||
|
||||
let buf = c2g.synthesize_level(this.stored_level);
|
||||
let stringy_buf = string_from_buffer_ascii(buf);
|
||||
window.localStorage.setItem(this.stored_level.editor_metadata.key, stringy_buf);
|
||||
});
|
||||
if (this.stored_level) {
|
||||
this.save_button.disabled = ! this.conductor.stored_game.editor_metadata;
|
||||
}
|
||||
_make_button("Share", ev => {
|
||||
let buf = c2g.synthesize_level(this.stored_level);
|
||||
// FIXME Not ideal, but btoa() wants a string rather than any of the myriad binary types
|
||||
let stringy_buf = Array.from(new Uint8Array(buf)).map(n => String.fromCharCode(n)).join('');
|
||||
// Make URL-safe and strip trailing padding
|
||||
let data = btoa(stringy_buf).replace(/[+]/g, '-').replace(/[/]/g, '_').replace(/=+$/, '');
|
||||
let url = new URL(location);
|
||||
url.searchParams.delete('level');
|
||||
url.searchParams.delete('setpath');
|
||||
url.searchParams.append('level', data);
|
||||
new EditorShareOverlay(this.conductor, url.toString()).open();
|
||||
});
|
||||
//_make_button("Toggle green objects");
|
||||
|
||||
// Tile palette
|
||||
let palette_el = this.root.querySelector('.palette');
|
||||
this.palette = {}; // name => element
|
||||
@ -899,15 +1014,113 @@ export class Editor extends PrimaryView {
|
||||
this.renderer.draw();
|
||||
}
|
||||
|
||||
_make_cell() {
|
||||
let cell = new format_base.StoredCell;
|
||||
cell.push({type: TILE_TYPES['floor']});
|
||||
return cell;
|
||||
}
|
||||
|
||||
_make_empty_level(number, size_x, size_y) {
|
||||
let stored_level = new format_base.StoredLevel(number);
|
||||
stored_level.size_x = size_x;
|
||||
stored_level.size_y = size_y;
|
||||
for (let i = 0; i < size_x * size_y; i++) {
|
||||
stored_level.linear_cells.push(this._make_cell());
|
||||
}
|
||||
stored_level.linear_cells[0].push({type: TILE_TYPES['player'], direction: 'south'});
|
||||
return stored_level;
|
||||
}
|
||||
|
||||
create_pack() {
|
||||
// TODO get a dialog for asking about level meta first? or is jumping directly into the editor better?
|
||||
let stored_level = this._make_empty_level(1, 32, 32);
|
||||
|
||||
let pack_key = `LLP-${Date.now()}`;
|
||||
let level_key = `LLL-${Date.now()}`;
|
||||
let stored_pack = new format_base.StoredPack(pack_key);
|
||||
stored_pack.editor_metadata = {
|
||||
key: pack_key,
|
||||
};
|
||||
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: 0,
|
||||
number: 1,
|
||||
});
|
||||
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);
|
||||
|
||||
save_json_to_storage(pack_key, {
|
||||
levels: [{
|
||||
key: level_key,
|
||||
title: stored_level.title,
|
||||
last_modified: Date.now(),
|
||||
}],
|
||||
});
|
||||
|
||||
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.level_metadata.push({
|
||||
stored_level: stored_level,
|
||||
});
|
||||
this.conductor.load_game(stored_pack);
|
||||
|
||||
this.conductor.switch_to_editor();
|
||||
}
|
||||
|
||||
load_editor_pack(pack_key) {
|
||||
let pack_stash = load_json_from_storage(pack_key);
|
||||
let stored_pack = new format_base.StoredPack(pack_key, meta => {
|
||||
let buf = bytestring_to_buffer(localStorage.getItem(meta.key));
|
||||
let stored_level = c2g.parse_level(buf, meta.number);
|
||||
stored_level.editor_metadata = {
|
||||
key: meta.key,
|
||||
};
|
||||
return stored_level;
|
||||
});
|
||||
stored_pack.editor_metadata = {
|
||||
key: pack_key,
|
||||
};
|
||||
|
||||
for (let [i, leveldata] of pack_stash.levels.entries()) {
|
||||
stored_pack.level_metadata.push({
|
||||
key: leveldata.key,
|
||||
title: leveldata.title,
|
||||
index: i,
|
||||
number: i + 1,
|
||||
});
|
||||
}
|
||||
this.conductor.load_game(stored_pack);
|
||||
|
||||
this.conductor.switch_to_editor();
|
||||
}
|
||||
|
||||
load_game(stored_game) {
|
||||
}
|
||||
|
||||
load_level(stored_level) {
|
||||
// TODO support a game too i guess
|
||||
this.stored_level = stored_level;
|
||||
this.update_viewport_size();
|
||||
|
||||
// XXX need this for renderer compat. but i guess it's nice in general idk
|
||||
_xxx_update_stored_level_cells() {
|
||||
// XXX need this for renderer compat, not used otherwise
|
||||
this.stored_level.cells = [];
|
||||
let row;
|
||||
for (let [i, cell] of this.stored_level.linear_cells.entries()) {
|
||||
@ -917,6 +1130,14 @@ export class Editor extends PrimaryView {
|
||||
}
|
||||
row.push(cell);
|
||||
}
|
||||
}
|
||||
|
||||
load_level(stored_level) {
|
||||
// TODO support a game too i guess
|
||||
this.stored_level = stored_level;
|
||||
this.update_viewport_size();
|
||||
|
||||
this._xxx_update_stored_level_cells();
|
||||
|
||||
// Load connections
|
||||
this.connections_g.textContent = '';
|
||||
@ -938,6 +1159,10 @@ export class Editor extends PrimaryView {
|
||||
if (this.active) {
|
||||
this.renderer.draw();
|
||||
}
|
||||
|
||||
if (this.save_button) {
|
||||
this.save_button.disabled = ! this.conductor.stored_game.editor_metadata;
|
||||
}
|
||||
}
|
||||
|
||||
update_viewport_size() {
|
||||
@ -1014,7 +1239,7 @@ export class Editor extends PrimaryView {
|
||||
|
||||
cell(x, y) {
|
||||
if (this.is_in_bounds(x, y)) {
|
||||
return this.stored_level.cells[y][x];
|
||||
return this.stored_level.linear_cells[this.stored_level.coords_to_scalar(x, y)];
|
||||
}
|
||||
else {
|
||||
return null;
|
||||
@ -1026,7 +1251,7 @@ export class Editor extends PrimaryView {
|
||||
if (! tile)
|
||||
return;
|
||||
|
||||
let cell = this.stored_level.cells[y][x];
|
||||
let cell = this.cell(x, y);
|
||||
// Replace whatever's on the same layer
|
||||
// TODO probably not the best heuristic yet, since i imagine you can
|
||||
// combine e.g. the tent with thin walls
|
||||
@ -1081,6 +1306,22 @@ export class Editor extends PrimaryView {
|
||||
this.mouse_op = null;
|
||||
}
|
||||
}
|
||||
|
||||
resize_level(size_x, size_y, x0 = 0, y0 = 0) {
|
||||
let new_cells = [];
|
||||
for (let y = y0; y < y0 + size_y; y++) {
|
||||
for (let x = x0; x < x0 + size_x; x++) {
|
||||
new_cells.push(this.cell(x, y) ?? this._make_cell());
|
||||
}
|
||||
}
|
||||
|
||||
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.update_viewport_size();
|
||||
this.renderer.draw();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
81
js/main.js
81
js/main.js
@ -456,6 +456,9 @@ class Player extends PrimaryView {
|
||||
this.current_keys_new = new Set; // keys that were pressed since input was last read
|
||||
// TODO this could all probably be more rigorous but it's fine for now
|
||||
key_target.addEventListener('keydown', ev => {
|
||||
if (! this.active)
|
||||
return;
|
||||
|
||||
if (ev.key === 'p' || ev.key === 'Pause') {
|
||||
this.toggle_pause();
|
||||
return;
|
||||
@ -505,6 +508,9 @@ class Player extends PrimaryView {
|
||||
}
|
||||
});
|
||||
key_target.addEventListener('keyup', ev => {
|
||||
if (! this.active)
|
||||
return;
|
||||
|
||||
if (ev.key === 'z') {
|
||||
if (this.state === 'rewinding') {
|
||||
this.set_state('playing');
|
||||
@ -1455,28 +1461,33 @@ class Splash extends PrimaryView {
|
||||
await this.search_multi_source(new util.EntryFileSource(entries));
|
||||
},
|
||||
});
|
||||
|
||||
// Bind to "create level" button
|
||||
this.root.querySelector('#splash-create-level').addEventListener('click', ev => {
|
||||
let stored_level = new format_base.StoredLevel(1);
|
||||
stored_level.size_x = 32;
|
||||
stored_level.size_y = 32;
|
||||
for (let i = 0; i < 1024; i++) {
|
||||
let cell = new format_base.StoredCell;
|
||||
cell.push({type: TILE_TYPES['floor']});
|
||||
stored_level.linear_cells.push(cell);
|
||||
}
|
||||
stored_level.linear_cells[0].push({type: TILE_TYPES['player'], direction: 'south'});
|
||||
|
||||
// FIXME definitely gonna need a name here chief
|
||||
let stored_game = new format_base.StoredGame(null);
|
||||
stored_game.level_metadata.push({
|
||||
stored_level: stored_level,
|
||||
setup() {
|
||||
// Editor interface
|
||||
// (this has to be handled here because we need to examine the editor,
|
||||
// which hasn't yet been created in our constructor)
|
||||
// Bind to "create" buttons
|
||||
this.root.querySelector('#splash-create-pack').addEventListener('click', ev => {
|
||||
this.conductor.editor.create_pack();
|
||||
});
|
||||
this.conductor.load_game(stored_game);
|
||||
|
||||
this.conductor.switch_to_editor();
|
||||
this.root.querySelector('#splash-create-level').addEventListener('click', ev => {
|
||||
this.conductor.editor.create_scratch_level();
|
||||
});
|
||||
// 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);
|
||||
let editor_list = this.root.querySelector('#splash-your-levels');
|
||||
for (let key of pack_keys) {
|
||||
let pack = packs[key];
|
||||
let button = mk('button', {type: 'button'}, pack.title);
|
||||
// TODO make a container so this can be 1 event
|
||||
button.addEventListener('click', ev => {
|
||||
this.conductor.editor.load_editor_pack(key);
|
||||
});
|
||||
editor_list.append(button);
|
||||
}
|
||||
}
|
||||
|
||||
// Look for something we can load, and load it
|
||||
@ -1808,8 +1819,16 @@ class LevelBrowserOverlay extends DialogOverlay {
|
||||
}
|
||||
|
||||
// Central dispatcher of what we're doing and what we've got loaded
|
||||
// We store several kinds of things in localStorage:
|
||||
// Main storage:
|
||||
// packs
|
||||
// options
|
||||
const STORAGE_KEY = "Lexy's Labyrinth";
|
||||
// Records for playing a pack
|
||||
const STORAGE_PACK_PREFIX = "Lexy's Labyrinth: ";
|
||||
// Metadata for an edited pack
|
||||
// - list of the levels they own and basic metadata like name
|
||||
// Stored individual levels: given dummy names, all indexed on their own
|
||||
class Conductor {
|
||||
constructor(tileset) {
|
||||
this.stored_game = null;
|
||||
@ -1943,6 +1962,8 @@ class Conductor {
|
||||
};
|
||||
}
|
||||
|
||||
this.level_pack_name_el.textContent = stored_game.title;
|
||||
|
||||
this.player.load_game(stored_game);
|
||||
this.editor.load_game(stored_game);
|
||||
|
||||
@ -1955,16 +1976,14 @@ class Conductor {
|
||||
this.stored_level = this.stored_game.load_level(level_index);
|
||||
}
|
||||
catch (e) {
|
||||
console.error(e);
|
||||
new LevelErrorOverlay(this, e).open();
|
||||
return false;
|
||||
}
|
||||
|
||||
this.level_index = level_index;
|
||||
|
||||
// FIXME do better
|
||||
this.level_name_el.textContent = `Level ${level_index + 1} — ${this.stored_level.title}`;
|
||||
|
||||
document.title = `${PAGE_TITLE} - ${this.stored_level.title}`;
|
||||
this.update_level_title();
|
||||
this.update_nav_buttons();
|
||||
|
||||
this.player.load_level(this.stored_level);
|
||||
@ -1972,6 +1991,12 @@ class Conductor {
|
||||
return true;
|
||||
}
|
||||
|
||||
update_level_title() {
|
||||
this.level_name_el.textContent = `Level ${this.stored_level.number} — ${this.stored_level.title}`;
|
||||
|
||||
document.title = `${this.stored_level.title} [#${this.stored_level.number}] — ${this.stored_game.title} — ${PAGE_TITLE}`;
|
||||
}
|
||||
|
||||
update_nav_buttons() {
|
||||
this.nav_choose_level_button.disabled = !this.stored_game;
|
||||
this.nav_prev_button.disabled = !this.stored_game || this.level_index <= 0;
|
||||
@ -2019,7 +2044,7 @@ class Conductor {
|
||||
// TODO handle errors
|
||||
// TODO cancel a download if we start another one?
|
||||
let buf = await util.fetch(path);
|
||||
await this.parse_and_load_game(buf, new util.HTTPFileSource(new URL(location)), path);
|
||||
await this.parse_and_load_game(buf, new util.HTTPFileSource(new URL(location)), path, title);
|
||||
}
|
||||
|
||||
async parse_and_load_game(buf, source, path, identifier, title) {
|
||||
@ -2027,9 +2052,6 @@ class Conductor {
|
||||
identifier = this.extract_identifier_from_path(path);
|
||||
}
|
||||
|
||||
// TODO get title out of C2G when it's supported
|
||||
this.level_pack_name_el.textContent = title ?? identifier ?? '(untitled)';
|
||||
|
||||
// TODO also support tile world's DAC when reading from local??
|
||||
// TODO ah, there's more metadata in CCX, crapola
|
||||
let magic = String.fromCharCode.apply(null, new Uint8Array(buf.slice(0, 4)));
|
||||
@ -2045,7 +2067,6 @@ class Conductor {
|
||||
else if (magic.toLowerCase() === 'game') {
|
||||
// TODO this isn't really a magic number and isn't required to be first, so, maybe
|
||||
// this one should just go by filename
|
||||
console.log(path);
|
||||
let dir;
|
||||
if (! path.match(/[/]/)) {
|
||||
dir = '';
|
||||
@ -2058,6 +2079,12 @@ class Conductor {
|
||||
else {
|
||||
throw new Error("Unrecognized file format");
|
||||
}
|
||||
|
||||
// TODO load title for a C2G
|
||||
if (! stored_game.title) {
|
||||
stored_game.title = title ?? identifier ?? "Untitled pack";
|
||||
}
|
||||
|
||||
if (this.load_game(stored_game, identifier)) {
|
||||
this.switch_to_player();
|
||||
}
|
||||
|
||||
@ -157,6 +157,11 @@ export function string_from_buffer_ascii(buf, start = 0, len) {
|
||||
return String.fromCharCode.apply(null, new Uint8Array(buf, start, len));
|
||||
}
|
||||
|
||||
// Converts a string to a buffer, using NO ENCODING, assuming single-byte characters
|
||||
export function bytestring_to_buffer(bytestring) {
|
||||
return Uint8Array.from(bytestring, c => c.charCodeAt(0)).buffer;
|
||||
}
|
||||
|
||||
// Cast a line through a grid and yield every cell it touches
|
||||
export function* walk_grid(x0, y0, x1, y1, min_a, min_b, max_a, max_b) {
|
||||
// TODO if the ray starts outside the grid (extremely unlikely), we should
|
||||
|
||||
Loading…
Reference in New Issue
Block a user