Compare commits
10 Commits
feat/combo
...
feat/offli
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
09b8c02307 | ||
|
|
9106d3f84c | ||
| 3f02359748 | |||
| 85a146b690 | |||
| cb2e48bf9a | |||
|
|
8d43b5c911 | ||
|
|
8cdabe9771 | ||
|
|
5c88fe77be | ||
|
|
931473e8f8 | ||
|
|
612eb1f4d5 |
495
game.js
495
game.js
@@ -58,7 +58,8 @@ const G = {
|
||||
ezra: 0,
|
||||
timmy: 0,
|
||||
fenrir: 0,
|
||||
bilbo: 0
|
||||
bilbo: 0,
|
||||
memPalace: 0
|
||||
},
|
||||
|
||||
// Boost multipliers
|
||||
@@ -106,12 +107,17 @@ 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
|
||||
|
||||
// Bulk buy multiplier (1, 10, or -1 for max)
|
||||
buyAmount: 1,
|
||||
|
||||
// Time tracking
|
||||
playTime: 0,
|
||||
startTime: 0
|
||||
@@ -298,6 +304,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.'
|
||||
}
|
||||
];
|
||||
|
||||
@@ -812,6 +826,57 @@ 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;
|
||||
let count = G.buildings[id] || 0;
|
||||
let bought = 0;
|
||||
while (true) {
|
||||
const cost = {};
|
||||
for (const [resource, amount] of Object.entries(def.baseCost)) {
|
||||
cost[resource] = Math.floor(amount * Math.pow(def.costMult, count));
|
||||
}
|
||||
let canAfford = true;
|
||||
for (const [resource, amount] of Object.entries(cost)) {
|
||||
if ((G[resource] || 0) < amount) { canAfford = false; break; }
|
||||
}
|
||||
if (!canAfford) break;
|
||||
// Spend from temporary copy
|
||||
for (const [resource, amount] of Object.entries(cost)) {
|
||||
G[resource] -= amount;
|
||||
}
|
||||
count++;
|
||||
bought++;
|
||||
}
|
||||
// Refund: add back what we spent
|
||||
let count2 = G.buildings[id] || 0;
|
||||
for (let i = 0; i < bought; i++) {
|
||||
for (const [resource, amount] of Object.entries(def.baseCost)) {
|
||||
G[resource] += Math.floor(amount * Math.pow(def.costMult, count2));
|
||||
}
|
||||
count2++;
|
||||
}
|
||||
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)) {
|
||||
@@ -909,6 +974,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 ===
|
||||
@@ -927,7 +999,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));
|
||||
|
||||
@@ -1043,15 +1115,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();
|
||||
}
|
||||
|
||||
@@ -1127,46 +1215,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);
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -1174,6 +1290,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;
|
||||
@@ -1182,11 +1299,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);
|
||||
}
|
||||
}
|
||||
];
|
||||
@@ -1223,6 +1381,26 @@ 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;
|
||||
@@ -1409,18 +1587,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}">`;
|
||||
@@ -1483,11 +1705,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 = '<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);
|
||||
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 += `<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() {
|
||||
@@ -1538,6 +1868,29 @@ function renderCombo() {
|
||||
}
|
||||
}
|
||||
|
||||
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 = '<h2 style="color:#f44336;font-size:11px;margin-bottom:6px">ACTIVE PROBLEMS</h2>';
|
||||
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 += `<div style="background:#1a0808;border:1px solid ${afford ? '#f44336' : '#2a1010'};border-radius:4px;padding:6px 8px;margin-bottom:4px;display:flex;justify-content:space-between;align-items:center">`;
|
||||
html += `<div><div style="color:#f44336;font-weight:600;font-size:10px">${d.title}</div><div style="color:#888;font-size:9px">${d.desc}</div></div>`;
|
||||
if (d.resolveCost) {
|
||||
html += `<button class="ops-btn" style="border-color:${afford ? '#4caf50' : '#333'};color:${afford ? '#4caf50' : '#555'};font-size:9px;padding:4px 8px;white-space:nowrap" onclick="resolveEvent('${d.id}')" ${afford ? '' : 'disabled'} title="Resolve: ${costStr}">Fix (${costStr})</button>`;
|
||||
}
|
||||
html += '</div>';
|
||||
}
|
||||
container.innerHTML = html;
|
||||
}
|
||||
|
||||
function render() {
|
||||
renderResources();
|
||||
renderPhase();
|
||||
@@ -1548,6 +1901,7 @@ function render() {
|
||||
renderAlignment();
|
||||
renderProgress();
|
||||
renderCombo();
|
||||
renderDebuffs();
|
||||
}
|
||||
|
||||
function renderAlignment() {
|
||||
@@ -1571,6 +1925,35 @@ 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';
|
||||
}
|
||||
|
||||
// === SAVE / LOAD ===
|
||||
function showSaveToast() {
|
||||
const el = document.getElementById('save-toast');
|
||||
@@ -1587,6 +1970,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,
|
||||
@@ -1607,6 +1992,9 @@ 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,
|
||||
buyAmount: G.buyAmount || 1,
|
||||
savedAt: Date.now()
|
||||
};
|
||||
|
||||
@@ -1622,6 +2010,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
|
||||
@@ -1651,13 +2053,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(', ')}`);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1684,6 +2109,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 B=Buy x1/x10/MAX');
|
||||
}
|
||||
|
||||
window.addEventListener('load', function () {
|
||||
@@ -1719,4 +2145,15 @@ 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');
|
||||
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);
|
||||
}
|
||||
});
|
||||
|
||||
32
index.html
32
index.html
@@ -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}
|
||||
@@ -98,6 +115,7 @@ body{background:var(--bg);color:var(--text);font-family:'SF Mono','Cascadia Code
|
||||
<h2>ACTIONS</h2>
|
||||
<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 id="debuffs" style="display:none;margin-top:8px"></div>
|
||||
<div class="action-btn-group">
|
||||
<button class="ops-btn" onclick="doOps('boost_code')">Ops -> Code</button>
|
||||
<button class="ops-btn" onclick="doOps('boost_compute')">Ops -> Compute</button>
|
||||
@@ -128,8 +146,10 @@ Projects Done: <span id="st-projects">0</span><br>
|
||||
Time Played: <span id="st-time">0:00</span><br>
|
||||
Clicks: <span id="st-clicks">0</span><br>
|
||||
Harmony: <span id="st-harmony">50</span><br>
|
||||
Drift: <span id="st-drift">0</span>
|
||||
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">
|
||||
@@ -154,5 +174,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>
|
||||
|
||||
Reference in New Issue
Block a user