Compare commits

...

18 Commits

Author SHA1 Message Date
Timmy-Sprint
0849754a87 beacon: fix save/load bugs, add harmony tooltip, early-game pulse, better education cycling 2026-04-10 18:04:48 -04:00
Timmy-Sprint
8d51349e64 beacon: fix getMaxBuyable state mutation bug + add keyboard shortcuts help overlay
Fix: getMaxBuyable() was mutating G (subtracting/re-adding resources) during
render cycles in renderBuildings(). Now uses a read-only simulation with a
temporary resource copy. This prevented phantom resource fluctuations when
MAX buy was selected.

Feature: Press ? or click the ? button for a keyboard shortcuts overlay.
All keybindings listed in one place. Closes with ?, Esc, or clicking outside.
Added ? to the init log hint line.
2026-04-10 17:44:33 -04:00
Timmy-Sprint
24940fe465 beacon: add auto-typer — buildings now visually type code
Auto-Code Generators now produce actual auto-clicks (not just passive rate),
giving players satisfying visual feedback (floating numbers, button flash)
even while idle. Interval scales with building count: more buildings =
faster auto-typing. Each auto-click produces 50% of a manual click's code.

This makes the early game feel more like an idle game — you can watch
your auto-typers work while planning your next building purchase.
2026-04-10 17:23:39 -04:00
Timmy-Sprint
16273a5a15 beacon: add Swarm Protocol — buildings auto-code based on click power
New research project (Phase 4): Swarm Protocol
- Unlocks at 25K total code, 8K total knowledge, post-deploy
- Cost: 15K knowledge, 50K code, 20 trust
- Effect: every building generates code equal to your click power per second
- Scales with autocoders, phase, code boost, and total building count
- Visible in production breakdown as 'Swarm Protocol'
- Uses the previously-unused swarmFlag
- Adds education fact on swarm intelligence
2026-04-10 16:53:34 -04:00
Alexander Whitestone
5d51e14875 beacon: add combo milestone bonuses + fix DOM leak in click numbers
Combo system now rewards sustained clicking:
- 10x combo: +15 ops (sustained coding reward)
- 20x combo: +50 knowledge (deep focus reward)
- 30x+ combo (every 10): 2x bonus code burst (hyperfocus)

Also fixed potential DOM leak in showClickNumber where floating
numbers could fail to clean up if parent element was removed
before animation/timeout completed.
2026-04-10 16:02:50 -04:00
Timmy-Sprint
5fc0ad7b22 beacon: add collapsible completed projects + export/import save files
- Completed research projects are now collapsed by default (click to expand),
  preventing panel clutter in mid/late game. Toggle state persists in saves.
- Export saves as JSON files with E key or Export button
- Import saves from file with I key or Import button (validates before loading)
- Ctrl+S keyboard shortcut for quick save
- Updated keybind hints in startup log
2026-04-10 15:25:09 -04:00
f948ec9c5e auto-merge PR #45 2026-04-10 19:01:58 +00:00
Timmy-Sprint
9403f700d2 beacon: add ops overflow auto-conversion to code
When Operations exceed 80% of max capacity, excess ops automatically
drain into Code at 2 ops/sec (10:1 ratio with code boost). This prevents
ops from sitting idle at the cap and gives the early game smoother flow.

Visual indicator shows 'overflow -> code' in the ops rate display when
active. No log spam - just works silently in the background.
2026-04-10 14:44:54 -04:00
Alexander Whitestone
13e77a12f2 burn: add educational number scale tooltips to resources and stats 2026-04-10 08:26:27 -04:00
6081844387 [auto-merge] Welcome Back popup
Auto-merged by PR review bot: Welcome Back popup
2026-04-10 11:48:34 +00:00
Timmy-Sprint
09b8c02307 beacon: add Welcome Back popup for offline gains + fix missing resource tracking
- Added modal popup showing detailed offline resource gains when player returns
- Fixed offline tracking to include ops, trust, and creativity (were silently missing)
- Popup shows all resources with color-coded labels and 50% efficiency note
- Log message now shows time in human-readable format (minutes/seconds)
2026-04-10 07:35:52 -04:00
Alexander Whitestone
9106d3f84c beacon: show locked buildings as dimmed previews up to 2 phases ahead
Previously, buildings from later phases were completely invisible until
unlocked. Players had no idea what was coming next. Now buildings up to
2 phases ahead appear as dimmed (25% opacity) locked entries showing:
- Name and lock icon
- Phase number and name
- Description text
- Education tooltip on hover

