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;