Improve splash page slightly; add pack saving in editor

This commit is contained in:
Eevee (Evelyn Woods) 2020-12-06 14:03:36 -07:00
parent 70df85187f
commit 076aa9133a
5 changed files with 95 additions and 22 deletions

View File

@ -47,23 +47,22 @@
<div class="drag-overlay"></div> <div class="drag-overlay"></div>
<header> <header>
<h1><img src="og-preview.png" alt="">Lexy's Labyrinth</h1> <h1><img src="og-preview.png" alt="">Lexy's Labyrinth</h1>
<p>an unapproved Chip's Challenge emulator</p>
</header> </header>
<section id="splash-intro"> <section id="splash-intro">
<p><strong>Welcome</strong> to Lexy's Labyrinth, an open source puzzle game that is curiously similar to — but legally distinct from — the Atari classic <a href="https://en.wikipedia.org/wiki/Chip%27s_Challenge">Chip's Challenge</a>!</p> <p><strong>Welcome</strong> to Lexy's Labyrinth, an open source puzzle game that is curiously similar to — but legally distinct from — the Atari classic <a href="https://en.wikipedia.org/wiki/Chip%27s_Challenge">Chip's Challenge</a>!</p>
<p>(This is a Chip's Challenge <em>emulator</em>, designed to be an accessible way to play community-made levels with free assets. It's 99% compatible with Chip's Challenge 1, and support for Chip's Challenge 2 is underway. But you can safely ignore all that and treat this as its own game.)</p> <p>Pick a level pack to get started! You can also load and play any levels you've got lying around, or brave the level editor and make one of your own.</p>
<p>Please note that <em>levels themselves</em> may contain hints or lore referring to a guy named Chip collecting computer chips, even though you are clearly a fox named Lexy collecting hearts. Weird, right? Sorry for any confusion!</p> <p>If you're not familiar with the game, read up on <a href="https://github.com/eevee/lexys-labyrinth/wiki/How-To-Play">how to play</a>. You can also get more technical details or report bugs on <a href="https://github.com/eevee/lexys-labyrinth">GitHub</a>, find out more about Chip's Challenge via the <a href="https://bitbusters.club/">Bit Busters Club</a> fansite, or support this endeavor (and other things I do) on <a href="https://www.patreon.com/eevee">Patreon</a>.</p>
<p>Pick a level pack to get started! You can also get more technical details or report bugs on <a href="https://github.com/eevee/lexys-labyrinth">GitHub</a>, find out more about Chip's Challenge via the <a href="https://bitbusters.club/">Bit Busters Club</a> fansite, or support this endeavor (and other things I do) via <a href="https://www.patreon.com/eevee">Patreon</a>!</p>
<!-- TODO i want to make clear this is a chip's challenge emulator without bogging people down too much about what that means -->
</section> </section>
<section id="splash-stock-levels"> <section id="splash-stock-levels">
<h2>Just play something</h2> <h2>Play community levels</h2>
<!-- populated by js --> <!-- populated by js -->
</section> </section>
<section id="splash-upload-levels"> <section id="splash-upload-levels">
<h2>Other levels</h2> <h2>Load other levels</h2>
<p>Load and play any levels you have on hand — both the original levels and any custom ones, perhaps ones you found on the <a href="https://sets.bitbusters.club/">Bit Busters Club set list</a>. Supports CCL/DAT, C2G, and individual C2Ms (though scores aren't saved for those).</p> <p>Load and play any levels you have on hand, including the original levels. Supports CCL/DAT, C2G, and individual C2Ms (though scores aren't saved for those). Find more on the <a href="https://sets.bitbusters.club/">Bit Busters Club set list</a>.</p>
<!-- TODO zip files! --> <!-- TODO zip files! -->
<p>You can also drag and drop files or directories into this window.</p> <p>You can also drag and drop files or directories into this window.</p>
<input id="splash-upload-file" type="file" accept=".dat,.ccl,.c2m,.ccs" multiple> <input id="splash-upload-file" type="file" accept=".dat,.ccl,.c2m,.ccs" multiple>

View File