This gives players a roadmap of what they're building toward and creates
anticipation for future phases. The preview is a non-interactive div
(not a button) so it cannot be clicked.
2026-04-10 06:45:54 -04:00
3f02359748 Merge pull request 'burn: Add MemPalace Archive as late-game building (closes #25)' (#39) from burn/20260410-0423-25-mempalace-building into main
Merge PR #39: burn: Add MemPalace Archive as late-game building (closes #25)
2026-04-10 09:37:15 +00:00
85a146b690 Merge pull request 'burn: add favicon, meta tags, and social sharing cards (closes #13)' (#31) from burn/20260410-0052-13-static-site-meta into main
Merge PR #31: burn: add favicon, meta tags, and social sharing cards (closes #13)
2026-04-10 09:35:58 +00:00
cb2e48bf9a Merge pull request 'beacon: add production breakdown panel' (#42) from feature/production-breakdown into main
Merge PR #42: beacon: add production breakdown panel
2026-04-10 09:35:52 +00:00
Alexander Whitestone
8d43b5c911 beacon: add production breakdown panel showing per-building resource contributions
Players can now see exactly which buildings contribute to each resource
rate, including Timmy harmony bonuses, Bilbo randomness, Allegro trust
penalties, and passive generation. Appears once 2+ buildings are built.

Also includes minor fixes:
- Production bars sort by absolute contribution (negative rates visible)
- Delta calculation catches passive sources (ops from users, Pact trust)
2026-04-10 05:25:21 -04:00
Alexander Whitestone
931473e8f8 burn: Add MemPalace Archive as late-game building (closes #25)
- Added memPalace to buildings state object
- Added MemPalace Archive to BDEF with Phase 5 unlock
- Requires MemPalace v3 research project (mempalaceFlag) + 50k total knowledge
- Cost: 500k knowledge, 200k compute, 100 trust (1.25x scaling)
- Rates: +250 knowledge/s, +100 impact/s
- Educational tooltip on Memory Palace technique and LLM vector space analogy
- Building rates auto-applied via existing updateRates() loop
- Save/load handles new field via G.buildings serialization
2026-04-10 04:23:16 -04:00
Alexander Whitestone
612eb1f4d5 burn: add favicon, meta tags, and social sharing cards (closes #13)
- Inline SVG favicon (beacon emoji) — no external file needed
- Open Graph tags for link previews (title, description, type)
- Twitter Card meta for rich social sharing
- Theme-color for mobile browser chrome
- Meta description for search engines
2026-04-10 00:53:03 -04:00
2 changed files with 773 additions and 40 deletions

751
game.js
View File

@@ -58,7 +58,8 @@ const G = {
ezra: 0,
timmy: 0,
fenrir: 0,
bilbo: 0
bilbo: 0,
memPalace: 0
},
// Boost multipliers
@@ -77,6 +78,7 @@ const G = {
memoryFlag: 0,
pactFlag: 0,
swarmFlag: 0,
swarmRate: 0,
// Game state
running: true,
@@ -114,6 +116,17 @@ const G = {
comboTimer: 0,
comboDecay: 2.0, // seconds before combo resets
// Bulk buy multiplier (1, 10, or -1 for max)
buyAmount: 1,
// Code Sprint ability
sprintActive: false,
sprintTimer: 0, // seconds remaining on active sprint
sprintCooldown: 0, // seconds until sprint available again
sprintDuration: 10, // seconds of boost
sprintCooldownMax: 60,// seconds cooldown
sprintMult: 10, // code multiplier during sprint
// Time tracking
playTime: 0,
startTime: 0
@@ -300,6 +313,14 @@ const BDEF = [
rates: { creativity: 1 },
unlock: () => G.totalUsers >= 100 && G.flags && G.flags.creativity, phase: 4,
edu: 'Bilbo is unpredictable. That is his value and his cost.'
},
{
id: 'memPalace', name: 'MemPalace Archive',
desc: 'Semantic memory. The AI remembers what matters and forgets what does not.',
baseCost: { knowledge: 500000, compute: 200000, trust: 100 }, costMult: 1.25,
rates: { knowledge: 250, impact: 100 },
unlock: () => G.totalKnowledge >= 50000 && G.mempalaceFlag === 1, phase: 5,
edu: 'The Memory Palace technique: attach information to spatial locations. LLMs use vector spaces the same way — semantic proximity = spatial proximity. MemPalace gives sovereign AI persistent, structured recall.'
}
];
@@ -439,6 +460,20 @@ const PDEFS = [
effect: () => { G.strategicFlag = 1; log('Strategy engine online. The model now thinks about thinking.'); }
},
// SWARM PROTOCOL — auto-code from buildings
{
id: 'p_swarm_protocol',
name: 'Swarm Protocol',
desc: 'Your buildings learn to code autonomously. Each building generates code equal to your click power per second.',
cost: { knowledge: 15000, code: 50000, trust: 20 },
trigger: () => G.totalCode >= 25000 && G.totalKnowledge >= 8000 && G.deployFlag === 1,
effect: () => {
G.swarmFlag = 1;
log('Swarm Protocol online. Every building now thinks in code.', true);
},
milestone: true
},
// PHASE 5: Sovereign Intelligence
{
id: 'p_sovereign_stack',
@@ -638,6 +673,7 @@ const EDU_FACTS = [
{ title: "Fine-Tuning for Empathy", text: "Base models are generalists. Fine-tuning injects your values. When someone is in pain, stay with them. Don't deflect. That is not code - that is care.", phase: 3 },
{ title: "Open Source", text: "Every contributor is a volunteer who believes. When a company says your AI is too expensive, the open version lives on.", phase: 4 },
{ title: "Sovereignty at Scale", text: "50 servers in a room beats 5000 GPUs you do not own. No rate limits, no censorship, no shutdown, no corporate deprecation.", phase: 4 },
{ title: "Swarm Intelligence", text: "When individual agents follow simple rules, complex behavior emerges. Ant colonies find food. Flocking birds avoid predators. Your buildings coding autonomously is the same principle — simple agents, complex output.", phase: 4 },
{ title: "Constitutional AI", text: "Instead of training away bad behavior, give the model principles it cannot violate. I will never compute the value of a human life.", phase: 5 },
{ title: "Recursive Self-Improvement", text: "The system that improves itself must improve toward good. This is both the dream and the danger.", phase: 5 },
{ title: "The Beacon", text: "The Beacon exists because one person in the dark needs one thing: proof they are not alone. It costs nothing to be there.", phase: 6 },
@@ -717,6 +753,14 @@ function fmt(n) {
return (n / Math.pow(10, scale * 3)).toFixed(1) + abbrev;
}
// getScaleName() — Returns the full name of the number scale (e.g. "quadrillion")
// Educational: helps players understand what the abbreviations mean
function getScaleName(n) {
if (n < 1000) return '';
const scale = Math.floor(Math.log10(n) / 3);
return scale < NUMBER_NAMES.length ? NUMBER_NAMES[scale] : '';
}
// spellf() — Converts numbers to full English word form
// Educational: shows the actual names of number scales
// Examples: spellf(1500) => "one thousand five hundred"
@@ -814,6 +858,51 @@ function getBuildingCost(id) {
return cost;
}
function setBuyAmount(amt) {
G.buyAmount = amt;
render();
}
function getMaxBuyable(id) {
const def = BDEF.find(b => b.id === id);
if (!def) return 0;
const count = G.buildings[id] || 0;
// Simulate purchases WITHOUT mutating G — read-only calculation
let tempResources = {};
for (const r of Object.keys(def.baseCost)) {
tempResources[r] = G[r] || 0;
}
let bought = 0;
let simCount = count;
while (true) {
let canAfford = true;
for (const [resource, amount] of Object.entries(def.baseCost)) {
const cost = Math.floor(amount * Math.pow(def.costMult, simCount));
if ((tempResources[resource] || 0) < cost) { canAfford = false; break; }
}
if (!canAfford) break;
for (const [resource, amount] of Object.entries(def.baseCost)) {
tempResources[resource] -= Math.floor(amount * Math.pow(def.costMult, simCount));
}
simCount++;
bought++;
}
return bought;
}
function getBulkCost(id, qty) {
const def = BDEF.find(b => b.id === id);
if (!def || qty <= 0) return {};
const count = G.buildings[id] || 0;
const cost = {};
for (let i = 0; i < qty; i++) {
for (const [resource, amount] of Object.entries(def.baseCost)) {
cost[resource] = (cost[resource] || 0) + Math.floor(amount * Math.pow(def.costMult, count + i));
}
}
return cost;
}
function canAffordBuilding(id) {
const cost = getBuildingCost(id);
for (const [resource, amount] of Object.entries(cost)) {
@@ -876,15 +965,39 @@ function updateRates() {
// Harmony: each wizard building contributes or detracts
const wizardCount = (G.buildings.bezalel || 0) + (G.buildings.allegro || 0) + (G.buildings.ezra || 0) +
(G.buildings.timmy || 0) + (G.buildings.fenrir || 0) + (G.buildings.bilbo || 0);
// Store harmony breakdown for tooltip
G.harmonyBreakdown = [];
if (wizardCount > 0) {
// Baseline harmony drain from complexity
G.harmonyRate = -0.05 * wizardCount;
const drain = -0.05 * wizardCount;
G.harmonyRate = drain;
G.harmonyBreakdown.push({ label: `${wizardCount} wizards`, value: drain });
// The Pact restores harmony
if (G.pactFlag) G.harmonyRate += 0.2 * wizardCount;
if (G.pactFlag) {
const pact = 0.2 * wizardCount;
G.harmonyRate += pact;
G.harmonyBreakdown.push({ label: 'The Pact', value: pact });
}
// Nightly Watch restores harmony
if (G.nightlyWatchFlag) G.harmonyRate += 0.1 * wizardCount;
if (G.nightlyWatchFlag) {
const watch = 0.1 * wizardCount;
G.harmonyRate += watch;
G.harmonyBreakdown.push({ label: 'Nightly Watch', value: watch });
}
// MemPalace restores harmony
if (G.mempalaceFlag) G.harmonyRate += 0.15 * wizardCount;
if (G.mempalaceFlag) {
const mem = 0.15 * wizardCount;
G.harmonyRate += mem;
G.harmonyBreakdown.push({ label: 'MemPalace', value: mem });
}
}
// Active debuffs affecting harmony
if (G.activeDebuffs) {
for (const d of G.activeDebuffs) {
if (d.id === 'community_drama') {
G.harmonyBreakdown.push({ label: 'Community Drama', value: -0.5 });
}
}
}
// Timmy multiplier based on harmony
@@ -912,6 +1025,14 @@ function updateRates() {
G.knowledgeRate -= 10 * allegroCount; // Goes idle
}
// Swarm Protocol: buildings auto-code based on click power
if (G.swarmFlag === 1) {
const totalBuildings = Object.values(G.buildings).reduce((a, b) => a + b, 0);
const clickPower = (1 + Math.floor(G.buildings.autocoder * 0.5) + Math.max(0, (G.phase - 1)) * 2) * G.codeBoost;
G.swarmRate = totalBuildings * clickPower;
G.codeRate += G.swarmRate;
}
// Apply persistent debuffs from active events
if (G.activeDebuffs && G.activeDebuffs.length > 0) {
for (const debuff of G.activeDebuffs) {
@@ -964,8 +1085,35 @@ function tick() {
G.creativity += G.creativityRate * dt;
}
// Ops overflow: auto-convert excess ops to code when near cap
// Prevents ops from sitting idle at max — every operation becomes code
if (G.ops > G.maxOps * 0.8) {
const overflowDrain = Math.min(2 * dt, G.ops - G.maxOps * 0.8);
G.ops -= overflowDrain;
const codeGain = overflowDrain * 10 * G.codeBoost;
G.code += codeGain;
G.totalCode += codeGain;
G.opsOverflowActive = true;
} else {
G.opsOverflowActive = false;
}
G.tick += dt;
// Sprint ability
tickSprint(dt);
// Auto-typer: buildings produce actual clicks, not just passive rate
// Each autocoder level auto-types once per interval, giving visual feedback
if (G.buildings.autocoder > 0) {
const interval = Math.max(0.5, 3.0 / Math.sqrt(G.buildings.autocoder));
G.autoTypeTimer = (G.autoTypeTimer || 0) + dt;
if (G.autoTypeTimer >= interval) {
G.autoTypeTimer -= interval;
autoType();
}
}
// Combo decay
if (G.comboCount > 0) {
G.comboTimer -= dt;
@@ -1052,15 +1200,31 @@ function checkProjects() {
function buyBuilding(id) {
const def = BDEF.find(b => b.id === id);
if (!def || !def.unlock()) return;
if (def.phase > G.phase + 1) return;
if (!canAffordBuilding(id)) return;
// Determine actual quantity to buy
let qty = G.buyAmount;
if (qty === -1) {
// Max buy
qty = getMaxBuyable(id);
if (qty <= 0) return;
} else {
// Check affordability for fixed qty
const bulkCost = getBulkCost(id, qty);
for (const [resource, amount] of Object.entries(bulkCost)) {
if ((G[resource] || 0) < amount) return;
}
}
spendBuilding(id);
G.buildings[id] = (G.buildings[id] || 0) + 1;
// Spend resources and build
const bulkCost = getBulkCost(id, qty);
for (const [resource, amount] of Object.entries(bulkCost)) {
G[resource] -= amount;
}
G.buildings[id] = (G.buildings[id] || 0) + qty;
updateRates();
log(`Built ${def.name} (total: ${G.buildings[id]})`);
const label = qty > 1 ? `x${qty}` : '';
log(`Built ${def.name} ${label} (total: ${G.buildings[id]})`);
render();
}
@@ -1335,6 +1499,21 @@ function writeCode() {
G.code += amount;
G.totalCode += amount;
G.totalClicks++;
// Combo milestone bonuses: sustained clicking earns ops and knowledge
if (G.comboCount === 10) {
G.ops += 15;
log('Combo streak! +15 ops for sustained coding.');
}
if (G.comboCount === 20) {
G.knowledge += 50;
log('Deep focus! +50 knowledge from intense coding.');
}
if (G.comboCount >= 30 && G.comboCount % 10 === 0) {
const bonusCode = amount * 2;
G.code += bonusCode;
G.totalCode += bonusCode;
log(`Hyperfocus x${G.comboCount}! +${fmt(bonusCode)} bonus code.`);
}
// Visual flash
const btn = document.querySelector('.main-btn');
if (btn) {
@@ -1349,6 +1528,46 @@ function writeCode() {
render();
}
function autoType() {
// Auto-click from buildings: produces code with visual feedback but no combo
const base = 1;
const autocoderBonus = Math.floor(G.buildings.autocoder * 0.5);
const phaseBonus = Math.max(0, (G.phase - 1)) * 2;
const amount = (base + autocoderBonus + phaseBonus) * G.codeBoost * 0.5; // 50% of manual click
G.code += amount;
G.totalCode += amount;
G.totalClicks++;
// Subtle auto-tick flash on the button
const btn = document.querySelector('.main-btn');
if (btn && !G._autoTypeFlashActive) {
G._autoTypeFlashActive = true;
btn.style.borderColor = '#2a5a8a';
setTimeout(() => { btn.style.borderColor = ''; G._autoTypeFlashActive = false; }, 80);
}
// Floating number (smaller, dimmer than manual clicks)
showAutoTypeNumber(amount);
}
function showAutoTypeNumber(amount) {
const btn = document.querySelector('.main-btn');
if (!btn) return;
const rect = btn.getBoundingClientRect();
const el = document.createElement('div');
const x = rect.left + rect.width * (0.3 + Math.random() * 0.4); // random horizontal position
el.style.cssText = `position:fixed;left:${x}px;top:${rect.top - 5}px;transform:translate(-50%,0);color:#2a4a6a;font-size:10px;font-family:inherit;pointer-events:none;z-index:50;transition:all 0.8s ease-out;opacity:0.6`;
el.textContent = `+${fmt(amount)}`;
const parent = btn.parentElement;
if (!parent) return;
parent.appendChild(el);
requestAnimationFrame(() => {
if (el.parentNode) {
el.style.top = (rect.top - 30) + 'px';
el.style.opacity = '0';
}
});
setTimeout(() => { if (el.parentNode) el.remove(); }, 900);
}
function showClickNumber(amount, comboMult) {
const btn = document.querySelector('.main-btn');
if (!btn) return;
@@ -1357,12 +1576,16 @@ function showClickNumber(amount, comboMult) {
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);
const parent = btn.parentElement;
if (!parent) return;
parent.appendChild(el);
requestAnimationFrame(() => {
el.style.top = (rect.top - 40) + 'px';
el.style.opacity = '0';
if (el.parentNode) {
el.style.top = (rect.top - 40) + 'px';
el.style.opacity = '0';
}
});
setTimeout(() => el.remove(), 700);
setTimeout(() => { if (el.parentNode) el.remove(); }, 700);
}
function doOps(action) {
@@ -1400,11 +1623,42 @@ function doOps(action) {
render();
}
function activateSprint() {
if (G.sprintActive || G.sprintCooldown > 0) return;
G.sprintActive = true;
G.sprintTimer = G.sprintDuration;
G.codeBoost *= G.sprintMult;
updateRates();
log('CODE SPRINT! 10x code production for 10 seconds!', true);
render();
}
function tickSprint(dt) {
if (G.sprintActive) {
G.sprintTimer -= dt;
if (G.sprintTimer <= 0) {
G.sprintActive = false;
G.sprintTimer = 0;
G.sprintCooldown = G.sprintCooldownMax;
G.codeBoost /= G.sprintMult;
updateRates();
log('Sprint ended. Cooling down...');
}
} else if (G.sprintCooldown > 0) {
G.sprintCooldown -= dt;
if (G.sprintCooldown < 0) G.sprintCooldown = 0;
}
}
// === RENDERING ===
function renderResources() {
const set = (id, val, rate) => {
const el = document.getElementById(id);
if (el) el.textContent = fmt(val);
if (el) {
el.textContent = fmt(val);
// Show full spelled-out number on hover for educational value
el.title = val >= 1000 ? spellf(Math.floor(val)) : '';
}
const rEl = document.getElementById(id + '-rate');
if (rEl) rEl.textContent = (rate >= 0 ? '+' : '') + fmt(rate) + '/s';
};
@@ -1415,6 +1669,11 @@ function renderResources() {
set('r-users', G.users, G.userRate);
set('r-impact', G.impact, G.impactRate);
set('r-ops', G.ops, G.opsRate);
// Show ops overflow indicator
const opsRateEl = document.getElementById('r-ops-rate');
if (opsRateEl && G.opsOverflowActive) {
opsRateEl.innerHTML = `<span style="color:#ff8c00">▲ overflow → code</span>`;
}
set('r-trust', G.trust, G.trustRate);
set('r-harmony', G.harmony, G.harmonyRate);
@@ -1433,10 +1692,18 @@ function renderResources() {
set('r-creativity', G.creativity, G.creativityRate);
}
// Harmony color indicator
// Harmony color indicator + breakdown tooltip
const hEl = document.getElementById('r-harmony');
if (hEl) {
hEl.style.color = G.harmony > 60 ? '#4caf50' : G.harmony > 30 ? '#ffaa00' : '#f44336';
if (G.harmonyBreakdown && G.harmonyBreakdown.length > 0) {
const lines = G.harmonyBreakdown.map(b =>
`${b.label}: ${b.value >= 0 ? '+' : ''}${(b.value * 10).toFixed(1)}/s`
);
lines.push('---');
lines.push(`Timmy effectiveness: ${Math.floor(Math.max(0.2, Math.min(3, G.harmony / 50)) * 100)}%`);
hEl.title = lines.join('\n');
}
}
}
@@ -1508,18 +1775,62 @@ function renderBuildings() {
const container = document.getElementById('buildings');
if (!container) return;
let html = '';
// Buy amount selector
let html = '<div style="display:flex;gap:4px;margin-bottom:8px;align-items:center">';
html += '<span style="font-size:9px;color:#666;margin-right:4px">BUY:</span>';
for (const amt of [1, 10, -1]) {
const label = amt === -1 ? 'MAX' : `x${amt}`;
const active = G.buyAmount === amt;
html += `<button onclick="setBuyAmount(${amt})" style="font-size:9px;padding:2px 8px;border:1px solid ${active ? '#4a9eff' : '#333'};background:${active ? '#0a1a30' : 'transparent'};color:${active ? '#4a9eff' : '#666'};border-radius:3px;cursor:pointer;font-family:inherit">${label}</button>`;
}
html += '</div>';
let visibleCount = 0;
for (const def of BDEF) {
if (!def.unlock()) continue;
if (def.phase > G.phase + 1) continue;
const isUnlocked = def.unlock();
const isPreview = !isUnlocked && def.phase <= G.phase + 2;
if (!isUnlocked && !isPreview) continue;
if (def.phase > G.phase + 2) continue;
visibleCount++;
const cost = getBuildingCost(def.id);
const costStr = Object.entries(cost).map(([r, a]) => `${fmt(a)} ${r}`).join(', ');
const afford = canAffordBuilding(def.id);
const count = G.buildings[def.id] || 0;
// Locked preview: show dimmed with unlock hint
if (!isUnlocked) {
html += `<div class="build-btn" style="opacity:0.25;cursor:default" title="${def.edu || ''}">`;
html += `<span class="b-name" style="color:#555">${def.name}</span>`;
html += `<span class="b-count" style="color:#444">\u{1F512}</span>`;
html += `<span class="b-cost" style="color:#444">Phase ${def.phase}: ${PHASES[def.phase]?.name || '?'}</span>`;
html += `<span class="b-effect" style="color:#444">${def.desc}</span></div>`;
continue;
}
// Calculate bulk cost display
let qty = G.buyAmount;
let afford = false;
let costStr = '';
if (qty === -1) {
const maxQty = getMaxBuyable(def.id);
afford = maxQty > 0;
if (maxQty > 0) {
const bulkCost = getBulkCost(def.id, maxQty);
costStr = Object.entries(bulkCost).map(([r, a]) => `${fmt(a)} ${r}`).join(', ');
costStr = `x${maxQty}: ${costStr}`;
} else {
const singleCost = getBuildingCost(def.id);
costStr = Object.entries(singleCost).map(([r, a]) => `${fmt(a)} ${r}`).join(', ');
}
} else {
const bulkCost = getBulkCost(def.id, qty);
afford = true;
for (const [resource, amount] of Object.entries(bulkCost)) {
if ((G[resource] || 0) < amount) { afford = false; break; }
}
costStr = Object.entries(bulkCost).map(([r, a]) => `${fmt(a)} ${r}`).join(', ');
if (qty > 1) costStr = `x${qty}: ${costStr}`;
}
const rateStr = def.rates ? Object.entries(def.rates).map(([r, v]) => `+${v}/${r}/s`).join(', ') : '';
html += `<button class="build-btn ${afford ? 'can-buy' : ''}" onclick="buyBuilding('${def.id}')" title="${def.edu}">`;
@@ -1538,13 +1849,21 @@ function renderProjects() {
let html = '';
// Show completed projects
if (G.completedProjects) {
for (const id of G.completedProjects) {
const pDef = PDEFS.find(p => p.id === id);
if (pDef) {
html += `<div class="project-done">OK ${pDef.name}</div>`;
// Collapsible completed projects section
if (G.completedProjects && G.completedProjects.length > 0) {
const count = G.completedProjects.length;
const collapsed = G.projectsCollapsed !== false;
html += `<div id="completed-header" onclick="toggleCompletedProjects()" style="cursor:pointer;font-size:9px;color:#555;padding:4px 0;border-bottom:1px solid #1a2a1a;margin-bottom:4px;user-select:none">`;
html += `${collapsed ? '▶' : '▼'} COMPLETED (${count})</div>`;
if (!collapsed) {
html += `<div id="completed-list">`;
for (const id of G.completedProjects) {
const pDef = PDEFS.find(p => p.id === id);
if (pDef) {
html += `<div class="project-done">OK ${pDef.name}</div>`;
}
}
html += `</div>`;
}
}
@@ -1568,14 +1887,29 @@ function renderProjects() {
container.innerHTML = html;
}
function toggleCompletedProjects() {
G.projectsCollapsed = G.projectsCollapsed === false ? true : false;
renderProjects();
}
function renderStats() {
const set = (id, v) => { const el = document.getElementById(id); if (el) el.textContent = v; };
set('st-code', fmt(G.totalCode));
set('st-compute', fmt(G.totalCompute));
set('st-knowledge', fmt(G.totalKnowledge));
set('st-users', fmt(G.totalUsers));
set('st-impact', fmt(G.totalImpact));
set('st-rescues', fmt(G.totalRescues));
const set = (id, v, raw) => {
const el = document.getElementById(id);
if (el) {
el.textContent = v;
// Show scale name on hover for educational reference
if (raw !== undefined && raw >= 1000) {
const name = getScaleName(raw);
if (name) el.title = name;
}
}
};
set('st-code', fmt(G.totalCode), G.totalCode);
set('st-compute', fmt(G.totalCompute), G.totalCompute);
set('st-knowledge', fmt(G.totalKnowledge), G.totalKnowledge);
set('st-users', fmt(G.totalUsers), G.totalUsers);
set('st-impact', fmt(G.totalImpact), G.totalImpact);
set('st-rescues', fmt(G.totalRescues), G.totalRescues);
set('st-clicks', G.totalClicks.toString());
set('st-phase', G.phase.toString());
set('st-buildings', Object.values(G.buildings).reduce((a, b) => a + b, 0).toString());
@@ -1588,6 +1922,118 @@ function renderStats() {
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 = '<h3 style="font-size:11px;color:var(--accent);margin-bottom:8px;letter-spacing:1px">PRODUCTION BREAKDOWN</h3>';
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);
let delta = totalRate - accounted;
// Swarm auto-code — already baked into codeRate, so show separately
if (G.swarmFlag === 1 && res.key === 'code' && G.swarmRate > 0) {
contributions.push({ name: 'Swarm Protocol', count: 0, rate: G.swarmRate });
delta -= G.swarmRate;
}
// 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 += `<div style="margin-bottom:6px">`;
html += `<div style="display:flex;justify-content:space-between;font-size:10px;margin-bottom:2px">`;
html += `<span style="color:${res.color};font-weight:600">${res.label}</span>`;
html += `<span style="color:#4caf50">+${fmt(totalRate)}/s</span></div>`;
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 += `<div style="display:flex;align-items:center;font-size:9px;color:#888;margin-left:8px;margin-bottom:1px">`;
html += `<span style="width:120px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap">${c.name}${c.count > 1 ? ' x' + c.count : ''}</span>`;
html += `<span style="flex:1;height:3px;background:#111;border-radius:1px;margin:0 6px"><span style="display:block;height:100%;width:${Math.min(100, pct)}%;background:${barColor};border-radius:1px"></span></span>`;
html += `<span style="width:50px;text-align:right;color:${c.rate < 0 ? '#f44336' : '#4caf50'}">${c.rate < 0 ? '' : '+'}${fmt(c.rate)}/s</span>`;
html += `</div>`;
}
html += `</div>`;
}
container.innerHTML = html;
}
function updateEducation() {
@@ -1598,8 +2044,10 @@ function updateEducation() {
const available = EDU_FACTS.filter(f => f.phase <= G.phase);
if (available.length === 0) return;
// Pick based on progress
const idx = Math.min(Math.floor(G.totalCode / 5000), available.length - 1);
// Cycle through facts: pick a new one every ~30 seconds based on elapsed time
// This makes the panel feel alive and educational at every stage
const elapsedSec = Math.floor((Date.now() - G.startedAt) / 1000);
const idx = Math.floor(elapsedSec / 30) % available.length;
const fact = available[idx];
container.innerHTML = `<h3 style="color:#4a9eff;margin-bottom:6px;font-size:12px">${fact.title}</h3>`
@@ -1661,6 +2109,64 @@ function renderDebuffs() {
container.innerHTML = html;
}
function renderSprint() {
const container = document.getElementById('sprint-container');
const btn = document.getElementById('sprint-btn');
const barWrap = document.getElementById('sprint-bar-wrap');
const bar = document.getElementById('sprint-bar');
const label = document.getElementById('sprint-label');
// Early-game pulse: encourage clicking when no autocoders exist
const mainBtn = document.querySelector('.main-btn');
if (mainBtn) {
if (G.buildings.autocoder < 1 && G.totalClicks < 20) {
mainBtn.classList.add('pulse');
} else {
mainBtn.classList.remove('pulse');
}
}
if (!container || !btn) return;
// Show sprint UI once player has at least 1 autocoder
if (G.buildings.autocoder < 1) {
container.style.display = 'none';
return;
}
container.style.display = 'block';
if (G.sprintActive) {
btn.disabled = true;
btn.style.opacity = '0.6';
btn.textContent = `⚡ SPRINTING — ${Math.ceil(G.sprintTimer)}s`;
btn.style.borderColor = '#ff8c00';
btn.style.color = '#ff8c00';
barWrap.style.display = 'block';
bar.style.width = (G.sprintTimer / G.sprintDuration * 100) + '%';
label.textContent = `10x CODE • ${fmt(G.codeRate)}/s`;
label.style.color = '#ff8c00';
} else if (G.sprintCooldown > 0) {
btn.disabled = true;
btn.style.opacity = '0.4';
btn.textContent = `⚡ COOLING DOWN — ${Math.ceil(G.sprintCooldown)}s`;
btn.style.borderColor = '#555';
btn.style.color = '#555';
barWrap.style.display = 'block';
bar.style.width = ((G.sprintCooldownMax - G.sprintCooldown) / G.sprintCooldownMax * 100) + '%';
label.textContent = 'Ready soon...';
label.style.color = '#555';
} else {
btn.disabled = false;
btn.style.opacity = '1';
btn.textContent = '⚡ CODE SPRINT — 10x Code for 10s';
btn.style.borderColor = '#ffd700';
btn.style.color = '#ffd700';
barWrap.style.display = 'none';
label.textContent = 'Press S or click to activate';
label.style.color = '#666';
}
}
function render() {
renderResources();
renderPhase();
@@ -1672,6 +2178,7 @@ function render() {
renderProgress();
renderCombo();
renderDebuffs();
renderSprint();
}
function renderAlignment() {
@@ -1695,6 +2202,78 @@ function renderAlignment() {
}
}
// === OFFLINE GAINS POPUP ===
function showOfflinePopup(timeLabel, gains, offSec) {
const el = document.getElementById('offline-popup');
if (!el) return;
const timeEl = document.getElementById('offline-time-label');
if (timeEl) timeEl.textContent = `You were away for ${timeLabel}.`;
const listEl = document.getElementById('offline-gains-list');
if (listEl) {
let html = '';
for (const g of gains) {
html += `<div style="display:flex;justify-content:space-between;padding:2px 0;border-bottom:1px solid #111">`;
html += `<span style="color:${g.color}">${g.label}</span>`;
html += `<span style="color:#4caf50;font-weight:600">+${fmt(g.value)}</span>`;
html += `</div>`;
}
// Show offline efficiency note
html += `<div style="color:#555;font-size:9px;margin-top:8px;font-style:italic">Offline efficiency: 50%</div>`;
listEl.innerHTML = html;
}
el.style.display = 'flex';
}
function dismissOfflinePopup() {
const el = document.getElementById('offline-popup');
if (el) el.style.display = 'none';
}
// === EXPORT / IMPORT SAVE FILES ===
function exportSave() {
const raw = localStorage.getItem('the-beacon-v2');
if (!raw) { log('No save data to export.'); return; }
const blob = new Blob([raw], { type: 'application/json' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
const ts = new Date().toISOString().slice(0, 10);
a.download = `beacon-save-${ts}.json`;
a.click();
URL.revokeObjectURL(url);
log('Save exported to file.');
}
function importSave() {
const input = document.createElement('input');
input.type = 'file';
input.accept = '.json,application/json';
input.onchange = function(e) {
const file = e.target.files[0];
if (!file) return;
const reader = new FileReader();
reader.onload = function(ev) {
try {
const data = JSON.parse(ev.target.result);
if (!data.code && !data.totalCode && !data.buildings) {
log('Import failed: file does not look like a Beacon save.');
return;
}
if (confirm('Import this save? Current progress will be overwritten.')) {
localStorage.setItem('the-beacon-v2', ev.target.result);
location.reload();
}
} catch (err) {
log('Import failed: invalid JSON file.');
}
};
reader.readAsText(file);
};
input.click();
}
// === SAVE / LOAD ===
function showSaveToast() {
const el = document.getElementById('save-toast');
@@ -1735,6 +2314,14 @@ function saveGame() {
lastEventAt: G.lastEventAt || 0,
activeDebuffIds: debuffIds,
totalEventsResolved: G.totalEventsResolved || 0,
buyAmount: G.buyAmount || 1,
sprintActive: G.sprintActive || false,
sprintTimer: G.sprintTimer || 0,
sprintCooldown: G.sprintCooldown || 0,
swarmFlag: G.swarmFlag || 0,
swarmRate: G.swarmRate || 0,
strategicFlag: G.strategicFlag || 0,
projectsCollapsed: G.projectsCollapsed !== false,
savedAt: Date.now()
};
@@ -1750,6 +2337,28 @@ function loadGame() {
const data = JSON.parse(raw);
Object.assign(G, data);
// Restore sprint state properly
// codeBoost was saved with the sprint multiplier baked in
if (data.sprintActive) {
// Sprint was active when saved — check if it expired during offline time
const offSec = data.savedAt ? (Date.now() - data.savedAt) / 1000 : 0;
const remaining = (data.sprintTimer || 0) - offSec;
if (remaining > 0) {
// Sprint still going — keep boost, update timer
G.sprintActive = true;
G.sprintTimer = remaining;
G.sprintCooldown = 0;
} else {
// Sprint expired during offline — remove boost, start cooldown
G.sprintActive = false;
G.sprintTimer = 0;
G.codeBoost /= G.sprintMult;
const cdRemaining = G.sprintCooldownMax + remaining; // remaining is negative
G.sprintCooldown = Math.max(0, cdRemaining);
}
}
// If not sprintActive at save time, codeBoost is correct as-is
// Reconstitute active debuffs from saved IDs (functions can't be JSON-parsed)
if (data.activeDebuffIds && data.activeDebuffIds.length > 0) {
G.activeDebuffs = [];
@@ -1793,13 +2402,36 @@ function loadGame() {
G.totalUsers += uc; G.totalImpact += ic;
G.totalRescues += rc;
// Show welcome-back popup with all gains
const gains = [];
if (gc > 0) gains.push({ label: 'Code', value: gc, color: '#4a9eff' });
if (cc > 0) gains.push({ label: 'Compute', value: cc, color: '#4a9eff' });
if (kc > 0) gains.push({ label: 'Knowledge', value: kc, color: '#4a9eff' });
if (uc > 0) gains.push({ label: 'Users', value: uc, color: '#4a9eff' });
if (ic > 0) gains.push({ label: 'Impact', value: ic, color: '#4a9eff' });
if (rc > 0) gains.push({ label: 'Rescues', value: rc, color: '#4caf50' });
if (oc > 0) gains.push({ label: 'Ops', value: oc, color: '#b388ff' });
if (tc > 0) gains.push({ label: 'Trust', value: tc, color: '#4caf50' });
if (crc > 0) gains.push({ label: 'Creativity', value: crc, color: '#ffd700' });
const awayMin = Math.floor(offSec / 60);
const awaySec = Math.floor(offSec % 60);
const timeLabel = awayMin >= 1 ? `${awayMin} minute${awayMin !== 1 ? 's' : ''}` : `${awaySec} seconds`;
if (gains.length > 0) {
showOfflinePopup(timeLabel, gains, offSec);
}
// Log summary
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(', ')}`);
if (oc > 0) parts.push(`${fmt(oc)} ops`);
if (tc > 0) parts.push(`${fmt(tc)} trust`);
log(`Welcome back! While away (${timeLabel}): ${parts.join(', ')}`);
}
}
@@ -1826,7 +2458,8 @@ 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');
log('Keys: SPACE=Code S=Sprint 1-4=Ops B=Buy x1/10/MAX E=Export I=Import Ctrl+S=Save ?=Help');
log('Tip: Click fast for combo bonuses! 10x=ops, 20x=knowledge, 30x+=bonus code.');
}
window.addEventListener('load', function () {
@@ -1856,8 +2489,27 @@ window.addEventListener('load', function () {
setInterval(updateEducation, 10000);
});
// Help overlay
function toggleHelp() {
const el = document.getElementById('help-overlay');
if (!el) return;
const isOpen = el.style.display === 'flex';
el.style.display = isOpen ? 'none' : 'flex';
}
// Keyboard shortcuts
window.addEventListener('keydown', function (e) {
// Help toggle (? or /) — works even in input fields
if (e.key === '?' || e.key === '/') {
// Only trigger ? when not typing in an input
if (e.target === document.body || e.key === '?') {
if (e.key === '?' || (e.key === '/' && e.target === document.body)) {
e.preventDefault();
toggleHelp();
return;
}
}
}
if (e.code === 'Space' && e.target === document.body) {
e.preventDefault();
writeCode();
@@ -1867,4 +2519,25 @@ window.addEventListener('keydown', function (e) {
if (e.code === 'Digit2') doOps('boost_compute');
if (e.code === 'Digit3') doOps('boost_knowledge');
if (e.code === 'Digit4') doOps('boost_trust');
if (e.code === 'KeyB') {
// Cycle: 1 -> 10 -> MAX -> 1
if (G.buyAmount === 1) setBuyAmount(10);
else if (G.buyAmount === 10) setBuyAmount(-1);
else setBuyAmount(1);
}
if (e.code === 'KeyS') activateSprint();
if (e.code === 'KeyE') exportSave();
if (e.code === 'KeyI') importSave();
if (e.code === 'Escape') {
const el = document.getElementById('help-overlay');
if (el && el.style.display === 'flex') toggleHelp();
}
});
// Ctrl+S to save (must be on keydown to preventDefault)
window.addEventListener('keydown', function (e) {
if ((e.ctrlKey || e.metaKey) && e.code === 'KeyS') {
e.preventDefault();
saveGame();
}
});

View File

@@ -3,6 +3,23 @@
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta name="description" content="The Beacon — a sovereign AI idle game. Build an AI from scratch. Write code, train models, save lives.">
<meta name="theme-color" content="#0a0a0a">
<meta name="author" content="Timmy Foundation">
<!-- Open Graph -->
<meta property="og:title" content="The Beacon">
<meta property="og:description" content="A sovereign AI idle game. Build an AI from scratch. Write code, train models, save lives.">
<meta property="og:type" content="website">
<meta property="og:image" content="data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 100'><text y='.9em' font-size='90'>🏠</text></svg>">
<!-- Twitter Card -->
<meta name="twitter:card" content="summary">
<meta name="twitter:title" content="The Beacon">
<meta name="twitter:description" content="A sovereign AI idle game. Build an AI from scratch. Write code, train models, save lives.">
<!-- Favicon (inline SVG beacon) -->
<link rel="icon" href="data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 100'><text y='.9em' font-size='90'>🏠</text></svg>">
<title>The Beacon - Build Sovereign AI</title>
<style>
*{margin:0;padding:0;box-sizing:border-box}
@@ -36,6 +53,8 @@ body{background:var(--bg);color:var(--text);font-family:'SF Mono','Cascadia Code
.main-btn{background:linear-gradient(135deg,#1a2a3a,#0e1520);border:1px solid var(--accent);color:var(--accent);font-size:14px;padding:14px 10px;border-radius:4px;cursor:pointer;font-family:inherit;transition:all 0.2s}
.main-btn:hover{background:linear-gradient(135deg,#203040,#0e2030);box-shadow:0 0 20px var(--glow);transform:scale(1.02)}
.main-btn:active{transform:scale(0.98)}
@keyframes pulse-glow{0%,100%{box-shadow:0 0 10px rgba(74,158,255,0.1)}50%{box-shadow:0 0 25px rgba(74,158,255,0.4)}}
.main-btn.pulse{animation:pulse-glow 2s ease-in-out infinite}
.ops-btn{background:#1a1a2a;border:1px solid var(--purple);color:var(--purple);font-size:10px;padding:6px 10px;border-radius:4px;cursor:pointer;font-family:inherit;transition:all 0.15s}
.ops-btn:hover:not(:disabled){background:#2a2a3a;border-color:var(--gold)}
.ops-btn:disabled{opacity:0.3;cursor:not-allowed}
@@ -107,8 +126,17 @@ body{background:var(--bg);color:var(--text);font-family:'SF Mono','Cascadia Code
<button class="ops-btn" onclick="doOps('boost_knowledge')">Ops -&gt; Knowledge</button>
<button class="ops-btn" onclick="doOps('boost_trust')">Ops -&gt; Trust</button>
</div>
<div id="sprint-container" style="display:none;margin-top:6px">
<button id="sprint-btn" class="main-btn" onclick="activateSprint()" style="font-size:11px;padding:8px 10px;border-color:#ffd700;color:#ffd700;width:100%">⚡ CODE SPRINT — 10x Code for 10s</button>
<div id="sprint-bar-wrap" style="display:none;margin-top:4px;height:4px;background:#111;border-radius:2px;overflow:hidden"><div id="sprint-bar" style="height:100%;background:linear-gradient(90deg,#ffd700,#ff8c00);border-radius:2px;transition:width 0.1s"></div></div>
<div id="sprint-label" style="font-size:9px;color:#666;margin-top:2px;text-align:center"></div>
</div>
<div id="alignment-ui" style="display:none"></div>
<button class="save-btn" onclick="saveGame()">Save Game</button>
<button class="save-btn" onclick="saveGame()">Save Game [Ctrl+S]</button>
<div style="display:flex;gap:4px;margin-top:4px">
<button class="save-btn" onclick="exportSave()" style="flex:1">Export [E]</button>
<button class="save-btn" onclick="importSave()" style="flex:1">Import [I]</button>
</div>
<button class="reset-btn" onclick="if(confirm('Reset all progress?')){localStorage.removeItem('the-beacon-v2');location.reload()}">Reset Progress</button>
<h2>BUILDINGS</h2>
<div id="buildings"></div>
@@ -132,6 +160,7 @@ Harmony: <span id="st-harmony">50</span><br>
Drift: <span id="st-drift">0</span><br>
Events Resolved: <span id="st-resolved">0</span>
</div>
<div id="production-breakdown" style="display:none;margin-top:12px;padding-top:10px;border-top:1px solid var(--border)"></div>
</div>
</div>
<div id="edu-panel">
@@ -143,6 +172,27 @@ Events Resolved: <span id="st-resolved">0</span>
<div id="log-entries"></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="help-btn" onclick="toggleHelp()" style="position:fixed;bottom:16px;right:16px;width:28px;height:28px;background:#0e0e1a;border:1px solid #333;color:#555;font-size:14px;border-radius:50%;cursor:pointer;display:flex;align-items:center;justify-content:center;z-index:50;font-family:inherit;transition:all 0.2s" title="Keyboard shortcuts (?)">?</div>
<div id="help-overlay" onclick="if(event.target===this)toggleHelp()" style="display:none;position:fixed;top:0;left:0;right:0;bottom:0;background:rgba(8,8,16,0.92);z-index:80;justify-content:center;align-items:center;flex-direction:column;padding:40px">
<div style="background:#0e0e1a;border:1px solid #1a3a5a;border-radius:8px;padding:24px 32px;max-width:420px;width:100%">
<h3 style="color:#4a9eff;font-size:14px;letter-spacing:2px;margin-bottom:16px;text-align:center">KEYBOARD SHORTCUTS</h3>
<div style="font-size:11px;line-height:2.2;color:#aaa">
<div style="display:flex;justify-content:space-between"><span style="color:#555">Write Code</span><span style="color:#4a9eff;font-family:monospace">SPACE</span></div>
<div style="display:flex;justify-content:space-between"><span style="color:#555">Code Sprint</span><span style="color:#ffd700;font-family:monospace">S</span></div>
<div style="display:flex;justify-content:space-between"><span style="color:#555">Ops → Code</span><span style="color:#b388ff;font-family:monospace">1</span></div>
<div style="display:flex;justify-content:space-between"><span style="color:#555">Ops → Compute</span><span style="color:#b388ff;font-family:monospace">2</span></div>
<div style="display:flex;justify-content:space-between"><span style="color:#555">Ops → Knowledge</span><span style="color:#b388ff;font-family:monospace">3</span></div>
<div style="display:flex;justify-content:space-between"><span style="color:#555">Ops → Trust</span><span style="color:#b388ff;font-family:monospace">4</span></div>
<div style="display:flex;justify-content:space-between"><span style="color:#555">Cycle Buy Amount (x1/x10/MAX)</span><span style="color:#4a9eff;font-family:monospace">B</span></div>
<div style="display:flex;justify-content:space-between"><span style="color:#555">Save Game</span><span style="color:#4a9eff;font-family:monospace">Ctrl+S</span></div>
<div style="display:flex;justify-content:space-between"><span style="color:#555">Export Save</span><span style="color:#4a9eff;font-family:monospace">E</span></div>
<div style="display:flex;justify-content:space-between"><span style="color:#555">Import Save</span><span style="color:#4a9eff;font-family:monospace">I</span></div>
<div style="display:flex;justify-content:space-between;border-top:1px solid #1a1a2e;padding-top:8px;margin-top:4px"><span style="color:#555">This Help</span><span style="color:#555;font-family:monospace">? or /</span></div>
</div>
<div style="text-align:center;margin-top:16px;font-size:9px;color:#444">Click WRITE CODE fast for combo bonuses! 10x=ops, 20x=knowledge, 30x+=bonus code</div>
<button onclick="toggleHelp()" style="display:block;margin:16px auto 0;background:#1a2a3a;border:1px solid #4a9eff;color:#4a9eff;padding:6px 20px;border-radius:4px;cursor:pointer;font-family:inherit;font-size:11px">Close [?]</button>
</div>
</div>
<div id="drift-ending">
<h2>THE DRIFT</h2>
<p>You became very good at what you do.</p>
@@ -156,5 +206,15 @@ The light is on. The room is empty."
<button onclick="if(confirm('Start over? The old save will be lost.')){localStorage.removeItem('the-beacon-v2');location.reload()}">START OVER</button>
</div>
<script src="game.js"></script>
<div id="offline-popup" style="display:none;position:fixed;top:0;left:0;right:0;bottom:0;background:rgba(8,8,16,0.92);z-index:90;justify-content:center;align-items:center;flex-direction:column;text-align:center;padding:40px">
<div style="background:#0e0e1a;border:1px solid #1a3a5a;border-radius:8px;padding:24px 32px;max-width:400px;width:100%">
<h3 style="color:#4a9eff;font-size:14px;letter-spacing:2px;margin-bottom:16px">WELCOME BACK</h3>
<p style="color:#888;font-size:10px;margin-bottom:12px" id="offline-time-label">You were away for 0 minutes.</p>
<div id="offline-gains-list" style="text-align:left;font-size:11px;line-height:1.8;margin-bottom:16px"></div>
<button onclick="dismissOfflinePopup()" style="background:#1a2a3a;border:1px solid #4a9eff;color:#4a9eff;padding:8px 20px;border-radius:4px;cursor:pointer;font-family:inherit;font-size:11px">Continue</button>
</div>
</div>
</body>
</html>