From 93a844f07ba63ee10181bf585e64671d87095bec Mon Sep 17 00:00:00 2001 From: Brian Fertig Date: Sun, 20 Sep 2026 20:25:06 -0600 Subject: [PATCH] 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. --- data/music.json | 5 ++++- dev/bounty-hunts.test.mjs | 18 ++++++++++++++++++ dev/quest-tracker.test.mjs | 21 +++++++++++++++++---- js/ui/QuestTrackerHud.js | 13 ++++++++++++- 4 files changed, 51 insertions(+), 6 deletions(-) diff --git a/data/music.json b/data/music.json index ffbfe69..ccae2ea 100644 --- a/data/music.json +++ b/data/music.json @@ -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" + } } diff --git a/dev/bounty-hunts.test.mjs b/dev/bounty-hunts.test.mjs index 3ac42df..6bded20 100644 --- a/dev/bounty-hunts.test.mjs +++ b/dev/bounty-hunts.test.mjs @@ -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); diff --git a/dev/quest-tracker.test.mjs b/dev/quest-tracker.test.mjs index 5c27621..659ccae 100644 --- a/dev/quest-tracker.test.mjs +++ b/dev/quest-tracker.test.mjs @@ -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')); diff --git a/js/ui/QuestTrackerHud.js b/js/ui/QuestTrackerHud.js index fd50aa8..c15d816 100644 --- a/js/ui/QuestTrackerHud.js +++ b/js/ui/QuestTrackerHud.js @@ -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)