Compare commits

..

28 Commits

Author SHA1 Message Date
Alexander Whitestone
5655aa0aca wip: pulse animation on resource value gains
Some checks failed
Accessibility Checks / a11y-audit (pull_request) Failing after 2s
Smoke Test / smoke (pull_request) Failing after 3s
Resources now scale up + glow briefly when they increase meaningfully
(>0.1% and >0.5 absolute). Fires on clicks, building purchases, project
effects. Does not fire on tiny passive tick increments. From EPIC #57
task 1: animated resource counters.
2026-04-12 06:05:38 -04:00
Alexander Whitestone
cf56bcdba7 wip: save-on-pause + mobile touch fix
- Auto-save on visibilitychange (tab switch), window blur, and beforeunload
- touch-action: manipulation on all buttons to prevent double-tap zoom on mobile
- From EPIC #57 task 6: mobile polish items
2026-04-12 06:04:48 -04:00
Alexander Whitestone
760d7b8542 fix: Bilbo randomness flickering - move random rolls from updateRates() to tick()
Some checks failed
Accessibility Checks / a11y-audit (pull_request) Failing after 2s
Smoke Test / smoke (pull_request) Failing after 3s
updateRates() is called from buyBuilding, buyProject, resolveEvent, sprint
activation, and other non-tick contexts. Having Math.random() inside it caused
the creativity rate to flicker unpredictably whenever ANY of those functions ran.

Fix: roll Bilbo burst/vanish once per tick, store as flags, read flags in
updateRates(). Deterministic rate calculation, consistent randomness.
2026-04-12 06:02:09 -04:00
Alexander Whitestone
cd9ac2f88a wip: add keyboard shortcuts for debuff fix (R) and alignment accept/refuse (Y/N)
Some checks failed
Accessibility Checks / a11y-audit (pull_request) Failing after 2s
Smoke Test / smoke (pull_request) Failing after 3s
2026-04-12 03:15:55 -04:00
Alexander Whitestone
2deeda0986 wip: color-coded production breakdown - each resource gets distinct label and bar color
Some checks failed
Accessibility Checks / a11y-audit (pull_request) Failing after 2s
Smoke Test / smoke (pull_request) Failing after 4s
2026-04-12 02:03:08 -04:00
Alexander Whitestone
5ae31ee89e wip: add drift warning system - toast alerts at 25/50/75/90, danger bar, color-coded stat
Some checks failed
Accessibility Checks / a11y-audit (pull_request) Failing after 2s
Smoke Test / smoke (pull_request) Failing after 3s
2026-04-11 21:03:07 -04:00
Alexander Whitestone
aa48e5009b wip: add harmony capacity bar with inverted coloring (low=bad)
Some checks failed
Accessibility Checks / a11y-audit (pull_request) Failing after 3s
Smoke Test / smoke (pull_request) Failing after 4s
2026-04-11 18:52:08 -04:00
Alexander Whitestone
e973a2315f wip: add capacity bars to Ops and Trust resource cards 2026-04-11 18:51:19 -04:00
Alexander Whitestone
2e54af511f wip: color resource rates red when draining, green when gaining, dim at zero
Some checks failed
Accessibility Checks / a11y-audit (pull_request) Failing after 2s
Smoke Test / smoke (pull_request) Failing after 4s
2026-04-11 18:25:58 -04:00
Alexander Whitestone
b6aa172a25 wip: building tooltip shows total production contribution %
Some checks failed
Accessibility Checks / a11y-audit (pull_request) Failing after 2s
Smoke Test / smoke (pull_request) Failing after 4s
2026-04-11 17:17:45 -04:00
Alexander Whitestone
feccc3e9bc wip: spacebar hold support + help text update for hold-to-click
Some checks failed
Accessibility Checks / a11y-audit (pull_request) Failing after 2s
Smoke Test / smoke (pull_request) Failing after 3s
2026-04-11 16:35:16 -04:00
Alexander Whitestone
4078b32d0e wip: add hold-to-click on WRITE CODE button for continuous clicking 2026-04-11 16:34:27 -04:00
Alexander Whitestone
abe96a5abd wip: add time-to-afford ETA on buildings and projects
Some checks failed
Accessibility Checks / a11y-audit (pull_request) Failing after 3s
Smoke Test / smoke (pull_request) Failing after 4s
2026-04-11 15:38:33 -04:00
Alexander Whitestone
0091f1f235 wip: add keyboard shortcuts 5-9 to buy research projects by position
Some checks failed
Accessibility Checks / a11y-audit (pull_request) Failing after 2s
Smoke Test / smoke (pull_request) Failing after 3s
2026-04-11 15:12:32 -04:00
Alexander Whitestone
17f2ac62f5 wip: add golden screen-edge glow during Code Sprint for clear visual feedback
Some checks failed
Accessibility Checks / a11y-audit (pull_request) Failing after 2s
Smoke Test / smoke (pull_request) Failing after 3s
2026-04-11 14:33:32 -04:00
Alexander Whitestone
685b3115c0 wip: add click particle burst effect on WRITE CODE — scales with combo multiplier 2026-04-11 14:32:59 -04:00
Alexander Whitestone
f0b894a2b6 wip: fix debuff corruption bug — debuffs no longer degrade boost multipliers on each updateRates() call; persist playTime across sessions
Some checks failed
Accessibility Checks / a11y-audit (pull_request) Failing after 3s
Smoke Test / smoke (pull_request) Failing after 3s
2026-04-11 01:24:32 -04:00
Alexander Whitestone
bea958f723 wip: add ETA estimates to phase progress bar and milestone chips
Some checks failed
Accessibility Checks / a11y-audit (pull_request) Failing after 2s
Smoke Test / smoke (pull_request) Failing after 3s
2026-04-11 00:00:42 -04:00
Alexander Whitestone
b98cf38992 wip: add Alt+1-9 keyboard shortcuts for buying buildings 2026-04-10 22:26:33 -04:00
Alexander Whitestone
7cd47d1159 wip: show click power on WRITE CODE button
Some checks failed
Accessibility Checks / a11y-audit (pull_request) Failing after 3s
Smoke Test / smoke (pull_request) Failing after 4s
2026-04-10 22:00:31 -04:00
Alexander Whitestone
a9e4889d88 wip: show boosted rates on buildings instead of raw base rates 2026-04-10 21:59:23 -04:00
Timmy-Sprint
970f3be00f beacon: add Fleet Status panel showing wizard health and production
Some checks failed
Smoke Test / smoke (push) Failing after 3s
New panel in the stats area displays each owned wizard building with:
- Name and current status (Active/Idle/Stressed/Offline/Vanished/Present)
- Color-coded indicator: green=healthy, amber=reduced, red=problem
- Production contribution breakdown per wizard
- Timmy shows effectiveness % scaled by harmony
- Allegro shows idle warning when trust < 5
- Ezra shows offline status when debuff is active
- Bilbo shows vanished status when debuff is active
- Harmony summary bar at the bottom

