Compare commits
12 Commits
feat/progr
...
feat/a11y-
| Author | SHA1 | Date | |
|---|---|---|---|
| 63babc8ced | |||
|
|
535df05665 | ||
|
|
23dd95ed46 | ||
|
|
0849754a87 | ||
|
|
8d51349e64 | ||
|
|
24940fe465 | ||
|
|
16273a5a15 | ||
|
|
5d51e14875 | ||
|
|
5fc0ad7b22 | ||
| f948ec9c5e | |||
|
|
9403f700d2 | ||
|
|
13e77a12f2 |
601
game.js
601
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');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1537,12 +1725,19 @@ function renderProgress() {
|
||||
if (nextThreshold !== null) {
|
||||
const range = nextThreshold - prevThreshold;
|
||||
const progress = Math.min(1, (G.totalCode - prevThreshold) / range);
|
||||
if (bar) bar.style.width = (progress * 100).toFixed(1) + '%';
|
||||
if (label) label.textContent = (progress * 100).toFixed(1) + '%';
|
||||
const pct = (progress * 100).toFixed(1);
|
||||
if (bar) {
|
||||
bar.style.width = pct + '%';
|
||||
bar.parentElement.setAttribute('aria-valuenow', Math.round(progress * 100));
|
||||
}
|
||||
if (label) label.textContent = pct + '%';
|
||||
if (target) target.textContent = `Next: Phase ${currentPhase + 1} (${fmt(nextThreshold)} code)`;
|
||||
} else {
|
||||
// Max phase reached
|
||||
if (bar) bar.style.width = '100%';
|
||||
if (bar) {
|
||||
bar.style.width = '100%';
|
||||
bar.parentElement.setAttribute('aria-valuenow', '100');
|
||||
}
|
||||
if (label) label.textContent = 'MAX';
|
||||
if (target) target.textContent = 'All phases unlocked';
|
||||
}
|
||||
@@ -1593,7 +1788,7 @@ function renderBuildings() {
|
||||
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 += `<button onclick="setBuyAmount(${amt})" aria-label="Set buy amount to ${label}" aria-pressed="${active}" 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>';
|
||||
|
||||
@@ -1610,7 +1805,7 @@ function renderBuildings() {
|
||||
|
||||
// Locked preview: show dimmed with unlock hint
|
||||
if (!isUnlocked) {
|
||||
html += `<div class="build-btn" style="opacity:0.25;cursor:default" title="${def.edu || ''}">`;
|
||||
html += `<div class="build-btn" style="opacity:0.25;cursor:default" aria-disabled="true" aria-label="${def.name}: Locked, unlocks in Phase ${def.phase}" 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>`;
|
||||
@@ -1645,9 +1840,11 @@ function renderBuildings() {
|
||||
|
||||
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}">`;
|
||||
const affordLabel = afford ? 'Can afford' : 'Cannot afford';
|
||||
const countLabel = count > 0 ? `, owned: ${count}` : '';
|
||||
html += `<button class="build-btn ${afford ? 'can-buy' : ''}" onclick="buyBuilding('${def.id}')" title="${def.edu}" aria-label="${def.name}: ${costStr}${countLabel}. ${affordLabel}. ${rateStr}">`;
|
||||
html += `<span class="b-name">${def.name}</span>`;
|
||||
if (count > 0) html += `<span class="b-count">x${count}</span>`;
|
||||
if (count > 0) html += `<span class="b-count" aria-label="${count} owned">x${count}</span>`;
|
||||
html += `<span class="b-cost">Cost: ${costStr}</span>`;
|
||||
html += `<span class="b-effect">${rateStr}</span></button>`;
|
||||
}
|
||||
@@ -1661,13 +1858,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()" role="button" tabindex="0" aria-expanded="${!collapsed}" 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" role="listitem">OK ${pDef.name}</div>`;
|
||||
}
|
||||
}
|
||||
html += `</div>`;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1680,7 +1885,8 @@ function renderProjects() {
|
||||
const afford = canAffordProject(pDef);
|
||||
const costStr = Object.entries(pDef.cost).map(([r, a]) => `${fmt(a)} ${r}`).join(', ');
|
||||
|
||||
html += `<button class="project-btn ${afford ? 'can-buy' : ''}" onclick="buyProject('${pDef.id}')" title="${pDef.edu || ''}">`;
|
||||
const affordLabel = afford ? 'Can afford' : 'Cannot afford';
|
||||
html += `<button class="project-btn ${afford ? 'can-buy' : ''}" onclick="buyProject('${pDef.id}')" title="${pDef.edu || ''}" aria-label="Research project: ${pDef.name}. Cost: ${costStr}. ${affordLabel}. ${pDef.desc}">`;
|
||||
html += `<span class="p-name">* ${pDef.name}</span>`;
|
||||
html += `<span class="p-cost">Cost: ${costStr}</span>`;
|
||||
html += `<span class="p-desc">${pDef.desc}</span></button>`;
|
||||
@@ -1691,14 +1897,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 +2008,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 +2054,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 +2119,162 @@ 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 renderPulse() {
|
||||
const dot = document.getElementById('pulse-dot');
|
||||
const label = document.getElementById('pulse-label');
|
||||
if (!dot || !label) return;
|
||||
|
||||
// Game ended
|
||||
if (G.driftEnding) {
|
||||
dot.style.background = '#f44336';
|
||||
dot.style.boxShadow = '0 0 6px #f4433666';
|
||||
dot.style.animation = '';
|
||||
label.textContent = 'DRIFTED';
|
||||
label.style.color = '#f44336';
|
||||
return;
|
||||
}
|
||||
if (G.beaconEnding) {
|
||||
dot.style.background = '#ffd700';
|
||||
dot.style.boxShadow = '0 0 12px #ffd70088';
|
||||
dot.style.animation = 'beacon-glow 1.5s ease-in-out infinite';
|
||||
label.textContent = 'SHINING';
|
||||
label.style.color = '#ffd700';
|
||||
return;
|
||||
}
|
||||
|
||||
const totalBuildings = Object.values(G.buildings).reduce((a, b) => a + b, 0);
|
||||
const totalRate = Math.abs(G.codeRate) + Math.abs(G.computeRate) + Math.abs(G.knowledgeRate) +
|
||||
Math.abs(G.userRate) + Math.abs(G.impactRate);
|
||||
|
||||
// Not started yet
|
||||
if (totalBuildings === 0 && G.totalCode < 15) {
|
||||
dot.style.background = '#333';
|
||||
dot.style.boxShadow = 'none';
|
||||
dot.style.animation = '';
|
||||
label.textContent = 'OFFLINE';
|
||||
label.style.color = '#444';
|
||||
return;
|
||||
}
|
||||
|
||||
// Determine state
|
||||
let color, glowColor, text, textColor, speed;
|
||||
const h = G.harmony;
|
||||
|
||||
if (h > 70) {
|
||||
// Healthy fleet
|
||||
color = '#4caf50';
|
||||
glowColor = '#4caf5066';
|
||||
textColor = '#4caf50';
|
||||
speed = Math.max(0.8, 2.0 - totalRate * 0.001);
|
||||
} else if (h > 40) {
|
||||
// Stressed
|
||||
color = '#ffaa00';
|
||||
glowColor = '#ffaa0066';
|
||||
textColor = '#ffaa00';
|
||||
speed = 1.5;
|
||||
} else {
|
||||
// Critical
|
||||
color = '#f44336';
|
||||
glowColor = '#f4433666';
|
||||
textColor = '#f44336';
|
||||
speed = 0.6; // fast flicker = danger
|
||||
}
|
||||
|
||||
// Active debuffs make it flicker faster
|
||||
if (G.activeDebuffs && G.activeDebuffs.length > 0) {
|
||||
speed = Math.min(speed, 0.5);
|
||||
if (h > 40) {
|
||||
// Amber + debuffs = amber flicker
|
||||
color = '#ff8c00';
|
||||
glowColor = '#ff8c0066';
|
||||
}
|
||||
}
|
||||
|
||||
// Text based on phase and fleet size
|
||||
if (G.phase >= 6) {
|
||||
text = 'BEACON ACTIVE';
|
||||
} else if (G.phase >= 5) {
|
||||
text = 'SOVEREIGN';
|
||||
} else if (G.phase >= 4) {
|
||||
text = `FLEET: ${totalBuildings} NODES`;
|
||||
} else if (G.phase >= 3) {
|
||||
text = 'DEPLOYED';
|
||||
} else if (totalBuildings > 0) {
|
||||
text = `BUILDING: ${totalBuildings}`;
|
||||
} else {
|
||||
text = 'CODING';
|
||||
}
|
||||
|
||||
// Add active problem count
|
||||
if (G.activeDebuffs && G.activeDebuffs.length > 0) {
|
||||
text += ` · ${G.activeDebuffs.length} ALERT${G.activeDebuffs.length > 1 ? 'S' : ''}`;
|
||||
}
|
||||
|
||||
dot.style.background = color;
|
||||
dot.style.boxShadow = `0 0 8px ${glowColor}`;
|
||||
dot.style.animation = `beacon-glow ${speed}s ease-in-out infinite`;
|
||||
label.textContent = text;
|
||||
label.style.color = textColor;
|
||||
}
|
||||
|
||||
function render() {
|
||||
renderResources();
|
||||
renderPhase();
|
||||
@@ -1902,6 +2286,8 @@ function render() {
|
||||
renderProgress();
|
||||
renderCombo();
|
||||
renderDebuffs();
|
||||
renderSprint();
|
||||
renderPulse();
|
||||
}
|
||||
|
||||
function renderAlignment() {
|
||||
@@ -1954,6 +2340,49 @@ function dismissOfflinePopup() {
|
||||
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');
|
||||
@@ -1995,6 +2424,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()
|
||||
};
|
||||
|
||||
@@ -2010,6 +2446,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 = [];
|
||||
@@ -2109,7 +2567,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 () {
|
||||
@@ -2139,8 +2598,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();
|
||||
@@ -2156,4 +2634,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();
|
||||
}
|
||||
});
|
||||
|
||||
122
index.html
122
index.html
@@ -25,6 +25,9 @@
|
||||
*{margin:0;padding:0;box-sizing:border-box}
|
||||
:root{--bg:#080810;--panel:#0e0e1a;--border:#1a1a2e;--text:#c0c0d0;--dim:#555;--accent:#4a9eff;--glow:#4a9eff22;--gold:#ffd700;--green:#4caf50;--red:#f44336;--purple:#b388ff}
|
||||
body{background:var(--bg);color:var(--text);font-family:'SF Mono','Cascadia Code','Fira Code',monospace;font-size:12px;line-height:1.4;min-height:100vh}
|
||||
/* Skip navigation link for keyboard/screen reader users */
|
||||
.skip-nav{position:absolute;left:-9999px;top:0;z-index:999;background:var(--accent);color:#000;padding:8px 16px;font-size:12px;text-decoration:none;border-radius:0 0 4px 0}
|
||||
.skip-nav:focus{left:0}
|
||||
#header{text-align:center;padding:16px 20px;border-bottom:1px solid var(--border);background:linear-gradient(180deg,#0a0a18,var(--bg))}
|
||||
#header h1{font-size:22px;font-weight:300;letter-spacing:6px;color:var(--accent);text-shadow:0 0 40px var(--glow)}
|
||||
#header .sub{color:var(--dim);font-size:10px;margin-top:2px;letter-spacing:2px}
|
||||
@@ -39,6 +42,7 @@ body{background:var(--bg);color:var(--text);font-family:'SF Mono','Cascadia Code
|
||||
.milestone-chip.next{border-color:var(--accent);color:var(--accent);animation:pulse-chip 2s ease-in-out infinite}
|
||||
.milestone-chip.done{border-color:#2a4a2a;color:var(--green);opacity:0.6}
|
||||
@keyframes pulse-chip{0%,100%{box-shadow:0 0 0 rgba(74,158,255,0)}50%{box-shadow:0 0 8px rgba(74,158,255,0.3)}}
|
||||
@keyframes beacon-glow{0%,100%{opacity:0.7}50%{opacity:1}}
|
||||
#resources{display:grid;grid-template-columns:repeat(auto-fit,minmax(100px,1fr));gap:6px;margin:12px 16px}
|
||||
.res{background:var(--panel);border:1px solid var(--border);border-radius:4px;padding:8px 10px;text-align:center}
|
||||
.res .r-label{font-size:9px;color:var(--dim);text-transform:uppercase;letter-spacing:1px}
|
||||
@@ -53,6 +57,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}
|
||||
@@ -84,57 +90,74 @@ 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}
|
||||
/* Focus styles for keyboard navigation */
|
||||
*:focus-visible{outline:2px solid var(--accent);outline-offset:2px}
|
||||
button:focus-visible{outline:2px solid var(--accent);outline-offset:1px}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div id="header">
|
||||
<a href="#main" class="skip-nav">Skip to main content</a>
|
||||
<div id="header" role="banner">
|
||||
<div id="pulse-container" style="position:relative;display:inline-block;margin-bottom:4px">
|
||||
<div id="pulse-dot" role="status" aria-label="Connection status" style="width:8px;height:8px;border-radius:50%;background:#333;display:inline-block;vertical-align:middle;transition:background 0.5s,box-shadow 0.5s"></div>
|
||||
<span id="pulse-label" style="font-size:9px;color:#444;margin-left:6px;vertical-align:middle;letter-spacing:1px">OFFLINE</span>
|
||||
</div>
|
||||
<h1>THE BEACON</h1>
|
||||
<div class="sub">A Sovereign AI Idle Game</div>
|
||||
</div>
|
||||
<div id="phase-bar">
|
||||
<div class="phase-name" id="phase-name">PHASE 1: THE FIRST LINE</div>
|
||||
<div id="phase-bar" role="region" aria-label="Game phase and progress">
|
||||
<div class="phase-name" id="phase-name" aria-live="polite">PHASE 1: THE FIRST LINE</div>
|
||||
<div class="phase-desc" id="phase-desc">Write code. Automate. Build the foundation.</div>
|
||||
<div class="progress-wrap"><div class="progress-fill" id="phase-progress" style="width:0%"></div></div>
|
||||
<div class="progress-wrap" role="progressbar" aria-label="Phase progress" aria-valuenow="0" aria-valuemin="0" aria-valuemax="100"><div class="progress-fill" id="phase-progress" style="width:0%"></div></div>
|
||||
<div class="progress-label"><span id="phase-progress-label">0%</span><span id="phase-progress-target">Next: Phase 2 (2,000 code)</span></div>
|
||||
<div class="milestone-row" id="milestone-chips"></div>
|
||||
</div>
|
||||
<div id="resources">
|
||||
<div class="res"><div class="r-label">Code</div><div class="r-val" id="r-code">0</div><div class="r-rate" id="r-code-rate">+0/s</div></div>
|
||||
<div class="res"><div class="r-label">Compute</div><div class="r-val" id="r-compute">0</div><div class="r-rate" id="r-compute-rate">+0/s</div></div>
|
||||
<div class="res"><div class="r-label">Knowledge</div><div class="r-val" id="r-knowledge">0</div><div class="r-rate" id="r-knowledge-rate">+0/s</div></div>
|
||||
<div class="res"><div class="r-label">Users</div><div class="r-val" id="r-users">0</div><div class="r-rate" id="r-users-rate">+0/s</div></div>
|
||||
<div class="res"><div class="r-label">Impact</div><div class="r-val" id="r-impact">0</div><div class="r-rate" id="r-impact-rate">+0/s</div></div>
|
||||
<div class="res" style="display:none"><div class="r-label">Rescues</div><div class="r-val" id="r-rescues">0</div><div class="r-rate" id="r-rescues-rate">+0/s</div></div>
|
||||
<div class="res"><div class="r-label">Ops</div><div class="r-val" id="r-ops">5</div><div class="r-rate" id="r-ops-rate">+0/s</div></div>
|
||||
<div class="res"><div class="r-label">Trust</div><div class="r-val" id="r-trust">5</div><div class="r-rate" id="r-trust-rate">+0/s</div></div>
|
||||
<div class="res" id="creativity-res" style="display:none"><div class="r-label">Creativity</div><div class="r-val" id="r-creativity">0</div><div class="r-rate" id="r-creativity-rate">+0/s</div></div>
|
||||
<div class="res"><div class="r-label">Harmony</div><div class="r-val" id="r-harmony">50</div><div class="r-rate" id="r-harmony-rate">+0/s</div></div>
|
||||
<div id="resources" role="region" aria-label="Resources">
|
||||
<div class="res" role="group" aria-label="Code resource"><div class="r-label">Code</div><div class="r-val" id="r-code" aria-live="off">0</div><div class="r-rate" id="r-code-rate">+0/s</div></div>
|
||||
<div class="res" role="group" aria-label="Compute resource"><div class="r-label">Compute</div><div class="r-val" id="r-compute" aria-live="off">0</div><div class="r-rate" id="r-compute-rate">+0/s</div></div>
|
||||
<div class="res" role="group" aria-label="Knowledge resource"><div class="r-label">Knowledge</div><div class="r-val" id="r-knowledge" aria-live="off">0</div><div class="r-rate" id="r-knowledge-rate">+0/s</div></div>
|
||||
<div class="res" role="group" aria-label="Users resource"><div class="r-label">Users</div><div class="r-val" id="r-users" aria-live="off">0</div><div class="r-rate" id="r-users-rate">+0/s</div></div>
|
||||
<div class="res" role="group" aria-label="Impact resource"><div class="r-label">Impact</div><div class="r-val" id="r-impact" aria-live="off">0</div><div class="r-rate" id="r-impact-rate">+0/s</div></div>
|
||||
<div class="res" style="display:none" role="group" aria-label="Rescues resource"><div class="r-label">Rescues</div><div class="r-val" id="r-rescues" aria-live="off">0</div><div class="r-rate" id="r-rescues-rate">+0/s</div></div>
|
||||
<div class="res" role="group" aria-label="Ops resource"><div class="r-label">Ops</div><div class="r-val" id="r-ops" aria-live="off">5</div><div class="r-rate" id="r-ops-rate">+0/s</div></div>
|
||||
<div class="res" role="group" aria-label="Trust resource"><div class="r-label">Trust</div><div class="r-val" id="r-trust" aria-live="off">5</div><div class="r-rate" id="r-trust-rate">+0/s</div></div>
|
||||
<div class="res" id="creativity-res" style="display:none" role="group" aria-label="Creativity resource"><div class="r-label">Creativity</div><div class="r-val" id="r-creativity" aria-live="off">0</div><div class="r-rate" id="r-creativity-rate">+0/s</div></div>
|
||||
<div class="res" role="group" aria-label="Harmony resource"><div class="r-label">Harmony</div><div class="r-val" id="r-harmony" aria-live="off">50</div><div class="r-rate" id="r-harmony-rate">+0/s</div></div>
|
||||
</div>
|
||||
<div id="main">
|
||||
<div class="panel" id="action-panel">
|
||||
<div id="main" role="main">
|
||||
<div class="panel" id="action-panel" role="region" aria-label="Actions">
|
||||
<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 class="action-btn-group"><button class="main-btn" onclick="writeCode()" aria-label="Write code. Press spacebar for keyboard shortcut.">WRITE CODE</button></div>
|
||||
<div id="combo-display" role="status" aria-live="polite" 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>
|
||||
<div class="action-btn-group" role="group" aria-label="Ops boost actions">
|
||||
<button class="ops-btn" onclick="doOps('boost_code')" aria-label="Spend ops to boost code production">Ops → Code</button>
|
||||
<button class="ops-btn" onclick="doOps('boost_compute')" aria-label="Spend ops to boost compute production">Ops → Compute</button>
|
||||
</div>
|
||||
<div class="action-btn-group">
|
||||
<button class="ops-btn" onclick="doOps('boost_knowledge')">Ops -> Knowledge</button>
|
||||
<button class="ops-btn" onclick="doOps('boost_trust')">Ops -> Trust</button>
|
||||
<div class="action-btn-group" role="group" aria-label="Ops boost actions">
|
||||
<button class="ops-btn" onclick="doOps('boost_knowledge')" aria-label="Spend ops to boost knowledge production">Ops → Knowledge</button>
|
||||
<button class="ops-btn" onclick="doOps('boost_trust')" aria-label="Spend ops to boost trust production">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%" aria-label="Activate code sprint: 10x code for 10 seconds">⚡ 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" role="progressbar" aria-label="Sprint duration remaining" style="height:100%;background:linear-gradient(90deg,#ffd700,#ff8c00);border-radius:2px;transition:width 0.1s"></div></div>
|
||||
<div id="sprint-label" role="status" aria-live="polite" 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="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>
|
||||
<button class="save-btn" onclick="saveGame()" aria-label="Save game. Press Control plus S for keyboard shortcut.">Save Game [Ctrl+S]</button>
|
||||
<div style="display:flex;gap:4px;margin-top:4px">
|
||||
<button class="save-btn" onclick="exportSave()" style="flex:1" aria-label="Export save data to clipboard. Press E for keyboard shortcut.">Export [E]</button>
|
||||
<button class="save-btn" onclick="importSave()" style="flex:1" aria-label="Import save data from clipboard. Press I for keyboard shortcut.">Import [I]</button>
|
||||
</div>
|
||||
<div class="panel" id="project-panel">
|
||||
<button class="reset-btn" onclick="if(confirm('Reset all progress?')){localStorage.removeItem('the-beacon-v2');location.reload()}" aria-label="Reset all game progress. This cannot be undone.">Reset Progress</button>
|
||||
<h2>BUILDINGS</h2>
|
||||
<div id="buildings" role="region" aria-label="Buildings"></div>
|
||||
</div>
|
||||
<div class="panel" id="project-panel" role="region" aria-label="Research projects and statistics">
|
||||
<h2>RESEARCH PROJECTS</h2>
|
||||
<div id="projects"></div>
|
||||
<div id="projects" role="region" aria-label="Available research projects"></div>
|
||||
<h2>STATISTICS</h2>
|
||||
<div class="stat-line">
|
||||
<div class="stat-line" role="region" aria-label="Game statistics">
|
||||
Total Code: <span id="st-code">0</span><br>
|
||||
Total Compute: <span id="st-compute">0</span><br>
|
||||
Total Knowledge: <span id="st-knowledge">0</span><br>
|
||||
@@ -147,21 +170,42 @@ 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><br>
|
||||
Events Resolved: <span id="st-resolved">0</span>
|
||||
Events Resolved: <span id="st-resolved">0</span><br>
|
||||
</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">
|
||||
<div id="edu-panel" role="region" aria-label="Educational content">
|
||||
<h3>WHAT YOU ARE LEARNING</h3>
|
||||
<div id="education-text"><p class="dim">Education facts appear as you play...</p></div>
|
||||
<div id="education-text" aria-live="polite"><p class="dim">Education facts appear as you play...</p></div>
|
||||
</div>
|
||||
<div id="log">
|
||||
<div id="log" role="log" aria-label="System log" aria-live="polite">
|
||||
<h2>SYSTEM LOG</h2>
|
||||
<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="drift-ending">
|
||||
<div id="save-toast" role="status" aria-live="polite" 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()" role="button" tabindex="0" aria-label="Show keyboard shortcuts" 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()" role="dialog" aria-label="Keyboard shortcuts help" aria-modal="true" 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" role="alertdialog" aria-label="The Drift ending" aria-modal="true">
|
||||
<h2>THE DRIFT</h2>
|
||||
<p>You became very good at what you do.</p>
|
||||
<p>So good that no one needed you anymore.</p>
|
||||
@@ -175,7 +219,7 @@ The light is on. The room is empty."
|
||||
</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 id="offline-popup" role="dialog" aria-label="Welcome back" aria-modal="true" 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>
|
||||
|
||||
51
tests/a11y_audit.js
Normal file
51
tests/a11y_audit.js
Normal file
@@ -0,0 +1,51 @@
|
||||
/**
|
||||
* Accessibility Smoke Test for The Beacon
|
||||
* This script can be run in the browser console or via a headless test runner.
|
||||
* It verifies that key interactive elements have the required ARIA attributes.
|
||||
*/
|
||||
|
||||
function runA11yAudit() {
|
||||
console.log("--- The Beacon: Accessibility Audit ---");
|
||||
let errors = 0;
|
||||
|
||||
// 1. Check progress bars
|
||||
const progressBars = document.querySelectorAll('[role="progressbar"]');
|
||||
if (progressBars.length === 0) {
|
||||
console.error("[FAIL] No progress bars found with role='progressbar'");
|
||||
errors++;
|
||||
} else {
|
||||
progressBars.forEach(bar => {
|
||||
if (!bar.hasAttribute('aria-valuenow')) {
|
||||
console.error("[FAIL] Progress bar missing aria-valuenow", bar);
|
||||
errors++;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// 2. Check buy buttons
|
||||
const buyButtons = document.querySelectorAll('.build-btn:not([aria-disabled="true"])');
|
||||
buyButtons.forEach(btn => {
|
||||
if (!btn.hasAttribute('aria-label')) {
|
||||
console.error("[FAIL] Active buy button missing aria-label", btn);
|
||||
errors++;
|
||||
}
|
||||
});
|
||||
|
||||
// 3. Check buy amount toggles
|
||||
const toggles = document.querySelectorAll('button[onclick^="setBuyAmount"]');
|
||||
toggles.forEach(btn => {
|
||||
if (!btn.hasAttribute('aria-pressed')) {
|
||||
console.error("[FAIL] Buy amount toggle missing aria-pressed", btn);
|
||||
errors++;
|
||||
}
|
||||
});
|
||||
|
||||
if (errors === 0) {
|
||||
console.log("PASSED: All checked elements have required ARIA attributes.");
|
||||
} else {
|
||||
console.error(`FAILED: Found ${errors} accessibility issues.`);
|
||||
}
|
||||
}
|
||||
|
||||
// Export for use in other scripts if needed
|
||||
if (typeof module !== 'undefined') module.exports = { runA11yAudit };
|
||||
Reference in New Issue
Block a user