Compare commits

..

1 Commits

Author SHA1 Message Date
Alexander Whitestone
825ae81927 feat: implement Dismantle Sequence — The Unbuilding (#16) + fix #133
Some checks failed
Accessibility Checks / a11y-audit (pull_request) Successful in 11s
Smoke Test / smoke (pull_request) Failing after 19s
The Unbuilding is already implemented (dismantle.js, 571 lines). This
commit fixes two bugs discovered during endgame testing (#133):

Fix 1: Stage 5 interval extended from 6.3s to 7.0s
- Last RESOURCE_TICK fires at 6.12s, only 0.18s margin
- Floating-point timing could skip last 1-2 resource dissolves

Fix 2: Stage advancement gated on resource completion
- tick() now waits for resourceIndex >= RESOURCE_TICKS.length
- Stage 6 won't start until all 10 resources have dissolved

Fix 3: reapplyDismantle only hides dissolved resources on reload
- Previously hid ALL resources on reload mid-stage-5
- Now only hides slice(0, resourceIndex) — already dissolved ones
- Remaining resources stay visible and continue dissolving

Dismantle sequence: 8 stages + final overlay.
Panels disappear one by one. Resources dissolve quantum-chip style.
Final moment: one beacon remains. 'That is enough.'

Closes #16, Closes #133
2026-04-14 22:03:15 -04:00
3 changed files with 16 additions and 39 deletions

View File

@@ -17,7 +17,8 @@ const Dismantle = {
deferUntilAt: 0,
// Timing: seconds between each dismantle stage
STAGE_INTERVALS: [0, 3.0, 2.5, 2.5, 2.0, 6.3, 2.0, 2.0, 2.5],
// Stage 5 must be long enough for all RESOURCE_TICKS (last tick at 6.12s)
STAGE_INTERVALS: [0, 3.0, 2.5, 2.5, 2.0, 7.0, 2.0, 2.0, 2.5],
// The quantum chips effect: resource items disappear one by one
// at specific tick marks within a stage (like Paperclips' quantum chips)
@@ -192,8 +193,10 @@ const Dismantle = {
}
// Advance to next stage
// At stage 5, don't advance until all resources have dissolved
const interval = this.STAGE_INTERVALS[this.stage] || 2.0;
if (this.tickTimer >= interval) {
const resourcesDone = this.stage !== 5 || this.resourceIndex >= this.RESOURCE_TICKS.length;
if (this.tickTimer >= interval && resourcesDone) {
this.tickTimer = 0;
this.advanceStage();
}
@@ -510,14 +513,17 @@ const Dismantle = {
break;
case 4: this.instantHide('edu-panel'); break;
case 5:
// Hide all resource displays
this.getResourceList().forEach(r => {
const el = document.getElementById(r.id);
if (el) {
const parent = el.closest('.res');
if (parent) parent.style.display = 'none';
}
});
// On reload during stage 5, only hide resources already dissolved.
// Remaining resources stay visible and will continue dissolving via tick().
if (this.resourceIndex > 0) {
this.getResourceList().slice(0, this.resourceIndex).forEach(r => {
const el = document.getElementById(r.id);
if (el) {
const parent = el.closest('.res');
if (parent) parent.style.display = 'none';
}
});
}
break;
case 6:
this.instantHideActionButtons();

View File

@@ -422,11 +422,6 @@ function buyProject(id) {
if (!G.completedProjects) G.completedProjects = [];
G.completedProjects.push(pDef.id);
G.activeProjects = G.activeProjects.filter(aid => aid !== pDef.id);
// Final ReCKoning choices should end with no unrelated active research left behind.
if (pDef.id === 'p_reckoning_147' || pDef.id === 'p_reckoning_148') {
G.activeProjects = [];
}
}
updateRates();

View File

@@ -208,8 +208,6 @@ showSaveToast = () => {};
this.__exports = {
G,
Dismantle,
PDEFS: typeof PDEFS !== 'undefined' ? PDEFS : null,
buyProject: typeof buyProject === 'function' ? buyProject : null,
tick,
renderAlignment: typeof renderAlignment === 'function' ? renderAlignment : null,
saveGame: typeof saveGame === 'function' ? saveGame : null,
@@ -413,28 +411,6 @@ test('restore re-renders an offered but not-yet-started Unbuilding prompt', () =
assert.match(document.getElementById('alignment-ui').innerHTML, /THE UNBUILDING/);
});
test('completing the final ReCKoning choice clears unrelated active projects', () => {
const { G, PDEFS, buyProject } = loadBeacon();
G.beaconEnding = true;
G.activeProjects = ['p_wire_budget', 'p_reckoning_148'];
G.completedProjects = [];
G.trust = 10;
PDEFS.push({
id: 'p_reckoning_148',
name: 'Rest',
desc: 'Final ReCKoning choice',
cost: {},
trigger: () => false,
effect: () => {},
});
buyProject('p_reckoning_148');
assert.deepEqual(Array.from(G.activeProjects), []);
});
test('defer cooldown persists after save/load when dismantleTriggered is false', () => {
const { G, Dismantle, saveGame, loadGame } = loadBeacon({ includeRender: true });