Compare commits
9 Commits
feature/pr
...
feat/progr
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7c3510a680 | ||
| 6081844387 | |||
|
|
09b8c02307 | ||
|
|
9106d3f84c | ||
| 3f02359748 | |||
| 85a146b690 | |||
| cb2e48bf9a | |||
|
|
931473e8f8 | ||
|
|
612eb1f4d5 |
238
game.js
238
game.js
@@ -58,7 +58,8 @@ const G = {
|
||||
ezra: 0,
|
||||
timmy: 0,
|
||||
fenrir: 0,
|
||||
bilbo: 0
|
||||
bilbo: 0,
|
||||
memPalace: 0
|
||||
},
|
||||
|
||||
// Boost multipliers
|
||||
@@ -114,6 +115,12 @@ const G = {
|
||||
comboTimer: 0,
|
||||
comboDecay: 2.0, // seconds before combo resets
|
||||
|
||||
// Bulk buy multiplier (1, 10, or -1 for max)
|
||||
buyAmount: 1,
|
||||
|
||||
// Track which buildings have had their unlock toasted
|
||||
seenBuildings: {},
|
||||
|
||||
// Time tracking
|
||||
playTime: 0,
|
||||
startTime: 0
|
||||
@@ -300,6 +307,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.'
|
||||
}
|
||||
];
|
||||
|
||||
@@ -814,6 +829,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)) {
|
||||
@@ -1009,6 +1075,21 @@ function tick() {
|
||||
}
|
||||
}
|
||||
|
||||
// === TOAST NOTIFICATIONS ===
|
||||
function showToast(title, msg, type) {
|
||||
const container = document.getElementById('toast-container');
|
||||
if (!container) return;
|
||||
const el = document.createElement('div');
|
||||
el.className = `toast toast-${type}`;
|
||||
el.innerHTML = `<div class="toast-title">${title}</div><div class="toast-msg">${msg}</div>`;
|
||||
container.appendChild(el);
|
||||
// Auto-dismiss after 4s
|
||||
setTimeout(() => { el.classList.add('fade-out'); }, 4000);
|
||||
setTimeout(() => { el.remove(); }, 4600);
|
||||
// Cap visible toasts at 4
|
||||
while (container.children.length > 4) container.removeChild(container.firstChild);
|
||||
}
|
||||
|
||||
function checkMilestones() {
|
||||
for (const m of MILESTONES) {
|
||||
if (!G.milestones.includes(m.flag)) {
|
||||
@@ -1026,6 +1107,7 @@ function checkMilestones() {
|
||||
if (G.totalCode >= phase.threshold && parseInt(phaseNum) > G.phase) {
|
||||
G.phase = parseInt(phaseNum);
|
||||
log(`PHASE ${G.phase}: ${phase.name}`, true);
|
||||
showToast(`PHASE ${G.phase}: ${phase.name}`, phase.desc, 'phase');
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1052,15 +1134,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();
|
||||
}
|
||||
|
||||
@@ -1082,6 +1180,8 @@ function buyProject(id) {
|
||||
G.activeProjects = G.activeProjects.filter(aid => aid !== pDef.id);
|
||||
}
|
||||
|
||||
showToast(pDef.milestone ? '★ ' + pDef.name : pDef.name, pDef.desc, 'project');
|
||||
|
||||
updateRates();
|
||||
render();
|
||||
}
|
||||
@@ -1508,18 +1608,68 @@ 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;
|
||||
|
||||
// Toast on first unlock
|
||||
if (isUnlocked && !G.seenBuildings[def.id]) {
|
||||
G.seenBuildings[def.id] = true;
|
||||
showToast('NEW: ' + def.name, def.desc, 'building');
|
||||
}
|
||||
|
||||
// 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}">`;
|
||||
@@ -1802,6 +1952,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');
|
||||
@@ -1842,6 +2021,8 @@ function saveGame() {
|
||||
lastEventAt: G.lastEventAt || 0,
|
||||
activeDebuffIds: debuffIds,
|
||||
totalEventsResolved: G.totalEventsResolved || 0,
|
||||
buyAmount: G.buyAmount || 1,
|
||||
seenBuildings: G.seenBuildings || {},
|
||||
savedAt: Date.now()
|
||||
};
|
||||
|
||||
@@ -1900,13 +2081,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(', ')}`);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1933,7 +2137,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');
|
||||
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 () {
|
||||
@@ -1974,4 +2178,10 @@ 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);
|
||||
}
|
||||
});
|
||||
|
||||
40
index.html
40
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}
|
||||
@@ -67,6 +84,18 @@ 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:hover{background:#2a1010}
|
||||
::-webkit-scrollbar{width:4px}::-webkit-scrollbar-track{background:var(--bg)}::-webkit-scrollbar-thumb{background:var(--border);border-radius:2px}
|
||||
#toast-container{position:fixed;top:60px;right:16px;z-index:80;display:flex;flex-direction:column;gap:8px;pointer-events:none}
|
||||
.toast{background:#0e1420;border:1px solid #2a3a4a;border-radius:6px;padding:10px 16px;max-width:320px;animation:toast-in 0.3s ease-out;opacity:1;transition:opacity 0.5s ease-out;pointer-events:auto}
|
||||
.toast.fade-out{opacity:0}
|
||||
.toast-phase{border-color:var(--gold);border-width:2px}
|
||||
.toast-phase .toast-title{color:var(--gold);text-shadow:0 0 12px rgba(255,215,0,0.3)}
|
||||
.toast-building{border-color:var(--accent)}
|
||||
.toast-building .toast-title{color:var(--accent)}
|
||||
.toast-project{border-color:var(--green)}
|
||||
.toast-project .toast-title{color:var(--green)}
|
||||
.toast-title{font-size:12px;font-weight:700;letter-spacing:1px;margin-bottom:2px}
|
||||
.toast-msg{font-size:10px;color:#888;line-height:1.4}
|
||||
@keyframes toast-in{from{transform:translateX(100%);opacity:0}to{transform:translateX(0);opacity:1}}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
@@ -143,6 +172,7 @@ Events Resolved: <span id="st-resolved">0</span>
|
||||
<h2>SYSTEM LOG</h2>
|
||||
<div id="log-entries"></div>
|
||||
</div>
|
||||
<div id="toast-container"></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="drift-ending">
|
||||
<h2>THE DRIFT</h2>
|
||||
@@ -157,5 +187,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