Show best score under cleared level nodes
Cleared nodes on the level select map now display the player's best score in a gold pill that rolls up from 0 on entrance, replacing the previous "kids saved" label. The tally scene records each run's final score alongside kids saved/total via `markLevelComplete`, and new `getBestScore` helper exposes it for display.
This commit is contained in:
parent
d6630dce3a
commit
b81ffac547
|
|
@ -124,6 +124,13 @@ Per-campaign notes:
|
|||
badge to be a different display size than ~58-71 units, adjust
|
||||
`BADGE_FRAC_OF_FRAME` / the per-state `BADGE_FILE_FRAC` entries in
|
||||
`src/util/mapArt.js` (the click area follows the art, not these knobs).
|
||||
- **Cleared nodes show the player's best score** under the badge, as a small
|
||||
gold pill (the game's `SCORE_PILL_STYLE` in `src/scenes/LevelSelectScene.js`)
|
||||
with the number rolling up from 0 on entrance. The pill is drawn by the
|
||||
game, not part of the badge art - the `map_node` badge should stay a plain
|
||||
cleared marker (no digits baked in). The score itself comes from
|
||||
`bestScore` in `src/util/progress.js` (localStorage), recorded by the
|
||||
tally scene after each run.
|
||||
- **`map_finish.png`** - the flag sits just past the last node, so a base/mound
|
||||
that grounds it looks best. Currently ships as a tight 512x512 image (flag
|
||||
ink is ~60% of the width, full height); the scene displays it at 0.18x the
|
||||
|
|
|
|||
|
|
@ -233,8 +233,9 @@ export default class LevelScoreScene extends Phaser.Scene {
|
|||
const y = GAME_HEIGHT * 0.86;
|
||||
const { bg, text } = createButton(this, GAME_WIDTH / 2, y, 'CONTINUE', () => {
|
||||
this._stopCrowdCheer();
|
||||
// Record the result and head straight back to the map.
|
||||
markLevelComplete(this.levelId, this.kidsSaved, this.total);
|
||||
// Record the result (including this run's final score, so the level
|
||||
// select map can show the player's best) and head back to the map.
|
||||
markLevelComplete(this.levelId, this.kidsSaved, this.total, this.runningScore);
|
||||
this.scene.stop('LevelScore');
|
||||
this.scene.start('LevelSelect');
|
||||
});
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@ import Phaser from 'phaser';
|
|||
import { GAME_WIDTH, GAME_HEIGHT, WORLD_SCALE } from '../config.js';
|
||||
import { CAMPAIGNS } from '../data/levels/index.js';
|
||||
import { autoLayoutPoints, sampleSpline } from '../data/levels/positioning.js';
|
||||
import { isLevelComplete, getLevelResult } from '../util/progress.js';
|
||||
import { isLevelComplete, getBestScore } from '../util/progress.js';
|
||||
import { playMenuMusic } from '../util/music.js';
|
||||
import { nodeDisplaySize, scalePos, BADGE_HIT_FRAC, BADGE_HIT_RADIUS_MULT } from '../util/mapArt.js';
|
||||
|
||||
|
|
@ -24,15 +24,28 @@ const TITLE_STYLE = {
|
|||
|
||||
const PILL_STYLE = {
|
||||
fontFamily: 'monospace',
|
||||
// 13 (not 14) so the longest tag - "10/10 kids saved" on a cleared node -
|
||||
// fits comfortably under ~100-unit-spaced nodes once a campaign grows past
|
||||
// a handful of levels.
|
||||
// 13 (not 14) so the level-name tag under the current node stays compact
|
||||
// even for campaigns with many levels; the best-score pills use their own
|
||||
// SCORE_PILL_STYLE below.
|
||||
fontSize: `${13 * WORLD_SCALE}px`,
|
||||
color: '#1a1f29',
|
||||
backgroundColor: '#ffffffaa',
|
||||
padding: { x: 9 * WORLD_SCALE, y: 5 * WORLD_SCALE },
|
||||
};
|
||||
|
||||
// The "best score" pill that rolls up under cleared nodes - gold, to match
|
||||
// the gold score numerals in the score tally scene.
|
||||
const SCORE_PILL_STYLE = {
|
||||
fontFamily: 'monospace',
|
||||
fontSize: `${14 * WORLD_SCALE}px`,
|
||||
fontStyle: 'bold',
|
||||
color: '#1a1f29',
|
||||
backgroundColor: '#f2c14e',
|
||||
stroke: '#ffffff',
|
||||
strokeThickness: 2 * WORLD_SCALE,
|
||||
padding: { x: 10 * WORLD_SCALE, y: 5 * WORLD_SCALE },
|
||||
};
|
||||
|
||||
export default class LevelSelectScene extends Phaser.Scene {
|
||||
constructor() {
|
||||
super('LevelSelect');
|
||||
|
|
@ -200,12 +213,11 @@ export default class LevelSelectScene extends Phaser.Scene {
|
|||
}
|
||||
this._groups.nodes.push(node);
|
||||
|
||||
const result = getLevelResult(level.id);
|
||||
let label = '';
|
||||
if (state === 'done') label = result ? `${result.kidsSaved}/${result.kidsTotal} kids saved` : 'cleared!';
|
||||
else if (state === 'current') label = level.name;
|
||||
if (label) {
|
||||
this._groups.tags.push(this.add.text(p.x, p.y + nh * 0.68, label, PILL_STYLE).setOrigin(0.5).setDepth(DEPTH.tag));
|
||||
if (state === 'done') {
|
||||
const best = getBestScore(level.id);
|
||||
if (best != null) this._scoreRollup(p, nh, best, 80 + i * 70);
|
||||
} else if (state === 'current') {
|
||||
this._groups.tags.push(this.add.text(p.x, p.y + nh * 0.68, level.name, PILL_STYLE).setOrigin(0.5).setDepth(DEPTH.tag));
|
||||
}
|
||||
});
|
||||
|
||||
|
|
@ -241,6 +253,39 @@ export default class LevelSelectScene extends Phaser.Scene {
|
|||
}
|
||||
}
|
||||
|
||||
// Cleared nodes wear a small gold "best score" pill under the badge (gold
|
||||
// matches the gold score numerals in the tally scene). On entrance the
|
||||
// number rolls up from 0 to the player's best - an odometer that reads as
|
||||
// the map remembering the run. Duration scales with the score's magnitude
|
||||
// (capped) so big scores get the longer, more satisfying count.
|
||||
_scoreRollup(p, nh, best, delay) {
|
||||
const pill = this.add
|
||||
.text(p.x, p.y + nh * 0.68, '★ 0', SCORE_PILL_STYLE)
|
||||
.setOrigin(0.5)
|
||||
.setDepth(DEPTH.tag)
|
||||
.setAlpha(0)
|
||||
.setScale(0.4);
|
||||
this._groups.tags.push(pill);
|
||||
this.tweens.add({
|
||||
targets: pill,
|
||||
alpha: 1,
|
||||
scale: 1,
|
||||
delay,
|
||||
duration: 220,
|
||||
ease: 'Back.easeOut',
|
||||
onComplete: () => {
|
||||
const counter = { v: 0 };
|
||||
this.tweens.add({
|
||||
targets: counter,
|
||||
v: best,
|
||||
duration: Math.min(1500, 500 + Math.sqrt(best) * 40),
|
||||
ease: 'Cubic.easeOut',
|
||||
onUpdate: () => pill.setText(`★ ${Math.round(counter.v).toLocaleString()}`),
|
||||
});
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
_startPulse(index, delay = 0) {
|
||||
if (this._pulseTween) this._pulseTween.stop();
|
||||
const node = this._groups.nodes[index];
|
||||
|
|
|
|||
|
|
@ -9,16 +9,7 @@ function readAll() {
|
|||
}
|
||||
}
|
||||
|
||||
export function isLevelComplete(levelId) {
|
||||
return Boolean(readAll()[levelId]);
|
||||
}
|
||||
|
||||
export function markLevelComplete(levelId, kidsSaved, kidsTotal) {
|
||||
const all = readAll();
|
||||
const existing = all[levelId];
|
||||
if (!existing || kidsSaved > existing.kidsSaved) {
|
||||
all[levelId] = { kidsSaved, kidsTotal };
|
||||
}
|
||||
function writeAll(all) {
|
||||
try {
|
||||
localStorage.setItem(STORAGE_KEY, JSON.stringify(all));
|
||||
} catch (e) {
|
||||
|
|
@ -26,6 +17,33 @@ export function markLevelComplete(levelId, kidsSaved, kidsTotal) {
|
|||
}
|
||||
}
|
||||
|
||||
export function isLevelComplete(levelId) {
|
||||
return Boolean(readAll()[levelId]);
|
||||
}
|
||||
|
||||
// Records a completed run. kidsSaved/kidsTotal are kept (they're how
|
||||
// "isLevelComplete" and any legacy saves work), and bestScore is the
|
||||
// player's highest final score - the level select map shows it under each
|
||||
// cleared node. A run only "counts" if it improves the best on either
|
||||
// axis, but whichever axis improved wins; both are kept at their max.
|
||||
export function markLevelComplete(levelId, kidsSaved, kidsTotal, score = null) {
|
||||
const all = readAll();
|
||||
const existing = all[levelId] || {};
|
||||
all[levelId] = {
|
||||
kidsSaved: Math.max(existing.kidsSaved || 0, kidsSaved),
|
||||
kidsTotal,
|
||||
bestScore: Math.max(existing.bestScore || 0, score || 0),
|
||||
};
|
||||
writeAll(all);
|
||||
}
|
||||
|
||||
export function getLevelResult(levelId) {
|
||||
return readAll()[levelId] || null;
|
||||
}
|
||||
|
||||
// The player's best score for a level, or null if none has been recorded
|
||||
// yet (e.g. a legacy save from before best scores existed).
|
||||
export function getBestScore(levelId) {
|
||||
const result = readAll()[levelId];
|
||||
return result && typeof result.bestScore === 'number' && result.bestScore > 0 ? result.bestScore : null;
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue