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:
parent
a8800838d4
commit
1968420027
@ -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>
|
||||||
|
|||||||
192
js/main.js
192
js/main.js
@ -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;
|
||||||
}
|
}
|
||||||
packinfo.total_score = this.current_pack_savefile.total_score;
|
let keys = ['total_score', 'total_time', 'total_abstime', 'total_levels', 'cleared_levels', 'aidless_levels'];
|
||||||
this.save_stash();
|
if (keys.some(key => packinfo[key] !== this.current_pack_savefile[key])) {
|
||||||
|
for (let key of keys) {
|
||||||
|
packinfo[key] = this.current_pack_savefile[key];
|
||||||
|
}
|
||||||
|
this.splash.update_pack_score(this._pack_identifier);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
35
style.css
35
style.css
@ -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:
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user