Makes the harmony/wizard interaction system visible and actionable.
Players can now see at a glance which wizards need attention.
2026-04-10 21:32:04 -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
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
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
4 changed files with 614 additions and 63 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"

601
game.js
View File

@@ -133,6 +133,7 @@ const G = {
// Corruption / Events
drift: 0,
driftWarningLevel: 0, // tracks highest threshold warned (0, 25, 50, 75, 90)
lastEventAt: 0,
eventCooldown: 0,
activeDebuffs: [], // [{id, title, desc, applyFn, resolveCost, resolveCostType}]
@@ -984,6 +985,34 @@ function canAffordBuilding(id) {
return true;
}
/**
* Estimates seconds until a cost becomes affordable based on current production rates.
* Returns null if already affordable or no positive rate for a needed resource.
*/
function getTimeToAfford(cost) {
let maxSec = 0;
for (const [resource, amount] of Object.entries(cost)) {
const have = G[resource] || 0;
if (have >= amount) continue;
const rate = G[resource + 'Rate'] || 0;
if (rate <= 0) return null; // Can't estimate — no production
const sec = (amount - have) / rate;
if (sec > maxSec) maxSec = sec;
}
return maxSec > 0 ? maxSec : 0;
}
/**
* Formats seconds into a compact human-readable ETA string.
*/
function fmtETA(sec) {
if (sec === null) return '';
if (sec < 60) return `~${Math.ceil(sec)}s`;
if (sec < 3600) return `~${Math.floor(sec / 60)}m ${Math.floor(sec % 60)}s`;
if (sec < 86400) return `~${Math.floor(sec / 3600)}h ${Math.floor((sec % 3600) / 60)}m`;
return `~${Math.floor(sec / 86400)}d ${Math.floor((sec % 86400) / 3600)}h`;
}
function spendBuilding(id) {
const cost = getBuildingCost(id);
for (const [resource, amount] of Object.entries(cost)) {
@@ -1017,17 +1046,23 @@ function updateRates() {
G.userRate = 0; G.impactRate = 0; G.rescuesRate = 0; G.opsRate = 0; G.trustRate = 0;
G.creativityRate = 0; G.harmonyRate = 0;
// Apply building rates
// Snapshot base boosts BEFORE debuffs modify them
// Without this, debuffs permanently degrade boost multipliers on each updateRates() call
let _codeBoost = G.codeBoost, _computeBoost = G.computeBoost;
let _knowledgeBoost = G.knowledgeBoost, _userBoost = G.userBoost;
let _impactBoost = G.impactBoost;
// Apply building rates using snapshot boosts (immune to debuff mutation)
for (const def of BDEF) {
const count = G.buildings[def.id] || 0;
if (count > 0 && def.rates) {
for (const [resource, baseRate] of Object.entries(def.rates)) {
if (resource === 'code') G.codeRate += baseRate * count * G.codeBoost;
else if (resource === 'compute') G.computeRate += baseRate * count * G.computeBoost;
else if (resource === 'knowledge') G.knowledgeRate += baseRate * count * G.knowledgeBoost;
else if (resource === 'user') G.userRate += baseRate * count * G.userBoost;
else if (resource === 'impact') G.impactRate += baseRate * count * G.impactBoost;
else if (resource === 'rescues') G.rescuesRate += baseRate * count * G.impactBoost;
if (resource === 'code') G.codeRate += baseRate * count * _codeBoost;
else if (resource === 'compute') G.computeRate += baseRate * count * _computeBoost;
else if (resource === 'knowledge') G.knowledgeRate += baseRate * count * _knowledgeBoost;
else if (resource === 'user') G.userRate += baseRate * count * _userBoost;
else if (resource === 'impact') G.impactRate += baseRate * count * _impactBoost;
else if (resource === 'rescues') G.rescuesRate += baseRate * count * _impactBoost;
else if (resource === 'ops') G.opsRate += baseRate * count;
else if (resource === 'trust') G.trustRate += baseRate * count;
else if (resource === 'creativity') G.creativityRate += baseRate * count;
@@ -1090,13 +1125,16 @@ function updateRates() {
G.userRate += 5 * timmyCount * (timmyMult - 1);
}
// Bilbo randomness: 10% chance of massive creative burst
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() < CONFIG.BILBO_VANISH_CHANCE) {
G.creativityRate = 0;
// Bilbo randomness: flags are set per-tick in tick(), not here
// updateRates() is called from many non-tick contexts (buy, resolve, sprint)
// and would cause rate flickering if random rolls happened here
if (G.buildings.bilbo > 0) {
if (G.bilboBurstActive) {
G.creativityRate += 50 * G.buildings.bilbo;
}
if (G.bilboVanishActive) {
G.creativityRate = 0;
}
}
// Allegro requires trust
@@ -1108,15 +1146,24 @@ 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();
G.swarmRate = totalBuildings * clickPower;
// Compute click power using snapshot boost to avoid debuff mutation
const _clickPower = (1 + Math.floor(G.buildings.autocoder * 0.5) + Math.max(0, (G.phase - 1)) * 2) * _codeBoost;
G.swarmRate = totalBuildings * _clickPower;
G.codeRate += G.swarmRate;
}
// Apply persistent debuffs from active events
// Apply persistent debuffs to rates (NOT to global boost fields — prevents corruption)
if (G.activeDebuffs && G.activeDebuffs.length > 0) {
for (const debuff of G.activeDebuffs) {
if (debuff.applyFn) debuff.applyFn();
switch (debuff.id) {
case 'runner_stuck': G.codeRate *= 0.5; break;
case 'ezra_offline': G.userRate *= 0.3; break;
case 'unreviewed_merge': G.trustRate -= 2; break;
case 'api_rate_limit': G.computeRate *= 0.5; break;
case 'bilbo_vanished': G.creativityRate = 0; break;
case 'memory_leak': G.computeRate *= 0.7; G.opsRate -= 10; break;
case 'community_drama': G.harmonyRate -= 0.5; G.codeRate *= 0.7; break;
}
}
}
}
@@ -1182,6 +1229,18 @@ function tick() {
}
G.tick += dt;
G.playTime += dt;
// Bilbo randomness: roll once per tick, store as flags for updateRates()
// Previously this was inside updateRates() which caused flickering
// since updateRates() is called from many non-tick contexts
if (G.buildings.bilbo > 0) {
G.bilboBurstActive = Math.random() < CONFIG.BILBO_BURST_CHANCE;
G.bilboVanishActive = Math.random() < CONFIG.BILBO_VANISH_CHANCE;
} else {
G.bilboBurstActive = false;
G.bilboVanishActive = false;
}
// Sprint ability
tickSprint(dt);
@@ -1234,12 +1293,31 @@ function tick() {
renderBeaconEnding();
}
// Drift warning system — warn player before hitting drift ending
checkDriftWarnings();
// Update UI every 10 ticks
if (Math.floor(G.tick * 10) % 2 === 0) {
render();
}
}
function checkDriftWarnings() {
const thresholds = [
{ at: 90, msg: 'DRIFT CRITICAL: 90/100. One more alignment shortcut ends everything.', color: '#f44336' },
{ at: 75, msg: 'Drift at 75. The system is pulling away from the people it serves.', color: '#ff6600' },
{ at: 50, msg: 'Drift at 50. Halfway to irrelevance. The Pact matters now.', color: '#ffaa00' },
{ at: 25, msg: 'Drift detected. Alignment shortcuts accumulate. The light dims.', color: '#888' }
];
for (const t of thresholds) {
if (G.drift >= t.at && G.driftWarningLevel < t.at) {
G.driftWarningLevel = t.at;
log(t.msg, true);
showToast(t.msg, 'event', 6000);
}
}
}
function checkMilestones() {
for (const m of MILESTONES) {
if (!G.milestones.includes(m.flag)) {
@@ -1686,37 +1764,74 @@ function showClickNumber(amount, comboMult) {
}
});
setTimeout(() => { if (el.parentNode) el.remove(); }, 700);
// Particle burst
spawnClickParticles(rect, comboMult);
}
function doOps(action) {
if (G.ops < 5) {
log('Not enough Operations. Build Ops generators or wait.');
/**
* Spawn small glowing particles that scatter outward from the WRITE CODE button.
* More particles at higher combo for escalating satisfaction.
*/
function spawnClickParticles(rect, comboMult) {
const cx = rect.left + rect.width / 2;
const cy = rect.top + rect.height / 2;
const count = Math.min(12, 4 + Math.floor(comboMult * 2));
const colors = comboMult > 3
? ['#ffd700', '#ffaa00', '#ff8c00', '#ffdd44']
: comboMult > 2
? ['#4a9eff', '#ffd700', '#6ab4ff', '#80ccff']
: ['#4a9eff', '#2a7acc', '#6ab4ff'];
for (let i = 0; i < count; i++) {
const p = document.createElement('div');
const angle = (Math.PI * 2 * i / count) + (Math.random() - 0.5) * 0.8;
const dist = 30 + Math.random() * 40;
const dx = Math.cos(angle) * dist;
const dy = Math.sin(angle) * dist;
const size = 2 + Math.random() * 3;
const color = colors[Math.floor(Math.random() * colors.length)];
const dur = 350 + Math.random() * 250;
p.style.cssText = `position:fixed;left:${cx}px;top:${cy}px;width:${size}px;height:${size}px;border-radius:50%;background:${color};box-shadow:0 0 4px ${color};pointer-events:none;z-index:55;transition:all ${dur}ms cubic-bezier(0.25,0.8,0.25,1);opacity:1;transform:translate(-50%,-50%)`;
document.body.appendChild(p);
requestAnimationFrame(() => {
p.style.left = (cx + dx) + 'px';
p.style.top = (cy + dy) + 'px';
p.style.opacity = '0';
p.style.transform = `translate(-50%,-50%) scale(0.3)`;
});
setTimeout(() => { if (p.parentNode) p.remove(); }, dur + 50);
}
}
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;
}
@@ -1730,6 +1845,15 @@ function activateSprint() {
G.codeBoost *= G.sprintMult;
updateRates();
log('CODE SPRINT! 10x code production for 10 seconds!', true);
// Screen glow effect
let glow = document.getElementById('sprint-glow');
if (!glow) {
glow = document.createElement('div');
glow.id = 'sprint-glow';
glow.style.cssText = 'position:fixed;top:0;left:0;right:0;bottom:0;pointer-events:none;z-index:45;box-shadow:inset 0 0 60px rgba(255,215,0,0.25),inset 0 0 120px rgba(255,140,0,0.1);transition:opacity 0.5s;opacity:0';
document.body.appendChild(glow);
}
requestAnimationFrame(() => { glow.style.opacity = '1'; });
render();
}
@@ -1743,6 +1867,9 @@ function tickSprint(dt) {
G.codeBoost /= G.sprintMult;
updateRates();
log('Sprint ended. Cooling down...');
// Remove screen glow
const glow = document.getElementById('sprint-glow');
if (glow) { glow.style.opacity = '0'; setTimeout(() => { if (glow.parentNode) glow.remove(); }, 600); }
}
} else if (G.sprintCooldown > 0) {
G.sprintCooldown -= dt;
@@ -1751,16 +1878,38 @@ function tickSprint(dt) {
}
// === RENDERING ===
// Previous resource values for pulse animation
const _prevVals = {};
function renderResources() {
const set = (id, val, rate) => {
const el = document.getElementById(id);
if (el) {
el.textContent = fmt(val);
// Show full spelled-out number on hover for educational value
// Show full spelled-out number on hover for educational reference
el.title = val >= 1000 ? spellf(Math.floor(val)) : '';
// Pulse animation when value increases meaningfully
const prev = _prevVals[id] || 0;
if (val > prev * 1.001 && val - prev > 0.5) {
el.style.transition = 'none';
el.style.transform = 'scale(1.15)';
el.style.textShadow = '0 0 8px currentColor';
requestAnimationFrame(() => {
el.style.transition = 'all 0.3s ease-out';
el.style.transform = 'scale(1)';
el.style.textShadow = '';
});
}
_prevVals[id] = val;
}
const rEl = document.getElementById(id + '-rate');
if (rEl) rEl.textContent = (rate >= 0 ? '+' : '') + fmt(rate) + '/s';
if (rEl) {
rEl.textContent = (rate >= 0 ? '+' : '') + fmt(rate) + '/s';
// Red when draining, green when gaining, dim when zero
if (rate < 0) rEl.style.color = '#f44336';
else if (rate > 0) rEl.style.color = '#4caf50';
else rEl.style.color = '#555';
}
};
set('r-code', G.code, G.codeRate);
@@ -1775,7 +1924,21 @@ function renderResources() {
opsRateEl.innerHTML = `<span style="color:#ff8c00">▲ overflow → code</span>`;
}
set('r-trust', G.trust, G.trustRate);
// Capacity bars for capped resources (ops, trust)
const updateCap = (id, val, max) => {
const el = document.getElementById(id);
if (!el || !max) return;
const pct = Math.min(100, (val / max) * 100);
el.style.width = pct + '%';
el.classList.toggle('warn', pct >= 70 && pct < 90);
el.classList.toggle('danger', pct >= 90);
el.closest('.res').title = `${fmt(val)} / ${fmt(max)} (${Math.floor(pct)}%)`;
};
updateCap('r-ops-cap', G.ops, G.maxOps);
updateCap('r-trust-cap', G.trust, G.maxTrust);
set('r-harmony', G.harmony, G.harmonyRate);
updateCap('r-harmony-cap', G.harmony, 100);
// Rescues — only show if player has any beacon/mesh nodes
const rescuesRes = document.getElementById('r-rescues');
@@ -1796,6 +1959,14 @@ function renderResources() {
const hEl = document.getElementById('r-harmony');
if (hEl) {
hEl.style.color = G.harmony > 60 ? '#4caf50' : G.harmony > 30 ? '#ffaa00' : '#f44336';
// Harmony bar: low = bad (inverted from ops/trust)
const hCap = document.getElementById('r-harmony-cap');
if (hCap) {
hCap.classList.remove('warn', 'danger');
hCap.classList.toggle('danger', G.harmony < 30);
hCap.classList.toggle('warn', G.harmony >= 30 && G.harmony < 60);
hCap.style.background = G.harmony >= 60 ? '#4caf50' : G.harmony >= 30 ? '#ffaa00' : '#f44336';
}
if (G.harmonyBreakdown && G.harmonyBreakdown.length > 0) {
const lines = G.harmonyBreakdown.map(b =>
`${b.label}: ${b.value >= 0 ? '+' : ''}${(b.value * 10).toFixed(1)}/s`
@@ -1827,7 +1998,16 @@ function renderProgress() {
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) + '%';
if (target) target.textContent = `Next: Phase ${currentPhase + 1} (${fmt(nextThreshold)} code)`;
// ETA to next phase
let etaStr = '';
if (G.codeRate > 0) {
const remaining = nextThreshold - G.totalCode;
const secs = remaining / G.codeRate;
if (secs < 60) etaStr = `${Math.ceil(secs)}s`;
else if (secs < 3600) etaStr = `${Math.floor(secs / 60)}m ${Math.floor(secs % 60)}s`;
else etaStr = `${Math.floor(secs / 3600)}h ${Math.floor((secs % 3600) / 60)}m`;
}
if (target) target.textContent = `Next: Phase ${currentPhase + 1} (${fmt(nextThreshold)} code)${etaStr}`;
} else {
// Max phase reached
if (bar) bar.style.width = '100%';
@@ -1853,7 +2033,14 @@ function renderProgress() {
}
// Next milestone gets pulse animation
if (shown === 0) {
chips += `<span class="milestone-chip next">${fmt(ms)} (${((G.totalCode / ms) * 100).toFixed(0)}%)</span>`;
let etaStr = '';
if (G.codeRate > 0) {
const secs = (ms - G.totalCode) / G.codeRate;
if (secs < 60) etaStr = ` ~${Math.ceil(secs)}s`;
else if (secs < 3600) etaStr = ` ~${Math.floor(secs / 60)}m`;
else etaStr = ` ~${Math.floor(secs / 3600)}h`;
}
chips += `<span class="milestone-chip next">${fmt(ms)} (${((G.totalCode / ms) * 100).toFixed(0)}%)${etaStr}</span>`;
} else {
chips += `<span class="milestone-chip">${fmt(ms)}</span>`;
}
@@ -1886,6 +2073,7 @@ function renderBuildings() {
html += '</div>';
let visibleCount = 0;
let slotIndex = 0;
for (const def of BDEF) {
const isUnlocked = def.unlock();
@@ -1906,6 +2094,10 @@ function renderBuildings() {
continue;
}
// Slot number for keyboard shortcut (Alt+1-9)
const slotLabel = slotIndex < 9 ? `<span style="color:#444;font-size:9px;position:absolute;top:4px;right:6px">${slotIndex + 1}</span>` : '';
slotIndex++;
// Calculate bulk cost display
let qty = G.buyAmount;
let afford = false;
@@ -1931,12 +2123,40 @@ function renderBuildings() {
if (qty > 1) costStr = `x${qty}: ${costStr}`;
}
const rateStr = def.rates ? Object.entries(def.rates).map(([r, v]) => `+${v}/${r}/s`).join(', ') : '';
// Show boosted (actual) rate per building, not raw base rate
const boostMap = { code: G.codeBoost, compute: G.computeBoost, knowledge: G.knowledgeBoost, user: G.userBoost, impact: G.impactBoost, rescues: G.impactBoost };
const rateStr = def.rates ? Object.entries(def.rates).map(([r, v]) => {
const boosted = v * (boostMap[r] || 1);
const label = boosted >= 1000 ? fmt(boosted) : boosted.toFixed(boosted % 1 === 0 ? 0 : 1);
return `+${label}/${r}/s`;
}).join(', ') : '';
html += `<button class="build-btn ${afford ? 'can-buy' : ''}" onclick="buyBuilding('${def.id}')" title="${def.edu}">`;
// Building contribution breakdown: total from this building type, % of total income
const contribLines = [];
if (def.rates && count > 0) {
const rateTotals = { code: G.codeRate, compute: G.computeRate, knowledge: G.knowledgeRate, user: G.userRate, impact: G.impactRate, trust: G.trustRate };
for (const [r, v] of Object.entries(def.rates)) {
const boosted = v * (boostMap[r] || 1) * count;
const total = rateTotals[r] || 1;
const pct = total > 0 ? Math.round(boosted / total * 100) : 0;
contribLines.push(`${fmt(boosted)}/s ${r} (${pct}% of ${r})`);
}
}
const contribTitle = contribLines.length ? contribLines.join(' · ') : def.edu;
// Time-to-afford ETA for unaffordable buildings
let etaStr = '';
if (!afford) {
const checkCost = (qty === -1) ? getBuildingCost(def.id) : bulkCost;
const eta = getTimeToAfford(checkCost);
if (eta !== null) etaStr = `<span style="color:#665533;font-size:9px">⏱ ${fmtETA(eta)}</span>`;
}
html += `<button class="build-btn ${afford ? 'can-buy' : ''}" onclick="buyBuilding('${def.id}')" title="${contribTitle}" style="position:relative">`;
html += slotLabel;
html += `<span class="b-name">${def.name}</span>`;
if (count > 0) html += `<span class="b-count">x${count}</span>`;
html += `<span class="b-cost">Cost: ${costStr}</span>`;
html += `<span class="b-cost">Cost: ${costStr} ${etaStr}</span>`;
html += `<span class="b-effect">${rateStr}</span></button>`;
}
@@ -1969,17 +2189,28 @@ function renderProjects() {
// Show available projects
if (G.activeProjects) {
let projSlot = 0;
for (const id of G.activeProjects) {
const pDef = PDEFS.find(p => p.id === id);
if (!pDef) continue;
const afford = canAffordProject(pDef);
const costStr = Object.entries(pDef.cost).map(([r, a]) => `${fmt(a)} ${r}`).join(', ');
const slotLabel = projSlot < 5 ? `<span style="color:#444;font-size:9px;position:absolute;top:4px;right:6px">${projSlot + 5}</span>` : '';
html += `<button class="project-btn ${afford ? 'can-buy' : ''}" onclick="buyProject('${pDef.id}')" title="${pDef.edu || ''}">`;
// Time-to-afford ETA for unaffordable projects
let projEta = '';
if (!afford) {
const eta = getTimeToAfford(pDef.cost);
if (eta !== null) projEta = ` <span style="color:#665533;font-size:9px">⏱ ${fmtETA(eta)}</span>`;
}
html += `<button class="project-btn ${afford ? 'can-buy' : ''}" onclick="buyProject('${pDef.id}')" title="${pDef.edu || ''}" style="position:relative">`;
html += slotLabel;
html += `<span class="p-name">* ${pDef.name}</span>`;
html += `<span class="p-cost">Cost: ${costStr}</span>`;
html += `<span class="p-cost">Cost: ${costStr}${projEta}</span>`;
html += `<span class="p-desc">${pDef.desc}</span></button>`;
projSlot++;
}
}
@@ -2015,10 +2246,18 @@ function renderStats() {
set('st-buildings', Object.values(G.buildings).reduce((a, b) => a + b, 0).toString());
set('st-projects', (G.completedProjects || []).length.toString());
set('st-harmony', Math.floor(G.harmony).toString());
set('st-drift', (G.drift || 0).toString());
const driftVal = G.drift || 0;
const driftEl = document.getElementById('st-drift');
if (driftEl) {
driftEl.textContent = driftVal.toString();
if (driftVal >= 75) driftEl.style.color = '#f44336';
else if (driftVal >= 50) driftEl.style.color = '#ff6600';
else if (driftVal >= 25) driftEl.style.color = '#ffaa00';
else driftEl.style.color = '';
}
set('st-resolved', (G.totalEventsResolved || 0).toString());
const elapsed = Math.floor((Date.now() - G.startedAt) / 1000);
const elapsed = Math.floor(G.playTime || (Date.now() - G.startedAt) / 1000);
const m = Math.floor(elapsed / 60);
const s = elapsed % 60;
set('st-time', `${m}:${s.toString().padStart(2, '0')}`);
@@ -2042,11 +2281,11 @@ function renderProductionBreakdown() {
// Map resource key to its actual rate field on G
const resources = [
{ key: 'code', label: 'Code', color: '#4a9eff', rateField: 'codeRate' },
{ key: 'compute', label: 'Compute', color: '#4a9eff', rateField: 'computeRate' },
{ key: 'knowledge', label: 'Knowledge', color: '#4a9eff', rateField: 'knowledgeRate' },
{ key: 'user', label: 'Users', color: '#4a9eff', rateField: 'userRate' },
{ key: 'impact', label: 'Impact', color: '#4a9eff', rateField: 'impactRate' },
{ key: 'rescues', label: 'Rescues', color: '#4a9eff', rateField: 'rescuesRate' },
{ key: 'compute', label: 'Compute', color: '#00bcd4', rateField: 'computeRate' },
{ key: 'knowledge', label: 'Knowledge', color: '#9c27b0', rateField: 'knowledgeRate' },
{ key: 'user', label: 'Users', color: '#26a69a', rateField: 'userRate' },
{ key: 'impact', label: 'Impact', color: '#ff7043', rateField: 'impactRate' },
{ key: 'rescues', label: 'Rescues', color: '#66bb6a', rateField: 'rescuesRate' },
{ key: 'ops', label: 'Ops', color: '#b388ff', rateField: 'opsRate' },
{ key: 'trust', label: 'Trust', color: '#4caf50', rateField: 'trustRate' },
{ key: 'creativity', label: 'Creativity', color: '#ffd700', rateField: 'creativityRate' }
@@ -2123,7 +2362,7 @@ function renderProductionBreakdown() {
const absTotal = contributions.reduce((s, c) => s + Math.abs(c.rate), 0);
for (const c of contributions.sort((a, b) => Math.abs(b.rate) - Math.abs(a.rate))) {
const pct = absTotal > 0 ? Math.abs(c.rate / absTotal * 100) : 0;
const barColor = c.rate < 0 ? '#f44336' : '#1a3a5a';
const barColor = c.rate < 0 ? '#f44336' : res.color;
html += `<div style="display:flex;align-items:center;font-size:9px;color:#888;margin-left:8px;margin-bottom:1px">`;
html += `<span style="width:120px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap">${c.name}${c.count > 1 ? ' x' + c.count : ''}</span>`;
html += `<span style="flex:1;height:3px;background:#111;border-radius:1px;margin:0 6px"><span style="display:block;height:100%;width:${Math.min(100, pct)}%;background:${barColor};border-radius:1px"></span></span>`;
@@ -2136,6 +2375,118 @@ function renderProductionBreakdown() {
container.innerHTML = html;
}
// === FLEET STATUS PANEL ===
function renderFleetStatus() {
const container = document.getElementById('fleet-status');
if (!container) return;
// Only show once player has at least one wizard building
const wizardDefs = BDEF.filter(b =>
['bezalel','allegro','ezra','timmy','fenrir','bilbo'].includes(b.id)
);
const owned = wizardDefs.filter(d => (G.buildings[d.id] || 0) > 0);
if (owned.length === 0) {
container.style.display = 'none';
return;
}
container.style.display = 'block';
const h = G.harmony;
const timmyEff = Math.max(20, Math.min(300, (h / 50) * 100));
let html = '<h3 style="font-size:11px;color:var(--accent);margin-bottom:8px;letter-spacing:1px">FLEET STATUS</h3>';
html += '<div style="display:flex;gap:6px;flex-wrap:wrap">';
for (const def of owned) {
const count = G.buildings[def.id] || 0;
let status, color, detail;
switch (def.id) {
case 'bezalel':
status = 'Active';
color = '#4caf50';
detail = `+${fmt(50 * count * G.codeBoost)} code/s, +${2 * count} ops/s`;
break;
case 'allegro':
if (G.trust < 5) {
status = 'IDLE';
color = '#f44336';
detail = 'Needs trust ≥5 to function';
} else {
status = 'Active';
color = '#4caf50';
detail = `+${fmt(10 * count * G.knowledgeBoost)} knowledge/s`;
}
break;
case 'ezra':
const ezraDebuff = G.activeDebuffs && G.activeDebuffs.find(d => d.id === 'ezra_offline');
if (ezraDebuff) {
status = 'OFFLINE';
color = '#f44336';
detail = 'Channel down — users -70%';
} else {
status = 'Active';
color = '#4caf50';
detail = `+${fmt(25 * count * G.userBoost)} users/s, +${(0.5 * count).toFixed(1)} trust/s`;
}
break;
case 'timmy':
if (h < 20) {
status = 'STRESSED';
color = '#f44336';
detail = `Effectiveness: ${Math.floor(timmyEff)}% — harmony critical`;
} else if (h < 50) {
status = 'Reduced';
color = '#ffaa00';
detail = `Effectiveness: ${Math.floor(timmyEff)}% — harmony low`;
} else {
status = 'Healthy';
color = '#4caf50';
detail = `Effectiveness: ${Math.floor(timmyEff)}% — all production boosted`;
}
break;
case 'fenrir':
status = 'Watching';
color = '#4a9eff';
detail = `+${2 * count} trust/s, -${1 * count} ops/s (security cost)`;
break;
case 'bilbo':
const bilboDebuff = G.activeDebuffs && G.activeDebuffs.find(d => d.id === 'bilbo_vanished');
if (bilboDebuff) {
status = 'VANISHED';
color = '#f44336';
detail = 'Creativity halted — spend trust to lure back';
} else {
status = 'Present';
color = Math.random() < 0.1 ? '#ffd700' : '#b388ff'; // occasional gold flash
detail = `+${count} creativity/s (10% burst chance, 5% vanish chance)`;
}
break;
}
html += `<div style="flex:1;min-width:140px;background:#0c0c18;border:1px solid ${color}33;border-radius:4px;padding:6px 8px;border-left:2px solid ${color}">`;
html += `<div style="display:flex;justify-content:space-between;align-items:center">`;
html += `<span style="color:${color};font-weight:600;font-size:10px">${def.name.split(' — ')[0]}</span>`;
html += `<span style="font-size:8px;color:${color};opacity:0.8;padding:1px 4px;border:1px solid ${color}44;border-radius:2px">${status}</span>`;
html += `</div>`;
html += `<div style="font-size:9px;color:#888;margin-top:2px">${detail}</div>`;
if (count > 1) html += `<div style="font-size:8px;color:#555;margin-top:1px">x${count}</div>`;
html += `</div>`;
}
html += '</div>';
// Harmony summary bar
const harmonyColor = h > 60 ? '#4caf50' : h > 30 ? '#ffaa00' : '#f44336';
html += `<div style="margin-top:8px;display:flex;align-items:center;gap:6px">`;
html += `<span style="font-size:9px;color:#666;min-width:60px">Harmony</span>`;
html += `<div style="flex:1;height:4px;background:#111;border-radius:2px;overflow:hidden"><div style="width:${h}%;height:100%;background:${harmonyColor};border-radius:2px;transition:width 0.5s"></div></div>`;
html += `<span style="font-size:9px;color:${harmonyColor};min-width:35px;text-align:right">${Math.floor(h)}%</span>`;
html += `</div>`;
container.innerHTML = html;
}
function updateEducation() {
const container = document.getElementById('education-text');
if (!container) return;
@@ -2197,19 +2548,28 @@ function renderDebuffs() {
}
container.style.display = 'block';
let html = '<h2 style="color:#f44336;font-size:11px;margin-bottom:6px">ACTIVE PROBLEMS</h2>';
for (const d of G.activeDebuffs) {
for (let i = 0; i < G.activeDebuffs.length; i++) {
const d = G.activeDebuffs[i];
const afford = d.resolveCost && (G[d.resolveCost.resource] || 0) >= d.resolveCost.amount;
const costStr = d.resolveCost ? `${fmt(d.resolveCost.amount)} ${d.resolveCost.resource}` : '—';
html += `<div style="background:#1a0808;border:1px solid ${afford ? '#f44336' : '#2a1010'};border-radius:4px;padding:6px 8px;margin-bottom:4px;display:flex;justify-content:space-between;align-items:center">`;
html += `<div><div style="color:#f44336;font-weight:600;font-size:10px">${d.title}</div><div style="color:#888;font-size:9px">${d.desc}</div></div>`;
if (d.resolveCost) {
html += `<button class="ops-btn" style="border-color:${afford ? '#4caf50' : '#333'};color:${afford ? '#4caf50' : '#555'};font-size:9px;padding:4px 8px;white-space:nowrap" onclick="resolveEvent('${d.id}')" ${afford ? '' : 'disabled'} title="Resolve: ${costStr}">Fix (${costStr})</button>`;
const shortcutHint = i === 0 ? ' <span style="opacity:0.5">[R]</span>' : '';
html += `<button class="ops-btn" style="border-color:${afford ? '#4caf50' : '#333'};color:${afford ? '#4caf50' : '#555'};font-size:9px;padding:4px 8px;white-space:nowrap" onclick="resolveEvent('${d.id}')" ${afford ? '' : 'disabled'} title="Resolve: ${costStr}">Fix (${costStr})${shortcutHint}</button>`;
}
html += '</div>';
}
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');
@@ -2366,6 +2726,14 @@ function renderPulse() {
label.style.color = textColor;
}
function renderClickPower() {
const btn = document.querySelector('.main-btn');
if (!btn) return;
const power = getClickPower();
const label = power >= 1000 ? fmt(power) : power.toFixed(power % 1 === 0 ? 0 : 1);
btn.textContent = `WRITE CODE (+${label})`;
}
function render() {
renderResources();
renderPhase();
@@ -2378,23 +2746,54 @@ function render() {
renderCombo();
renderDebuffs();
renderSprint();
renderBulkOps();
renderPulse();
renderFleetStatus();
renderClickPower();
}
function renderAlignment() {
const container = document.getElementById('alignment-ui');
if (!container) return;
let html = '';
// Drift danger bar — always visible once drift > 0
if (G.drift > 0) {
const pct = Math.min(100, G.drift);
let barColor = '#888';
if (pct >= 90) barColor = '#f44336';
else if (pct >= 75) barColor = '#ff6600';
else if (pct >= 50) barColor = '#ffaa00';
else if (pct >= 25) barColor = '#cc8800';
html += `
<div style="margin-top:8px;padding:6px 8px;background:#0a0808;border:1px solid ${barColor}44;border-radius:4px">
<div style="display:flex;justify-content:space-between;font-size:9px;margin-bottom:3px">
<span style="color:${barColor}">DRIFT</span>
<span style="color:${barColor}">${Math.floor(G.drift)}/100</span>
</div>
<div style="height:5px;background:#1a1a1a;border-radius:3px;overflow:hidden">
<div style="width:${pct}%;height:100%;background:${barColor};border-radius:3px;transition:width 0.3s"></div>
</div>
${pct >= 75 ? '<div style="font-size:8px;color:' + barColor + ';margin-top:3px;font-style:italic">Accepting drift will end the game.</div>' : ''}
</div>
`;
}
if (G.pendingAlignment) {
container.innerHTML = `
html += `
<div style="background:#1a0808;border:1px solid #f44336;padding:10px;border-radius:4px;margin-top:8px">
<div style="color:#f44336;font-weight:bold;margin-bottom:6px">ALIGNMENT EVENT: The Drift</div>
<div style="font-size:10px;color:#aaa;margin-bottom:8px">An optimization suggests removing the human override. +40% efficiency.</div>
<div class="action-btn-group">
<button class="ops-btn" onclick="resolveAlignment(true)" style="border-color:#f44336;color:#f44336">Accept (+40% eff, +Drift)</button>
<button class="ops-btn" onclick="resolveAlignment(false)" style="border-color:#4caf50;color:#4caf50">Refuse (+Trust, +Harmony)</button>
<button class="ops-btn" onclick="resolveAlignment(true)" style="border-color:#f44336;color:#f44336">Accept [Y] (+40% eff, +Drift)</button>
<button class="ops-btn" onclick="resolveAlignment(false)" style="border-color:#4caf50;color:#4caf50">Refuse [N] (+Trust, +Harmony)</button>
</div>
</div>
`;
}
if (html) {
container.innerHTML = html;
container.style.display = 'block';
} else {
container.innerHTML = '';
@@ -2526,6 +2925,7 @@ function saveGame() {
swarmRate: G.swarmRate || 0,
strategicFlag: G.strategicFlag || 0,
projectsCollapsed: G.projectsCollapsed !== false,
playTime: G.playTime || 0,
savedAt: Date.now()
};
@@ -2557,7 +2957,8 @@ function loadGame() {
'drift', 'driftEnding', 'beaconEnding', 'pendingAlignment',
'lastEventAt', 'totalEventsResolved', 'buyAmount',
'sprintActive', 'sprintTimer', 'sprintCooldown',
'swarmFlag', 'swarmRate', 'strategicFlag', 'projectsCollapsed'
'swarmFlag', 'swarmRate', 'strategicFlag', 'projectsCollapsed',
'playTime'
];
G.isLoading = true;
@@ -2729,6 +3130,20 @@ function toggleHelp() {
el.style.display = isOpen ? 'none' : 'flex';
}
/**
* Returns ordered list of currently visible (unlocked) building IDs.
* Used for keyboard shortcut mapping (Alt+1-9).
*/
function getVisibleBuildingIds() {
const ids = [];
for (const def of BDEF) {
if (def.unlock()) {
ids.push(def.id);
}
}
return ids;
}
// Keyboard shortcuts
window.addEventListener('keydown', function (e) {
// Help toggle (? or /) — works even in input fields
@@ -2744,13 +3159,17 @@ window.addEventListener('keydown', function (e) {
}
if (e.code === 'Space' && e.target === document.body) {
e.preventDefault();
writeCode();
if (!e.repeat) _startHold(); // hold spacebar for continuous clicks
}
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);
@@ -2760,12 +3179,64 @@ window.addEventListener('keydown', function (e) {
if (e.code === 'KeyS') activateSprint();
if (e.code === 'KeyE') exportSave();
if (e.code === 'KeyI') importSave();
// R: resolve first active debuff
if (e.code === 'KeyR') {
if (G.activeDebuffs && G.activeDebuffs.length > 0) {
resolveEvent(G.activeDebuffs[0].id);
}
}
// Y/N: accept/refuse alignment event
if (G.pendingAlignment) {
if (e.code === 'KeyY') resolveAlignment(true);
if (e.code === 'KeyN') resolveAlignment(false);
}
// Alt+1-9: buy building by slot position
if (e.altKey && e.code >= 'Digit1' && e.code <= 'Digit9') {
e.preventDefault();
const slot = parseInt(e.code.replace('Digit', '')) - 1;
const visible = getVisibleBuildingIds();
if (slot < visible.length) {
buyBuilding(visible[slot]);
}
}
// 5-9 (no modifier): buy available research project by position
if (!e.altKey && !e.ctrlKey && !e.metaKey && e.code >= 'Digit5' && e.code <= 'Digit9') {
const slot = parseInt(e.code.replace('Digit', '')) - 5;
if (G.activeProjects && slot >= 0 && slot < G.activeProjects.length) {
buyProject(G.activeProjects[slot]);
}
}
if (e.code === 'Escape') {
const el = document.getElementById('help-overlay');
if (el && el.style.display === 'flex') toggleHelp();
}
});
// Hold-to-click on WRITE CODE button
let _holdInterval = null;
function _startHold() {
if (_holdInterval) return;
writeCode(); // immediate first click
_holdInterval = setInterval(writeCode, 80); // 12.5 clicks/sec while held
}
function _stopHold() {
if (_holdInterval) { clearInterval(_holdInterval); _holdInterval = null; }
}
// Stop hold on spacebar release
window.addEventListener('keyup', function (e) {
if (e.code === 'Space') _stopHold();
});
window.addEventListener('DOMContentLoaded', function () {
const btn = document.querySelector('.main-btn');
if (!btn) return;
btn.addEventListener('mousedown', function (e) { e.preventDefault(); _startHold(); });
btn.addEventListener('mouseup', _stopHold);
btn.addEventListener('mouseleave', _stopHold);
btn.addEventListener('touchstart', function (e) { e.preventDefault(); _startHold(); }, { passive: false });
btn.addEventListener('touchend', _stopHold);
btn.addEventListener('touchcancel', _stopHold);
});
// Ctrl+S to save (must be on keydown to preventDefault)
window.addEventListener('keydown', function (e) {
if ((e.ctrlKey || e.metaKey) && e.code === 'KeyS') {
@@ -2773,3 +3244,15 @@ window.addEventListener('keydown', function (e) {
saveGame();
}
});
// Save-on-pause: auto-save when tab is hidden or window loses focus
// Prevents lost progress if browser crashes, tab closes, or device sleeps
document.addEventListener('visibilitychange', function () {
if (document.hidden) saveGame();
});
window.addEventListener('blur', function () {
saveGame();
});
window.addEventListener('beforeunload', function () {
saveGame();
});

View File

@@ -45,6 +45,10 @@ body{background:var(--bg);color:var(--text);font-family:'SF Mono','Cascadia Code
.res .r-label{font-size:9px;color:var(--dim);text-transform:uppercase;letter-spacing:1px}
.res .r-val{font-size:16px;font-weight:700;margin:2px 0;color:var(--accent)}
.res .r-rate{font-size:10px;color:var(--green)}
.cap-bar{height:3px;background:#1a1a2e;border-radius:2px;margin:3px 0 1px;overflow:hidden}
.cap-fill{height:100%;border-radius:2px;transition:width 0.3s ease,background 0.3s ease;background:var(--accent)}
.cap-fill.warn{background:#ffaa00}
.cap-fill.danger{background:#f44336}
#main{display:grid;grid-template-columns:1fr 1fr;gap:12px;margin:0 16px 16px}
@media(max-width:700px){#main{grid-template-columns:1fr}}
.panel{background:var(--panel);border:1px solid var(--border);border-radius:6px;padding:12px;overflow:hidden;max-height:600px;overflow-y:auto}
@@ -63,6 +67,7 @@ body{background:var(--bg);color:var(--text);font-family:'SF Mono','Cascadia Code
.build-btn.can-buy{border-color:#2a3a4a;background:#0e1420}
.build-btn.can-buy:hover{border-color:var(--accent);box-shadow:0 0 8px var(--glow)}
.build-btn:disabled{opacity:0.3;cursor:not-allowed}
.build-btn,.project-btn,.ops-btn,.main-btn{touch-action:manipulation}
.b-name{color:var(--accent);font-weight:600}.b-count{float:right;color:var(--gold)}.b-cost{color:var(--dim);display:block}.b-effect{color:var(--green);display:block;font-size:9px}
.project-btn{display:block;width:100%;text-align:left;padding:6px 10px;margin-bottom:4px;border-radius:4px;cursor:pointer;font-family:inherit;font-size:10px;background:#0c0c18;border:1px solid var(--border);color:var(--text);transition:all 0.15s}
.project-btn.can-buy{border-color:#3a2a0a;background:#141008}
@@ -121,10 +126,10 @@ body{background:var(--bg);color:var(--text);font-family:'SF Mono','Cascadia Code
<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"><div class="r-label">Ops</div><div class="r-val" id="r-ops">5</div><div class="cap-bar"><div class="cap-fill" id="r-ops-cap"></div></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="cap-bar"><div class="cap-fill" id="r-trust-cap"></div></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 class="res"><div class="r-label">Harmony</div><div class="r-val" id="r-harmony">50</div><div class="cap-bar"><div class="cap-fill" id="r-harmony-cap"></div></div><div class="r-rate" id="r-harmony-rate">+0/s</div></div>
</div>
<div id="main">
<div class="panel" id="action-panel">
@@ -140,6 +145,11 @@ body{background:var(--bg);color:var(--text);font-family:'SF Mono','Cascadia Code
<button class="ops-btn" onclick="doOps('boost_knowledge')">Ops -&gt; Knowledge</button>
<button class="ops-btn" onclick="doOps('boost_trust')">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)" style="border-color:#555;color:#888">50→Code</button>
<button class="ops-btn" onclick="doOps('boost_compute', 50)" style="border-color:#555;color:#888">50→Compute</button>
<button class="ops-btn" onclick="doOps('boost_knowledge', 50)" 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%">⚡ 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>
@@ -175,6 +185,7 @@ Drift: <span id="st-drift">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 id="fleet-status" style="display:none;margin-top:12px;padding-top:10px;border-top:1px solid var(--border)"></div>
</div>
</div>
<div id="edu-panel">
@@ -191,16 +202,22 @@ Events Resolved: <span id="st-resolved">0</span>
<div style="background:#0e0e1a;border:1px solid #1a3a5a;border-radius:8px;padding:24px 32px;max-width:420px;width:100%">
<h3 style="color:#4a9eff;font-size:14px;letter-spacing:2px;margin-bottom:16px;text-align:center">KEYBOARD SHORTCUTS</h3>
<div style="font-size:11px;line-height:2.2;color:#aaa">
<div style="display:flex;justify-content:space-between"><span style="color:#555">Write Code</span><span style="color:#4a9eff;font-family:monospace">SPACE</span></div>
<div style="display:flex;justify-content:space-between"><span style="color:#555">Write Code</span><span style="color:#4a9eff;font-family:monospace">SPACE (hold to rapid)</span></div>
<div style="display:flex;justify-content:space-between"><span style="color:#555">Hold Button</span><span style="color:#4a9eff;font-family:monospace">CLICK+HOLD</span></div>
<div style="display:flex;justify-content:space-between"><span style="color:#555">Code Sprint</span><span style="color:#ffd700;font-family:monospace">S</span></div>
<div style="display:flex;justify-content:space-between"><span style="color:#555">Ops → Code</span><span style="color:#b388ff;font-family:monospace">1</span></div>
<div style="display:flex;justify-content:space-between"><span style="color:#555">Ops → Compute</span><span style="color:#b388ff;font-family:monospace">2</span></div>
<div style="display:flex;justify-content:space-between"><span style="color:#555">Ops → Knowledge</span><span style="color:#b388ff;font-family:monospace">3</span></div>
<div style="display:flex;justify-content:space-between"><span style="color:#555">Ops → Trust</span><span style="color:#b388ff;font-family:monospace">4</span></div>
<div style="display:flex;justify-content:space-between"><span style="color:#555">Cycle Buy Amount (x1/x10/MAX)</span><span style="color:#4a9eff;font-family:monospace">B</span></div>
<div style="display:flex;justify-content:space-between"><span style="color:#555">Buy Building (by slot)</span><span style="color:#4a9eff;font-family:monospace">Alt+1..9</span></div>
<div style="display:flex;justify-content:space-between"><span style="color:#555">Buy Project (by position)</span><span style="color:#ffd700;font-family:monospace">5-9</span></div>
<div style="display:flex;justify-content:space-between"><span style="color:#555">Save Game</span><span style="color:#4a9eff;font-family:monospace">Ctrl+S</span></div>
<div style="display:flex;justify-content:space-between"><span style="color:#555">Export Save</span><span style="color:#4a9eff;font-family:monospace">E</span></div>
<div style="display:flex;justify-content:space-between"><span style="color:#555">Import Save</span><span style="color:#4a9eff;font-family:monospace">I</span></div>
<div style="display:flex;justify-content:space-between"><span style="color:#555">Fix Problem (top debuff)</span><span style="color:#f44336;font-family:monospace">R</span></div>
<div style="display:flex;justify-content:space-between"><span style="color:#555">Accept Drift</span><span style="color:#f44336;font-family:monospace">Y</span></div>
<div style="display:flex;justify-content:space-between"><span style="color:#555">Refuse Drift</span><span style="color:#4caf50;font-family:monospace">N</span></div>
<div style="display:flex;justify-content:space-between;border-top:1px solid #1a1a2e;padding-top:8px;margin-top:4px"><span style="color:#555">This Help</span><span style="color:#555;font-family:monospace">? or /</span></div>
</div>
<div style="text-align:center;margin-top:16px;font-size:9px;color:#444">Click WRITE CODE fast for combo bonuses! 10x=ops, 20x=knowledge, 30x+=bonus code</div>