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 = '

PRODUCTION BREAKDOWN

'; + + for (const res of resources) { + const totalRate = G[res.rateField]; + if (totalRate === 0) continue; + + // Collect building contributions (base rates × count, before boost) + const contributions = []; + let buildingSubtotal = 0; + for (const def of BDEF) { + const count = G.buildings[def.id] || 0; + if (count === 0 || !def.rates || !def.rates[res.key]) continue; + const baseRate = def.rates[res.key] * count; + // Apply the appropriate boost to match updateRates() + let boosted = baseRate; + if (res.key === 'code') boosted *= G.codeBoost; + else if (res.key === 'compute') boosted *= G.computeBoost; + else if (res.key === 'knowledge') boosted *= G.knowledgeBoost; + else if (res.key === 'user') boosted *= G.userBoost; + else if (res.key === 'impact' || res.key === 'rescues') boosted *= G.impactBoost; + if (boosted !== 0) contributions.push({ name: def.name, count, rate: boosted }); + buildingSubtotal += boosted; + } + + // Timmy harmony bonus (applied separately in updateRates) + if (G.buildings.timmy > 0 && (res.key === 'code' || res.key === 'compute' || res.key === 'knowledge' || res.key === 'user')) { + const timmyMult = Math.max(0.2, Math.min(3, G.harmony / 50)); + const timmyBase = { code: 5, compute: 2, knowledge: 2, user: 5 }[res.key]; + const bonus = timmyBase * G.buildings.timmy * (timmyMult - 1); + if (Math.abs(bonus) > 0.01) { + contributions.push({ name: 'Timmy (harmony)', count: 0, rate: bonus }); + } + } + + // Bilbo random burst (show expected value) + if (G.buildings.bilbo > 0 && res.key === 'creativity') { + contributions.push({ name: 'Bilbo (random)', count: 0, rate: 5 * G.buildings.bilbo }); // 10% × 50 = 5 EV + } + + // Allegro trust penalty + if (G.buildings.allegro > 0 && G.trust < 5 && res.key === 'knowledge') { + contributions.push({ name: 'Allegro (idle)', count: 0, rate: -10 * G.buildings.allegro }); + } + + // Show delta: total rate minus what we accounted for + const accounted = contributions.reduce((s, c) => s + c.rate, 0); + const delta = totalRate - accounted; + // Passive sources (ops from users, creativity from users, pact trust, etc.) + if (Math.abs(delta) > 0.01) { + let label = 'Passive'; + if (res.key === 'ops') label = 'Passive (from users)'; + else if (res.key === 'creativity') label = 'Idle creativity'; + else if (res.key === 'trust' && G.pactFlag) label = 'The Pact'; + contributions.push({ name: label, count: 0, rate: delta }); + } + + if (contributions.length === 0) continue; + + html += `
`; + html += `
`; + html += `${res.label}`; + html += `+${fmt(totalRate)}/s
`; + + const absTotal = contributions.reduce((s, c) => s + Math.abs(c.rate), 0); + for (const c of contributions.sort((a, b) => Math.abs(b.rate) - Math.abs(a.rate))) { + const pct = absTotal > 0 ? Math.abs(c.rate / absTotal * 100) : 0; + const barColor = c.rate < 0 ? '#f44336' : '#1a3a5a'; + html += `
`; + html += `${c.name}${c.count > 1 ? ' x' + c.count : ''}`; + html += ``; + html += `${c.rate < 0 ? '' : '+'}${fmt(c.rate)}/s`; + html += `
`; + } + html += `
`; + } + + container.innerHTML = html; } function updateEducation() { @@ -1425,6 +1732,42 @@ function log(msg, isMilestone) { while (container.children.length > 60) container.removeChild(container.lastChild); } +function renderCombo() { + const el = document.getElementById('combo-display'); + if (!el) return; + if (G.comboCount > 1) { + const mult = Math.min(5, 1 + G.comboCount * 0.2); + const bar = Math.min(100, (G.comboTimer / G.comboDecay) * 100); + const color = mult > 3 ? '#ffd700' : mult > 2 ? '#ffaa00' : '#4a9eff'; + el.innerHTML = `COMBO x${mult.toFixed(1)} `; + } else { + el.innerHTML = ''; + } +} + +function renderDebuffs() { + const container = document.getElementById('debuffs'); + if (!container) return; + if (!G.activeDebuffs || G.activeDebuffs.length === 0) { + container.style.display = 'none'; + container.innerHTML = ''; + return; + } + container.style.display = 'block'; + let html = '

ACTIVE PROBLEMS

'; + for (const d of G.activeDebuffs) { + const afford = d.resolveCost && (G[d.resolveCost.resource] || 0) >= d.resolveCost.amount; + const costStr = d.resolveCost ? `${fmt(d.resolveCost.amount)} ${d.resolveCost.resource}` : '—'; + html += `
`; + html += `
${d.title}
${d.desc}
`; + if (d.resolveCost) { + html += ``; + } + html += '
'; + } + container.innerHTML = html; +} + function render() { renderResources(); renderPhase(); @@ -1433,6 +1776,9 @@ function render() { renderStats(); updateEducation(); renderAlignment(); + renderProgress(); + renderCombo(); + renderDebuffs(); } function renderAlignment() { @@ -1472,6 +1818,8 @@ function showSaveToast() { } function saveGame() { + // Save debuff IDs (can't serialize functions) + const debuffIds = (G.activeDebuffs || []).map(d => d.id); const saveData = { code: G.code, compute: G.compute, knowledge: G.knowledge, users: G.users, impact: G.impact, ops: G.ops, trust: G.trust, creativity: G.creativity, harmony: G.harmony, @@ -1492,6 +1840,8 @@ function saveGame() { rescues: G.rescues || 0, totalRescues: G.totalRescues || 0, drift: G.drift || 0, driftEnding: G.driftEnding || false, beaconEnding: G.beaconEnding || false, pendingAlignment: G.pendingAlignment || false, lastEventAt: G.lastEventAt || 0, + activeDebuffIds: debuffIds, + totalEventsResolved: G.totalEventsResolved || 0, savedAt: Date.now() }; @@ -1507,6 +1857,20 @@ function loadGame() { const data = JSON.parse(raw); Object.assign(G, data); + // Reconstitute active debuffs from saved IDs (functions can't be JSON-parsed) + if (data.activeDebuffIds && data.activeDebuffIds.length > 0) { + G.activeDebuffs = []; + for (const id of data.activeDebuffIds) { + const evDef = EVENTS.find(e => e.id === id); + if (evDef) { + // Re-fire the event to get the full debuff object with applyFn + evDef.effect(); + } + } + } else { + G.activeDebuffs = []; + } + updateRates(); // Offline progress @@ -1521,12 +1885,28 @@ function loadGame() { const uc = G.userRate * offSec * f; const ic = G.impactRate * offSec * f; + const rc = G.rescuesRate * offSec * f; + const oc = G.opsRate * offSec * f; + const tc = G.trustRate * offSec * f; + const crc = G.creativityRate * offSec * f; + const hc = G.harmonyRate * offSec * f; + G.code += gc; G.compute += cc; G.knowledge += kc; G.users += uc; G.impact += ic; + G.rescues += rc; G.ops += oc; G.trust += tc; + G.creativity += crc; + G.harmony = Math.max(0, Math.min(100, G.harmony + hc)); G.totalCode += gc; G.totalCompute += cc; G.totalKnowledge += kc; G.totalUsers += uc; G.totalImpact += ic; + G.totalRescues += rc; - log(`Welcome back! While away (${Math.floor(offSec / 60)}m): ${fmt(gc)} code, ${fmt(kc)} knowledge, ${fmt(uc)} users`); + const parts = []; + if (gc > 0) parts.push(`${fmt(gc)} code`); + if (kc > 0) parts.push(`${fmt(kc)} knowledge`); + if (uc > 0) parts.push(`${fmt(uc)} users`); + if (ic > 0) parts.push(`${fmt(ic)} impact`); + if (rc > 0) parts.push(`${fmt(rc)} rescues`); + log(`Welcome back! While away (${Math.floor(offSec / 60)}m): ${parts.join(', ')}`); } } @@ -1553,6 +1933,7 @@ function initGame() { log('Click WRITE CODE or press SPACE to start.'); log('Build AutoCode for passive production.'); log('Watch for Research Projects to appear.'); + log('Keys: SPACE=Code 1=Ops->Code 2=Ops->Compute 3=Ops->Knowledge 4=Ops->Trust'); } window.addEventListener('load', function () { @@ -1588,4 +1969,9 @@ window.addEventListener('keydown', function (e) { e.preventDefault(); writeCode(); } + if (e.target !== document.body) return; + if (e.code === 'Digit1') doOps('boost_code'); + if (e.code === 'Digit2') doOps('boost_compute'); + if (e.code === 'Digit3') doOps('boost_knowledge'); + if (e.code === 'Digit4') doOps('boost_trust'); }); diff --git a/index.html b/index.html index 6f6a2e8..ac72747 100644 --- a/index.html +++ b/index.html @@ -14,6 +14,14 @@ body{background:var(--bg);color:var(--text);font-family:'SF Mono','Cascadia Code #phase-bar{text-align:center;padding:10px;margin:12px 16px;background:var(--panel);border:1px solid var(--border);border-radius:6px} #phase-bar .phase-name{font-size:14px;font-weight:700;color:var(--gold);letter-spacing:2px} #phase-bar .phase-desc{font-size:10px;color:var(--dim);margin-top:4px;font-style:italic} +.progress-wrap{margin-top:8px;height:6px;background:#111;border-radius:3px;overflow:hidden;position:relative} +.progress-fill{height:100%;border-radius:3px;transition:width 0.5s ease;background:linear-gradient(90deg,#1a3a5a,var(--accent))} +.progress-label{font-size:9px;color:var(--dim);margin-top:4px;display:flex;justify-content:space-between} +.milestone-row{display:flex;gap:6px;margin-top:6px;justify-content:center;flex-wrap:wrap} +.milestone-chip{font-size:9px;padding:2px 8px;border-radius:10px;border:1px solid var(--border);color:var(--dim);background:#0a0a14} +.milestone-chip.next{border-color:var(--accent);color:var(--accent);animation:pulse-chip 2s ease-in-out infinite} +.milestone-chip.done{border-color:#2a4a2a;color:var(--green);opacity:0.6} +@keyframes pulse-chip{0%,100%{box-shadow:0 0 0 rgba(74,158,255,0)}50%{box-shadow:0 0 8px rgba(74,158,255,0.3)}} #resources{display:grid;grid-template-columns:repeat(auto-fit,minmax(100px,1fr));gap:6px;margin:12px 16px} .res{background:var(--panel);border:1px solid var(--border);border-radius:4px;padding:8px 10px;text-align:center} .res .r-label{font-size:9px;color:var(--dim);text-transform:uppercase;letter-spacing:1px} @@ -69,6 +77,9 @@ body{background:var(--bg);color:var(--text);font-family:'SF Mono','Cascadia Code
PHASE 1: THE FIRST LINE
Write code. Automate. Build the foundation.
+
+
0%Next: Phase 2 (2,000 code)
+
Code
0
+0/s
@@ -86,6 +97,8 @@ body{background:var(--bg);color:var(--text);font-family:'SF Mono','Cascadia Code

ACTIONS

+
+
@@ -116,8 +129,10 @@ Projects Done: 0
Time Played: 0:00
Clicks: 0
Harmony: 50
-Drift: 0 +Drift: 0
+Events Resolved: 0
+