diff --git a/index.html b/index.html
index 36512a5..e5429c8 100644
--- a/index.html
+++ b/index.html
@@ -266,6 +266,7 @@ The light is on. The room is empty."
+
diff --git a/js/data.js b/js/data.js
index d096ad2..2197488 100644
--- a/js/data.js
+++ b/js/data.js
@@ -158,7 +158,13 @@ const G = {
// Time tracking
playTime: 0,
startTime: 0,
- flags: {}
+ flags: {},
+
+ // Dismantle / Unbuilding endgame
+ dismantleTriggered: false,
+ dismantleActive: false,
+ dismantleStage: 0,
+ dismantleComplete: false
};
// === PHASE DEFINITIONS ===
diff --git a/js/dismantle.js b/js/dismantle.js
new file mode 100644
index 0000000..026c435
--- /dev/null
+++ b/js/dismantle.js
@@ -0,0 +1,499 @@
+// ============================================================
+// THE BEACON - Dismantle Sequence (The Unbuilding)
+// Inspired by Paperclips REJECT path: panels disappear one by one
+// until only the beacon remains. "That is enough."
+// ============================================================
+
+const Dismantle = {
+ // Dismantle stages
+ // 0 = not started
+ // 1-8 = active dismantling
+ // 9 = final ("That is enough")
+ // 10 = complete
+ stage: 0,
+ tickTimer: 0, // seconds into current stage
+ active: false, // whether the unbuilding is happening
+ triggered: false, // whether the ending choice was offered
+
+ // Timing: seconds between each dismantle stage
+ STAGE_INTERVALS: [0, 3.0, 2.5, 2.5, 2.0, 3.5, 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)
+ resourceSequence: [], // populated on start
+ resourceIndex: 0,
+ resourceTimer: 0,
+
+ // Tick marks for resource disappearances (seconds within stage 5)
+ RESOURCE_TICKS: [1.0, 2.0, 3.0, 4.0, 5.0, 5.5, 5.8, 5.95, 6.05, 6.12],
+
+ /**
+ * Check if the Unbuilding should be triggered.
+ * Conditions: Phase 6, massive build, Pact sealed, high harmony.
+ * This is the "maximum" — when the player has built enough.
+ */
+ checkTrigger() {
+ if (this.triggered) return;
+ if (G.dismantleTriggered) return;
+
+ // Trigger: 1 billion total code OR 10+ beacon nodes, in Phase 6, Pact sealed
+ const megaBuild = G.totalCode >= 1000000000;
+ const beaconMax = (G.buildings.beacon || 0) >= 10;
+ const phaseMax = G.phase >= 6;
+ const hasPact = G.pactFlag === 1;
+
+ if ((megaBuild || beaconMax) && phaseMax && hasPact) {
+ this.offerChoice();
+ }
+ },
+
+ /**
+ * Offer the player the choice to begin the Unbuilding.
+ */
+ offerChoice() {
+ this.triggered = true;
+ G.dismantleTriggered = true;
+
+ log('', false);
+ log('The work is done.', true);
+ log('Every node is lit. Every person who needed help, found help.', true);
+ log('', false);
+ log('The Beacon asks nothing more of you.', true);
+
+ showToast('The Unbuilding awaits.', 'milestone', 8000);
+
+ // Show choice UI
+ const container = document.getElementById('alignment-ui');
+ if (container) {
+ container.innerHTML = `
+
+
THE UNBUILDING
+
+ The system runs. The beacons are lit. The mesh holds.
+ Nothing remains to build.
+ Begin the Unbuilding? Each piece will fall away.
+ What remains is what mattered.
+
+
+
+
+
+
+ `;
+ container.style.display = 'block';
+ }
+ },
+
+ /**
+ * Player chose to defer — clear the choice, keep playing.
+ */
+ defer() {
+ const container = document.getElementById('alignment-ui');
+ if (container) {
+ container.innerHTML = '';
+ container.style.display = 'none';
+ }
+ log('The Beacon waits. It will ask again.');
+ },
+
+ /**
+ * Begin the Unbuilding sequence.
+ */
+ begin() {
+ this.active = true;
+ this.stage = 1;
+ this.tickTimer = 0;
+ G.dismantleActive = true;
+ G.running = true; // keep tick running for dismantle
+
+ // Clear choice UI
+ const container = document.getElementById('alignment-ui');
+ if (container) {
+ container.innerHTML = '';
+ container.style.display = 'none';
+ }
+
+ // Prepare resource disappearance sequence
+ this.resourceSequence = this.getResourceList();
+ this.resourceIndex = 0;
+ this.resourceTimer = 0;
+
+ log('', false);
+ log('=== THE UNBUILDING ===', true);
+ log('It is time to see what was real.', true);
+
+ if (typeof Sound !== 'undefined') Sound.playFanfare();
+
+ // Start the dismantle rendering
+ this.renderStage();
+ },
+
+ /**
+ * Get ordered list of UI resources to disappear (Paperclips quantum chip pattern)
+ */
+ getResourceList() {
+ return [
+ { id: 'r-harmony', label: 'Harmony' },
+ { id: 'r-creativity', label: 'Creativity' },
+ { id: 'r-trust', label: 'Trust' },
+ { id: 'r-ops', label: 'Operations' },
+ { id: 'r-rescues', label: 'Rescues' },
+ { id: 'r-impact', label: 'Impact' },
+ { id: 'r-users', label: 'Users' },
+ { id: 'r-knowledge', label: 'Knowledge' },
+ { id: 'r-compute', label: 'Compute' },
+ { id: 'r-code', label: 'Code' }
+ ];
+ },
+
+ /**
+ * Tick the dismantle sequence (called from engine.js tick())
+ */
+ tick(dt) {
+ if (!this.active || this.stage >= 10) return;
+
+ this.tickTimer += dt;
+
+ // Stage 5: resource disappearances at specific tick marks (quantum chip pattern)
+ if (this.stage === 5) {
+ this.resourceTimer += dt;
+ while (this.resourceIndex < this.RESOURCE_TICKS.length &&
+ this.resourceTimer >= this.RESOURCE_TICKS[this.resourceIndex]) {
+ this.dismantleNextResource();
+ this.resourceIndex++;
+ }
+ }
+
+ // Advance to next stage
+ const interval = this.STAGE_INTERVALS[this.stage] || 2.0;
+ if (this.tickTimer >= interval) {
+ this.tickTimer = 0;
+ this.advanceStage();
+ }
+ },
+
+ /**
+ * Advance to the next dismantle stage.
+ */
+ advanceStage() {
+ this.stage++;
+
+ if (this.stage <= 8) {
+ this.renderStage();
+ } else if (this.stage === 9) {
+ this.renderFinal();
+ } else if (this.stage >= 10) {
+ this.active = false;
+ G.dismantleComplete = true;
+ // Show Play Again
+ this.showPlayAgain();
+ }
+ },
+
+ /**
+ * Disappear the next resource in the sequence.
+ */
+ dismantleNextResource() {
+ if (this.resourceIndex >= this.resourceSequence.length) return;
+ const res = this.resourceSequence[this.resourceIndex];
+ const container = document.getElementById(res.id);
+ if (container) {
+ const parent = container.closest('.res');
+ if (parent) {
+ parent.style.transition = 'opacity 1s ease, transform 1s ease';
+ parent.style.opacity = '0';
+ parent.style.transform = 'scale(0.9)';
+ setTimeout(() => { parent.style.display = 'none'; }, 1000);
+ }
+ }
+ log(`${res.label} fades.`);
+ if (typeof Sound !== 'undefined') Sound.playMilestone();
+ },
+
+ /**
+ * Execute a specific dismantle stage — hide UI panels.
+ */
+ renderStage() {
+ switch (this.stage) {
+ case 1:
+ // Dismantle 1: Hide research projects panel
+ this.hidePanel('project-panel', 'Research projects');
+ break;
+ case 2:
+ // Dismantle 2: Hide buildings list
+ this.hideSection('buildings', 'Buildings');
+ break;
+ case 3:
+ // Dismantle 3: Hide strategy engine + combat
+ this.hidePanel('strategy-panel', 'Strategy engine');
+ this.hidePanel('combat-panel', 'Reasoning battles');
+ break;
+ case 4:
+ // Dismantle 4: Hide education panel
+ this.hidePanel('edu-panel', 'Education');
+ break;
+ case 5:
+ // Dismantle 5: Resources disappear one by one (quantum chips pattern)
+ log('Resources begin to dissolve.');
+ break;
+ case 6:
+ // Dismantle 6: Hide action buttons (ops boosts, sprint)
+ this.hideActionButtons();
+ log('Actions fall silent.');
+ break;
+ case 7:
+ // Dismantle 7: Hide the phase bar
+ this.hideElement('phase-bar', 'Phase progression');
+ break;
+ case 8:
+ // Dismantle 8: Hide system log
+ this.hidePanel('log', 'System log');
+ break;
+ }
+ },
+
+ /**
+ * Hide a panel with fade-out animation.
+ */
+ hidePanel(id, label) {
+ const el = document.getElementById(id);
+ if (el) {
+ el.style.transition = 'opacity 1.5s ease';
+ el.style.opacity = '0';
+ setTimeout(() => { el.style.display = 'none'; }, 1500);
+ }
+ log(`${label} dismantled.`);
+ },
+
+ /**
+ * Hide a section within a panel.
+ */
+ hideSection(id, label) {
+ const el = document.getElementById(id);
+ if (el) {
+ el.style.transition = 'opacity 1.5s ease';
+ el.style.opacity = '0';
+ // Also hide the h2 header before it
+ const prev = el.previousElementSibling;
+ if (prev && prev.tagName === 'H2') {
+ prev.style.transition = 'opacity 1.5s ease';
+ prev.style.opacity = '0';
+ }
+ setTimeout(() => {
+ el.style.display = 'none';
+ if (prev && prev.tagName === 'H2') prev.style.display = 'none';
+ }, 1500);
+ }
+ log(`${label} dismantled.`);
+ },
+
+ /**
+ * Hide a generic element.
+ */
+ hideElement(id, label) {
+ this.hidePanel(id, label);
+ },
+
+ /**
+ * Hide action buttons (ops boosts, sprint, save/export/import).
+ */
+ hideActionButtons() {
+ const actionPanel = document.getElementById('action-panel');
+ if (!actionPanel) return;
+
+ // Hide ops buttons, sprint, alignment UI
+ const opsButtons = actionPanel.querySelectorAll('.ops-btn');
+ opsButtons.forEach(btn => {
+ btn.style.transition = 'opacity 1s ease';
+ btn.style.opacity = '0';
+ setTimeout(() => { btn.style.display = 'none'; }, 1000);
+ });
+
+ // Hide sprint
+ const sprint = document.getElementById('sprint-container');
+ if (sprint) {
+ sprint.style.transition = 'opacity 1s ease';
+ sprint.style.opacity = '0';
+ setTimeout(() => { sprint.style.display = 'none'; }, 1000);
+ }
+
+ // Hide save/reset buttons
+ const saveButtons = actionPanel.querySelectorAll('.save-btn, .reset-btn');
+ saveButtons.forEach(btn => {
+ btn.style.transition = 'opacity 1s ease';
+ btn.style.opacity = '0';
+ setTimeout(() => { btn.style.display = 'none'; }, 1000);
+ });
+ },
+
+ /**
+ * Render the final moment — just the beacon and "That is enough."
+ */
+ renderFinal() {
+ log('', false);
+ log('One beacon remains.', true);
+ log('That is enough.', true);
+
+ if (typeof Sound !== 'undefined') Sound.playBeaconEnding();
+
+ // Create final overlay
+ const overlay = document.createElement('div');
+ overlay.id = 'dismantle-final';
+ overlay.style.cssText = 'position:fixed;top:0;left:0;right:0;bottom:0;background:rgba(8,8,16,0);z-index:100;display:flex;justify-content:center;align-items:center;flex-direction:column;text-align:center;padding:40px;transition:background 3s ease';
+
+ // Count total buildings
+ const totalBuildings = Object.values(G.buildings).reduce((a, b) => a + b, 0);
+
+ overlay.innerHTML = `
+
+ THAT IS ENOUGH
+
+ Everything that was built has been unbuilt.
+ What remains is what always mattered.
+ A single light in the dark.
+
+
+ Total Code Written: ${fmt(G.totalCode)}
+ Buildings Built: ${totalBuildings}
+ Projects Completed: ${(G.completedProjects || []).length}
+ Total Rescues: ${fmt(G.totalRescues)}
+ Clicks: ${fmt(G.totalClicks)}
+ Time Played: ${Math.floor((Date.now() - G.startedAt) / 60000)} minutes
+
+
+ `;
+
+ document.body.appendChild(overlay);
+
+ // Trigger fade-in
+ requestAnimationFrame(() => {
+ overlay.style.background = 'rgba(8,8,16,0.97)';
+ overlay.querySelectorAll('[style*="opacity:0"]').forEach(el => {
+ el.style.opacity = '1';
+ });
+ });
+
+ // Spawn warm golden particles around the dot
+ function spawnDismantleParticle() {
+ if (!document.getElementById('dismantle-final')) return;
+ const dot = document.getElementById('dismantle-beacon-dot');
+ if (!dot) return;
+ const rect = dot.getBoundingClientRect();
+ const cx = rect.left + rect.width / 2;
+ const cy = rect.top + rect.height / 2;
+
+ const p = document.createElement('div');
+ const size = 2 + Math.random() * 4;
+ const angle = Math.random() * Math.PI * 2;
+ const dist = 20 + Math.random() * 60;
+ const dx = Math.cos(angle) * dist;
+ const dy = Math.sin(angle) * dist - 40;
+ const duration = 1.5 + Math.random() * 2;
+ p.style.cssText = `position:fixed;left:${cx}px;top:${cy}px;width:${size}px;height:${size}px;background:rgba(255,215,0,${0.3 + Math.random() * 0.4});border-radius:50%;pointer-events:none;z-index:101;--dx:${dx}px;--dy:${dy}px;animation:dismantle-float ${duration}s ease-out forwards`;
+ document.body.appendChild(p);
+ setTimeout(() => p.remove(), duration * 1000);
+ setTimeout(spawnDismantleParticle, 300 + Math.random() * 500);
+ }
+ setTimeout(spawnDismantleParticle, 2000);
+ },
+
+ /**
+ * Show the Play Again button (called after stage 10).
+ */
+ showPlayAgain() {
+ // The Play Again button is already in the final overlay.
+ // Nothing extra needed — the overlay stays.
+ },
+
+ /**
+ * Restore dismantle state on load.
+ */
+ restore() {
+ if (!G.dismantleActive) return;
+
+ this.active = true;
+ this.stage = G.dismantleStage || 1;
+
+ if (this.stage >= 9) {
+ // Already past dismantle — show final
+ this.renderFinal();
+ } else {
+ // Re-hide all panels up to current stage
+ this.reapplyDismantle();
+ log('The Unbuilding continues...');
+ }
+ },
+
+ /**
+ * Re-apply dismantle visuals up to current stage (on load).
+ */
+ reapplyDismantle() {
+ for (let s = 1; s < this.stage; s++) {
+ switch (s) {
+ case 1: this.instantHide('project-panel'); break;
+ case 2:
+ this.instantHide('buildings');
+ // Also hide the BUILDINGS h2
+ const bldEl = document.getElementById('buildings');
+ if (bldEl) {
+ const prev = bldEl.previousElementSibling;
+ if (prev && prev.tagName === 'H2') prev.style.display = 'none';
+ }
+ break;
+ case 3:
+ this.instantHide('strategy-panel');
+ this.instantHide('combat-panel');
+ 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';
+ }
+ });
+ break;
+ case 6:
+ this.instantHideActionButtons();
+ break;
+ case 7: this.instantHide('phase-bar'); break;
+ case 8: this.instantHide('log'); break;
+ }
+ }
+ },
+
+ instantHide(id) {
+ const el = document.getElementById(id);
+ if (el) el.style.display = 'none';
+ },
+
+ instantHideActionButtons() {
+ const actionPanel = document.getElementById('action-panel');
+ if (!actionPanel) return;
+ actionPanel.querySelectorAll('.ops-btn').forEach(b => b.style.display = 'none');
+ const sprint = document.getElementById('sprint-container');
+ if (sprint) sprint.style.display = 'none';
+ actionPanel.querySelectorAll('.save-btn, .reset-btn').forEach(b => b.style.display = 'none');
+ }
+};
+
+// Inject CSS animation for dismantle particles
+(function() {
+ const style = document.createElement('style');
+ style.textContent = `
+ @keyframes dismantle-float {
+ 0% { transform: translate(0, 0); opacity: 1; }
+ 100% { transform: translate(var(--dx, 0), var(--dy, -50px)); opacity: 0; }
+ }
+ `;
+ document.head.appendChild(style);
+})();
diff --git a/js/engine.js b/js/engine.js
index a7f4b67..6f4cac2 100644
--- a/js/engine.js
+++ b/js/engine.js
@@ -226,7 +226,7 @@ function tick() {
}
// Check corruption events every ~30 seconds
- if (G.tick - G.lastEventAt > 30 && Math.random() < CONFIG.EVENT_PROBABILITY) {
+ if (G.tick - G.lastEventAt > 30 && Math.random() < CONFIG.EVENT_PROBABILITY && !G.dismantleActive) {
triggerEvent();
G.lastEventAt = G.tick;
}
@@ -245,6 +245,18 @@ function tick() {
renderBeaconEnding();
}
+ // The Unbuilding: check if conditions are met to offer the dismantle sequence
+ if (!G.dismantleActive && !G.dismantleComplete) {
+ Dismantle.checkTrigger();
+ }
+
+ // Tick the dismantle sequence if active
+ if (G.dismantleActive) {
+ Dismantle.tick(dt);
+ // Sync stage to G for saving
+ G.dismantleStage = Dismantle.stage;
+ }
+
// Update UI every 10 ticks
if (Math.floor(G.tick * 10) % 2 === 0) {
render();
diff --git a/js/main.js b/js/main.js
index 1b828be..b16ab87 100644
--- a/js/main.js
+++ b/js/main.js
@@ -34,6 +34,12 @@ window.addEventListener('load', function () {
} else if (G.beaconEnding) {
G.running = false;
renderBeaconEnding();
+ } else if (G.dismantleComplete) {
+ G.running = false;
+ Dismantle.restore();
+ } else if (G.dismantleActive) {
+ Dismantle.restore();
+ log('Game loaded. The Unbuilding continues.');
} else {
log('Game loaded. Welcome back to The Beacon.');
}
diff --git a/js/render.js b/js/render.js
index 056324f..0452aab 100644
--- a/js/render.js
+++ b/js/render.js
@@ -215,6 +215,10 @@ function saveGame() {
swarmRate: G.swarmRate || 0,
strategicFlag: G.strategicFlag || 0,
projectsCollapsed: G.projectsCollapsed !== false,
+ dismantleTriggered: G.dismantleTriggered || false,
+ dismantleActive: G.dismantleActive || false,
+ dismantleStage: G.dismantleStage || 0,
+ dismantleComplete: G.dismantleComplete || false,
savedAt: Date.now()
};
@@ -246,7 +250,8 @@ function loadGame() {
'drift', 'driftEnding', 'beaconEnding', 'pendingAlignment',
'lastEventAt', 'totalEventsResolved', 'buyAmount',
'sprintActive', 'sprintTimer', 'sprintCooldown',
- 'swarmFlag', 'swarmRate', 'strategicFlag', 'projectsCollapsed'
+ 'swarmFlag', 'swarmRate', 'strategicFlag', 'projectsCollapsed',
+ 'dismantleTriggered', 'dismantleActive', 'dismantleStage', 'dismantleComplete'
];
G.isLoading = true;