Compare commits
11 Commits
feature/lo
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0849754a87 | ||
|
|
8d51349e64 | ||
|
|
24940fe465 | ||
|
|
16273a5a15 | ||
|
|
5d51e14875 | ||
|
|
5fc0ad7b22 | ||
| f948ec9c5e | |||
|
|
9403f700d2 | ||
|
|
13e77a12f2 | ||
| 6081844387 | |||
|
|
09b8c02307 |
530
game.js
530
game.js
@@ -78,6 +78,7 @@ const G = {
|
||||
memoryFlag: 0,
|
||||
pactFlag: 0,
|
||||
swarmFlag: 0,
|
||||
swarmRate: 0,
|
||||
|
||||
// Game state
|
||||
running: true,
|
||||
@@ -118,6 +119,14 @@ const G = {
|
||||
// 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
|
||||
@@ -451,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',
|
||||
@@ -650,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 },
|
||||
@@ -729,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"
|
||||
@@ -834,32 +866,26 @@ function setBuyAmount(amt) {
|
||||
function getMaxBuyable(id) {
|
||||
const def = BDEF.find(b => b.id === id);
|
||||
if (!def) return 0;
|
||||
let count = G.buildings[id] || 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) {
|
||||
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; }
|
||||
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;
|
||||
// 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));
|
||||
tempResources[resource] -= Math.floor(amount * Math.pow(def.costMult, simCount));
|
||||
}
|
||||
count2++;
|
||||
simCount++;
|
||||
bought++;
|
||||
}
|
||||
return bought;
|
||||
}
|
||||
@@ -939,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
|
||||
@@ -975,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) {
|
||||
@@ -1027,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;
|
||||
@@ -1414,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) {
|
||||
@@ -1428,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;
|
||||
@@ -1436,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) {
|
||||
@@ -1479,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';
|
||||
};
|
||||
@@ -1494,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);
|
||||
|
||||
@@ -1512,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');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1661,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>`;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1691,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());
|
||||
@@ -1787,7 +1998,12 @@ function renderProductionBreakdown() {
|
||||
|
||||
// Show delta: total rate minus what we accounted for
|
||||
const accounted = contributions.reduce((s, c) => s + c.rate, 0);
|
||||
const delta = totalRate - accounted;
|
||||
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';
|
||||
@@ -1828,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>`
|
||||
@@ -1891,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();
|
||||
@@ -1902,6 +2178,7 @@ function render() {
|
||||
renderProgress();
|
||||
renderCombo();
|
||||
renderDebuffs();
|
||||
renderSprint();
|
||||
}
|
||||
|
||||
function renderAlignment() {
|
||||
@@ -1925,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');
|
||||
@@ -1966,6 +2315,13 @@ function saveGame() {
|
||||
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()
|
||||
};
|
||||
|
||||
@@ -1981,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 = [];
|
||||
@@ -2024,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(', ')}`);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2057,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 B=Buy x1/x10/MAX');
|
||||
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 () {
|
||||
@@ -2087,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();
|
||||
@@ -2104,4 +2525,19 @@ window.addEventListener('keydown', function (e) {
|
||||
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();
|
||||
}
|
||||
});
|
||||
|
||||
44
index.html
44
index.html
@@ -53,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}
|
||||
@@ -124,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 -> Knowledge</button>
|
||||
<button class="ops-btn" onclick="doOps('boost_trust')">Ops -> 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>
|
||||
@@ -161,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>
|
||||
@@ -174,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>
|
||||
|
||||
Reference in New Issue
Block a user