@ -37,6 +37,10 @@ export class Overlay {
this.root.addEventListener('click', ev => { this.root.addEventListener('click', ev => {
ev.stopPropagation(); ev.stopPropagation();
}); });
// Block any window-level key handlers from firing when we type
this.root.addEventListener('keydown', ev => {
ev.stopPropagation();
});
} }
open() { open() {

View File

@ -7,10 +7,40 @@ import CanvasRenderer from './renderer-canvas.js';
import TILE_TYPES from './tiletypes.js'; import TILE_TYPES from './tiletypes.js';
import { SVG_NS, mk, mk_svg, string_from_buffer_ascii, bytestring_to_buffer, walk_grid } from './util.js'; import { SVG_NS, mk, mk_svg, string_from_buffer_ascii, bytestring_to_buffer, walk_grid } from './util.js';
class EditorPackMetaOverlay extends DialogOverlay {
constructor(conductor, stored_pack) {
super(conductor);
this.set_title("pack properties");
let dl = mk('dl.formgrid');
this.main.append(dl);
dl.append(
mk('dt', "Title"),
mk('dd', mk('input', {name: 'title', type: 'text', value: stored_pack.title})),
);
// TODO...? what else is a property of the pack itself
this.add_button("save", () => {
let els = this.root.elements;
let title = els.title.value;
if (title !== stored_pack.title) {
stored_pack.title = title;
this.conductor.update_level_title();
}
this.close();
});
this.add_button("nevermind", () => {
this.close();
});
}
}
class EditorLevelMetaOverlay extends DialogOverlay { class EditorLevelMetaOverlay extends DialogOverlay {
constructor(conductor, stored_level) { constructor(conductor, stored_level) {
super(conductor); super(conductor);
this.set_title("edit level metadata"); this.set_title("level properties");
let dl = mk('dl.formgrid'); let dl = mk('dl.formgrid');
this.main.append(dl); this.main.append(dl);
@ -84,18 +114,14 @@ class EditorLevelMetaOverlay extends DialogOverlay {
this.root.elements['viewport'].value = stored_level.viewport_size; this.root.elements['viewport'].value = stored_level.viewport_size;
this.root.elements['blob_behavior'].value = stored_level.blob_behavior; this.root.elements['blob_behavior'].value = stored_level.blob_behavior;
// TODO: // TODO:
// - author
// - chips? // - chips?
// - password??? // - password???
// - comment // - comment
// - viewport size mode
// - rng/blob behavior
// - use CC1 tools // - use CC1 tools
// - hide logic // - hide logic
// - "unviewable", "read only" // - "unviewable", "read only"
let ok = mk('button', {type: 'button'}, "make it so"); this.add_button("save", () => {
ok.addEventListener('click', ev => {
let els = this.root.elements; let els = this.root.elements;
let title = els.title.value; let title = els.title.value;
@ -122,7 +148,9 @@ class EditorLevelMetaOverlay extends DialogOverlay {
this.close(); this.close();
}); });
this.footer.append(ok); this.add_button("nevermind", () => {
this.close();
});
} }
} }
@ -1025,18 +1053,41 @@ export class Editor extends PrimaryView {
button_container.append(button); button_container.append(button);
return button; return button;
}; };
_make_button("Properties...", ev => { _make_button("Pack properties...", ev => {
new EditorPackMetaOverlay(this.conductor, this.conductor.stored_game).open();
});
_make_button("Level properties...", ev => {
new EditorLevelMetaOverlay(this.conductor, this.stored_level).open(); new EditorLevelMetaOverlay(this.conductor, this.stored_level).open();
}); });
this.save_button = _make_button("Save", ev => { this.save_button = _make_button("Save", ev => {
// TODO need feedback. or maybe not bc this should be replaced with autosave later // 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 // TODO also need to update the pack data's last modified time
if (! this.conductor.stored_game.editor_metadata) let stored_game = this.conductor.stored_game;
if (! stored_game.editor_metadata)
return; 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_stash = load_json_from_storage(pack_key);
pack_stash.title = stored_game.title;
pack_stash.last_modified = Date.now();
// Serialize the level itself
let buf = c2g.synthesize_level(this.stored_level); let buf = c2g.synthesize_level(this.stored_level);
let stringy_buf = string_from_buffer_ascii(buf); 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); window.localStorage.setItem(this.stored_level.editor_metadata.key, stringy_buf);
save_json_to_storage(pack_key, pack_stash);
save_json_to_storage("Lexy's Labyrinth editor", this.stash);
}); });
if (this.stored_level) { if (this.stored_level) {
this.save_button.disabled = ! this.conductor.stored_game.editor_metadata; this.save_button.disabled = ! this.conductor.stored_game.editor_metadata;
@ -1163,6 +1214,7 @@ export class Editor extends PrimaryView {
load_editor_pack(pack_key) { load_editor_pack(pack_key) {
let pack_stash = load_json_from_storage(pack_key); let pack_stash = load_json_from_storage(pack_key);
let stored_pack = new format_base.StoredPack(pack_key, meta => { let stored_pack = new format_base.StoredPack(pack_key, meta => {
let buf = bytestring_to_buffer(localStorage.getItem(meta.key)); let buf = bytestring_to_buffer(localStorage.getItem(meta.key));
let stored_level = c2g.parse_level(buf, meta.number); let stored_level = c2g.parse_level(buf, meta.number);
@ -1171,6 +1223,8 @@ export class Editor extends PrimaryView {
}; };
return stored_level; return stored_level;
}); });
// TODO should this also be in the pack's stash...?
stored_pack.title = this.stash.packs[pack_key].title;
stored_pack.editor_metadata = { stored_pack.editor_metadata = {
key: pack_key, key: pack_key,
}; };

View File

@ -1486,10 +1486,15 @@ class Splash extends PrimaryView {
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[a].last_modified - packs[b].last_modified);
let editor_list = this.root.querySelector('#splash-your-levels'); let editor_section = this.root.querySelector('#splash-your-levels');
let editor_list = editor_section;
for (let key of pack_keys) { for (let key of pack_keys) {
let pack = packs[key]; let pack = packs[key];
let button = mk('button', {type: 'button'}, pack.title); let button = mk('button.button-big.level-pack-button', {type: 'button'},
mk('h3', pack.title),
// TODO whether it's yours or not?
// TODO number of levels?
);
// TODO make a container so this can be 1 event // TODO make a container so this can be 1 event
button.addEventListener('click', ev => { button.addEventListener('click', ev => {
this.conductor.editor.load_editor_pack(key); this.conductor.editor.load_editor_pack(key);
@ -2052,7 +2057,7 @@ class Conductor {
// TODO handle errors // TODO handle errors
// TODO cancel a download if we start another one? // TODO cancel a download if we start another one?
let buf = await util.fetch(path); let buf = await util.fetch(path);
await this.parse_and_load_game(buf, new util.HTTPFileSource(new URL(location)), path, title); await this.parse_and_load_game(buf, new util.HTTPFileSource(new URL(location)), path, undefined, title);
} }
async parse_and_load_game(buf, source, path, identifier, title) { async parse_and_load_game(buf, source, path, identifier, title) {

View File

@ -110,10 +110,10 @@ a:visited {
text-decoration: underline dotted; text-decoration: underline dotted;
} }
a:link { a:link {
color: hsl(225, 50%, 60%); color: hsl(225, 50%, 75%);
} }
a:visited { a:visited {
color: hsl(300, 50%, 60%); color: hsl(255, 50%, 75%);
} }
a:link:hover, a:link:hover,
a:visited:hover { a:visited:hover {
@ -193,6 +193,12 @@ svg.svg-icon {
background: #f0d0d0; background: #f0d0d0;
padding: 0.5em 1em; padding: 0.5em 1em;
} }
.dialog a:link {
color: hsl(225, 50%, 50%);
}
.dialog a:visited {
color: hsl(255, 50%, 50%);
}
dl.formgrid { dl.formgrid {
display: grid; display: grid;
grid: auto-flow min-content / 1fr 4fr; grid: auto-flow min-content / 1fr 4fr;
@ -447,6 +453,11 @@ body[data-mode=player] #editor-play {
#splash > header h1 { #splash > header h1 {
font-size: 3em; font-size: 3em;
} }
#splash > header p {
margin: 0;
font-style: italic;
color: #909090;
}
#splash h2 { #splash h2 {
border-bottom: 1px solid #404040; border-bottom: 1px solid #404040;
color: #909090; color: #909090;
@ -459,7 +470,7 @@ body[data-mode=player] #editor-play {
} }
#splash > #splash-intro { #splash > #splash-intro {
grid-area: intro; grid-area: intro;
font-size: 18px; font-size: 20px;
} }
#splash > #splash-stock-levels { #splash > #splash-stock-levels {
grid-area: stock; grid-area: stock;