Track scores, including your total score for a pack
This commit is contained in:
parent
ac59f7b15d
commit
537e011f2a
@ -654,8 +654,8 @@ function decompress(buf) {
|
|||||||
return out;
|
return out;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function parse_level(buf) {
|
export function parse_level(buf, number = 1) {
|
||||||
let level = new util.StoredLevel;
|
let level = new util.StoredLevel(number);
|
||||||
let full_view = new DataView(buf);
|
let full_view = new DataView(buf);
|
||||||
let next_section_start = 0;
|
let next_section_start = 0;
|
||||||
let extra_hints = [];
|
let extra_hints = [];
|
||||||
|
|||||||
@ -119,8 +119,8 @@ const TILE_ENCODING = {
|
|||||||
0x6f: ['player', 'east'],
|
0x6f: ['player', 'east'],
|
||||||
};
|
};
|
||||||
|
|
||||||
function parse_level(buf) {
|
function parse_level(buf, number) {
|
||||||
let level = new util.StoredLevel;
|
let level = new util.StoredLevel(number);
|
||||||
// Map size is always fixed as 32x32 in CC1
|
// Map size is always fixed as 32x32 in CC1
|
||||||
level.size_x = 32;
|
level.size_x = 32;
|
||||||
level.size_y = 32;
|
level.size_y = 32;
|
||||||
@ -304,7 +304,7 @@ export function parse_game(buf) {
|
|||||||
let level_buf = buf.slice(p + 2, p + 2 + length);
|
let level_buf = buf.slice(p + 2, p + 2 + length);
|
||||||
p += 2 + length;
|
p += 2 + length;
|
||||||
|
|
||||||
let level = parse_level(level_buf);
|
let level = parse_level(level_buf, l);
|
||||||
game.levels.push(level);
|
game.levels.push(level);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -6,7 +6,8 @@ export class StoredCell extends Array {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export class StoredLevel {
|
export class StoredLevel {
|
||||||
constructor() {
|
constructor(number) {
|
||||||
|
this.number = number; // one-based
|
||||||
this.title = '';
|
this.title = '';
|
||||||
this.password = null;
|
this.password = null;
|
||||||
this.hint = '';
|
this.hint = '';
|
||||||
@ -39,7 +40,8 @@ export class StoredLevel {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export class StoredGame {
|
export class StoredGame {
|
||||||
constructor() {
|
constructor(identifier) {
|
||||||
|
this.identifier = identifier;
|
||||||
this.levels = [];
|
this.levels = [];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
20
js/game.js
20
js/game.js
@ -176,6 +176,7 @@ export class Level {
|
|||||||
this.actors = [];
|
this.actors = [];
|
||||||
this.chips_remaining = this.stored_level.chips_required;
|
this.chips_remaining = this.stored_level.chips_required;
|
||||||
this.bonus_points = 0;
|
this.bonus_points = 0;
|
||||||
|
this.aid = 0;
|
||||||
|
|
||||||
// Time
|
// Time
|
||||||
if (this.stored_level.time_limit === 0) {
|
if (this.stored_level.time_limit === 0) {
|
||||||
@ -185,6 +186,8 @@ export class Level {
|
|||||||
this.time_remaining = this.stored_level.time_limit * 20;
|
this.time_remaining = this.stored_level.time_limit * 20;
|
||||||
}
|
}
|
||||||
this.timer_paused = false;
|
this.timer_paused = false;
|
||||||
|
// Note that this clock counts *up*, even on untimed levels, and is unaffected by CC2's
|
||||||
|
// clock alteration shenanigans
|
||||||
this.tic_counter = 0;
|
this.tic_counter = 0;
|
||||||
// 0 to 7, indicating the first tic that teeth can move on.
|
// 0 to 7, indicating the first tic that teeth can move on.
|
||||||
// 0 is equivalent to even step; 4 is equivalent to odd step.
|
// 0 is equivalent to even step; 4 is equivalent to odd step.
|
||||||
@ -932,6 +935,8 @@ export class Level {
|
|||||||
}
|
}
|
||||||
|
|
||||||
undo() {
|
undo() {
|
||||||
|
this.aid = Math.max(1, this.aid);
|
||||||
|
|
||||||
let entry = this.undo_stack.pop();
|
let entry = this.undo_stack.pop();
|
||||||
// Undo in reverse order! There's no redo, so it's okay to destroy this
|
// Undo in reverse order! There's no redo, so it's okay to destroy this
|
||||||
entry.reverse();
|
entry.reverse();
|
||||||
@ -1018,6 +1023,21 @@ export class Level {
|
|||||||
throw new GameEnded;
|
throw new GameEnded;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
get_scorecard() {
|
||||||
|
if (this.state !== 'success') {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
let time = Math.floor((this.time_remaining ?? 0) / 20);
|
||||||
|
return {
|
||||||
|
time: time,
|
||||||
|
abstime: this.tic_counter,
|
||||||
|
bonus: this.bonus_points,
|
||||||
|
score: this.stored_level.number * 500 + time * 10 + this.bonus_points,
|
||||||
|
aid: this.aid,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
// Get the next direction a random force floor will use. They share global
|
// Get the next direction a random force floor will use. They share global
|
||||||
// state and cycle clockwise.
|
// state and cycle clockwise.
|
||||||
get_force_floor_direction() {
|
get_force_floor_direction() {
|
||||||
|
|||||||
216
js/main.js
216
js/main.js
@ -121,14 +121,7 @@ class PrimaryView {
|
|||||||
|
|
||||||
|
|
||||||
// TODO:
|
// TODO:
|
||||||
// - some kinda visual theme i guess lol
|
|
||||||
// - level password, if any
|
// - level password, if any
|
||||||
// - bonus points (cc2 only, or maybe only if got any so far this level)
|
|
||||||
// - intro splash with list of available level packs
|
|
||||||
// - button: quit to splash
|
|
||||||
// - implement winning and show score for this level
|
|
||||||
// - show current score so far
|
|
||||||
// - about, help
|
|
||||||
const ACTION_LABELS = {
|
const ACTION_LABELS = {
|
||||||
up: '⬆️\ufe0f',
|
up: '⬆️\ufe0f',
|
||||||
down: '⬇️\ufe0f',
|
down: '⬇️\ufe0f',
|
||||||
@ -621,6 +614,15 @@ class Player extends PrimaryView {
|
|||||||
}
|
}
|
||||||
|
|
||||||
load_level(stored_level) {
|
load_level(stored_level) {
|
||||||
|
// Do this here because we care about the latest level played, not the latest level opened
|
||||||
|
// in the editor or whatever
|
||||||
|
let savefile = this.conductor.current_pack_savefile;
|
||||||
|
savefile.current_level = stored_level.number;
|
||||||
|
if (savefile.highest_level < stored_level.number) {
|
||||||
|
savefile.highest_level = stored_level.number;
|
||||||
|
}
|
||||||
|
this.conductor.save_savefile();
|
||||||
|
|
||||||
this.level = new Level(stored_level, this.compat);
|
this.level = new Level(stored_level, this.compat);
|
||||||
this.level.sfx = this.sfx_player;
|
this.level.sfx = this.sfx_player;
|
||||||
this.renderer.set_level(this.level);
|
this.renderer.set_level(this.level);
|
||||||
@ -937,9 +939,32 @@ class Player extends PrimaryView {
|
|||||||
overlay_keyhint = "press space to try again, or Z to rewind";
|
overlay_keyhint = "press space to try again, or Z to rewind";
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
|
// We just beat the level! Hey, that's cool.
|
||||||
|
// Let's save the score while we're here.
|
||||||
|
let level_number = this.level.stored_level.number;
|
||||||
|
let level_index = level_number - 1;
|
||||||
|
let scorecard = this.level.get_scorecard();
|
||||||
|
let savefile = this.conductor.current_pack_savefile;
|
||||||
|
let old_scorecard;
|
||||||
|
if (! savefile.scorecards[level_index] ||
|
||||||
|
savefile.scorecards[level_index].score < scorecard.score)
|
||||||
|
{
|
||||||
|
old_scorecard = savefile.scorecards[level_index];
|
||||||
|
|
||||||
|
// Adjust the total score
|
||||||
|
savefile.total_score = savefile.total_score ?? 0;
|
||||||
|
if (old_scorecard) {
|
||||||
|
savefile.total_score -= old_scorecard.score;
|
||||||
|
}
|
||||||
|
savefile.total_score += scorecard.score;
|
||||||
|
|
||||||
|
savefile.scorecards[level_index] = scorecard;
|
||||||
|
this.conductor.save_savefile();
|
||||||
|
}
|
||||||
|
|
||||||
overlay_reason = 'success';
|
overlay_reason = 'success';
|
||||||
let base = (this.conductor.level_index + 1) * 500;
|
let base = level_number * 500;
|
||||||
let time = Math.ceil((this.level.time_remaining ?? 0) / 20) * 10;
|
let time = scorecard.time * 10;
|
||||||
// Pick a success message
|
// Pick a success message
|
||||||
// TODO done on first try; took many tries
|
// TODO done on first try; took many tries
|
||||||
let time_left_fraction = null;
|
let time_left_fraction = null;
|
||||||
@ -981,14 +1006,39 @@ class Player extends PrimaryView {
|
|||||||
mk('dd', base),
|
mk('dd', base),
|
||||||
mk('dt', "time bonus"),
|
mk('dt', "time bonus"),
|
||||||
mk('dd', `+ ${time}`),
|
mk('dd', `+ ${time}`),
|
||||||
mk('dt', "score bonus"),
|
);
|
||||||
mk('dd', `+ ${this.level.bonus_points}`),
|
// It should be impossible to ever have a bonus and then drop back to 0 with CC2
|
||||||
|
// rules; thieves can halve it, but the amount taken is rounded down.
|
||||||
|
// That is to say, I don't need to track whether we ever got a score bonus
|
||||||
|
if (this.level.bonus_points) {
|
||||||
|
overlay_middle.append(
|
||||||
|
mk('dt', "score bonus"),
|
||||||
|
mk('dd', `+ ${this.level.bonus_points}`),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
overlay_middle.append(mk('dt', ""), mk('dd', ""));
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO show your time, bold time...?
|
||||||
|
overlay_middle.append(
|
||||||
mk('dt.-sum', "level score"),
|
mk('dt.-sum', "level score"),
|
||||||
mk('dd.-sum', base + time + this.level.bonus_points),
|
mk('dd.-sum', `${scorecard.score} ${scorecard.aid === 0 ? '★' : ''}`),
|
||||||
mk('dt', "improvement"),
|
);
|
||||||
mk('dd', "(TODO)"),
|
|
||||||
|
if (old_scorecard) {
|
||||||
|
overlay_middle.append(
|
||||||
|
mk('dt', "improvement"),
|
||||||
|
mk('dd', `+ ${scorecard.score - old_scorecard.score}`),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
overlay_middle.append(mk('dt', ""), mk('dd', ""));
|
||||||
|
}
|
||||||
|
|
||||||
|
overlay_middle.append(
|
||||||
mk('dt', "total score"),
|
mk('dt', "total score"),
|
||||||
mk('dd', "(TODO)"),
|
mk('dd', savefile.total_score),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -1594,18 +1644,22 @@ class Editor extends PrimaryView {
|
|||||||
|
|
||||||
const BUILTIN_LEVEL_PACKS = [{
|
const BUILTIN_LEVEL_PACKS = [{
|
||||||
path: 'levels/CCLP1.ccl',
|
path: 'levels/CCLP1.ccl',
|
||||||
|
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.",
|
||||||
}, {
|
}, {
|
||||||
path: 'levels/CCLP4.ccl',
|
path: 'levels/CCLP4.ccl',
|
||||||
|
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.",
|
||||||
}, {
|
}, {
|
||||||
path: 'levels/CCLXP2.ccl',
|
path: 'levels/CCLXP2.ccl',
|
||||||
|
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.",
|
||||||
}, {
|
}, {
|
||||||
path: 'levels/CCLP3.ccl',
|
path: 'levels/CCLP3.ccl',
|
||||||
|
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.",
|
||||||
}];
|
}];
|
||||||
@ -1617,10 +1671,21 @@ class Splash extends PrimaryView {
|
|||||||
// Populate the list of available level packs
|
// Populate the list of available level packs
|
||||||
let pack_list = document.querySelector('#splash-stock-levels');
|
let pack_list = document.querySelector('#splash-stock-levels');
|
||||||
for (let packdef of BUILTIN_LEVEL_PACKS) {
|
for (let packdef of BUILTIN_LEVEL_PACKS) {
|
||||||
|
let score;
|
||||||
|
let packinfo = conductor.stash.packs[packdef.ident];
|
||||||
|
if (packinfo && packinfo.total_score !== undefined) {
|
||||||
|
// 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',
|
let button = mk('button.button-big.level-pack-button',
|
||||||
mk('h3', packdef.title),
|
mk('h3', packdef.title),
|
||||||
mk('p', packdef.desc),
|
mk('p', packdef.desc),
|
||||||
mk('span.-score', "unplayed"),
|
mk('span.-score', score),
|
||||||
);
|
);
|
||||||
button.addEventListener('click', ev => {
|
button.addEventListener('click', ev => {
|
||||||
this.fetch_pack(packdef.path, packdef.title);
|
this.fetch_pack(packdef.path, packdef.title);
|
||||||
@ -1638,7 +1703,7 @@ class Splash extends PrimaryView {
|
|||||||
upload_el.addEventListener('change', async ev => {
|
upload_el.addEventListener('change', async ev => {
|
||||||
let file = ev.target.files[0];
|
let file = ev.target.files[0];
|
||||||
let buf = await file.arrayBuffer();
|
let buf = await file.arrayBuffer();
|
||||||
this.load_file(buf);
|
this.load_file(buf, this.extract_identifier_from_path(file.name));
|
||||||
// TODO get title out of C2G when it's supported
|
// TODO get title out of C2G when it's supported
|
||||||
this.conductor.level_pack_name_el.textContent = file.name;
|
this.conductor.level_pack_name_el.textContent = file.name;
|
||||||
});
|
});
|
||||||
@ -1655,7 +1720,8 @@ class Splash extends PrimaryView {
|
|||||||
}
|
}
|
||||||
stored_level.linear_cells[0].push({type: TILE_TYPES['player']});
|
stored_level.linear_cells[0].push({type: TILE_TYPES['player']});
|
||||||
|
|
||||||
let stored_game = new format_util.StoredGame;
|
// FIXME definitely gonna need a name here chief
|
||||||
|
let stored_game = new format_util.StoredGame(null);
|
||||||
stored_game.levels.push(stored_level);
|
stored_game.levels.push(stored_level);
|
||||||
this.conductor.load_game(stored_game);
|
this.conductor.load_game(stored_game);
|
||||||
|
|
||||||
@ -1663,17 +1729,28 @@ class Splash extends PrimaryView {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
extract_identifier_from_path(path) {
|
||||||
|
let ident = path.match(/^(?:.*\/)?[.]*([^.]+)(?:[.]|$)/)[1];
|
||||||
|
if (ident) {
|
||||||
|
return ident.toLowerCase();
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO wait why aren't these just on conductor
|
||||||
async fetch_pack(path, title) {
|
async fetch_pack(path, title) {
|
||||||
// TODO indicate we're downloading something
|
// TODO indicate we're downloading something
|
||||||
// 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 fetch(path);
|
let buf = await fetch(path);
|
||||||
this.load_file(buf);
|
this.load_file(buf, this.extract_identifier_from_path(path));
|
||||||
// TODO get title out of C2G when it's supported
|
// TODO get title out of C2G when it's supported
|
||||||
this.conductor.level_pack_name_el.textContent = title || path;
|
this.conductor.level_pack_name_el.textContent = title || path;
|
||||||
}
|
}
|
||||||
|
|
||||||
load_file(buf) {
|
load_file(buf, identifier = null) {
|
||||||
// TODO also support tile world's DAC when reading from local??
|
// TODO also support tile world's DAC when reading from local??
|
||||||
// TODO ah, there's more metadata in CCX, crapola
|
// TODO ah, there's more metadata in CCX, crapola
|
||||||
let magic = String.fromCharCode.apply(null, new Uint8Array(buf.slice(0, 4)));
|
let magic = String.fromCharCode.apply(null, new Uint8Array(buf.slice(0, 4)));
|
||||||
@ -1681,6 +1758,8 @@ class Splash extends PrimaryView {
|
|||||||
if (magic === 'CC2M' || magic === 'CCS ') {
|
if (magic === 'CC2M' || magic === 'CCS ') {
|
||||||
stored_game = new format_util.StoredGame;
|
stored_game = new format_util.StoredGame;
|
||||||
stored_game.levels.push(c2m.parse_level(buf));
|
stored_game.levels.push(c2m.parse_level(buf));
|
||||||
|
// Don't make a savefile for individual levels
|
||||||
|
identifier = null;
|
||||||
}
|
}
|
||||||
else if (magic === '\xac\xaa\x02\x00' || magic == '\xac\xaa\x02\x01') {
|
else if (magic === '\xac\xaa\x02\x00' || magic == '\xac\xaa\x02\x01') {
|
||||||
stored_game = dat.parse_game(buf);
|
stored_game = dat.parse_game(buf);
|
||||||
@ -1688,7 +1767,7 @@ class Splash extends PrimaryView {
|
|||||||
else {
|
else {
|
||||||
throw new Error("Unrecognized file format");
|
throw new Error("Unrecognized file format");
|
||||||
}
|
}
|
||||||
this.conductor.load_game(stored_game);
|
this.conductor.load_game(stored_game, identifier);
|
||||||
this.conductor.switch_to_player();
|
this.conductor.switch_to_player();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -1855,20 +1934,58 @@ class LevelBrowserOverlay extends DialogOverlay {
|
|||||||
constructor(conductor) {
|
constructor(conductor) {
|
||||||
super(conductor);
|
super(conductor);
|
||||||
this.set_title("choose a level");
|
this.set_title("choose a level");
|
||||||
let table = mk('table.level-browser');
|
let thead = mk('thead', mk('tr',
|
||||||
|
mk('th', ""),
|
||||||
|
mk('th', "Level"),
|
||||||
|
mk('th', "Your time"),
|
||||||
|
mk('th', mk('abbr', {
|
||||||
|
title: "Actual time it took you to play the level, even on untimed levels, and ignoring any CC2 clock altering effects",
|
||||||
|
}, "Real time")),
|
||||||
|
mk('th', "Your score"),
|
||||||
|
));
|
||||||
|
let tbody = mk('tbody');
|
||||||
|
let table = mk('table.level-browser', thead, tbody);
|
||||||
this.main.append(table);
|
this.main.append(table);
|
||||||
|
let savefile = conductor.current_pack_savefile;
|
||||||
|
// TODO if i stop eagerloading everything in a .DAT then this will not make sense any more
|
||||||
for (let [i, stored_level] of conductor.stored_game.levels.entries()) {
|
for (let [i, stored_level] of conductor.stored_game.levels.entries()) {
|
||||||
table.append(mk('tr',
|
let scorecard = savefile.scorecards[i];
|
||||||
|
let score = "—", time = "—", abstime = "—";
|
||||||
|
if (scorecard) {
|
||||||
|
score = scorecard.score.toLocaleString();
|
||||||
|
if (scorecard.aid === 0) {
|
||||||
|
score += '★';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (scorecard.time === 0) {
|
||||||
|
// This level is untimed
|
||||||
|
time = "n/a";
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
time = String(scorecard.time);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Express absolute time as mm:ss, with two decimals on the seconds (which should be
|
||||||
|
// able to exactly count a number of tics)
|
||||||
|
abstime = `${Math.floor(scorecard.abstime / 20 / 60)}:${(scorecard.abstime / 20 % 60).toFixed(2)}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
tbody.append(mk(i >= savefile.highest_level ? 'tr.--unvisited' : 'tr',
|
||||||
{'data-index': i},
|
{'data-index': i},
|
||||||
mk('td', i + 1),
|
mk('td.-number', i + 1),
|
||||||
mk('td', stored_level.title),
|
mk('td.-title', stored_level.title),
|
||||||
// TODO score?
|
mk('td.-time', time),
|
||||||
// TODO other stats??
|
mk('td.-time', abstime),
|
||||||
mk('td', '▶'),
|
mk('td.-score', score),
|
||||||
|
// TODO show your time? include 999 times for untimed levels (which i don't know at
|
||||||
|
// this point whoops but i guess if the time is zero then that answers that)? show
|
||||||
|
// your wallclock time also?
|
||||||
|
// TODO other stats?? num chips, time limit? don't know that without loading all
|
||||||
|
// the levels upfront though, which i currently do but want to stop doing
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
table.addEventListener('click', ev => {
|
tbody.addEventListener('click', ev => {
|
||||||
let tr = ev.target.closest('table.level-browser tr');
|
let tr = ev.target.closest('table.level-browser tr');
|
||||||
if (! tr)
|
if (! tr)
|
||||||
return;
|
return;
|
||||||
@ -1886,6 +2003,7 @@ class LevelBrowserOverlay extends DialogOverlay {
|
|||||||
|
|
||||||
// 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
|
||||||
const STORAGE_KEY = "Lexy's Labyrinth";
|
const STORAGE_KEY = "Lexy's Labyrinth";
|
||||||
|
const STORAGE_PACK_PREFIX = "Lexy's Labyrinth: ";
|
||||||
class Conductor {
|
class Conductor {
|
||||||
constructor(tileset) {
|
constructor(tileset) {
|
||||||
this.stored_game = null;
|
this.stored_game = null;
|
||||||
@ -1899,6 +2017,9 @@ class Conductor {
|
|||||||
if (! this.stash.options) {
|
if (! this.stash.options) {
|
||||||
this.stash.options = {};
|
this.stash.options = {};
|
||||||
}
|
}
|
||||||
|
if (! this.stash.packs) {
|
||||||
|
this.stash.packs = {};
|
||||||
|
}
|
||||||
// Handy aliases
|
// Handy aliases
|
||||||
this.options = this.stash.options;
|
this.options = this.stash.options;
|
||||||
|
|
||||||
@ -1990,9 +2111,25 @@ class Conductor {
|
|||||||
document.body.setAttribute('data-mode', 'player');
|
document.body.setAttribute('data-mode', 'player');
|
||||||
}
|
}
|
||||||
|
|
||||||
load_game(stored_game) {
|
load_game(stored_game, identifier = null) {
|
||||||
this.stored_game = stored_game;
|
this.stored_game = stored_game;
|
||||||
|
|
||||||
|
this._pack_identifier = identifier;
|
||||||
|
this.current_pack_savefile = null;
|
||||||
|
if (identifier !== null) {
|
||||||
|
// TODO again, enforce something about the shape here
|
||||||
|
this.current_pack_savefile = JSON.parse(window.localStorage.getItem(STORAGE_PACK_PREFIX + identifier));
|
||||||
|
}
|
||||||
|
if (! this.current_pack_savefile) {
|
||||||
|
this.current_pack_savefile = {
|
||||||
|
total_score: 0,
|
||||||
|
current_level: 1,
|
||||||
|
highest_level: 1,
|
||||||
|
// level scorecard: { time, abstime, bonus, score, aid } or null
|
||||||
|
scorecards: [],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
this.player.load_game(stored_game);
|
this.player.load_game(stored_game);
|
||||||
this.editor.load_game(stored_game);
|
this.editor.load_game(stored_game);
|
||||||
|
|
||||||
@ -2022,6 +2159,25 @@ class Conductor {
|
|||||||
save_stash() {
|
save_stash() {
|
||||||
window.localStorage.setItem(STORAGE_KEY, JSON.stringify(this.stash));
|
window.localStorage.setItem(STORAGE_KEY, JSON.stringify(this.stash));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
save_savefile() {
|
||||||
|
if (! this._pack_identifier)
|
||||||
|
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
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
packinfo.total_score = this.current_pack_savefile.total_score;
|
||||||
|
this.save_stash();
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
49
style.css
49
style.css
@ -160,13 +160,40 @@ table.level-browser {
|
|||||||
margin-bottom: 1em;
|
margin-bottom: 1em;
|
||||||
line-height: 1.25;
|
line-height: 1.25;
|
||||||
border-spacing: 0;
|
border-spacing: 0;
|
||||||
cursor: pointer;
|
}
|
||||||
|
table.level-browser thead {
|
||||||
|
position: sticky;
|
||||||
|
top: -1em; /* counteract padding so cells don't appear above us */
|
||||||
|
background: #f4f4f4; /* match dialog background */
|
||||||
|
}
|
||||||
|
table.level-browser thead tr th {
|
||||||
|
border-bottom: 2px solid hsl(225, 20%, 60%);
|
||||||
}
|
}
|
||||||
table.level-browser td {
|
table.level-browser td {
|
||||||
padding: 0 0.25em;
|
padding: 0.25em;
|
||||||
}
|
}
|
||||||
table.level-browser tr:hover {
|
table.level-browser td.-number {
|
||||||
background: hsl(225, 60%, 90%);
|
color: #404040;
|
||||||
|
text-align: right;
|
||||||
|
}
|
||||||
|
table.level-browser td.-time {
|
||||||
|
text-align: right;
|
||||||
|
}
|
||||||
|
table.level-browser td.-score {
|
||||||
|
text-align: right;
|
||||||
|
}
|
||||||
|
table.level-browser tr.--unvisited {
|
||||||
|
color: #606060;
|
||||||
|
font-style: italic;
|
||||||
|
}
|
||||||
|
table.level-browser tbody tr {
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
table.level-browser tbody tr:hover {
|
||||||
|
background: hsl(225, 60%, 85%);
|
||||||
|
}
|
||||||
|
table.level-browser tbody tr:nth-child(10n) td {
|
||||||
|
border-bottom: 2px solid hsl(225, 20%, 80%);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Options dialog */
|
/* Options dialog */
|
||||||
@ -534,10 +561,20 @@ dl.score-chart .-sum {
|
|||||||
color: hsl(225, 10%, 30%);
|
color: hsl(225, 10%, 30%);
|
||||||
}
|
}
|
||||||
.time output.--warning {
|
.time output.--warning {
|
||||||
color: hsl(30, 100%, 40%);
|
color: hsl(345, 60%, 60%);
|
||||||
}
|
}
|
||||||
.time output.--danger {
|
.time output.--danger {
|
||||||
color: hsl(345, 60%, 60%);
|
color: hsl(330, 60%, 60%);
|
||||||
|
/* TODO this can get out of sync and keeps going at 0, but is a neat idea */
|
||||||
|
/* animation: time-pulse 1s linear infinite; */
|
||||||
|
}
|
||||||
|
@keyframes time-pulse {
|
||||||
|
0% {
|
||||||
|
transform: scale(1.1);
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
transform: scale(1);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
.time output.--frozen {
|
.time output.--frozen {
|
||||||
color: hsl(225, 10%, 30%);
|
color: hsl(225, 10%, 30%);
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user