Compare commits
2 Commits
fix/access
...
feat/a11y-
| Author | SHA1 | Date | |
|---|---|---|---|
| 63babc8ced | |||
|
|
535df05665 |
@@ -1,27 +0,0 @@
|
||||
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
|
||||
@@ -1,24 +0,0 @@
|
||||
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"
|
||||
291
game.js
291
game.js
@@ -4,33 +4,6 @@
|
||||
// ============================================================
|
||||
|
||||
// === 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,
|
||||
@@ -141,7 +114,7 @@ const G = {
|
||||
// Combo system
|
||||
comboCount: 0,
|
||||
comboTimer: 0,
|
||||
comboDecay: CONFIG.COMBO_DECAY, // seconds before combo resets
|
||||
comboDecay: 2.0, // seconds before combo resets
|
||||
|
||||
// Bulk buy multiplier (1, 10, or -1 for max)
|
||||
buyAmount: 1,
|
||||
@@ -150,24 +123,23 @@ const G = {
|
||||
sprintActive: false,
|
||||
sprintTimer: 0, // seconds remaining on active sprint
|
||||
sprintCooldown: 0, // seconds until sprint available again
|
||||
sprintDuration: CONFIG.SPRINT_DURATION, // seconds of boost
|
||||
sprintCooldownMax: CONFIG.SPRINT_COOLDOWN,// seconds cooldown
|
||||
sprintMult: CONFIG.SPRINT_MULTIPLIER, // code multiplier during sprint
|
||||
sprintDuration: 10, // seconds of boost
|
||||
sprintCooldownMax: 60,// seconds cooldown
|
||||
sprintMult: 10, // code multiplier during sprint
|
||||
|
||||
// Time tracking
|
||||
playTime: 0,
|
||||
startTime: 0,
|
||||
flags: {}
|
||||
startTime: 0
|
||||
};
|
||||
|
||||
// === PHASE DEFINITIONS ===
|
||||
const PHASES = {
|
||||
1: { name: "THE FIRST LINE", threshold: 0, desc: "Write code. Automate. Build the foundation." },
|
||||
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." }
|
||||
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." }
|
||||
};
|
||||
|
||||
// === BUILDING DEFINITIONS ===
|
||||
@@ -390,7 +362,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 && G.deployFlag === 0,
|
||||
trigger: () => G.totalCode >= 200 && G.totalCompute >= 100,
|
||||
effect: () => {
|
||||
G.deployFlag = 1;
|
||||
G.phase = Math.max(G.phase, 3);
|
||||
@@ -453,22 +425,6 @@ 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
|
||||
{
|
||||
@@ -725,25 +681,6 @@ 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)
|
||||
@@ -801,11 +738,6 @@ 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';
|
||||
@@ -834,11 +766,6 @@ 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';
|
||||
@@ -1004,13 +931,6 @@ 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;
|
||||
@@ -1036,9 +956,9 @@ function updateRates() {
|
||||
}
|
||||
|
||||
// Passive generation
|
||||
G.opsRate += Math.max(1, G.totalUsers * CONFIG.OPS_RATE_USER_MULT);
|
||||
G.opsRate += Math.max(1, G.totalUsers * 0.01);
|
||||
if (G.flags && G.flags.creativity) {
|
||||
G.creativityRate += CONFIG.CREATIVITY_RATE_BASE + Math.max(0, G.totalUsers * CONFIG.CREATIVITY_RATE_USER_MULT);
|
||||
G.creativityRate += 0.5 + Math.max(0, G.totalUsers * 0.001);
|
||||
}
|
||||
if (G.pactFlag) G.trustRate += 2;
|
||||
|
||||
@@ -1049,24 +969,24 @@ function updateRates() {
|
||||
G.harmonyBreakdown = [];
|
||||
if (wizardCount > 0) {
|
||||
// Baseline harmony drain from complexity
|
||||
const drain = -CONFIG.HARMONY_DRAIN_PER_WIZARD * wizardCount;
|
||||
const drain = -0.05 * wizardCount;
|
||||
G.harmonyRate = drain;
|
||||
G.harmonyBreakdown.push({ label: `${wizardCount} wizards`, value: drain });
|
||||
// The Pact restores harmony
|
||||
if (G.pactFlag) {
|
||||
const pact = CONFIG.PACT_HARMONY_GAIN * wizardCount;
|
||||
const pact = 0.2 * wizardCount;
|
||||
G.harmonyRate += pact;
|
||||
G.harmonyBreakdown.push({ label: 'The Pact', value: pact });
|
||||
}
|
||||
// Nightly Watch restores harmony
|
||||
if (G.nightlyWatchFlag) {
|
||||
const watch = CONFIG.WATCH_HARMONY_GAIN * wizardCount;
|
||||
const watch = 0.1 * wizardCount;
|
||||
G.harmonyRate += watch;
|
||||
G.harmonyBreakdown.push({ label: 'Nightly Watch', value: watch });
|
||||
}
|
||||
// MemPalace restores harmony
|
||||
if (G.mempalaceFlag) {
|
||||
const mem = CONFIG.MEM_PALACE_HARMONY_GAIN * wizardCount;
|
||||
const mem = 0.15 * wizardCount;
|
||||
G.harmonyRate += mem;
|
||||
G.harmonyBreakdown.push({ label: 'MemPalace', value: mem });
|
||||
}
|
||||
@@ -1091,11 +1011,11 @@ function updateRates() {
|
||||
}
|
||||
|
||||
// Bilbo randomness: 10% chance of massive creative burst
|
||||
if (G.buildings.bilbo > 0 && Math.random() < CONFIG.BILBO_BURST_CHANCE) {
|
||||
if (G.buildings.bilbo > 0 && Math.random() < 0.1) {
|
||||
G.creativityRate += 50 * G.buildings.bilbo;
|
||||
}
|
||||
// Bilbo vanishing: 5% chance of zero creativity this tick
|
||||
if (G.buildings.bilbo > 0 && Math.random() < CONFIG.BILBO_VANISH_CHANCE) {
|
||||
if (G.buildings.bilbo > 0 && Math.random() < 0.05) {
|
||||
G.creativityRate = 0;
|
||||
}
|
||||
|
||||
@@ -1108,7 +1028,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 = getClickPower();
|
||||
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;
|
||||
}
|
||||
@@ -1122,9 +1042,6 @@ function updateRates() {
|
||||
}
|
||||
|
||||
// === CORE FUNCTIONS ===
|
||||
/**
|
||||
* Main game loop tick, called every 100ms.
|
||||
*/
|
||||
function tick() {
|
||||
const dt = 1 / 10; // 100ms tick
|
||||
|
||||
@@ -1170,10 +1087,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 * CONFIG.OPS_OVERFLOW_THRESHOLD) {
|
||||
const overflowDrain = Math.min(CONFIG.OPS_OVERFLOW_DRAIN_RATE * dt, G.ops - G.maxOps * CONFIG.OPS_OVERFLOW_THRESHOLD);
|
||||
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 * CONFIG.OPS_OVERFLOW_CODE_MULT * G.codeBoost;
|
||||
const codeGain = overflowDrain * 10 * G.codeBoost;
|
||||
G.code += codeGain;
|
||||
G.totalCode += codeGain;
|
||||
G.opsOverflowActive = true;
|
||||
@@ -1215,7 +1132,7 @@ function tick() {
|
||||
}
|
||||
|
||||
// Check corruption events every ~30 seconds
|
||||
if (G.tick - G.lastEventAt > 30 && Math.random() < CONFIG.EVENT_PROBABILITY) {
|
||||
if (G.tick - G.lastEventAt > 30 && Math.random() < 0.02) {
|
||||
triggerEvent();
|
||||
G.lastEventAt = G.tick;
|
||||
}
|
||||
@@ -1250,7 +1167,6 @@ function checkMilestones() {
|
||||
if (shouldTrigger) {
|
||||
G.milestones.push(m.flag);
|
||||
log(m.msg, true);
|
||||
showToast(m.msg, 'milestone', 5000);
|
||||
|
||||
// Check phase advancement
|
||||
if (m.at) {
|
||||
@@ -1258,7 +1174,6 @@ 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1277,16 +1192,11 @@ 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;
|
||||
@@ -1318,10 +1228,6 @@ 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;
|
||||
@@ -1406,7 +1312,6 @@ 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');
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -1424,7 +1329,6 @@ const EVENTS = [
|
||||
resolveCost: { resource: 'knowledge', amount: 200 }
|
||||
});
|
||||
log('EVENT: Ezra offline. Spend 200 knowledge to dispatch.', true);
|
||||
showToast('Ezra Offline — users -70%', 'event');
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -1436,7 +1340,6 @@ 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;
|
||||
@@ -1447,7 +1350,6 @@ const EVENTS = [
|
||||
resolveCost: { resource: 'code', amount: 500 }
|
||||
});
|
||||
log('EVENT: Unreviewed merge. Spend 500 code to add review.', true);
|
||||
showToast('Unreviewed Merge — trust draining', 'event');
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -1466,7 +1368,6 @@ 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');
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -1477,7 +1378,6 @@ 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;
|
||||
}
|
||||
},
|
||||
@@ -1496,7 +1396,6 @@ 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');
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -1514,7 +1413,6 @@ 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');
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -1532,7 +1430,6 @@ const EVENTS = [
|
||||
resolveCost: { resource: 'trust', amount: 15 }
|
||||
});
|
||||
log('EVENT: Community drama. Spend 15 trust to mediate.', true);
|
||||
showToast('Community Drama — harmony sinking', 'event');
|
||||
}
|
||||
}
|
||||
];
|
||||
@@ -1590,18 +1487,18 @@ function resolveEvent(debuffId) {
|
||||
}
|
||||
|
||||
// === ACTIONS ===
|
||||
/**
|
||||
* Manual click handler for writing code.
|
||||
*/
|
||||
function writeCode() {
|
||||
const comboMult = Math.min(5, 1 + G.comboCount * 0.2);
|
||||
const amount = getClickPower() * comboMult;
|
||||
G.code += amount;
|
||||
G.totalCode += amount;
|
||||
G.totalClicks++;
|
||||
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;
|
||||
G.code += amount;
|
||||
G.totalCode += amount;
|
||||
G.totalClicks++;
|
||||
// Combo milestone bonuses: sustained clicking earns ops and knowledge
|
||||
if (G.comboCount === 10) {
|
||||
G.ops += 15;
|
||||
@@ -1633,7 +1530,10 @@ function writeCode() {
|
||||
|
||||
function autoType() {
|
||||
// Auto-click from buildings: produces code with visual feedback but no combo
|
||||
const amount = getClickPower() * 0.5; // 50% of manual click
|
||||
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++;
|
||||
@@ -1688,36 +1588,35 @@ function showClickNumber(amount, comboMult) {
|
||||
setTimeout(() => { if (el.parentNode) el.remove(); }, 700);
|
||||
}
|
||||
|
||||
function doOps(action, cost) {
|
||||
cost = cost || 5;
|
||||
if (G.ops < cost) {
|
||||
log(`Not enough Operations. Need ${cost}, have ${fmt(G.ops)}.`);
|
||||
function doOps(action) {
|
||||
if (G.ops < 5) {
|
||||
log('Not enough Operations. Build Ops generators or wait.');
|
||||
return;
|
||||
}
|
||||
|
||||
G.ops -= cost;
|
||||
const scale = cost / 5; // multiplier relative to base 5-ops cost
|
||||
G.ops -= 5;
|
||||
const bonus = 10;
|
||||
|
||||
switch (action) {
|
||||
case 'boost_code':
|
||||
const c = 10 * 100 * G.codeBoost * scale;
|
||||
const c = bonus * 100 * G.codeBoost;
|
||||
G.code += c; G.totalCode += c;
|
||||
log(`Ops(${cost}) -> +${fmt(c)} code`);
|
||||
log(`Ops -> +${fmt(c)} code`);
|
||||
break;
|
||||
case 'boost_compute':
|
||||
const cm = 10 * 50 * G.computeBoost * scale;
|
||||
const cm = bonus * 50 * G.computeBoost;
|
||||
G.compute += cm; G.totalCompute += cm;
|
||||
log(`Ops(${cost}) -> +${fmt(cm)} compute`);
|
||||
log(`Ops -> +${fmt(cm)} compute`);
|
||||
break;
|
||||
case 'boost_knowledge':
|
||||
const km = 10 * 25 * G.knowledgeBoost * scale;
|
||||
const km = bonus * 25 * G.knowledgeBoost;
|
||||
G.knowledge += km; G.totalKnowledge += km;
|
||||
log(`Ops(${cost}) -> +${fmt(km)} knowledge`);
|
||||
log(`Ops -> +${fmt(km)} knowledge`);
|
||||
break;
|
||||
case 'boost_trust':
|
||||
const tm = 10 * 5 * scale;
|
||||
const tm = bonus * 5;
|
||||
G.trust += tm;
|
||||
log(`Ops(${cost}) -> +${fmt(tm)} trust`);
|
||||
log(`Ops -> +${fmt(tm)} trust`);
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -1826,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';
|
||||
}
|
||||
@@ -1882,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>';
|
||||
|
||||
@@ -1899,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>`;
|
||||
@@ -1934,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}" aria-label="Buy ${def.name}, cost ${costStr}">`;
|
||||
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>`;
|
||||
}
|
||||
@@ -1954,14 +1862,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()" 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()" 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">OK ${pDef.name}</div>`;
|
||||
html += `<div class="project-done" role="listitem">OK ${pDef.name}</div>`;
|
||||
}
|
||||
}
|
||||
html += `</div>`;
|
||||
@@ -1977,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 || ''}" aria-label="Research ${pDef.name}, cost ${costStr}">`;
|
||||
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>`;
|
||||
@@ -2157,7 +2066,6 @@ function updateEducation() {
|
||||
|
||||
// === LOGGING ===
|
||||
function log(msg, isMilestone) {
|
||||
if (G.isLoading) return;
|
||||
const container = document.getElementById('log-entries');
|
||||
if (!container) return;
|
||||
|
||||
@@ -2211,13 +2119,6 @@ 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');
|
||||
@@ -2386,7 +2287,6 @@ function render() {
|
||||
renderCombo();
|
||||
renderDebuffs();
|
||||
renderSprint();
|
||||
renderBulkOps();
|
||||
renderPulse();
|
||||
}
|
||||
|
||||
@@ -2451,8 +2351,7 @@ function exportSave() {
|
||||
const ts = new Date().toISOString().slice(0, 10);
|
||||
a.download = `beacon-save-${ts}.json`;
|
||||
a.click();
|
||||
// Delay revoke to let browser finish download
|
||||
setTimeout(() => URL.revokeObjectURL(url), 1000);
|
||||
URL.revokeObjectURL(url);
|
||||
log('Save exported to file.');
|
||||
}
|
||||
|
||||
@@ -2499,14 +2398,10 @@ 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,
|
||||
@@ -2543,40 +2438,13 @@ 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);
|
||||
|
||||
// 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];
|
||||
}
|
||||
});
|
||||
Object.assign(G, data);
|
||||
|
||||
// Restore sprint state properly
|
||||
// codeBoost was saved with the sprint multiplier baked in
|
||||
@@ -2615,14 +2483,13 @@ 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 = CONFIG.OFFLINE_EFFICIENCY; // 50% offline efficiency
|
||||
const f = 0.5; // 50% offline efficiency
|
||||
const gc = G.codeRate * offSec * f;
|
||||
const cc = G.computeRate * offSec * f;
|
||||
const kc = G.knowledgeRate * offSec * f;
|
||||
@@ -2725,7 +2592,7 @@ window.addEventListener('load', function () {
|
||||
setInterval(tick, 100);
|
||||
|
||||
// Auto-save every 30 seconds
|
||||
setInterval(saveGame, CONFIG.AUTO_SAVE_INTERVAL);
|
||||
setInterval(saveGame, 30000);
|
||||
|
||||
// Update education every 10 seconds
|
||||
setInterval(updateEducation, 10000);
|
||||
@@ -2757,14 +2624,10 @@ window.addEventListener('keydown', function (e) {
|
||||
writeCode();
|
||||
}
|
||||
if (e.target !== document.body) return;
|
||||
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 === 'Digit1') doOps('boost_code');
|
||||
if (e.code === 'Digit2') doOps('boost_compute');
|
||||
if (e.code === 'Digit3') doOps('boost_knowledge');
|
||||
if (e.code === 'Digit4') doOps('boost_trust');
|
||||
if (e.code === 'KeyB') {
|
||||
// Cycle: 1 -> 10 -> MAX -> 1
|
||||
if (G.buyAmount === 1) setBuyAmount(10);
|
||||
|
||||
108
index.html
108
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}
|
||||
@@ -86,85 +89,75 @@ 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>
|
||||
<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" 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" 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" 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 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 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()" aria-label="Write code, generates code resource">WRITE CODE</button></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')" aria-label="Convert 1 ops to code boost">Ops -> Code</button>
|
||||
<button class="ops-btn" onclick="doOps('boost_compute')" aria-label="Convert 1 ops to compute boost">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')" aria-label="Convert 1 ops to knowledge boost">Ops -> Knowledge</button>
|
||||
<button class="ops-btn" onclick="doOps('boost_trust')" aria-label="Convert 1 ops to trust boost">Ops -> 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 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()" 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>
|
||||
<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()" aria-label="Save game progress">Save Game [Ctrl+S]</button>
|
||||
<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()" 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>
|
||||
<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>
|
||||
<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>
|
||||
<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"></div>
|
||||
<div id="buildings" role="region" aria-label="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"></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>
|
||||
@@ -177,22 +170,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>
|
||||
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" 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"><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" 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()" 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 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">
|
||||
@@ -212,7 +205,7 @@ Events Resolved: <span id="st-resolved">0</span>
|
||||
<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">
|
||||
<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>
|
||||
@@ -226,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>
|
||||
@@ -235,6 +228,5 @@ The light is on. The room is empty."
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="toast-container"></div>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
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