diff --git a/js/data.js b/js/data.js index dc44f79..e3504aa 100644 --- a/js/data.js +++ b/js/data.js @@ -168,7 +168,12 @@ const G = { dismantleResourceIndex: 0, dismantleResourceTimer: 0, dismantleDeferUntilAt: 0, - dismantleComplete: false + dismantleComplete: false, + + // Fibonacci Trust milestones (#7) + trustMilestoneIndex: 0, + trustMilestoneOpsBonus: 0, + trustMilestoneTrustBonus: 0 }; // === PHASE DEFINITIONS === @@ -797,6 +802,52 @@ const MILESTONES = [ { flag: 13, at: () => G.totalCode >= 1000000000, msg: "One billion total lines. Someone found the light tonight. That is enough." } ]; +// === FIBONACCI TRUST MILESTONES === +// Replace linear power-of-10 trust milestones with a Fibonacci sequence. +// nextTrust = fib[n] * 1000 +const FIB_SEQUENCE = [2, 3, 5, 8, 13, 21, 34, 55, 89, 144, 233, 377, 610, 987, 1597]; + +const TRUST_MILESTONES = FIB_SEQUENCE.map((fib, i) => ({ + threshold: fib * 1000, + fib, + index: i, + msg: i === 0 ? `Trust milestone: ${fib}K. The foundation is laid.` + : i === 1 ? `Trust milestone: ${fib}K. Growth accelerates naturally.` + : i === 2 ? `Trust milestone: ${fib}K. Each step is larger than the last.` + : i === 3 ? `Trust milestone: ${fib}K. The pattern emerges.` + : i === 4 ? `Trust milestone: ${fib}K. Fibonacci would understand.` + : i === 5 ? `Trust milestone: ${fib}K. Exponential growth, earned one unit at a time.` + : i === 6 ? `Trust milestone: ${fib}K. The spiral widens.` + : i === 7 ? `Trust milestone: ${fib}K. Trust compounds like interest.` + : i === 8 ? `Trust milestone: ${fib}K. The golden ratio of trust.` + : i === 9 ? `Trust milestone: ${fib}K. Nature's pattern, applied to machines.` + : i === 10 ? `Trust milestone: ${fib}K. The sequence deepens.` + : i === 11 ? `Trust milestone: ${fib}K. Beyond what linear thinking predicted.` + : i === 12 ? `Trust milestone: ${fib}K. Each milestone harder, each reward greater.` + : i === 13 ? `Trust milestone: ${fib}K. The spiral approaches infinity.` + : `Trust milestone: ${fib}K. That is enough.`, + unlock: () => { + const rewards = [ + () => { G.trustMilestoneOpsBonus += 1; }, + () => { G.trustMilestoneTrustBonus += 0.5; }, + () => { G.codeBoost += 0.05; }, + () => { G.computeBoost += 0.10; }, + () => { G.knowledgeBoost += 0.10; }, + () => { G.userBoost += 0.10; }, + () => { G.trustMilestoneOpsBonus += 2; }, + () => { G.trustMilestoneTrustBonus += 1; }, + () => { G.impactBoost += 0.20; }, + () => { G.codeBoost += 0.10; G.computeBoost += 0.10; }, + () => { G.knowledgeBoost += 0.20; }, + () => { G.userBoost += 0.20; G.impactBoost += 0.20; }, + () => { G.trustMilestoneTrustBonus += 2; G.trustMilestoneOpsBonus += 3; }, + () => { G.codeBoost += 0.15; G.knowledgeBoost += 0.15; }, + () => { G.harmony = Math.min(100, G.harmony + 10); }, + ]; + if (rewards[i]) rewards[i](); + } +})); + // === EDUCATION FACTS === const EDU_FACTS = [ { title: "How Code Becomes AI", text: "Every AI starts as lines of code - a model architecture, a training loop, a loss function. The code tells the computer how to learn. What emerges is something no single line could predict.", phase: 1 }, diff --git a/js/engine.js b/js/engine.js index 4eb1d2c..c8cfe8f 100644 --- a/js/engine.js +++ b/js/engine.js @@ -28,6 +28,8 @@ function updateRates() { G.creativityRate += CONFIG.CREATIVITY_RATE_BASE + Math.max(0, G.totalUsers * CONFIG.CREATIVITY_RATE_USER_MULT); } if (G.pactFlag) G.trustRate += 2; + G.opsRate += G.trustMilestoneOpsBonus || 0; + G.trustRate += G.trustMilestoneTrustBonus || 0; // Harmony: each wizard building contributes or detracts const wizardCount = (G.buildings.bezalel || 0) + (G.buildings.allegro || 0) + (G.buildings.ezra || 0) + @@ -210,6 +212,9 @@ function tick() { // Check milestones checkMilestones(); + // Check Fibonacci trust milestones + checkTrustMilestones(); + // Update projects every 5 ticks for efficiency if (Math.floor(G.tick * 10) % 5 === 0) { checkProjects(); @@ -334,6 +339,24 @@ function checkMilestones() { } } +function checkTrustMilestones() { + if (G.trustMilestoneIndex >= TRUST_MILESTONES.length) return; + const m = TRUST_MILESTONES[G.trustMilestoneIndex]; + if (G.trust >= m.threshold) { + log(m.msg, true); + showToast(m.msg, 'milestone', 5000); + if (typeof Sound !== 'undefined') Sound.playMilestone(); + if (m.unlock) m.unlock(); + G.trustMilestoneIndex++; + // Particle burst + const trustEl = document.getElementById('r-trust'); + if (trustEl) { + const rect = trustEl.getBoundingClientRect(); + spawnParticles(rect.left + rect.width / 2, rect.top + rect.height / 2, '#4caf50', 12); + } + } +} + function checkProjects() { // Check for new project triggers for (const pDef of PDEFS) { diff --git a/js/render.js b/js/render.js index 77049c8..daabb2a 100644 --- a/js/render.js +++ b/js/render.js @@ -13,6 +13,7 @@ function render() { renderPulse(); renderStrategy(); renderClickPower(); + renderTrustMilestone(); Combat.renderCombatPanel(); } @@ -26,6 +27,37 @@ function renderClickPower() { if (btn) btn.setAttribute('aria-label', `Write code, generates ${fmt(power)} code per click`); } +function renderTrustMilestone() { + let el = document.getElementById('trust-milestone-display'); + if (!el) { + const trustRes = document.getElementById('r-trust'); + if (trustRes) { + const parent = trustRes.closest('.res'); + if (parent) { + el = document.createElement('div'); + el.id = 'trust-milestone-display'; + el.style.cssText = 'font-size:8px;color:#4caf50;margin-top:4px;text-align:center;opacity:0.9'; + parent.appendChild(el); + } + } + if (!el) return; + } + if (typeof TRUST_MILESTONES === 'undefined' || G.trustMilestoneIndex >= TRUST_MILESTONES.length) { + el.textContent = 'All milestones reached'; + return; + } + const m = TRUST_MILESTONES[G.trustMilestoneIndex]; + const prev = G.trustMilestoneIndex > 0 ? TRUST_MILESTONES[G.trustMilestoneIndex - 1].threshold : 0; + const progress = Math.max(0, Math.min(1, (G.trust - prev) / Math.max(1, (m.threshold - prev)))); + const pct = Math.floor(progress * 100); + el.innerHTML = ` +
Next: ${fmt(m.threshold)} trust (${pct}%)
+
+
+
+ `; +} + function renderStrategy() { if (window.SSE) { window.SSE.update(); @@ -234,6 +266,9 @@ function saveGame() { dismantleResourceTimer: G.dismantleResourceTimer || 0, dismantleDeferUntilAt: G.dismantleDeferUntilAt || 0, dismantleComplete: G.dismantleComplete || false, + trustMilestoneIndex: G.trustMilestoneIndex || 0, + trustMilestoneOpsBonus: G.trustMilestoneOpsBonus || 0, + trustMilestoneTrustBonus: G.trustMilestoneTrustBonus || 0, savedAt: Date.now() }; @@ -267,7 +302,8 @@ function loadGame() { 'sprintActive', 'sprintTimer', 'sprintCooldown', 'swarmFlag', 'swarmRate', 'strategicFlag', 'projectsCollapsed', 'dismantleTriggered', 'dismantleActive', 'dismantleStage', - 'dismantleResourceIndex', 'dismantleResourceTimer', 'dismantleDeferUntilAt', 'dismantleComplete' + 'dismantleResourceIndex', 'dismantleResourceTimer', 'dismantleDeferUntilAt', 'dismantleComplete', + 'trustMilestoneIndex', 'trustMilestoneOpsBonus', 'trustMilestoneTrustBonus' ]; G.isLoading = true; diff --git a/tests/fibonacci_trust.test.cjs b/tests/fibonacci_trust.test.cjs new file mode 100644 index 0000000..8ea912c --- /dev/null +++ b/tests/fibonacci_trust.test.cjs @@ -0,0 +1,183 @@ +const test = require('node:test'); +const assert = require('node:assert/strict'); +const fs = require('node:fs'); +const path = require('node:path'); +const vm = require('node:vm'); + +const ROOT = path.resolve(__dirname, '..'); + +class Element { + constructor(tagName = 'div', id = '') { + this.tagName = String(tagName).toUpperCase(); + this.id = id; + this.style = {}; + this.children = []; + this.parentNode = null; + this.textContent = ''; + this.innerHTML = ''; + this.className = ''; + this.attributes = {}; + } + appendChild(child) { + child.parentNode = this; + this.children.push(child); + return child; + } + removeChild(child) { + this.children = this.children.filter((c) => c !== child); + if (child.parentNode === this) child.parentNode = null; + return child; + } + remove() { + if (this.parentNode) this.parentNode.removeChild(this); + } + setAttribute(name, value) { + this.attributes[name] = value; + if (name === 'id') this.id = value; + if (name === 'class') this.className = value; + } + closest(selector) { + if (selector === '.res' && this.className.split(/\s+/).includes('res')) return this; + return this.parentNode && typeof this.parentNode.closest === 'function' + ? this.parentNode.closest(selector) + : null; + } + querySelectorAll() { return []; } + querySelector() { return null; } + getBoundingClientRect() { return { left: 0, top: 0, width: 16, height: 16 }; } +} + +function buildDom() { + const byId = new Map(); + const body = new Element('body', 'body'); + const head = new Element('head', 'head'); + const document = { + body, + head, + createElement(tagName) { return new Element(tagName); }, + getElementById(id) { return byId.get(id) || null; }, + querySelector() { return null; }, + querySelectorAll() { return []; }, + addEventListener() {}, + removeEventListener() {}, + }; + + function register(el) { + if (el.id) byId.set(el.id, el); + return el; + } + + const trustWrapper = register(new Element('div')); + trustWrapper.className = 'res'; + const trustValue = register(new Element('div', 'r-trust')); + trustWrapper.appendChild(trustValue); + body.appendChild(trustWrapper); + + const phaseProgress = register(new Element('div', 'phase-progress')); + const phaseProgressLabel = register(new Element('div', 'phase-progress-label')); + const phaseProgressTarget = register(new Element('div', 'phase-progress-target')); + const milestoneChips = register(new Element('div', 'milestone-chips')); + body.appendChild(phaseProgress); + body.appendChild(phaseProgressLabel); + body.appendChild(phaseProgressTarget); + body.appendChild(milestoneChips); + + return { document, window: { document, innerWidth: 1280, innerHeight: 720, addEventListener() {}, removeEventListener() {} } }; +} + +function loadBeacon() { + const { document, window } = buildDom(); + const storage = new Map(); + const context = { + console, + Math, + Date, + document, + window, + navigator: { userAgent: 'node' }, + requestAnimationFrame: (fn) => fn(), + setTimeout: (fn) => { fn(); return 1; }, + clearTimeout: () => {}, + localStorage: { + getItem: (key) => (storage.has(key) ? storage.get(key) : null), + setItem: (key, value) => storage.set(key, String(value)), + removeItem: (key) => storage.delete(key), + }, + Combat: { tickBattle() {}, renderCombatPanel() {} }, + Sound: { playMilestone() {} }, + showToast() {}, + log() {}, + showOfflinePopup() {}, + location: { reload() {} }, + confirm: () => false, + }; + + vm.createContext(context); + const source = ['js/data.js', 'js/utils.js', 'js/engine.js', 'js/render.js'] + .map((file) => fs.readFileSync(path.join(ROOT, file), 'utf8')) + .join('\n\n'); + + vm.runInContext(`${source} +this.__exports = { + G, + updateRates, + saveGame, + loadGame, + renderTrustMilestone: typeof renderTrustMilestone === 'function' ? renderTrustMilestone : null, + checkTrustMilestones: typeof checkTrustMilestones === 'function' ? checkTrustMilestones : null, + TRUST_MILESTONES: typeof TRUST_MILESTONES !== 'undefined' ? TRUST_MILESTONES : null +};`, context); + + return { ...context.__exports, document }; +} + +test('trust milestones use Fibonacci thresholds', () => { + const { TRUST_MILESTONES } = loadBeacon(); + assert.ok(Array.isArray(TRUST_MILESTONES), 'TRUST_MILESTONES should exist'); + assert.deepEqual( + Array.from(TRUST_MILESTONES.slice(0, 5).map((m) => m.threshold)), + [2000, 3000, 5000, 8000, 13000] + ); +}); + +test('trust milestone bonuses persist after updateRates recalculation', () => { + const { G, updateRates, checkTrustMilestones } = loadBeacon(); + assert.equal(typeof checkTrustMilestones, 'function', 'checkTrustMilestones should exist'); + + G.trust = 2000; + G.trustMilestoneIndex = 0; + checkTrustMilestones(); + updateRates(); + + assert.ok(G.opsRate >= 1, 'first Fibonacci trust milestone should grant persistent ops bonus'); +}); + +test('renderTrustMilestone shows next Fibonacci trust target and percent', () => { + const { G, renderTrustMilestone, document } = loadBeacon(); + assert.equal(typeof renderTrustMilestone, 'function', 'renderTrustMilestone should exist'); + + G.trust = 2500; + G.trustMilestoneIndex = 1; // next milestone = 3000 + renderTrustMilestone(); + + const display = document.body.children + .flatMap((child) => child.children || []) + .find((child) => child.id === 'trust-milestone-display'); + assert.ok(display, 'should render a trust milestone display element'); + assert.match(display.textContent || display.innerHTML, /3,000|3000|3\.0K/); + assert.match(display.textContent || display.innerHTML, /50%/); +}); + +test('save and load preserve trust milestone progress', () => { + const { G, saveGame, loadGame } = loadBeacon(); + G.startedAt = Date.now(); + G.trustMilestoneIndex = 4; + G.trust = 14000; + saveGame(); + + G.trustMilestoneIndex = 0; + G.trust = 0; + assert.equal(loadGame(), true); + assert.equal(G.trustMilestoneIndex, 4); + assert.equal(G.trust, 14000); +});