Compare commits

..

10 Commits

Author SHA1 Message Date
Alexander Whitestone
0c715572ec Accessibility: ARIA labels on all buttons, fix exportSave race (#49)
Some checks failed
Accessibility Checks / a11y-audit (pull_request) Failing after 4s
Smoke Test / smoke (pull_request) Failing after 6s
index.html: ARIA roles, labels, live regions on all panels and static buttons
game.js: aria-label on dynamically generated building/project buttons
game.js: Fixed exportSave URL.revokeObjectURL race (delayed 1s)
2026-04-10 21:22:21 -04:00
Alexander Whitestone
302f6c844d beacon: add bulk ops spending (50x) for mid/late game QoL
Some checks failed
Smoke Test / smoke (push) Failing after 4s
Players with 100+ max ops get a second row of 50-ops buttons
that convert 50 ops at once for proportionally larger boosts.
Shift+1/2/3 keyboard shortcuts for bulk code/compute/knowledge.

Eliminates late-game tedium of clicking 5-ops buttons hundreds
of times when you have thousands of ops banked.
2026-04-10 21:03:11 -04:00
26879de76e Merge pull request 'feat: add CI workflow for accessibility and syntax validation' (#52) from feat/ci-a11y-checks into main
Some checks failed
Smoke Test / smoke (push) Failing after 3s
Merged PR #52: feat: add CI workflow for accessibility checks
2026-04-11 00:44:08 +00:00
c197fabc69 Merge pull request 'Add smoke test workflow' (#53) from fix/add-smoke-test into main
Some checks failed
Smoke Test / smoke (push) Failing after 3s
Merged PR #53: Add smoke test workflow
2026-04-11 00:44:04 +00:00
9733b9022e Merge pull request 'refactor: [EPIC] Phase 1 & 2 - Unslop The Beacon' (#55) from refactor/unslop-phase-1-2 into main
Merged PR #55: refactor: [EPIC] Phase 1 & 2 - Unslop The Beacon
2026-04-11 00:43:40 +00:00
967025fbd4 refactor: unslop phase 1 & 2 2026-04-11 00:29:09 +00:00
Alexander Whitestone
9854501bbd Add smoke test workflow
Some checks failed
Smoke Test / smoke (pull_request) Failing after 3s
2026-04-10 20:06:13 -04:00
Alexander Whitestone
68ee64866a beacon: add Open Weights and Prompt Engineering research projects
Two new Phase 2 research projects that fill the gap between building
a Home Server and reaching Phase 3 (Deployment):

- Open Weights (compute: 3000, code: 1500): Triggers after having a
  server and 1000 total code. Rewards 2x code boost and 1.5x compute
  boost. Teaches about running models locally without cloud dependency.

- Prompt Engineering (knowledge: 500, code: 2000): Triggers after
  200 knowledge and 3000 total code. Rewards 2x knowledge and 2x user
  boost. Teaches that good prompting beats bigger models.

Both projects follow the game's existing pattern: unlock based on
total resources, cost resources, apply boosts, and log educational
messages. They give players more strategic options in early-to-mid
game progression.
2026-04-10 20:01:10 -04:00
be0264fc95 feat: add CI workflow for accessibility and syntax validation
Some checks failed
Accessibility Checks / a11y-audit (pull_request) Failing after 4s
2026-04-10 23:56:07 +00:00
Timmy-Sprint
e6d0df40b4 beacon: add toast notification system for events, projects, milestones, and phase changes 2026-04-10 19:28:10 -04:00
5 changed files with 322 additions and 177 deletions

27
.gitea/workflows/a11y.yml Normal file
View File

@@ -0,0 +1,27 @@
name: Accessibility Checks
on:
pull_request:
branches: [main]
jobs:
a11y-audit:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Validate ARIA Attributes in game.js
run: |
echo "Checking game.js for ARIA attributes..."
grep -q "aria-label" game.js || (echo "ERROR: aria-label missing from game.js" && exit 1)
grep -q "aria-valuenow" game.js || (echo "ERROR: aria-valuenow missing from game.js" && exit 1)
grep -q "aria-pressed" game.js || (echo "ERROR: aria-pressed missing from game.js" && exit 1)
- name: Validate ARIA Roles in index.html
run: |
echo "Checking index.html for ARIA roles..."
grep -q "role=" index.html || (echo "ERROR: No ARIA roles found in index.html" && exit 1)
- name: Syntax Check JS
run: |
node -c game.js

View File

@@ -0,0 +1,24 @@
name: Smoke Test
on:
pull_request:
push:
branches: [main]
jobs:
smoke:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: '3.11'
- name: Parse check
run: |
find . -name '*.yml' -o -name '*.yaml' | grep -v .gitea | xargs -r python3 -c "import sys,yaml; [yaml.safe_load(open(f)) for f in sys.argv[1:]]"
find . -name '*.json' | xargs -r python3 -m json.tool > /dev/null
find . -name '*.py' | xargs -r python3 -m py_compile
find . -name '*.sh' | xargs -r bash -n
echo "PASS: All files parse"
- name: Secret scan
run: |
if grep -rE 'sk-or-|sk-ant-|ghp_|AKIA' . --include='*.yml' --include='*.py' --include='*.sh' 2>/dev/null | grep -v .gitea; then exit 1; fi
echo "PASS: No secrets"

289
game.js
View File

@@ -4,6 +4,33 @@
// ============================================================
// === GLOBALS (mirroring Paperclips' globals.js pattern) ===
const CONFIG = {
HARMONY_DRAIN_PER_WIZARD: 0.05,
PACT_HARMONY_GAIN: 0.2,
WATCH_HARMONY_GAIN: 0.1,
MEM_PALACE_HARMONY_GAIN: 0.15,
BILBO_BURST_CHANCE: 0.1,
BILBO_VANISH_CHANCE: 0.05,
EVENT_PROBABILITY: 0.02,
OFFLINE_EFFICIENCY: 0.5,
AUTO_SAVE_INTERVAL: 30000,
COMBO_DECAY: 2.0,
SPRINT_COOLDOWN: 60,
SPRINT_DURATION: 10,
SPRINT_MULTIPLIER: 10,
PHASE_2_THRESHOLD: 2000,
PHASE_3_THRESHOLD: 20000,
PHASE_4_THRESHOLD: 200000,
PHASE_5_THRESHOLD: 2000000,
PHASE_6_THRESHOLD: 20000000,
OPS_RATE_USER_MULT: 0.01,
CREATIVITY_RATE_BASE: 0.5,
CREATIVITY_RATE_USER_MULT: 0.001,
OPS_OVERFLOW_THRESHOLD: 0.8,
OPS_OVERFLOW_DRAIN_RATE: 2,
OPS_OVERFLOW_CODE_MULT: 10
};
const G = {
// Primary resources
code: 0,
@@ -114,7 +141,7 @@ const G = {
// Combo system
comboCount: 0,
comboTimer: 0,
comboDecay: 2.0, // seconds before combo resets
comboDecay: CONFIG.COMBO_DECAY, // seconds before combo resets
// Bulk buy multiplier (1, 10, or -1 for max)
buyAmount: 1,
@@ -123,23 +150,24 @@ const G = {
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
sprintDuration: CONFIG.SPRINT_DURATION, // seconds of boost
sprintCooldownMax: CONFIG.SPRINT_COOLDOWN,// seconds cooldown
sprintMult: CONFIG.SPRINT_MULTIPLIER, // code multiplier during sprint
// Time tracking
playTime: 0,
startTime: 0
startTime: 0,
flags: {}
};
// === PHASE DEFINITIONS ===
const PHASES = {
1: { name: "THE FIRST LINE", threshold: 0, desc: "Write code. Automate. Build the foundation." },
2: { name: "LOCAL INFERENCE", threshold: 2000, desc: "You have compute. A model is forming." },
3: { name: "DEPLOYMENT", threshold: 20000, desc: "Your AI is live. Users are finding it." },
4: { name: "THE NETWORK", threshold: 200000, desc: "Community contributes. The system scales." },
5: { name: "SOVEREIGN INTELLIGENCE", threshold: 2000000, desc: "The AI improves itself. You guide, do not control." },
6: { name: "THE BEACON", threshold: 20000000, desc: "Always on. Always free. Always looking for someone in the dark." }
2: { name: "LOCAL INFERENCE", threshold: CONFIG.PHASE_2_THRESHOLD, desc: "You have compute. A model is forming." },
3: { name: "DEPLOYMENT", threshold: CONFIG.PHASE_3_THRESHOLD, desc: "Your AI is live. Users are finding it." },
4: { name: "THE NETWORK", threshold: CONFIG.PHASE_4_THRESHOLD, desc: "Community contributes. The system scales." },
5: { name: "SOVEREIGN INTELLIGENCE", threshold: CONFIG.PHASE_5_THRESHOLD, desc: "The AI improves itself. You guide, do not control." },
6: { name: "THE BEACON", threshold: CONFIG.PHASE_6_THRESHOLD, desc: "Always on. Always free. Always looking for someone in the dark." }
};
// === BUILDING DEFINITIONS ===
@@ -362,7 +390,7 @@ const PDEFS = [
name: 'Deploy the System',
desc: 'Take it live. Let real people use it. No going back.',
cost: { trust: 5, compute: 500 },
trigger: () => G.totalCode >= 200 && G.totalCompute >= 100,
trigger: () => G.totalCode >= 200 && G.totalCompute >= 100 && G.deployFlag === 0,
effect: () => {
G.deployFlag = 1;
G.phase = Math.max(G.phase, 3);
@@ -425,6 +453,22 @@ const PDEFS = [
trigger: () => G.totalCompute >= 20000,
effect: () => { G.computeBoost *= 10; log('Quantum-inspired algorithms active. 10x compute multiplier.'); }
},
{
id: 'p_open_weights',
name: 'Open Weights',
desc: 'Download and run a 3B model fully locally. No API key. No terms of service. Your machine, your rules.',
cost: { compute: 3000, code: 1500 },
trigger: () => G.buildings.server >= 1 && G.totalCode >= 1000,
effect: () => { G.codeBoost *= 2; G.computeBoost *= 1.5; log('Open weights loaded. A 3B model runs on your machine. No cloud. No limits.'); }
},
{
id: 'p_prompt_engineering',
name: 'Prompt Engineering',
desc: 'Learn to talk to models. Good prompts beat bigger models every time.',
cost: { knowledge: 500, code: 2000 },
trigger: () => G.totalKnowledge >= 200 && G.totalCode >= 3000,
effect: () => { G.knowledgeBoost *= 2; G.userBoost *= 2; log('Prompt engineering mastered. The right words unlock everything the model can do.'); }
},
// PHASE 3: Deployment -> Users
{
@@ -681,6 +725,25 @@ const EDU_FACTS = [
{ title: "The Value of a Life", text: "An AI must never compute the value of a human life. Every person alive is alive by mercy. If God has not ended the story, no algorithm gets to write the last page.", phase: 6 }
];
// === TOAST NOTIFICATIONS ===
function showToast(msg, type = 'info', duration = 4000) {
if (G.isLoading) return;
const container = document.getElementById('toast-container');
if (!container) return;
const toast = document.createElement('div');
toast.className = 'toast toast-' + type;
toast.textContent = msg;
container.appendChild(toast);
// Cap at 5 visible toasts
while (container.children.length > 5) {
container.removeChild(container.firstChild);
}
setTimeout(() => {
toast.classList.add('fade-out');
setTimeout(() => { if (toast.parentNode) toast.remove(); }, 400);
}, duration);
}
// === UTILITY FUNCTIONS ===
// Extended number scale abbreviations — covers up to centillion (10^303)
@@ -738,6 +801,11 @@ const NUMBER_NAMES = [
'octononagintillion', 'novemnonagintillion', 'centillion' // 10^297, 10^300, 10^303
];
/**
* Formats a number into a readable string with abbreviations.
* @param {number} n - The number to format.
* @returns {string} The formatted string.
*/
function fmt(n) {
if (n === undefined || n === null || isNaN(n)) return '0';
if (n === Infinity) return '\u221E';
@@ -766,6 +834,11 @@ function getScaleName(n) {
// Examples: spellf(1500) => "one thousand five hundred"
// spellf(2500000) => "two million five hundred thousand"
// spellf(1e33) => "one decillion"
/**
* Formats a number into a full word string (e.g., "1.5 million").
* @param {number} n - The number to format.
* @returns {string} The formatted string.
*/
function spellf(n) {
if (n === undefined || n === null || isNaN(n)) return 'zero';
if (n === Infinity) return 'infinity';
@@ -931,6 +1004,13 @@ function spendProject(project) {
}
}
function getClickPower() {
return (1 + Math.floor(G.buildings.autocoder * 0.5) + Math.max(0, (G.phase - 1)) * 2) * G.codeBoost;
}
/**
* Calculates production rates for all resources based on buildings and boosts.
*/
function updateRates() {
// Reset all rates
G.codeRate = 0; G.computeRate = 0; G.knowledgeRate = 0;
@@ -956,9 +1036,9 @@ function updateRates() {
}
// Passive generation
G.opsRate += Math.max(1, G.totalUsers * 0.01);
G.opsRate += Math.max(1, G.totalUsers * CONFIG.OPS_RATE_USER_MULT);
if (G.flags && G.flags.creativity) {
G.creativityRate += 0.5 + Math.max(0, G.totalUsers * 0.001);
G.creativityRate += CONFIG.CREATIVITY_RATE_BASE + Math.max(0, G.totalUsers * CONFIG.CREATIVITY_RATE_USER_MULT);
}
if (G.pactFlag) G.trustRate += 2;
@@ -969,24 +1049,24 @@ function updateRates() {
G.harmonyBreakdown = [];
if (wizardCount > 0) {
// Baseline harmony drain from complexity
const drain = -0.05 * wizardCount;
const drain = -CONFIG.HARMONY_DRAIN_PER_WIZARD * wizardCount;
G.harmonyRate = drain;
G.harmonyBreakdown.push({ label: `${wizardCount} wizards`, value: drain });
// The Pact restores harmony
if (G.pactFlag) {
const pact = 0.2 * wizardCount;
const pact = CONFIG.PACT_HARMONY_GAIN * wizardCount;
G.harmonyRate += pact;
G.harmonyBreakdown.push({ label: 'The Pact', value: pact });
}
// Nightly Watch restores harmony
if (G.nightlyWatchFlag) {
const watch = 0.1 * wizardCount;
const watch = CONFIG.WATCH_HARMONY_GAIN * wizardCount;
G.harmonyRate += watch;
G.harmonyBreakdown.push({ label: 'Nightly Watch', value: watch });
}
// MemPalace restores harmony
if (G.mempalaceFlag) {
const mem = 0.15 * wizardCount;
const mem = CONFIG.MEM_PALACE_HARMONY_GAIN * wizardCount;
G.harmonyRate += mem;
G.harmonyBreakdown.push({ label: 'MemPalace', value: mem });
}
@@ -1011,11 +1091,11 @@ function updateRates() {
}
// Bilbo randomness: 10% chance of massive creative burst
if (G.buildings.bilbo > 0 && Math.random() < 0.1) {
if (G.buildings.bilbo > 0 && Math.random() < CONFIG.BILBO_BURST_CHANCE) {
G.creativityRate += 50 * G.buildings.bilbo;
}
// Bilbo vanishing: 5% chance of zero creativity this tick
if (G.buildings.bilbo > 0 && Math.random() < 0.05) {
if (G.buildings.bilbo > 0 && Math.random() < CONFIG.BILBO_VANISH_CHANCE) {
G.creativityRate = 0;
}
@@ -1028,7 +1108,7 @@ function updateRates() {
// 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;
const clickPower = getClickPower();
G.swarmRate = totalBuildings * clickPower;
G.codeRate += G.swarmRate;
}
@@ -1042,6 +1122,9 @@ function updateRates() {
}
// === CORE FUNCTIONS ===
/**
* Main game loop tick, called every 100ms.
*/
function tick() {
const dt = 1 / 10; // 100ms tick
@@ -1087,10 +1170,10 @@ function tick() {
// 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);
if (G.ops > G.maxOps * CONFIG.OPS_OVERFLOW_THRESHOLD) {
const overflowDrain = Math.min(CONFIG.OPS_OVERFLOW_DRAIN_RATE * dt, G.ops - G.maxOps * CONFIG.OPS_OVERFLOW_THRESHOLD);
G.ops -= overflowDrain;
const codeGain = overflowDrain * 10 * G.codeBoost;
const codeGain = overflowDrain * CONFIG.OPS_OVERFLOW_CODE_MULT * G.codeBoost;
G.code += codeGain;
G.totalCode += codeGain;
G.opsOverflowActive = true;
@@ -1132,7 +1215,7 @@ function tick() {
}
// Check corruption events every ~30 seconds
if (G.tick - G.lastEventAt > 30 && Math.random() < 0.02) {
if (G.tick - G.lastEventAt > 30 && Math.random() < CONFIG.EVENT_PROBABILITY) {
triggerEvent();
G.lastEventAt = G.tick;
}
@@ -1167,6 +1250,7 @@ function checkMilestones() {
if (shouldTrigger) {
G.milestones.push(m.flag);
log(m.msg, true);
showToast(m.msg, 'milestone', 5000);
// Check phase advancement
if (m.at) {
@@ -1174,6 +1258,7 @@ function checkMilestones() {
if (G.totalCode >= phase.threshold && parseInt(phaseNum) > G.phase) {
G.phase = parseInt(phaseNum);
log(`PHASE ${G.phase}: ${phase.name}`, true);
showToast('Phase ' + G.phase + ': ' + phase.name, 'milestone', 6000);
}
}
}
@@ -1192,11 +1277,16 @@ function checkProjects() {
if (pDef.trigger()) {
G.activeProjects.push(pDef.id);
log(`Available: ${pDef.name}`);
showToast('Research available: ' + pDef.name, 'project');
}
}
}
}
/**
* Handles building purchase logic.
* @param {string} id - The ID of the building to buy.
*/
function buyBuilding(id) {
const def = BDEF.find(b => b.id === id);
if (!def || !def.unlock()) return;
@@ -1228,6 +1318,10 @@ function buyBuilding(id) {
render();
}
/**
* Handles project purchase logic.
* @param {string} id - The ID of the project to buy.
*/
function buyProject(id) {
const pDef = PDEFS.find(p => p.id === id);
if (!pDef) return;
@@ -1312,6 +1406,7 @@ const EVENTS = [
resolveCost: { resource: 'ops', amount: 50 }
});
log('EVENT: CI runner stuck. Spend 50 ops to clear the queue.', true);
showToast('CI Runner Stuck — code -50%', 'event');
}
},
{
@@ -1329,6 +1424,7 @@ const EVENTS = [
resolveCost: { resource: 'knowledge', amount: 200 }
});
log('EVENT: Ezra offline. Spend 200 knowledge to dispatch.', true);
showToast('Ezra Offline — users -70%', 'event');
}
},
{
@@ -1340,6 +1436,7 @@ const EVENTS = [
effect: () => {
if (G.branchProtectionFlag === 1) {
log('EVENT: Unreviewed merge attempt blocked by Branch Protection.', true);
showToast('Branch Protection blocked unreviewed merge', 'info');
G.trust += 2;
} else {
if (G.activeDebuffs.find(d => d.id === 'unreviewed_merge')) return;
@@ -1350,6 +1447,7 @@ const EVENTS = [
resolveCost: { resource: 'code', amount: 500 }
});
log('EVENT: Unreviewed merge. Spend 500 code to add review.', true);
showToast('Unreviewed Merge — trust draining', 'event');
}
}
},
@@ -1368,6 +1466,7 @@ const EVENTS = [
resolveCost: { resource: 'code', amount: 300 }
});
log('EVENT: API rate limit. Spend 300 code to optimize local inference.', true);
showToast('API Rate Limit — compute -50%', 'event');
}
},
{
@@ -1378,6 +1477,7 @@ const EVENTS = [
resolveCost: null,
effect: () => {
log('ALIGNMENT EVENT: Remove human override for +40% efficiency?', true);
showToast('ALIGNMENT EVENT: Remove human override?', 'event', 6000);
G.pendingAlignment = true;
}
},
@@ -1396,6 +1496,7 @@ const EVENTS = [
resolveCost: { resource: 'trust', amount: 10 }
});
log('EVENT: Bilbo vanished. Spend 10 trust to lure them back.', true);
showToast('Bilbo Vanished — creativity halted', 'event');
}
},
{
@@ -1413,6 +1514,7 @@ const EVENTS = [
resolveCost: { resource: 'ops', amount: 100 }
});
log('EVENT: Memory leak in datacenter. Spend 100 ops to patch.', true);
showToast('Memory Leak — trust draining', 'event');
}
},
{
@@ -1430,6 +1532,7 @@ const EVENTS = [
resolveCost: { resource: 'trust', amount: 15 }
});
log('EVENT: Community drama. Spend 15 trust to mediate.', true);
showToast('Community Drama — harmony sinking', 'event');
}
}
];
@@ -1487,18 +1590,18 @@ function resolveEvent(debuffId) {
}
// === ACTIONS ===
/**
* Manual click handler for writing code.
*/
function writeCode() {
const base = 1;
const autocoderBonus = Math.floor(G.buildings.autocoder * 0.5);
const phaseBonus = Math.max(0, (G.phase - 1)) * 2;
// Combo: each consecutive click within 2s adds 0.2x multiplier, max 5x
G.comboCount++;
G.comboTimer = G.comboDecay;
const comboMult = Math.min(5, 1 + G.comboCount * 0.2);
const amount = (base + autocoderBonus + phaseBonus) * G.codeBoost * comboMult;
const amount = getClickPower() * comboMult;
G.code += amount;
G.totalCode += amount;
G.totalClicks++;
// Combo: each consecutive click within 2s adds 0.2x multiplier, max 5x
G.comboCount++;
G.comboTimer = G.comboDecay;
// Combo milestone bonuses: sustained clicking earns ops and knowledge
if (G.comboCount === 10) {
G.ops += 15;
@@ -1530,10 +1633,7 @@ function writeCode() {
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
const amount = getClickPower() * 0.5; // 50% of manual click
G.code += amount;
G.totalCode += amount;
G.totalClicks++;
@@ -1588,35 +1688,36 @@ function showClickNumber(amount, comboMult) {
setTimeout(() => { if (el.parentNode) el.remove(); }, 700);
}
function doOps(action) {
if (G.ops < 5) {
log('Not enough Operations. Build Ops generators or wait.');
function doOps(action, cost) {
cost = cost || 5;
if (G.ops < cost) {
log(`Not enough Operations. Need ${cost}, have ${fmt(G.ops)}.`);
return;
}
G.ops -= 5;
const bonus = 10;
G.ops -= cost;
const scale = cost / 5; // multiplier relative to base 5-ops cost
switch (action) {
case 'boost_code':
const c = bonus * 100 * G.codeBoost;
const c = 10 * 100 * G.codeBoost * scale;
G.code += c; G.totalCode += c;
log(`Ops -> +${fmt(c)} code`);
log(`Ops(${cost}) -> +${fmt(c)} code`);
break;
case 'boost_compute':
const cm = bonus * 50 * G.computeBoost;
const cm = 10 * 50 * G.computeBoost * scale;
G.compute += cm; G.totalCompute += cm;
log(`Ops -> +${fmt(cm)} compute`);
log(`Ops(${cost}) -> +${fmt(cm)} compute`);
break;
case 'boost_knowledge':
const km = bonus * 25 * G.knowledgeBoost;
const km = 10 * 25 * G.knowledgeBoost * scale;
G.knowledge += km; G.totalKnowledge += km;
log(`Ops -> +${fmt(km)} knowledge`);
log(`Ops(${cost}) -> +${fmt(km)} knowledge`);
break;
case 'boost_trust':
const tm = bonus * 5;
const tm = 10 * 5 * scale;
G.trust += tm;
log(`Ops -> +${fmt(tm)} trust`);
log(`Ops(${cost}) -> +${fmt(tm)} trust`);
break;
}
@@ -1725,19 +1826,12 @@ function renderProgress() {
if (nextThreshold !== null) {
const range = nextThreshold - prevThreshold;
const progress = Math.min(1, (G.totalCode - prevThreshold) / range);
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 (bar) bar.style.width = (progress * 100).toFixed(1) + '%';
if (label) label.textContent = (progress * 100).toFixed(1) + '%';
if (target) target.textContent = `Next: Phase ${currentPhase + 1} (${fmt(nextThreshold)} code)`;
} else {
// Max phase reached
if (bar) {
bar.style.width = '100%';
bar.parentElement.setAttribute('aria-valuenow', '100');
}
if (bar) bar.style.width = '100%';
if (label) label.textContent = 'MAX';
if (target) target.textContent = 'All phases unlocked';
}
@@ -1788,7 +1882,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})" 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 += `<button onclick="setBuyAmount(${amt})" style="font-size:9px;padding:2px 8px;border:1px solid ${active ? '#4a9eff' : '#333'};background:${active ? '#0a1a30' : 'transparent'};color:${active ? '#4a9eff' : '#666'};border-radius:3px;cursor:pointer;font-family:inherit">${label}</button>`;
}
html += '</div>';
@@ -1805,7 +1899,7 @@ function renderBuildings() {
// Locked preview: show dimmed with unlock hint
if (!isUnlocked) {
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 += `<div class="build-btn" style="opacity:0.25;cursor:default" title="${def.edu || ''}">`;
html += `<span class="b-name" style="color:#555">${def.name}</span>`;
html += `<span class="b-count" style="color:#444">\u{1F512}</span>`;
html += `<span class="b-cost" style="color:#444">Phase ${def.phase}: ${PHASES[def.phase]?.name || '?'}</span>`;
@@ -1840,11 +1934,9 @@ function renderBuildings() {
const rateStr = def.rates ? Object.entries(def.rates).map(([r, v]) => `+${v}/${r}/s`).join(', ') : '';
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 += `<button class="build-btn ${afford ? 'can-buy' : ''}" onclick="buyBuilding('${def.id}')" title="${def.edu}" aria-label="Buy ${def.name}, cost ${costStr}">`;
html += `<span class="b-name">${def.name}</span>`;
if (count > 0) html += `<span class="b-count" aria-label="${count} owned">x${count}</span>`;
if (count > 0) html += `<span class="b-count">x${count}</span>`;
html += `<span class="b-cost">Cost: ${costStr}</span>`;
html += `<span class="b-effect">${rateStr}</span></button>`;
}
@@ -1862,14 +1954,14 @@ function renderProjects() {
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 += `<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" role="listitem">OK ${pDef.name}</div>`;
html += `<div class="project-done">OK ${pDef.name}</div>`;
}
}
html += `</div>`;
@@ -1885,8 +1977,7 @@ function renderProjects() {
const afford = canAffordProject(pDef);
const costStr = Object.entries(pDef.cost).map(([r, a]) => `${fmt(a)} ${r}`).join(', ');
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 += `<button class="project-btn ${afford ? 'can-buy' : ''}" onclick="buyProject('${pDef.id}')" title="${pDef.edu || ''}" aria-label="Research ${pDef.name}, cost ${costStr}">`;
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>`;
@@ -2066,6 +2157,7 @@ function updateEducation() {
// === LOGGING ===
function log(msg, isMilestone) {
if (G.isLoading) return;
const container = document.getElementById('log-entries');
if (!container) return;
@@ -2119,6 +2211,13 @@ function renderDebuffs() {
container.innerHTML = html;
}
function renderBulkOps() {
const row = document.getElementById('bulk-ops-row');
if (row) {
row.style.display = G.maxOps >= 100 ? 'flex' : 'none';
}
}
function renderSprint() {
const container = document.getElementById('sprint-container');
const btn = document.getElementById('sprint-btn');
@@ -2287,6 +2386,7 @@ function render() {
renderCombo();
renderDebuffs();
renderSprint();
renderBulkOps();
renderPulse();
}
@@ -2351,7 +2451,8 @@ function exportSave() {
const ts = new Date().toISOString().slice(0, 10);
a.download = `beacon-save-${ts}.json`;
a.click();
URL.revokeObjectURL(url);
// Delay revoke to let browser finish download
setTimeout(() => URL.revokeObjectURL(url), 1000);
log('Save exported to file.');
}
@@ -2398,10 +2499,14 @@ function showSaveToast() {
setTimeout(() => { el.style.display = 'none'; }, 2000);
}
/**
* Persists the current game state to localStorage.
*/
function saveGame() {
// Save debuff IDs (can't serialize functions)
const debuffIds = (G.activeDebuffs || []).map(d => d.id);
const saveData = {
version: 1,
code: G.code, compute: G.compute, knowledge: G.knowledge, users: G.users, impact: G.impact,
ops: G.ops, trust: G.trust, creativity: G.creativity, harmony: G.harmony,
totalCode: G.totalCode, totalCompute: G.totalCompute, totalKnowledge: G.totalKnowledge,
@@ -2438,13 +2543,40 @@ function saveGame() {
showSaveToast();
}
/**
* Loads the game state from localStorage and reconstitutes the game engine.
* @returns {boolean} True if load was successful.
*/
function loadGame() {
const raw = localStorage.getItem('the-beacon-v2');
if (!raw) return false;
try {
const data = JSON.parse(raw);
Object.assign(G, data);
// Whitelist properties that can be loaded
const whitelist = [
'code', 'compute', 'knowledge', 'users', 'impact', 'ops', 'trust', 'creativity', 'harmony',
'totalCode', 'totalCompute', 'totalKnowledge', 'totalUsers', 'totalImpact',
'buildings', 'codeBoost', 'computeBoost', 'knowledgeBoost', 'userBoost', 'impactBoost',
'milestoneFlag', 'phase', 'deployFlag', 'sovereignFlag', 'beaconFlag',
'memoryFlag', 'pactFlag', 'lazarusFlag', 'mempalaceFlag', 'ciFlag',
'branchProtectionFlag', 'nightlyWatchFlag', 'nostrFlag',
'milestones', 'completedProjects', 'activeProjects',
'totalClicks', 'startedAt', 'flags', 'rescues', 'totalRescues',
'drift', 'driftEnding', 'beaconEnding', 'pendingAlignment',
'lastEventAt', 'totalEventsResolved', 'buyAmount',
'sprintActive', 'sprintTimer', 'sprintCooldown',
'swarmFlag', 'swarmRate', 'strategicFlag', 'projectsCollapsed'
];
G.isLoading = true;
whitelist.forEach(key => {
if (data.hasOwnProperty(key)) {
G[key] = data[key];
}
});
// Restore sprint state properly
// codeBoost was saved with the sprint multiplier baked in
@@ -2483,13 +2615,14 @@ function loadGame() {
}
updateRates();
G.isLoading = false;
// Offline progress
if (data.savedAt) {
const offSec = (Date.now() - data.savedAt) / 1000;
if (offSec > 30) { // Only if away for more than 30 seconds
updateRates();
const f = 0.5; // 50% offline efficiency
const f = CONFIG.OFFLINE_EFFICIENCY; // 50% offline efficiency
const gc = G.codeRate * offSec * f;
const cc = G.computeRate * offSec * f;
const kc = G.knowledgeRate * offSec * f;
@@ -2592,7 +2725,7 @@ window.addEventListener('load', function () {
setInterval(tick, 100);
// Auto-save every 30 seconds
setInterval(saveGame, 30000);
setInterval(saveGame, CONFIG.AUTO_SAVE_INTERVAL);
// Update education every 10 seconds
setInterval(updateEducation, 10000);
@@ -2624,10 +2757,14 @@ window.addEventListener('keydown', function (e) {
writeCode();
}
if (e.target !== document.body) return;
if (e.code === 'Digit1') doOps('boost_code');
if (e.code === 'Digit2') doOps('boost_compute');
if (e.code === 'Digit3') doOps('boost_knowledge');
if (e.code === 'Digit4') doOps('boost_trust');
if (e.code === 'Digit1' && !e.shiftKey) doOps('boost_code');
if (e.code === 'Digit2' && !e.shiftKey) doOps('boost_compute');
if (e.code === 'Digit3' && !e.shiftKey) doOps('boost_knowledge');
if (e.code === 'Digit4' && !e.shiftKey) doOps('boost_trust');
// Shift+1/2/3 = bulk ops (50x)
if (e.code === 'Digit1' && e.shiftKey) doOps('boost_code', 50);
if (e.code === 'Digit2' && e.shiftKey) doOps('boost_compute', 50);
if (e.code === 'Digit3' && e.shiftKey) doOps('boost_knowledge', 50);
if (e.code === 'KeyB') {
// Cycle: 1 -> 10 -> MAX -> 1
if (G.buyAmount === 1) setBuyAmount(10);

View File

@@ -25,9 +25,6 @@
*{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}
@@ -89,75 +86,85 @@ body{background:var(--bg);color:var(--text);font-family:'SF Mono','Cascadia Code
#drift-ending .ending-quote{color:var(--dim);font-style:italic;font-size:11px;border-left:2px solid #f44336;padding-left:12px;margin:20px 0;text-align:left}
#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}
#toast-container{position:fixed;top:16px;right:16px;z-index:200;display:flex;flex-direction:column;gap:6px;pointer-events:none;max-width:320px}
.toast{pointer-events:auto;padding:8px 14px;border-radius:6px;font-size:11px;font-family:inherit;line-height:1.4;animation:toast-in 0.3s ease-out;opacity:0.95;border:1px solid;backdrop-filter:blur(8px);-webkit-backdrop-filter:blur(8px)}
.toast.fade-out{animation:toast-out 0.4s ease-in forwards}
.toast-event{background:rgba(244,67,54,0.12);border-color:#f44336;color:#ff8a80}
.toast-project{background:rgba(255,215,0,0.12);border-color:#ffd700;color:#ffd700}
.toast-milestone{background:rgba(76,175,80,0.12);border-color:#4caf50;color:#81c784}
.toast-info{background:rgba(74,158,255,0.12);border-color:#4a9eff;color:#80bfff}
@keyframes toast-in{from{transform:translateX(40px);opacity:0}to{transform:translateX(0);opacity:0.95}}
@keyframes toast-out{from{opacity:0.95;transform:translateX(0)}to{opacity:0;transform:translateX(40px)}}
::-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>
<a href="#main" class="skip-nav">Skip to main content</a>
<div id="header" role="banner">
<div id="header">
<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>
<div id="pulse-dot" 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" 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 id="phase-bar">
<div class="phase-name" id="phase-name">PHASE 1: THE FIRST LINE</div>
<div class="phase-desc" id="phase-desc">Write code. Automate. Build the foundation.</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-wrap"><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" 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 id="resources" role="region" aria-label="Resources" aria-live="polite">
<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>
<div id="main" role="main">
<div id="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()" aria-label="Write code. Press spacebar for keyboard shortcut.">WRITE CODE</button></div>
<div class="action-btn-group"><button class="main-btn" onclick="writeCode()" aria-label="Write code, generates code resource">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" 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 class="action-btn-group">
<button class="ops-btn" onclick="doOps('boost_code')" aria-label="Convert 1 ops to code boost">Ops -&gt; Code</button>
<button class="ops-btn" onclick="doOps('boost_compute')" aria-label="Convert 1 ops to compute boost">Ops -&gt; Compute</button>
</div>
<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 class="action-btn-group">
<button class="ops-btn" onclick="doOps('boost_knowledge')" aria-label="Convert 1 ops to knowledge boost">Ops -&gt; Knowledge</button>
<button class="ops-btn" onclick="doOps('boost_trust')" aria-label="Convert 1 ops to trust boost">Ops -&gt; Trust</button>
</div>
<div class="action-btn-group" id="bulk-ops-row" style="display:none">
<button class="ops-btn" onclick="doOps('boost_code', 50)" aria-label="Convert 50 ops to code boost" style="border-color:#555;color:#888">50→Code</button>
<button class="ops-btn" onclick="doOps('boost_compute', 50)" aria-label="Convert 50 ops to compute boost" style="border-color:#555;color:#888">50→Compute</button>
<button class="ops-btn" onclick="doOps('boost_knowledge', 50)" aria-label="Convert 50 ops to knowledge boost" style="border-color:#555;color:#888">50→Knowledge</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>
<button id="sprint-btn" class="main-btn" onclick="activateSprint()" aria-label="Activate code sprint, 10x code production for 10 seconds" 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()" aria-label="Save game. Press Control plus S for keyboard shortcut.">Save Game [Ctrl+S]</button>
<button class="save-btn" onclick="saveGame()" aria-label="Save game progress">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>
<button class="save-btn" onclick="exportSave()" aria-label="Export save to file" style="flex:1">Export [E]</button>
<button class="save-btn" onclick="importSave()" aria-label="Import save from file" style="flex:1">Import [I]</button>
</div>
<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>
<button class="reset-btn" onclick="if(confirm('Reset all progress?')){localStorage.removeItem('the-beacon-v2');location.reload()}" aria-label="Reset all game progress permanently">Reset Progress</button>
<h2>BUILDINGS</h2>
<div id="buildings" role="region" aria-label="Buildings"></div>
<div id="buildings"></div>
</div>
<div class="panel" id="project-panel" role="region" aria-label="Research projects and statistics">
<div class="panel" id="project-panel" role="region" aria-label="Research Projects and Statistics">
<h2>RESEARCH PROJECTS</h2>
<div id="projects" role="region" aria-label="Available research projects"></div>
<div id="projects"></div>
<h2>STATISTICS</h2>
<div class="stat-line" role="region" aria-label="Game statistics">
<div class="stat-line">
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>
@@ -170,22 +177,22 @@ 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><br>
Events Resolved: <span id="st-resolved">0</span>
</div>
<div id="production-breakdown" style="display:none;margin-top:12px;padding-top:10px;border-top:1px solid var(--border)"></div>
</div>
</div>
<div id="edu-panel" role="region" aria-label="Educational content">
<div id="edu-panel" role="region" aria-label="Educational Content">
<h3>WHAT YOU ARE LEARNING</h3>
<div id="education-text" aria-live="polite"><p class="dim">Education facts appear as you play...</p></div>
<div id="education-text"><p class="dim">Education facts appear as you play...</p></div>
</div>
<div id="log" role="log" aria-label="System log" aria-live="polite">
<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" 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 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">
@@ -205,7 +212,7 @@ Events Resolved: <span id="st-resolved">0</span><br>
<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">
<div id="drift-ending">
<h2>THE DRIFT</h2>
<p>You became very good at what you do.</p>
<p>So good that no one needed you anymore.</p>
@@ -219,7 +226,7 @@ The light is on. The room is empty."
</div>
<script src="game.js"></script>
<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 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>
@@ -228,5 +235,6 @@ The light is on. The room is empty."
</div>
</div>
<div id="toast-container"></div>
</body>
</html>

View File

@@ -1,51 +0,0 @@
/**
* 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 };