diff --git a/game.js b/game.js index 94d1af7..cc9c6b9 100644 --- a/game.js +++ b/game.js @@ -106,6 +106,13 @@ const G = { drift: 0, lastEventAt: 0, eventCooldown: 0, + activeDebuffs: [], // [{id, title, desc, applyFn, resolveCost, resolveCostType}] + totalEventsResolved: 0, + + // Combo system + comboCount: 0, + comboTimer: 0, + comboDecay: 2.0, // seconds before combo resets // Time tracking playTime: 0, @@ -904,6 +911,13 @@ function updateRates() { const allegroCount = G.buildings.allegro; G.knowledgeRate -= 10 * allegroCount; // Goes idle } + + // Apply persistent debuffs from active events + if (G.activeDebuffs && G.activeDebuffs.length > 0) { + for (const debuff of G.activeDebuffs) { + if (debuff.applyFn) debuff.applyFn(); + } + } } // === CORE FUNCTIONS === @@ -922,7 +936,7 @@ function tick() { G.rescues += G.rescuesRate * dt; G.ops += G.opsRate * dt; G.trust += G.trustRate * dt; - G.creativity += G.creativityRate * dt; + // NOTE: creativity is added conditionally below (only when ops near max) G.harmony += G.harmonyRate * dt; G.harmony = Math.max(0, Math.min(100, G.harmony)); @@ -952,6 +966,15 @@ function tick() { G.tick += dt; + // Combo decay + if (G.comboCount > 0) { + G.comboTimer -= dt; + if (G.comboTimer <= 0) { + G.comboCount = 0; + G.comboTimer = 0; + } + } + // Check milestones checkMilestones(); @@ -1113,46 +1136,74 @@ const EVENTS = [ { id: 'runner_stuck', title: 'CI Runner Stuck', - desc: 'The forge pipeline has halted. Production slows until restarted.', + desc: 'The forge pipeline has halted. -50% code production until restarted.', weight: () => (G.ciFlag === 1 ? 2 : 0), + resolveCost: { resource: 'ops', amount: 50 }, effect: () => { - G.codeRate *= 0.5; - log('EVENT: CI runner stuck. Spend ops to clear the queue.', true); + if (G.activeDebuffs.find(d => d.id === 'runner_stuck')) return; + G.activeDebuffs.push({ + id: 'runner_stuck', title: 'CI Runner Stuck', + desc: 'Code production -50%', + applyFn: () => { G.codeRate *= 0.5; }, + resolveCost: { resource: 'ops', amount: 50 } + }); + log('EVENT: CI runner stuck. Spend 50 ops to clear the queue.', true); } }, { id: 'ezra_offline', title: 'Ezra is Offline', - desc: 'The herald channel is silent. User growth stalls.', + desc: 'The herald channel is silent. User growth drops 70%.', weight: () => (G.buildings.ezra >= 1 ? 3 : 0), + resolveCost: { resource: 'knowledge', amount: 200 }, effect: () => { - G.userRate *= 0.3; - log('EVENT: Ezra offline. Dispatch required.', true); + if (G.activeDebuffs.find(d => d.id === 'ezra_offline')) return; + G.activeDebuffs.push({ + id: 'ezra_offline', title: 'Ezra is Offline', + desc: 'User growth -70%', + applyFn: () => { G.userRate *= 0.3; }, + resolveCost: { resource: 'knowledge', amount: 200 } + }); + log('EVENT: Ezra offline. Spend 200 knowledge to dispatch.', true); } }, { id: 'unreviewed_merge', title: 'Unreviewed Merge', - desc: 'A change went in without eyes. Trust erodes.', + desc: 'A change went in without eyes. Trust erodes over time.', weight: () => (G.deployFlag === 1 ? 3 : 0), + resolveCost: { resource: 'trust', amount: 5 }, effect: () => { if (G.branchProtectionFlag === 1) { log('EVENT: Unreviewed merge attempt blocked by Branch Protection.', true); G.trust += 2; } else { - G.trust = Math.max(0, G.trust - 10); - log('EVENT: Unreviewed merge detected. Trust lost.', true); + if (G.activeDebuffs.find(d => d.id === 'unreviewed_merge')) return; + G.activeDebuffs.push({ + id: 'unreviewed_merge', title: 'Unreviewed Merge', + desc: 'Trust -2/s until reviewed', + applyFn: () => { G.trustRate -= 2; }, + resolveCost: { resource: 'code', amount: 500 } + }); + log('EVENT: Unreviewed merge. Spend 500 code to add review.', true); } } }, { id: 'api_rate_limit', title: 'API Rate Limit', - desc: 'External compute provider throttled.', + desc: 'External compute provider throttled. -50% compute.', weight: () => (G.totalCompute >= 1000 ? 2 : 0), + resolveCost: { resource: 'code', amount: 300 }, effect: () => { - G.computeRate *= 0.5; - log('EVENT: API rate limit hit. Local compute insufficient.', true); + if (G.activeDebuffs.find(d => d.id === 'api_rate_limit')) return; + G.activeDebuffs.push({ + id: 'api_rate_limit', title: 'API Rate Limit', + desc: 'Compute production -50%', + applyFn: () => { G.computeRate *= 0.5; }, + resolveCost: { resource: 'code', amount: 300 } + }); + log('EVENT: API rate limit. Spend 300 code to optimize local inference.', true); } }, { @@ -1160,6 +1211,7 @@ const EVENTS = [ title: 'The Drift', desc: 'An optimization suggests removing the human override. +40% efficiency.', weight: () => (G.totalImpact >= 10000 ? 2 : 0), + resolveCost: null, effect: () => { log('ALIGNMENT EVENT: Remove human override for +40% efficiency?', true); G.pendingAlignment = true; @@ -1168,11 +1220,52 @@ const EVENTS = [ { id: 'bilbo_vanished', title: 'Bilbo Vanished', - desc: 'The wildcard building has gone dark.', + desc: 'The wildcard building has gone dark. Creativity halts.', weight: () => (G.buildings.bilbo >= 1 ? 2 : 0), + resolveCost: { resource: 'trust', amount: 10 }, effect: () => { - G.creativityRate = 0; - log('EVENT: Bilbo has vanished. Creativity halts.', true); + if (G.activeDebuffs.find(d => d.id === 'bilbo_vanished')) return; + G.activeDebuffs.push({ + id: 'bilbo_vanished', title: 'Bilbo Vanished', + desc: 'Creativity production halted', + applyFn: () => { G.creativityRate = 0; }, + resolveCost: { resource: 'trust', amount: 10 } + }); + log('EVENT: Bilbo vanished. Spend 10 trust to lure them back.', true); + } + }, + { + id: 'memory_leak', + title: 'Memory Leak', + desc: 'A datacenter process is leaking. Compute drains to operations.', + weight: () => (G.buildings.datacenter >= 1 ? 1 : 0), + resolveCost: { resource: 'ops', amount: 100 }, + effect: () => { + if (G.activeDebuffs.find(d => d.id === 'memory_leak')) return; + G.activeDebuffs.push({ + id: 'memory_leak', title: 'Memory Leak', + desc: 'Compute -30%, Ops drain', + applyFn: () => { G.computeRate *= 0.7; G.opsRate -= 10; }, + resolveCost: { resource: 'ops', amount: 100 } + }); + log('EVENT: Memory leak in datacenter. Spend 100 ops to patch.', true); + } + }, + { + id: 'community_drama', + title: 'Community Drama', + desc: 'Contributors are arguing. Harmony drops until mediated.', + weight: () => (G.buildings.community >= 1 && G.harmony < 70 ? 1 : 0), + resolveCost: { resource: 'trust', amount: 15 }, + effect: () => { + if (G.activeDebuffs.find(d => d.id === 'community_drama')) return; + G.activeDebuffs.push({ + id: 'community_drama', title: 'Community Drama', + desc: 'Harmony -0.5/s, code boost -30%', + applyFn: () => { G.harmonyRate -= 0.5; G.codeBoost *= 0.7; }, + resolveCost: { resource: 'trust', amount: 15 } + }); + log('EVENT: Community drama. Spend 15 trust to mediate.', true); } } ]; @@ -1209,19 +1302,69 @@ function resolveAlignment(accept) { render(); } +function resolveEvent(debuffId) { + const idx = G.activeDebuffs.findIndex(d => d.id === debuffId); + if (idx === -1) return; + const debuff = G.activeDebuffs[idx]; + if (!debuff.resolveCost) return; + const { resource, amount } = debuff.resolveCost; + if ((G[resource] || 0) < amount) { + log(`Need ${fmt(amount)} ${resource} to resolve ${debuff.title}. Have ${fmt(G[resource])}.`); + return; + } + G[resource] -= amount; + G.activeDebuffs.splice(idx, 1); + G.totalEventsResolved = (G.totalEventsResolved || 0) + 1; + log(`Resolved: ${debuff.title}. Problem fixed.`, true); + // Refund partial trust for resolution effort + G.trust += 3; + updateRates(); + render(); +} + // === ACTIONS === function writeCode() { const base = 1; - const bonus = Math.floor(G.buildings.autocoder * 0.5); - const amount = (base + bonus) * G.codeBoost; + const autocoderBonus = Math.floor(G.buildings.autocoder * 0.5); + const phaseBonus = Math.max(0, (G.phase - 1)) * 2; + // Combo: each consecutive click within 2s adds 0.2x multiplier, max 5x + G.comboCount++; + G.comboTimer = G.comboDecay; + const comboMult = Math.min(5, 1 + G.comboCount * 0.2); + const amount = (base + autocoderBonus + phaseBonus) * G.codeBoost * comboMult; G.code += amount; G.totalCode += amount; G.totalClicks++; + // Visual flash + const btn = document.querySelector('.main-btn'); + if (btn) { + btn.style.boxShadow = '0 0 30px rgba(74,158,255,0.6)'; + btn.style.transform = 'scale(0.96)'; + setTimeout(() => { btn.style.boxShadow = ''; btn.style.transform = ''; }, 100); + } + // Float a number at the click position + showClickNumber(amount, comboMult); updateRates(); checkMilestones(); render(); } +function showClickNumber(amount, comboMult) { + const btn = document.querySelector('.main-btn'); + if (!btn) return; + const rect = btn.getBoundingClientRect(); + const el = document.createElement('div'); + el.style.cssText = `position:fixed;left:${rect.left + rect.width / 2}px;top:${rect.top - 10}px;transform:translate(-50%,0);color:${comboMult > 2 ? '#ffd700' : '#4a9eff'};font-size:${comboMult > 3 ? 16 : 12}px;font-weight:bold;font-family:inherit;pointer-events:none;z-index:50;transition:all 0.6s ease-out;opacity:1;text-shadow:0 0 8px currentColor`; + const comboStr = comboMult > 1 ? ` x${comboMult.toFixed(1)}` : ''; + el.textContent = `+${fmt(amount)}${comboStr}`; + btn.parentElement.appendChild(el); + requestAnimationFrame(() => { + el.style.top = (rect.top - 40) + 'px'; + el.style.opacity = '0'; + }); + setTimeout(() => el.remove(), 700); +} + function doOps(action) { if (G.ops < 5) { log('Not enough Operations. Build Ops generators or wait.'); @@ -1297,6 +1440,62 @@ function renderResources() { } } +// === PROGRESS TRACKING === +function renderProgress() { + // Phase progress bar + const phaseKeys = Object.keys(PHASES).map(Number).sort((a, b) => a - b); + const currentPhase = G.phase; + let prevThreshold = PHASES[currentPhase].threshold; + let nextThreshold = null; + for (const k of phaseKeys) { + if (k > currentPhase) { nextThreshold = PHASES[k].threshold; break; } + } + + const bar = document.getElementById('phase-progress'); + const label = document.getElementById('phase-progress-label'); + const target = document.getElementById('phase-progress-target'); + + if (nextThreshold !== null) { + const range = nextThreshold - prevThreshold; + const progress = Math.min(1, (G.totalCode - prevThreshold) / range); + if (bar) bar.style.width = (progress * 100).toFixed(1) + '%'; + if (label) label.textContent = (progress * 100).toFixed(1) + '%'; + if (target) target.textContent = `Next: Phase ${currentPhase + 1} (${fmt(nextThreshold)} code)`; + } else { + // Max phase reached + if (bar) bar.style.width = '100%'; + if (label) label.textContent = 'MAX'; + if (target) target.textContent = 'All phases unlocked'; + } + + // Milestone chips — show next 3 code milestones + const chipContainer = document.getElementById('milestone-chips'); + if (!chipContainer) return; + + const codeMilestones = [500, 2000, 10000, 50000, 200000, 1000000, 5000000, 10000000, 50000000, 100000000, 500000000, 1000000000]; + let chips = ''; + let shown = 0; + for (const ms of codeMilestones) { + if (G.totalCode >= ms) { + // Recently passed — show as done only if within 2x + if (G.totalCode < ms * 5 && shown < 1) { + chips += `${fmt(ms)} ✓`; + shown++; + } + continue; + } + // Next milestone gets pulse animation + if (shown === 0) { + chips += `${fmt(ms)} (${((G.totalCode / ms) * 100).toFixed(0)}%)`; + } else { + chips += `${fmt(ms)}`; + } + shown++; + if (shown >= 4) break; + } + chipContainer.innerHTML = chips; +} + function renderPhase() { const phase = PHASES[G.phase]; const nameEl = document.getElementById('phase-name'); @@ -1383,11 +1582,119 @@ function renderStats() { set('st-projects', (G.completedProjects || []).length.toString()); set('st-harmony', Math.floor(G.harmony).toString()); set('st-drift', (G.drift || 0).toString()); + set('st-resolved', (G.totalEventsResolved || 0).toString()); const elapsed = Math.floor((Date.now() - G.startedAt) / 1000); const m = Math.floor(elapsed / 60); const s = elapsed % 60; set('st-time', `${m}:${s.toString().padStart(2, '0')}`); + + // Production breakdown — show which buildings contribute to each resource + renderProductionBreakdown(); +} + +function renderProductionBreakdown() { + const container = document.getElementById('production-breakdown'); + if (!container) return; + + // Only show once the player has at least 2 buildings + const totalBuildings = Object.values(G.buildings).reduce((a, b) => a + b, 0); + if (totalBuildings < 2) { + container.style.display = 'none'; + return; + } + container.style.display = 'block'; + + // Map resource key to its actual rate field on G + const resources = [ + { key: 'code', label: 'Code', color: '#4a9eff', rateField: 'codeRate' }, + { key: 'compute', label: 'Compute', color: '#4a9eff', rateField: 'computeRate' }, + { key: 'knowledge', label: 'Knowledge', color: '#4a9eff', rateField: 'knowledgeRate' }, + { key: 'user', label: 'Users', color: '#4a9eff', rateField: 'userRate' }, + { key: 'impact', label: 'Impact', color: '#4a9eff', rateField: 'impactRate' }, + { key: 'rescues', label: 'Rescues', color: '#4a9eff', rateField: 'rescuesRate' }, + { key: 'ops', label: 'Ops', color: '#b388ff', rateField: 'opsRate' }, + { key: 'trust', label: 'Trust', color: '#4caf50', rateField: 'trustRate' }, + { key: 'creativity', label: 'Creativity', color: '#ffd700', rateField: 'creativityRate' } + ]; + + let html = '