Improve the pack handling experience somewhat

- Include links for the stock packs

- Show completion amount and total time for played packs

- Expose a list of all other packs the player has played

- Allow forgetting a pack

- Jump to the current level when reopening a pack

- Highlight the current level in the level browser, and scroll to it
This commit is contained in:
Eevee (Evelyn Woods) 2020-12-24 05:36:57 -07:00
parent a8800838d4
commit 1968420027
3 changed files with 197 additions and 39 deletions

View File

@ -85,6 +85,7 @@
</div> </div>
</main> </main>
<script> <script>
// FIXME not quite good enough; we'll get loading forever if there's a syntax error in the js
document.querySelector('#failed').setAttribute('hidden', ''); document.querySelector('#failed').setAttribute('hidden', '');
document.querySelector('#loading').removeAttribute('hidden'); document.querySelector('#loading').removeAttribute('hidden');
</script> </script>

View File

@ -1668,21 +1668,25 @@ const BUILTIN_LEVEL_PACKS = [{
ident: 'cclp1', ident: 'cclp1',
title: "Chip's Challenge Level Pack 1", title: "Chip's Challenge Level Pack 1",
desc: "Designed and recommended for new players, starting with gentle introductory levels. A prequel to the other packs.", desc: "Designed and recommended for new players, starting with gentle introductory levels. A prequel to the other packs.",
url: 'https://wiki.bitbusters.club/Chip%27s_Challenge_Level_Pack_1',
}, { }, {
path: 'levels/CCLP4.ccl', path: 'levels/CCLP4.ccl',
ident: 'cclp4', ident: 'cclp4',
title: "Chip's Challenge Level Pack 4", title: "Chip's Challenge Level Pack 4",
desc: "Moderately difficult, but not unfair.", desc: "Moderately difficult, but not unfair.",
url: 'https://wiki.bitbusters.club/Chip%27s_Challenge_Level_Pack_4',
}, { }, {
path: 'levels/CCLXP2.ccl', path: 'levels/CCLXP2.ccl',
ident: 'cclxp2', ident: 'cclxp2',
title: "Chip's Challenge Level Pack 2-X", title: "Chip's Challenge Level Pack 2-X",
desc: "The first community pack released, tricky and rough around the edges.", desc: "The first community pack released, tricky and rough around the edges.",
url: 'https://wiki.bitbusters.club/Chip%27s_Challenge_Level_Pack_2_(Lynx)',
}, { }, {
path: 'levels/CCLP3.ccl', path: 'levels/CCLP3.ccl',
ident: 'cclp3', ident: 'cclp3',
title: "Chip's Challenge Level Pack 3", title: "Chip's Challenge Level Pack 3",
desc: "A tough challenge, by and for veteran players.", desc: "A tough challenge, by and for veteran players.",
url: 'https://wiki.bitbusters.club/Chip%27s_Challenge_Level_Pack_3',
/* /*
* TODO: this is tricky. it's a massive hodgepodge of levels mostly made by individual people... * TODO: this is tricky. it's a massive hodgepodge of levels mostly made by individual people...
}, { }, {
@ -1786,34 +1790,22 @@ class Splash extends PrimaryView {
super(conductor, document.body.querySelector('main#splash')); super(conductor, document.body.querySelector('main#splash'));
// Populate the list of available level packs // Populate the list of available level packs
let pack_list = document.querySelector('#splash-stock-levels'); let stock_pack_list = mk('ul.played-pack-list');
document.querySelector('#splash-stock-levels').append(stock_pack_list);
this.played_pack_elements = {};
let stock_pack_idents = new Set;
for (let packdef of BUILTIN_LEVEL_PACKS) { for (let packdef of BUILTIN_LEVEL_PACKS) {
let score; stock_pack_idents.add(packdef.ident);
let packinfo = conductor.stash.packs[packdef.ident]; stock_pack_list.append(this._create_pack_element(packdef.ident, packdef));
if (packinfo && packinfo.total_score !== undefined) { }
if (packinfo.total_score === null) {
// Whoops, some NaNs got in here :(
score = "computing...";
}
else {
// TODO tack on a star if the game is "beaten"? what's that mean? every level
// beaten i guess?
score = packinfo.total_score.toLocaleString();
}
}
else {
score = "unplayed";
}
let button = mk('button.button-big.level-pack-button', // Populate the list of other packs you've played
mk('h3', packdef.title), let custom_pack_list = mk('ul.played-pack-list');
mk('p', packdef.desc), this.root.querySelector('#splash-upload-levels').append(custom_pack_list);
mk('span.-score', score), for (let [ident, packinfo] of Object.entries(this.conductor.stash.packs)) {
); if (stock_pack_idents.has(ident))
button.addEventListener('click', ev => { continue;
this.conductor.fetch_pack(packdef.path, packdef.title); custom_pack_list.append(this._create_pack_element(ident));
});
pack_list.append(button);
} }
// File loading: allow providing either a single file, multiple files, OR an entire // File loading: allow providing either a single file, multiple files, OR an entire
@ -1900,6 +1892,86 @@ class Splash extends PrimaryView {
} }
} }
_create_pack_element(ident, packdef = null) {
let title = packdef ? packdef.title : ident;
let button = mk('button.button-big.level-pack-button', {type: 'button'},
mk('h3', title));
if (packdef) {
button.addEventListener('click', ev => {
this.conductor.fetch_pack(packdef.path, packdef.title);
});
}
else {
button.disabled = true;
}
let li = mk('li.--unplayed', {'data-ident': ident}, button);
let forget_button = mk('button.-forget', {type: 'button'}, "Forget");
forget_button.addEventListener('click', ev => {
new ConfirmOverlay(this.conductor, `Clear all your progress for ${title}? This can't be undone.`, () => {
delete this.conductor.stash.packs[ident];
localStorage.removeItem(STORAGE_PACK_PREFIX + ident);
this.conductor.save_stash();
if (packdef) {
this.update_pack_score(ident);
}
else {
li.remove();
}
}).open();
});
if (packdef) {
li.append(mk('p', packdef.desc, " ", mk('a', {href: packdef.url}, "More...")));
}
li.append(mk('div.-progress',
mk('span.-score'),
mk('span.-time'),
mk('span.-levels'),
forget_button,
));
this.played_pack_elements[ident] = li;
this.update_pack_score(ident);
return li;
}
update_pack_score(ident) {
let li = this.played_pack_elements[ident];
let packinfo = this.conductor.stash.packs[ident];
li.classList.toggle('--unplayed', ! packinfo);
if (! packinfo)
return;
let progress = li.querySelector('.-progress');
let score;
if (packinfo.total_score === null) {
// Whoops, some NaNs got in here :(
score = "computing...";
}
else {
// TODO tack on a star if the game is "beaten"? what's that mean? every level
// beaten i guess?
score = packinfo.total_score.toLocaleString();
}
progress.querySelector('.-score').textContent = score;
// This stuff isn't available in old saves
if (packinfo.total_levels === undefined) {
progress.querySelector('.-time').textContent = "";
progress.querySelector('.-levels').textContent = "";
}
else {
// TODO not used: total_abstime, aidless_levels
progress.querySelector('.-time').textContent = util.format_duration(packinfo.total_time);
let levels = `${packinfo.cleared_levels}/${packinfo.total_levels}`;
if (packinfo.cleared_levels === packinfo.total_levels) {
levels += '★';
}
progress.querySelector('.-levels').textContent = levels;
}
}
// Look for something we can load, and load it // Look for something we can load, and load it
async search_multi_source(source) { async search_multi_source(source) {
// TODO not entiiirely kosher, but not sure if we should have an api for this or what // TODO not entiiirely kosher, but not sure if we should have an api for this or what
@ -2570,6 +2642,9 @@ class LevelBrowserOverlay extends DialogOverlay {
// the levels upfront though, which i currently do but want to stop doing // the levels upfront though, which i currently do but want to stop doing
); );
if (i === this.conductor.level_index) {
tr.classList.add('--current');
}
// TODO sigh, does not actually indicate visited in C2G world // TODO sigh, does not actually indicate visited in C2G world
if (i >= savefile.highest_level) { if (i >= savefile.highest_level) {
tr.classList.add('--unvisited'); tr.classList.add('--unvisited');
@ -2592,20 +2667,41 @@ class LevelBrowserOverlay extends DialogOverlay {
} }
}); });
this.tbody = tbody;
this.add_button("nevermind", ev => { this.add_button("nevermind", ev => {
this.close(); this.close();
}); });
} }
open() {
super.open();
this.tbody.childNodes[this.conductor.level_index].scrollIntoView({block: 'center'});
}
} }
// Central dispatcher of what we're doing and what we've got loaded // Central dispatcher of what we're doing and what we've got loaded
// We store several kinds of things in localStorage: // We store several kinds of things in localStorage:
// Main storage: // Main storage:
// packs // packs:
// total_score
// total_time
// total_levels
// cleared_levels
// aidless_levels
// options // options
// compat: (either a ruleset string or an object of individual flags) // compat: (either a ruleset string or an object of individual flags)
const STORAGE_KEY = "Lexy's Labyrinth"; const STORAGE_KEY = "Lexy's Labyrinth";
// Records for playing a pack // Records for a pack that has been played
// total_score
// highest_level
// current_level
// scorecards: []?
// time
// abstime
// bonus
// score
// aid
const STORAGE_PACK_PREFIX = "Lexy's Labyrinth: "; const STORAGE_PACK_PREFIX = "Lexy's Labyrinth: ";
// Metadata for an edited pack // Metadata for an edited pack
// - list of the levels they own and basic metadata like name // - list of the levels they own and basic metadata like name
@ -2766,11 +2862,34 @@ class Conductor {
if (identifier !== null) { if (identifier !== null) {
// TODO again, enforce something about the shape here // TODO again, enforce something about the shape here
this.current_pack_savefile = JSON.parse(window.localStorage.getItem(STORAGE_PACK_PREFIX + identifier)); this.current_pack_savefile = JSON.parse(window.localStorage.getItem(STORAGE_PACK_PREFIX + identifier));
let changed = false;
if (this.current_pack_savefile && this.current_pack_savefile.total_score === null) { if (this.current_pack_savefile && this.current_pack_savefile.total_score === null) {
// Fix some NaNs that slipped in // Fix some NaNs that slipped in
this.current_pack_savefile.total_score = this.current_pack_savefile.scorecards this.current_pack_savefile.total_score = this.current_pack_savefile.scorecards
.map(scorecard => scorecard ? scorecard.score : 0) .map(scorecard => scorecard ? scorecard.score : 0)
.reduce((a, b) => a + b, 0); .reduce((a, b) => a + b, 0);
changed = true;
}
if (this.current_pack_savefile && this.current_pack_savefile.cleared_levels === undefined) {
// Populate some more recently added fields
this.current_pack_savefile.total_levels = stored_game.level_metadata.length;
this.current_pack_savefile.total_time = 0;
this.current_pack_savefile.total_abstime = 0;
this.current_pack_savefile.cleared_levels = 0;
this.current_pack_savefile.aidless_levels = 0;
for (let scorecard of this.current_pack_savefile.scorecards) {
if (! scorecard)
continue;
this.current_pack_savefile.total_time += scorecard.time;
this.current_pack_savefile.total_abstime += scorecard.abstime;
this.current_pack_savefile.cleared_levels += 1;
if (scorecard.aid === 0) {
this.current_pack_savefile.aidless_levels += 1;
}
}
changed = true;
}
if (changed) {
this.save_savefile(); this.save_savefile();
} }
} }
@ -2787,7 +2906,7 @@ class Conductor {
this.player.load_game(stored_game); this.player.load_game(stored_game);
this.editor.load_game(stored_game); this.editor.load_game(stored_game);
return this.change_level(0); return this.change_level((this.current_pack_savefile.current_level ?? 1) - 1);
} }
change_level(level_index) { change_level(level_index) {
@ -2846,18 +2965,25 @@ class Conductor {
if (! this._pack_identifier) if (! this._pack_identifier)
return; return;
// Don't save if there's nothing to save
if (! this.current_pack_savefile.cleared_levels && this.current_pack_savefile.current_level === 1)
return;
window.localStorage.setItem(STORAGE_PACK_PREFIX + this._pack_identifier, JSON.stringify(this.current_pack_savefile)); window.localStorage.setItem(STORAGE_PACK_PREFIX + this._pack_identifier, JSON.stringify(this.current_pack_savefile));
// Also remember the total score in the stash, if it changed, so we can read it without // Also remember some stats in the stash, if it changed, so we can read it without having to
// having to parse every single one of these things // parse every single one of these things
let packinfo = this.stash.packs[this._pack_identifier]; let packinfo = this.stash.packs[this._pack_identifier];
if (! packinfo || packinfo.total_score !== this.current_pack_savefile.total_score) { if (! packinfo) {
if (! packinfo) { packinfo = {};
packinfo = {}; this.stash.packs[this._pack_identifier] = packinfo;
this.stash.packs[this._pack_identifier] = packinfo; }
let keys = ['total_score', 'total_time', 'total_abstime', 'total_levels', 'cleared_levels', 'aidless_levels'];
if (keys.some(key => packinfo[key] !== this.current_pack_savefile[key])) {
for (let key of keys) {
packinfo[key] = this.current_pack_savefile[key];
} }
packinfo.total_score = this.current_pack_savefile.total_score; this.splash.update_pack_score(this._pack_identifier);
this.save_stash();
} }
} }

View File

@ -20,6 +20,7 @@ body {
--button-bg-color: hsl(225, 10%, 25%); --button-bg-color: hsl(225, 10%, 25%);
--button-bg-hover-color: hsl(225, 15%, 30%); --button-bg-hover-color: hsl(225, 15%, 30%);
--generic-bg-hover-on-white: hsl(225, 60%, 85%); --generic-bg-hover-on-white: hsl(225, 60%, 85%);
--generic-bg-selected-on-white: hsl(225, 60%, 90%);
} }
/* Generic element styling */ /* Generic element styling */
@ -290,6 +291,9 @@ table.level-browser td.-time {
table.level-browser td.-score { table.level-browser td.-score {
text-align: right; text-align: right;
} }
table.level-browser tr.--current {
background: var(--generic-bg-selected-on-white);
}
table.level-browser tr.--unvisited { table.level-browser tr.--unvisited {
color: #606060; color: #606060;
font-style: italic; font-style: italic;
@ -302,7 +306,7 @@ table.level-browser tbody tr {
cursor: pointer; cursor: pointer;
} }
table.level-browser tbody tr:hover { table.level-browser tbody tr:hover {
background: var(--generic-hover-bg-on-white); background: var(--generic-bg-hover-on-white);
} }
table.level-browser tbody tr:nth-child(10n) td { table.level-browser tbody tr:nth-child(10n) td {
border-bottom: 2px solid hsl(225, 20%, 80%); border-bottom: 2px solid hsl(225, 20%, 80%);
@ -316,7 +320,7 @@ ul.compat-flags > li {
padding: 0.125em; padding: 0.125em;
} }
ul.compat-flags > li.-checked { ul.compat-flags > li.-checked {
background: hsl(225, 60%, 90%); background: var(--generic-bg-selected-on-white);
} }
ul.compat-flags > li:hover { ul.compat-flags > li:hover {
background: var(--generic-bg-hover-on-white); background: var(--generic-bg-hover-on-white);
@ -640,6 +644,33 @@ pre.stack-trace {
} }
} }
.played-pack-list {
}
.played-pack-list > li {
margin-bottom: 1em;
}
.played-pack-list p {
font-size: 0.833em;
font-style: italic;
}
.played-pack-list .-progress {
display: flex;
align-items: center;
gap: 0.5em;
text-align: right;
}
.played-pack-list > li.--unplayed .-progress {
display: none;
}
.played-pack-list .-progress > .-score,
.played-pack-list .-progress > .-time,
.played-pack-list .-progress > .-levels {
flex: 2;
}
.played-pack-list .-progress > button {
flex: 1;
}
button.level-pack-button { button.level-pack-button {
display: grid; display: grid;
grid: grid: