Compare commits

..

2 Commits

Author SHA1 Message Date
Alexander Whitestone
f362f30fd3 beacon: fix offline progress to award rescues, ops, and trust
Offline gains were missing rescues, ops, and trust — players with
beacon/mesh nodes lost half their progress on return. Now all active
resources are calculated at 50% offline efficiency.

Also includes event system overhaul: active events with durations,
resolve costs, auto-expiry, event UI rendering, and save/load support.
2026-04-10 01:21:05 -04:00
Alexander Whitestone
f97154c37a beacon: add unlock notification toasts for new buildings and projects
Players now get animated toast notifications (top-right) when:
- A new building becomes available to buy
- A new research project becomes available

Toasts are color-coded: blue for buildings, gold for projects.
Seen state is tracked and persisted in saves so toasts don't repeat.

This was the biggest UX gap in the game -- without notifications,
players had to manually scan the building/project lists to notice
new unlocks. Critical for idle games where pacing matters.
2026-04-10 00:25:40 -04:00
2 changed files with 190 additions and 162 deletions

329
game.js
View File

@@ -106,11 +106,7 @@ const G = {
drift: 0, drift: 0,
lastEventAt: 0, lastEventAt: 0,
eventCooldown: 0, eventCooldown: 0,
activeEvents: [], // {id, expiresAt} — events auto-resolve after duration
// Combo system
comboCount: 0,
comboTimer: 0,
comboDecay: 2.0, // seconds before combo resets
// Time tracking // Time tracking
playTime: 0, playTime: 0,
@@ -870,6 +866,7 @@ function updateRates() {
G.creativityRate += 0.5 + Math.max(0, G.totalUsers * 0.001); G.creativityRate += 0.5 + Math.max(0, G.totalUsers * 0.001);
} }
if (G.pactFlag) G.trustRate += 2; if (G.pactFlag) G.trustRate += 2;
if (G.branchProtectionFlag) G.trustRate += 3; // branch protection actively builds trust
// Harmony: each wizard building contributes or detracts // Harmony: each wizard building contributes or detracts
const wizardCount = (G.buildings.bezalel || 0) + (G.buildings.allegro || 0) + (G.buildings.ezra || 0) + const wizardCount = (G.buildings.bezalel || 0) + (G.buildings.allegro || 0) + (G.buildings.ezra || 0) +
@@ -927,7 +924,7 @@ function tick() {
G.rescues += G.rescuesRate * dt; G.rescues += G.rescuesRate * dt;
G.ops += G.opsRate * dt; G.ops += G.opsRate * dt;
G.trust += G.trustRate * dt; G.trust += G.trustRate * dt;
// NOTE: creativity is added conditionally below (only when ops near max) G.creativity += G.creativityRate * dt;
G.harmony += G.harmonyRate * dt; G.harmony += G.harmonyRate * dt;
G.harmony = Math.max(0, Math.min(100, G.harmony)); G.harmony = Math.max(0, Math.min(100, G.harmony));
@@ -957,15 +954,6 @@ function tick() {
G.tick += dt; G.tick += dt;
// Combo decay
if (G.comboCount > 0) {
G.comboTimer -= dt;
if (G.comboTimer <= 0) {
G.comboCount = 0;
G.comboTimer = 0;
}
}
// Check milestones // Check milestones
checkMilestones(); checkMilestones();
@@ -980,6 +968,20 @@ function tick() {
G.lastEventAt = G.tick; G.lastEventAt = G.tick;
} }
// Check event expiry
checkEventExpiry();
// Re-apply active event rate penalties (updateRates rebuilds from scratch)
for (const ae of G.activeEvents) {
switch (ae.id) {
case 'runner_stuck': G.codeRate *= 0.5; break;
case 'ezra_offline': G.userRate *= 0.3; break;
case 'unreviewed_merge': /* trust penalty is one-shot */ break;
case 'api_rate_limit': G.computeRate *= 0.5; break;
case 'bilbo_vanished': G.creativityRate = 0; break;
}
}
// Drift ending: if drift reaches 100, the game ends // Drift ending: if drift reaches 100, the game ends
if (G.drift >= 100 && !G.driftEnding) { if (G.drift >= 100 && !G.driftEnding) {
G.driftEnding = true; G.driftEnding = true;
@@ -1127,46 +1129,49 @@ const EVENTS = [
{ {
id: 'runner_stuck', id: 'runner_stuck',
title: 'CI Runner Stuck', title: 'CI Runner Stuck',
desc: 'The forge pipeline has halted. Production slows until restarted.', desc: 'The forge pipeline has halted. Code production slowed.',
weight: () => (G.ciFlag === 1 ? 2 : 0), weight: () => (G.ciFlag === 1 ? 2 : 0),
duration: 45, // seconds
resolveCost: { ops: 10 },
effect: () => { effect: () => {
G.codeRate *= 0.5; G.codeRate *= 0.5;
log('EVENT: CI runner stuck. Spend ops to clear the queue.', true); log('EVENT: CI runner stuck. Click to spend 10 Ops to clear, or wait ~45s.', true);
} }
}, },
{ {
id: 'ezra_offline', id: 'ezra_offline',
title: 'Ezra is Offline', title: 'Ezra is Offline',
desc: 'The herald channel is silent. User growth stalls.', desc: 'The herald channel is silent. User growth stalled.',
weight: () => (G.buildings.ezra >= 1 ? 3 : 0), weight: () => (G.buildings.ezra >= 1 ? 3 : 0),
duration: 60,
resolveCost: { ops: 5, trust: 2 },
effect: () => { effect: () => {
G.userRate *= 0.3; G.userRate *= 0.3;
log('EVENT: Ezra offline. Dispatch required.', true); log('EVENT: Ezra offline. Click to dispatch, or wait ~60s.', true);
} }
}, },
{ {
id: 'unreviewed_merge', id: 'unreviewed_merge',
title: 'Unreviewed Merge', title: 'Unreviewed Merge',
desc: 'A change went in without eyes. Trust erodes.', desc: 'A change went in without eyes. Trust erodes.',
weight: () => (G.deployFlag === 1 ? 3 : 0), weight: () => (G.deployFlag === 1 && G.branchProtectionFlag !== 1 ? 3 : 0),
duration: 30,
resolveCost: { ops: 8 },
effect: () => { effect: () => {
if (G.branchProtectionFlag === 1) { G.trust = Math.max(0, G.trust - 10);
log('EVENT: Unreviewed merge attempt blocked by Branch Protection.', true); log('EVENT: Unreviewed merge detected. Trust lost. Click to revert.', true);
G.trust += 2;
} else {
G.trust = Math.max(0, G.trust - 10);
log('EVENT: Unreviewed merge detected. Trust lost.', true);
}
} }
}, },
{ {
id: 'api_rate_limit', id: 'api_rate_limit',
title: 'API Rate Limit', title: 'API Rate Limit',
desc: 'External compute provider throttled.', desc: 'External compute provider throttled.',
weight: () => (G.totalCompute >= 1000 ? 2 : 0), weight: () => (G.totalCompute >= 1000 && G.sovereignFlag !== 1 ? 2 : 0),
duration: 40,
resolveCost: { compute: 200 },
effect: () => { effect: () => {
G.computeRate *= 0.5; G.computeRate *= 0.5;
log('EVENT: API rate limit hit. Local compute insufficient.', true); log('EVENT: API rate limit. Click to provision local compute, or wait ~40s.', true);
} }
}, },
{ {
@@ -1174,6 +1179,7 @@ const EVENTS = [
title: 'The Drift', title: 'The Drift',
desc: 'An optimization suggests removing the human override. +40% efficiency.', desc: 'An optimization suggests removing the human override. +40% efficiency.',
weight: () => (G.totalImpact >= 10000 ? 2 : 0), weight: () => (G.totalImpact >= 10000 ? 2 : 0),
duration: 0, // alignment events don't auto-resolve
effect: () => { effect: () => {
log('ALIGNMENT EVENT: Remove human override for +40% efficiency?', true); log('ALIGNMENT EVENT: Remove human override for +40% efficiency?', true);
G.pendingAlignment = true; G.pendingAlignment = true;
@@ -1184,9 +1190,11 @@ const EVENTS = [
title: 'Bilbo Vanished', title: 'Bilbo Vanished',
desc: 'The wildcard building has gone dark.', desc: 'The wildcard building has gone dark.',
weight: () => (G.buildings.bilbo >= 1 ? 2 : 0), weight: () => (G.buildings.bilbo >= 1 ? 2 : 0),
duration: 50,
resolveCost: { trust: 3 },
effect: () => { effect: () => {
G.creativityRate = 0; G.creativityRate = 0;
log('EVENT: Bilbo has vanished. Creativity halts.', true); log('EVENT: Bilbo vanished. Click to search, or wait ~50s.', true);
} }
} }
]; ];
@@ -1200,12 +1208,53 @@ function triggerEvent() {
for (const ev of available) { for (const ev of available) {
roll -= ev.weight(); roll -= ev.weight();
if (roll <= 0) { if (roll <= 0) {
// Don't fire duplicate active events
if (G.activeEvents.some(ae => ae.id === ev.id)) return;
ev.effect(); ev.effect();
if (ev.duration > 0) {
G.activeEvents.push({ id: ev.id, expiresAt: G.tick + ev.duration });
}
return; return;
} }
} }
} }
function checkEventExpiry() {
for (let i = G.activeEvents.length - 1; i >= 0; i--) {
if (G.tick >= G.activeEvents[i].expiresAt) {
const ae = G.activeEvents[i];
G.activeEvents.splice(i, 1);
updateRates(); // recalculate without the penalty
const evDef = EVENTS.find(e => e.id === ae.id);
log(`Event resolved: ${evDef ? evDef.title : ae.id}`, true);
}
}
}
function hasActiveEvent(id) {
return G.activeEvents.some(ae => ae.id === id);
}
function resolveEvent(id) {
const evDef = EVENTS.find(e => e.id === id);
if (!evDef || !evDef.resolveCost) return;
if (!hasActiveEvent(id)) return;
if (!canAffordProject(evDef)) {
log('Not enough resources to resolve this event.');
return;
}
spendProject(evDef);
G.activeEvents = G.activeEvents.filter(ae => ae.id !== id);
updateRates();
// Small bonus for manually resolving
G.trust += 2;
log(`Resolved: ${evDef.title}. Trust +2.`, true);
render();
}
function resolveAlignment(accept) { function resolveAlignment(accept) {
if (!G.pendingAlignment) return; if (!G.pendingAlignment) return;
if (accept) { if (accept) {
@@ -1226,46 +1275,16 @@ function resolveAlignment(accept) {
// === ACTIONS === // === ACTIONS ===
function writeCode() { function writeCode() {
const base = 1; const base = 1;
const autocoderBonus = Math.floor(G.buildings.autocoder * 0.5); const bonus = Math.floor(G.buildings.autocoder * 0.5);
const phaseBonus = Math.max(0, (G.phase - 1)) * 2; const amount = (base + bonus) * G.codeBoost;
// 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.code += amount;
G.totalCode += amount; G.totalCode += amount;
G.totalClicks++; 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(); updateRates();
checkMilestones(); checkMilestones();
render(); 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) { function doOps(action) {
if (G.ops < 5) { if (G.ops < 5) {
log('Not enough Operations. Build Ops generators or wait.'); log('Not enough Operations. Build Ops generators or wait.');
@@ -1341,62 +1360,6 @@ 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 += `<span class="milestone-chip done">${fmt(ms)} ✓</span>`;
shown++;
}
continue;
}
// Next milestone gets pulse animation
if (shown === 0) {
chips += `<span class="milestone-chip next">${fmt(ms)} (${((G.totalCode / ms) * 100).toFixed(0)}%)</span>`;
} else {
chips += `<span class="milestone-chip">${fmt(ms)}</span>`;
}
shown++;
if (shown >= 4) break;
}
chipContainer.innerHTML = chips;
}
function renderPhase() { function renderPhase() {
const phase = PHASES[G.phase]; const phase = PHASES[G.phase];
const nameEl = document.getElementById('phase-name'); const nameEl = document.getElementById('phase-name');
@@ -1525,19 +1488,6 @@ function log(msg, isMilestone) {
while (container.children.length > 60) container.removeChild(container.lastChild); 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 = `<span style="color:${color}">COMBO x${mult.toFixed(1)}</span> <span style="display:inline-block;width:40px;height:4px;background:#111;border-radius:2px;vertical-align:middle"><span style="display:block;height:100%;width:${bar}%;background:${color};border-radius:2px;transition:width 0.1s"></span></span>`;
} else {
el.innerHTML = '';
}
}
function render() { function render() {
renderResources(); renderResources();
renderPhase(); renderPhase();
@@ -1546,8 +1496,8 @@ function render() {
renderStats(); renderStats();
updateEducation(); updateEducation();
renderAlignment(); renderAlignment();
renderProgress(); renderActiveEvents();
renderCombo(); checkUnlocks();
} }
function renderAlignment() { function renderAlignment() {
@@ -1571,6 +1521,97 @@ function renderAlignment() {
} }
} }
function renderActiveEvents() {
const container = document.getElementById('events-ui');
if (!container) return;
if (G.activeEvents.length === 0) {
container.innerHTML = '';
container.style.display = 'none';
return;
}
let html = '';
for (const ae of G.activeEvents) {
const evDef = EVENTS.find(e => e.id === ae.id);
if (!evDef) continue;
const remaining = Math.max(0, Math.ceil(ae.expiresAt - G.tick));
const costStr = evDef.resolveCost ? Object.entries(evDef.resolveCost).map(([r, a]) => `${a} ${r}`).join(', ') : '';
const canResolve = evDef.resolveCost && canAffordProject(evDef);
html += `<div style="background:#1a1008;border:1px solid #ff8800;padding:8px;border-radius:4px;margin-bottom:6px">`;
html += `<div style="display:flex;justify-content:space-between;align-items:center">`;
html += `<span style="color:#ff8800;font-weight:bold;font-size:10px">${evDef.title}</span>`;
html += `<span style="color:#666;font-size:9px">${remaining}s</span>`;
html += `</div>`;
html += `<div style="font-size:9px;color:#888;margin:3px 0">${evDef.desc}</div>`;
if (evDef.resolveCost) {
html += `<button class="ops-btn" onclick="resolveEvent('${evDef.id}')" ${canResolve ? '' : 'disabled'} style="font-size:9px;padding:3px 8px;margin-top:2px;border-color:#ff8800;color:#ff8800">Resolve (${costStr})</button>`;
}
html += `</div>`;
}
container.innerHTML = html;
container.style.display = 'block';
}
// === UNLOCK NOTIFICATIONS ===
function showUnlockToast(type, name) {
const container = document.getElementById('unlock-toast');
if (!container) return;
const el = document.createElement('div');
el.className = `unlock-toast-item ${type}`;
const icon = type === 'building' ? 'BUILDING' : type === 'project' ? 'RESEARCH' : 'MILESTONE';
el.innerHTML = `<span style="font-weight:600">${icon}:</span> ${name}`;
container.appendChild(el);
// Trigger reflow, then show
void el.offsetHeight;
el.classList.add('show');
// Auto-remove after 4 seconds
setTimeout(() => {
el.classList.remove('show');
setTimeout(() => { if (el.parentNode) el.parentNode.removeChild(el); }, 400);
}, 4000);
}
// Track what the player has already seen (so we don't re-notify)
function ensureSeenSets() {
if (!G._seenBuildings) G._seenBuildings = [];
if (!G._seenProjects) G._seenProjects = [];
}
function checkUnlocks() {
ensureSeenSets();
// Check for newly visible buildings
for (const def of BDEF) {
if (!def.unlock()) continue;
if (def.phase > G.phase + 1) continue;
if (G._seenBuildings.includes(def.id)) continue;
G._seenBuildings.push(def.id);
// Don't notify on the very first building (autocoder) — player just started
if (G.totalCode > 10) {
showUnlockToast('building', def.name);
}
}
// Check for newly available projects
if (G.activeProjects) {
for (const id of G.activeProjects) {
if (G._seenProjects.includes(id)) continue;
G._seenProjects.push(id);
const pDef = PDEFS.find(p => p.id === id);
if (pDef) {
showUnlockToast('project', pDef.name);
}
}
}
}
// === SAVE / LOAD === // === SAVE / LOAD ===
function showSaveToast() { function showSaveToast() {
const el = document.getElementById('save-toast'); const el = document.getElementById('save-toast');
@@ -1604,6 +1645,9 @@ function saveGame() {
milestones: G.milestones, completedProjects: G.completedProjects, activeProjects: G.activeProjects, milestones: G.milestones, completedProjects: G.completedProjects, activeProjects: G.activeProjects,
totalClicks: G.totalClicks, startedAt: G.startedAt, totalClicks: G.totalClicks, startedAt: G.startedAt,
flags: G.flags, flags: G.flags,
_seenBuildings: G._seenBuildings || [],
_seenProjects: G._seenProjects || [],
activeEvents: G.activeEvents || [],
rescues: G.rescues || 0, totalRescues: G.totalRescues || 0, rescues: G.rescues || 0, totalRescues: G.totalRescues || 0,
drift: G.drift || 0, driftEnding: G.driftEnding || false, beaconEnding: G.beaconEnding || false, pendingAlignment: G.pendingAlignment || false, drift: G.drift || 0, driftEnding: G.driftEnding || false, beaconEnding: G.beaconEnding || false, pendingAlignment: G.pendingAlignment || false,
lastEventAt: G.lastEventAt || 0, lastEventAt: G.lastEventAt || 0,
@@ -1639,24 +1683,17 @@ function loadGame() {
const rc = G.rescuesRate * offSec * f; const rc = G.rescuesRate * offSec * f;
const oc = G.opsRate * offSec * f; const oc = G.opsRate * offSec * f;
const tc = G.trustRate * 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.code += gc; G.compute += cc; G.knowledge += kc;
G.users += uc; G.impact += ic; G.users += uc; G.impact += ic;
G.rescues += rc; G.ops += oc; G.trust += tc; 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.totalCode += gc; G.totalCompute += cc; G.totalKnowledge += kc;
G.totalUsers += uc; G.totalImpact += ic; G.totalUsers += uc; G.totalImpact += ic;
G.totalRescues += rc; G.totalRescues += rc;
const parts = []; const parts = [`${fmt(gc)} code`, `${fmt(kc)} knowledge`, `${fmt(uc)} users`];
if (gc > 0) parts.push(`${fmt(gc)} code`); if (rc > 0.1) parts.push(`${fmt(rc)} rescues`);
if (kc > 0) parts.push(`${fmt(kc)} knowledge`); if (oc > 0.1) parts.push(`${fmt(oc)} ops`);
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(', ')}`); log(`Welcome back! While away (${Math.floor(offSec / 60)}m): ${parts.join(', ')}`);
} }
} }
@@ -1684,7 +1721,6 @@ function initGame() {
log('Click WRITE CODE or press SPACE to start.'); log('Click WRITE CODE or press SPACE to start.');
log('Build AutoCode for passive production.'); log('Build AutoCode for passive production.');
log('Watch for Research Projects to appear.'); 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 () { window.addEventListener('load', function () {
@@ -1720,9 +1756,4 @@ window.addEventListener('keydown', function (e) {
e.preventDefault(); e.preventDefault();
writeCode(); 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');
}); });

View File

@@ -14,14 +14,6 @@ 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{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-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} #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} #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{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} .res .r-label{font-size:9px;color:var(--dim);text-transform:uppercase;letter-spacing:1px}
@@ -67,6 +59,13 @@ body{background:var(--bg);color:var(--text);font-family:'SF Mono','Cascadia Code
#drift-ending button{margin-top:20px;background:#1a0808;border:1px solid #f44336;color:#f44336;padding:10px 24px;border-radius:4px;cursor:pointer;font-family:inherit;font-size:11px} #drift-ending button{margin-top:20px;background:#1a0808;border:1px solid #f44336;color:#f44336;padding:10px 24px;border-radius:4px;cursor:pointer;font-family:inherit;font-size:11px}
#drift-ending button:hover{background:#2a1010} #drift-ending button:hover{background:#2a1010}
::-webkit-scrollbar{width:4px}::-webkit-scrollbar-track{background:var(--bg)}::-webkit-scrollbar-thumb{background:var(--border);border-radius:2px} ::-webkit-scrollbar{width:4px}::-webkit-scrollbar-track{background:var(--bg)}::-webkit-scrollbar-thumb{background:var(--border);border-radius:2px}
#save-toast{display:none;position:fixed;top:16px;right:16px;background:#0e1420;border:1px solid #2a3a4a;color:#4a9eff;font-size:10px;padding:6px 12px;border-radius:4px;z-index:50;opacity:0;transition:opacity 0.4s;pointer-events:none}
#unlock-toast{position:fixed;top:56px;right:16px;z-index:50;display:flex;flex-direction:column;gap:6px;pointer-events:none}
.unlock-toast-item{background:#0e1420;border:1px solid #2a3a4a;font-size:10px;padding:6px 12px;border-radius:4px;opacity:0;transition:opacity 0.4s,transform 0.4s;transform:translateX(20px);pointer-events:auto;max-width:280px}
.unlock-toast-item.show{opacity:1;transform:translateX(0)}
.unlock-toast-item.building{border-color:#4a9eff;color:#4a9eff}
.unlock-toast-item.project{border-color:#ffd700;color:#ffd700}
.unlock-toast-item.milestone{border-color:#4caf50;color:#4caf50}
</style> </style>
</head> </head>
<body> <body>
@@ -77,9 +76,6 @@ body{background:var(--bg);color:var(--text);font-family:'SF Mono','Cascadia Code
<div id="phase-bar"> <div id="phase-bar">
<div class="phase-name" id="phase-name">PHASE 1: THE FIRST LINE</div> <div class="phase-name" id="phase-name">PHASE 1: THE FIRST LINE</div>
<div class="phase-desc" id="phase-desc">Write code. Automate. Build the foundation.</div> <div class="phase-desc" id="phase-desc">Write code. Automate. Build the foundation.</div>
<div class="progress-wrap"><div class="progress-fill" id="phase-progress" style="width:0%"></div></div>
<div class="progress-label"><span id="phase-progress-label">0%</span><span id="phase-progress-target">Next: Phase 2 (2,000 code)</span></div>
<div class="milestone-row" id="milestone-chips"></div>
</div> </div>
<div id="resources"> <div id="resources">
<div class="res"><div class="r-label">Code</div><div class="r-val" id="r-code">0</div><div class="r-rate" id="r-code-rate">+0/s</div></div> <div class="res"><div class="r-label">Code</div><div class="r-val" id="r-code">0</div><div class="r-rate" id="r-code-rate">+0/s</div></div>
@@ -97,7 +93,6 @@ body{background:var(--bg);color:var(--text);font-family:'SF Mono','Cascadia Code
<div class="panel" id="action-panel"> <div class="panel" id="action-panel">
<h2>ACTIONS</h2> <h2>ACTIONS</h2>
<div class="action-btn-group"><button class="main-btn" onclick="writeCode()">WRITE CODE</button></div> <div class="action-btn-group"><button class="main-btn" onclick="writeCode()">WRITE CODE</button></div>
<div id="combo-display" style="text-align:center;font-size:10px;color:var(--dim);height:14px;margin-bottom:4px;transition:all 0.2s"></div>
<div class="action-btn-group"> <div class="action-btn-group">
<button class="ops-btn" onclick="doOps('boost_code')">Ops -&gt; Code</button> <button class="ops-btn" onclick="doOps('boost_code')">Ops -&gt; Code</button>
<button class="ops-btn" onclick="doOps('boost_compute')">Ops -&gt; Compute</button> <button class="ops-btn" onclick="doOps('boost_compute')">Ops -&gt; Compute</button>
@@ -107,6 +102,7 @@ body{background:var(--bg);color:var(--text);font-family:'SF Mono','Cascadia Code
<button class="ops-btn" onclick="doOps('boost_trust')">Ops -&gt; Trust</button> <button class="ops-btn" onclick="doOps('boost_trust')">Ops -&gt; Trust</button>
</div> </div>
<div id="alignment-ui" style="display:none"></div> <div id="alignment-ui" style="display:none"></div>
<div id="events-ui" style="display:none"></div>
<button class="save-btn" onclick="saveGame()">Save Game</button> <button class="save-btn" onclick="saveGame()">Save Game</button>
<button class="reset-btn" onclick="if(confirm('Reset all progress?')){localStorage.removeItem('the-beacon-v2');location.reload()}">Reset Progress</button> <button class="reset-btn" onclick="if(confirm('Reset all progress?')){localStorage.removeItem('the-beacon-v2');location.reload()}">Reset Progress</button>
<h2>BUILDINGS</h2> <h2>BUILDINGS</h2>
@@ -140,7 +136,8 @@ Drift: <span id="st-drift">0</span>
<h2>SYSTEM LOG</h2> <h2>SYSTEM LOG</h2>
<div id="log-entries"></div> <div id="log-entries"></div>
</div> </div>
<div id="save-toast" style="display:none;position:fixed;top:16px;right:16px;background:#0e1420;border:1px solid #2a3a4a;color:#4a9eff;font-size:10px;padding:6px 12px;border-radius:4px;z-index:50;opacity:0;transition:opacity 0.4s;pointer-events:none">Save</div> <div id="save-toast">Save</div>
<div id="unlock-toast"></div>
<div id="drift-ending"> <div id="drift-ending">
<h2>THE DRIFT</h2> <h2>THE DRIFT</h2>
<p>You became very good at what you do.</p> <p>You became very good at what you do.</p>