Fix bounty-hunt tracker so READY repaints when complete flips after all

- Include the quest's complete bit in the tracker row fingerprint so a later frame that only flips `complete` (with no check change) still triggers a repaint to READY.
- Update bounty-hunt and quest-tracker tests to pin the ordering: all legs checked while contract is still out, then COMPLETE landing on a subsequent frame with checks unchanged.
- Add station surface music tracks for spacestation variants 0 and 1 in data/music.json.
This commit is contained in:
Brian Fertig 2026-09-20 20:25:06 -06:00
parent 5cf2152359
commit 93a844f07b
4 changed files with 51 additions and 6 deletions

View File

@ -33,5 +33,8 @@
"18": "assets/music/capital-neutral-01.mp3"
},
"_stationFrames_comment": "stationFrames = the loop that plays on a deep-space station's surface — keyed by the spacestations.png VARIANT frame (the same key its landing/takeoff clips use, data/landing.json → stationVideos), so a station hums the track matching the art it is drawn with. Empty for now (a station surface is silent) until the station tracks are authored — add one line per variant when they land.",
"stationFrames": {}
"stationFrames": {
"0": "assets/music/spacestation-01.mp3",
"1": "assets/music/spacestation-02.mp3"
}
}

View File

@ -69,6 +69,7 @@ const SQ = configData.sidequests; // data/sidequests.json (categories / bounty /
const S = await import(pathToFileURL(join(root, 'js/quests/SideQuests.js')).href);
const { SideQuestState } = S;
const { GameScene } = await import(pathToFileURL(join(root, 'js/scenes/GameScene.js')).href);
const { QuestState } = await import(pathToFileURL(join(root, 'js/quests/QuestState.js')).href);
let failures = 0;
const check = (label, cond, extra = '') => {
@ -356,6 +357,7 @@ section('THE FLYING — accept → legs → mark → kill → claim (the real sc
s.planet = null;
s.tetherField = { tethers: [{ x: 0, y: 0, radius: 12000 }, { x: -400, y: 0, radius: 9000 }, { x: 400, y: 0, radius: 9000 }] };
s.sideQuestState = new SideQuestState();
s.questState = new QuestState(); // no story grants — the tracker's side row stands alone
s.sfx = [];
s.playSfx = (n) => { s.sfx.push(n); };
s.toasts = [];
@ -499,9 +501,25 @@ section('THE FLYING — accept → legs → mark → kill → claim (the real sc
at(plan.bountyAt); // stand in the lane so the field is read here
s.updateSideQuestRoute();
check('survivors keep it OUT of complete (the escort must stop shooting)', s.sideQuestState.isCompleted(id) === false);
// The tracker's view of the same frames (the upper-left quest summary):
// the kill has ALREADY checked every leg off — 100% painted — while the
// contract is still out, and the COMPLETE mark lands a frame LATER with
// no check change (the tracker's fingerprint must carry the bit —
// dev/quest-tracker.test.mjs pins the repaint; this pins the ORDER).
const snapRow = () => s.questSnapshot().quests.find((q) => q.id === id);
const rowOut = snapRow();
check('the summary shows every leg checked while the contract is still out',
rowOut.checks.length > 0 && rowOut.checks.every((c) => c.done) && rowOut.complete === false && rowOut.claimed === false,
JSON.stringify(rowOut));
for (const e of escort) e.dead = true;
s.combat.tickWaves(); // the last survivor drops — the run reads 'done'
s.updateSideQuestRoute();
const rowReady = snapRow();
check('the COMPLETE mark lands with the legs STILL checked (no check change)',
rowReady.complete === true && rowReady.claimed === false &&
JSON.stringify(rowReady.checks.map((c) => c.done)) === JSON.stringify(rowOut.checks.map((c) => c.done)) &&
rowReady.checks.every((c) => c.done),
JSON.stringify(rowReady));
check('with the last escort down the contract is COMPLETE', s.sideQuestState.isCompleted(id) === true, JSON.stringify(s.toasts.at(-1) ?? {}));
const completeToast = s.toasts.filter((t) => /COMPLETE/.test(t.text)).length;
check('the completion toasts once', completeToast === 1);

View File

@ -905,11 +905,24 @@ const T0 = 100000;
!scene.add.texts.some((t) => t.text === 'READY') &&
rowTints() === 0 && trk._shimmers.length === 0);
// The quest COMPLETES (a check flips) — the same-frame repaint goes READY.
snap.quests[0].checks[0].done = true;
snap.quests[0].complete = true;
snap.quests[0].progress = '1/1';
// The BOUNTY HUNT's ORDER (the tracker's one repaint trap): a quest's
// checks can all stand DONE before its COMPLETE mark lands — the hunt's
// kill logs every leg at once (100% painted, still open), and the
// field-clear completes it on a LATER frame with NO check flipping.
// The first stage must paint plain (all checks done ≠ complete) …
tweens.length = 0; // (the constructor's first poll is not the moment)
snap.quests[0].checks[0].done = true;
snap.quests[0].progress = '1/1';
trk.update(scene.time.now);
check('all checks done but NOT complete: the row stays plain (100%, no READY, no rim)',
scene.add.texts.some((t) => t.text === '100%') &&
!scene.add.texts.some((t) => t.text === 'READY') &&
rowTints() === 0 && trk._shimmers.length === 0);
// … and NOW ONLY the complete bit flips (the checks already painted) —
// the fingerprint carries the bit, so the same frame still repaints READY.
tweens.length = 0;
snap.quests[0].complete = true;
trk.update(scene.time.now);
check('READY row: the % readout flips to READY (amber, bold)',
scene.add.texts.some((t) => t.text === 'READY' && t.color === '#ffc94d' && t.style?.fontStyle === 'bold'));

View File

@ -32,7 +32,10 @@
* scene's toast + 'discovery' chime carry it when the tracker is slid
* off), and the row's popup gains a CLAIM REWARD button (the console's
* claim-plate palette) that claims in place (GameScene.claimQuest — the
* console's own path).
* console's own path). The fingerprint carries each quest's COMPLETE bit
* as well as its checks: a BOUNTY HUNT's mark lands on a frame AFTER its
* last check has painted (the kill logs every leg at once, the field-
* clear completes it) — that flip must still repaint the row READY.
*
* Lifecycle (driven by GameScene — see GameScene.updateHud /
* startExpand): the tracker is VISIBLE ⇔ the dossier is FOLDED, armed by
@ -318,7 +321,15 @@ export class QuestTrackerHud {
a: ids,
c: active.map((q) => {
const checks = q.checks ?? [];
// The quest's OWN complete bit LEADS its entry — a BOUNTY HUNT's
// ledger mark lands AFTER its last check flips (the kill logs every
// leg at once — 100% painted, still open — and the field-clear
// completes it with NO check change behind it). Without the bit
// here that frame repaints nothing and the READY state — the
// CLAIM's door — never lands (QuestWindow._fpOf carries it; the
// tracker must match).
return (
(q.complete === true ? 1 : 0) +
checks.map((c) => (c?.done ? 1 : 0)).join('') +
'|' +
checks.map((c) => String(c?.detail ?? '')).join(',').slice(0, 40)