From 19684200271fee412834a9c760f23f0621cd3814 Mon Sep 17 00:00:00 2001 From: "Eevee (Evelyn Woods)" Date: Thu, 24 Dec 2020 05:36:57 -0700 Subject: [PATCH] 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 --- index.html | 1 + js/main.js | 200 +++++++++++++++++++++++++++++++++++++++++++---------- style.css | 35 +++++++++- 3 files changed, 197 insertions(+), 39 deletions(-) diff --git a/index.html b/index.html index 89fb1b1..a0942a3 100644 --- a/index.html +++ b/index.html @@ -85,6 +85,7 @@ diff --git a/js/main.js b/js/main.js index 2be723e..660a957 100644 --- a/js/main.js +++ b/js/main.js @@ -1668,21 +1668,25 @@ const BUILTIN_LEVEL_PACKS = [{ ident: 'cclp1', 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.", + url: 'https://wiki.bitbusters.club/Chip%27s_Challenge_Level_Pack_1', }, { path: 'levels/CCLP4.ccl', ident: 'cclp4', title: "Chip's Challenge Level Pack 4", desc: "Moderately difficult, but not unfair.", + url: 'https://wiki.bitbusters.club/Chip%27s_Challenge_Level_Pack_4', }, { path: 'levels/CCLXP2.ccl', ident: 'cclxp2', title: "Chip's Challenge Level Pack 2-X", 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', ident: 'cclp3', title: "Chip's Challenge Level Pack 3", 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... }, { @@ -1786,34 +1790,22 @@ class Splash extends PrimaryView { super(conductor, document.body.querySelector('main#splash')); // 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) { - let score; - let packinfo = conductor.stash.packs[packdef.ident]; - 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"; - } + stock_pack_idents.add(packdef.ident); + stock_pack_list.append(this._create_pack_element(packdef.ident, packdef)); + } - let button = mk('button.button-big.level-pack-button', - mk('h3', packdef.title), - mk('p', packdef.desc), - mk('span.-score', score), - ); - button.addEventListener('click', ev => { - this.conductor.fetch_pack(packdef.path, packdef.title); - }); - pack_list.append(button); + // Populate the list of other packs you've played + let custom_pack_list = mk('ul.played-pack-list'); + this.root.querySelector('#splash-upload-levels').append(custom_pack_list); + for (let [ident, packinfo] of Object.entries(this.conductor.stash.packs)) { + if (stock_pack_idents.has(ident)) + continue; + custom_pack_list.append(this._create_pack_element(ident)); } // 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 async search_multi_source(source) { // 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 ); + if (i === this.conductor.level_index) { + tr.classList.add('--current'); + } // TODO sigh, does not actually indicate visited in C2G world if (i >= savefile.highest_level) { tr.classList.add('--unvisited'); @@ -2592,20 +2667,41 @@ class LevelBrowserOverlay extends DialogOverlay { } }); + this.tbody = tbody; + this.add_button("nevermind", ev => { 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 // We store several kinds of things in localStorage: // Main storage: -// packs +// packs: +// total_score +// total_time +// total_levels +// cleared_levels +// aidless_levels // options // compat: (either a ruleset string or an object of individual flags) 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: "; // Metadata for an edited pack // - list of the levels they own and basic metadata like name @@ -2766,11 +2862,34 @@ class Conductor { if (identifier !== null) { // TODO again, enforce something about the shape here 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) { // Fix some NaNs that slipped in this.current_pack_savefile.total_score = this.current_pack_savefile.scorecards .map(scorecard => scorecard ? scorecard.score : 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(); } } @@ -2787,7 +2906,7 @@ class Conductor { this.player.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) { @@ -2846,18 +2965,25 @@ class Conductor { if (! this._pack_identifier) 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)); - // Also remember the total score in the stash, if it changed, so we can read it without - // having to parse every single one of these things + // Also remember some stats in the stash, if it changed, so we can read it without having to + // parse every single one of these things let packinfo = this.stash.packs[this._pack_identifier]; - if (! packinfo || packinfo.total_score !== this.current_pack_savefile.total_score) { - if (! packinfo) { - packinfo = {}; - this.stash.packs[this._pack_identifier] = packinfo; + if (! packinfo) { + 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.save_stash(); + this.splash.update_pack_score(this._pack_identifier); } } diff --git a/style.css b/style.css index 8c98bcb..7351892 100644 --- a/style.css +++ b/style.css @@ -20,6 +20,7 @@ body { --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 element styling */ @@ -290,6 +291,9 @@ table.level-browser td.-time { table.level-browser td.-score { text-align: right; } +table.level-browser tr.--current { + background: var(--generic-bg-selected-on-white); +} table.level-browser tr.--unvisited { color: #606060; font-style: italic; @@ -302,7 +306,7 @@ table.level-browser tbody tr { cursor: pointer; } 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 { border-bottom: 2px solid hsl(225, 20%, 80%); @@ -316,7 +320,7 @@ ul.compat-flags > li { padding: 0.125em; } ul.compat-flags > li.-checked { - background: hsl(225, 60%, 90%); + background: var(--generic-bg-selected-on-white); } ul.compat-flags > li:hover { 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 { display: grid; grid: