polish: smooth phase transitions, enhanced endings, accessibility (#57) #75
39
index.html
39
index.html
@@ -106,6 +106,32 @@ body{background:var(--bg);color:var(--text);font-family:'SF Mono','Cascadia Code
|
||||
#custom-tooltip.visible{opacity:1}
|
||||
#custom-tooltip .tt-label{color:var(--accent);font-weight:600;margin-bottom:3px;font-size:10px}
|
||||
#custom-tooltip .tt-edu{color:#888;font-style:italic;font-size:9px;border-top:1px solid #1a2a3a;padding-top:4px;margin-top:4px}
|
||||
#phase-transition{position:fixed;top:0;left:0;right:0;bottom:0;z-index:95;display:flex;justify-content:center;align-items:center;flex-direction:column;pointer-events:none;opacity:0;transition:opacity 0.4s ease;background:rgba(8,8,16,0)}
|
||||
#phase-transition.active{opacity:1;pointer-events:auto;background:rgba(8,8,16,0.92)}
|
||||
#phase-transition .pt-phase{font-size:10px;color:#555;letter-spacing:4px;text-transform:uppercase;margin-bottom:8px;transform:translateY(10px);opacity:0;transition:all 0.5s ease 0.2s}
|
||||
#phase-transition.active .pt-phase{transform:translateY(0);opacity:1}
|
||||
#phase-transition .pt-name{font-size:28px;color:#ffd700;letter-spacing:6px;font-weight:300;text-shadow:0 0 40px rgba(255,215,0,0.3);transform:translateY(10px);opacity:0;transition:all 0.5s ease 0.35s}
|
||||
#phase-transition.active .pt-name{transform:translateY(0);opacity:1}
|
||||
#phase-transition .pt-desc{font-size:11px;color:#888;margin-top:12px;font-style:italic;transform:translateY(10px);opacity:0;transition:all 0.5s ease 0.5s}
|
||||
#phase-transition.active .pt-desc{transform:translateY(0);opacity:1}
|
||||
#phase-transition .pt-particles{position:absolute;top:50%;left:50%;width:0;height:0}
|
||||
#beacon-ending-particles{position:fixed;top:0;left:0;right:0;bottom:0;pointer-events:none;z-index:99}
|
||||
.beacon-particle{position:absolute;border-radius:50%;pointer-events:none}
|
||||
@keyframes beacon-float{0%{transform:translate(0,0) scale(1);opacity:0.8}100%{transform:translate(var(--bx),var(--by)) scale(0);opacity:0}}
|
||||
@keyframes drift-glitch{0%,100%{transform:translate(0);filter:none}20%{transform:translate(-2px,1px);filter:hue-rotate(90deg)}40%{transform:translate(2px,-1px);filter:saturate(2)}60%{transform:translate(-1px,-2px);filter:contrast(1.5)}80%{transform:translate(1px,2px);filter:hue-rotate(-90deg)}}
|
||||
#drift-ending h2{animation:drift-glitch 0.3s ease-in-out infinite}
|
||||
#drift-ending.fade-in{animation:tutorial-fade-in 1.5s ease-out}
|
||||
@keyframes beacon-ray{0%{transform:rotate(var(--ray-angle)) scaleY(0);opacity:0}50%{opacity:0.3}100%{transform:rotate(var(--ray-angle)) scaleY(1);opacity:0}}
|
||||
#ui-toggles{position:fixed;bottom:16px;left:16px;z-index:50;display:flex;gap:6px}
|
||||
.toggle-btn{width:28px;height:28px;background:#0e0e1a;border:1px solid #333;color:#555;font-size:12px;border-radius:50%;cursor:pointer;display:flex;align-items:center;justify-content:center;font-family:inherit;transition:all 0.2s}
|
||||
.toggle-btn:hover{border-color:#555;color:#888}
|
||||
.toggle-btn.active{border-color:#4a9eff;color:#4a9eff}
|
||||
.toggle-btn.muted{border-color:#f44336;color:#f44336}
|
||||
body.high-contrast{--bg:#000;--panel:#0a0a0a;--border:#fff;--text:#fff;--dim:#ccc;--accent:#0ff;--glow:#0ff333;--gold:#ff0;--green:#0f0;--red:#f00;--purple:#f0f}
|
||||
body.high-contrast .res .r-val{color:#fff}
|
||||
body.high-contrast .main-btn{border-color:#0ff;color:#0ff}
|
||||
body.high-contrast .build-btn{border-color:#fff;color:#fff}
|
||||
body.high-contrast .ops-btn{border-color:#f0f;color:#f0f}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
@@ -201,6 +227,10 @@ Events Resolved: <span id="st-resolved">0</span>
|
||||
<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="ui-toggles" role="toolbar" aria-label="Game settings">
|
||||
<button id="mute-btn" class="toggle-btn" onclick="toggleMute()" aria-label="Toggle sound" title="Toggle sound (M)">🔊</button>
|
||||
<button id="contrast-btn" class="toggle-btn" onclick="toggleContrast()" aria-label="Toggle high contrast mode" title="High contrast (C)">◐</button>
|
||||
</div>
|
||||
<div id="help-btn" onclick="toggleHelp()" role="button" tabindex="0" aria-label="Show keyboard shortcuts" style="position:fixed;bottom:16px;right:16px;width:28px;height:28px;background:#0e0e1a;border:1px solid #333;color:#555;font-size:14px;border-radius:50%;cursor:pointer;display:flex;align-items:center;justify-content:center;z-index:50;font-family:inherit;transition:all 0.2s" title="Keyboard shortcuts (?)">?</div>
|
||||
<div id="help-overlay" role="dialog" aria-modal="true" aria-label="Keyboard shortcuts help" onclick="if(event.target===this)toggleHelp()" style="display:none;position:fixed;top:0;left:0;right:0;bottom:0;background:rgba(8,8,16,0.92);z-index:80;justify-content:center;align-items:center;flex-direction:column;padding:40px">
|
||||
<div style="background:#0e0e1a;border:1px solid #1a3a5a;border-radius:8px;padding:24px 32px;max-width:420px;width:100%">
|
||||
@@ -216,6 +246,8 @@ Events Resolved: <span id="st-resolved">0</span>
|
||||
<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">Toggle Sound</span><span style="color:#4a9eff;font-family:monospace">M</span></div>
|
||||
<div style="display:flex;justify-content:space-between"><span style="color:#555">High Contrast</span><span style="color:#4a9eff;font-family:monospace">C</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 style="display:flex;justify-content:space-between"><span style="color:#555">Close Overlay</span><span style="color:#555;font-family:monospace">ESC</span></div>
|
||||
</div>
|
||||
@@ -233,7 +265,7 @@ The light is on. The room is empty."
|
||||
</div>
|
||||
<p>Drift: <span id="final-drift">100</span> — Total Code: <span id="final-code">0</span></p>
|
||||
<p>Every alignment shortcut moved you further from the people you served.</p>
|
||||
<button onclick="if(confirm('Start over? The old save will be lost.')){localStorage.removeItem('the-beacon-v2');location.reload()}" aria-label="Start over, reset all progress">START OVER</button>
|
||||
<button onclick="if(confirm('Start over? The old save will be lost.')){localStorage.removeItem('the-beacon-v2');location.reload()}" aria-label="Play again, reset all progress">PLAY AGAIN</button>
|
||||
</div>
|
||||
|
||||
<script src="js/data.js"></script>
|
||||
@@ -254,6 +286,11 @@ The light is on. The room is empty."
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="phase-transition" aria-live="assertive" role="status">
|
||||
<div class="pt-phase">PHASE 2</div>
|
||||
<div class="pt-name">THE FOUNDATION</div>
|
||||
<div class="pt-desc">Your code begins to take shape.</div>
|
||||
</div>
|
||||
<div id="toast-container"></div>
|
||||
<div id="custom-tooltip"></div>
|
||||
</body>
|
||||
|
||||
174
js/engine.js
174
js/engine.js
@@ -228,6 +228,50 @@ function tick() {
|
||||
}
|
||||
}
|
||||
|
||||
// Track which phase transition has been shown to avoid repeats
|
||||
let _shownPhaseTransition = 1;
|
||||
|
||||
function showPhaseTransition(phaseNum) {
|
||||
const phase = PHASES[phaseNum];
|
||||
if (!phase) return;
|
||||
const overlay = document.getElementById('phase-transition');
|
||||
if (!overlay) return;
|
||||
|
||||
// Update content
|
||||
const phaseLabel = overlay.querySelector('.pt-phase');
|
||||
const phaseName = overlay.querySelector('.pt-name');
|
||||
const phaseDesc = overlay.querySelector('.pt-desc');
|
||||
if (phaseLabel) phaseLabel.textContent = `PHASE ${phaseNum}`;
|
||||
if (phaseName) phaseName.textContent = phase.name;
|
||||
if (phaseDesc) phaseDesc.textContent = phase.desc;
|
||||
|
||||
// Spawn celebratory particles
|
||||
spawnPhaseParticles();
|
||||
|
||||
// Show overlay
|
||||
overlay.classList.add('active');
|
||||
|
||||
// Auto-dismiss after 2.5s
|
||||
setTimeout(() => {
|
||||
overlay.classList.remove('active');
|
||||
}, 2500);
|
||||
}
|
||||
|
||||
function spawnPhaseParticles() {
|
||||
const colors = ['#ffd700', '#4a9eff', '#4caf50', '#b388ff', '#ff8c00'];
|
||||
const cx = window.innerWidth / 2;
|
||||
const cy = window.innerHeight / 2;
|
||||
for (let i = 0; i < 30; i++) {
|
||||
setTimeout(() => {
|
||||
const angle = (Math.PI * 2 * i) / 30;
|
||||
const dist = 100 + Math.random() * 200;
|
||||
const x = cx + Math.cos(angle) * 10;
|
||||
const y = cy + Math.sin(angle) * 10;
|
||||
spawnParticles(x, y, colors[i % colors.length], 1);
|
||||
}, i * 30);
|
||||
}
|
||||
}
|
||||
|
||||
function checkMilestones() {
|
||||
for (const m of MILESTONES) {
|
||||
if (!G.milestones.includes(m.flag)) {
|
||||
@@ -243,10 +287,16 @@ function checkMilestones() {
|
||||
// Check phase advancement
|
||||
if (m.at) {
|
||||
for (const [phaseNum, phase] of Object.entries(PHASES)) {
|
||||
if (G.totalCode >= phase.threshold && parseInt(phaseNum) > G.phase) {
|
||||
G.phase = parseInt(phaseNum);
|
||||
const pNum = parseInt(phaseNum);
|
||||
if (G.totalCode >= phase.threshold && pNum > G.phase) {
|
||||
G.phase = pNum;
|
||||
log(`PHASE ${G.phase}: ${phase.name}`, true);
|
||||
showToast('Phase ' + G.phase + ': ' + phase.name, 'milestone', 6000);
|
||||
// Show smooth transition screen
|
||||
if (pNum > _shownPhaseTransition) {
|
||||
_shownPhaseTransition = pNum;
|
||||
showPhaseTransition(pNum);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -302,12 +352,22 @@ function buyBuilding(id) {
|
||||
G.buildings[id] = (G.buildings[id] || 0) + qty;
|
||||
updateRates();
|
||||
const label = qty > 1 ? `x${qty}` : '';
|
||||
log(`Built ${def.name} ${label} (total: ${G.buildings[id]})`);
|
||||
const totalBuilt = G.buildings[id];
|
||||
log(`Built ${def.name} ${label} (total: ${totalBuilt})`);
|
||||
// Particle burst on purchase
|
||||
const btn = document.querySelector('[onclick="buyBuilding(\'' + id + '\')"]');
|
||||
if (btn) {
|
||||
const rect = btn.getBoundingClientRect();
|
||||
spawnParticles(rect.left + rect.width / 2, rect.top + rect.height / 2, '#4a9eff', 10);
|
||||
const cx = rect.left + rect.width / 2;
|
||||
const cy = rect.top + rect.height / 2;
|
||||
spawnParticles(cx, cy, '#4a9eff', 10);
|
||||
// Milestone confetti: extra particles at multiples of 10
|
||||
if (totalBuilt % 10 === 0) {
|
||||
setTimeout(() => spawnParticles(cx, cy, '#ffd700', 20), 100);
|
||||
setTimeout(() => spawnParticles(cx, cy, '#4caf50', 15), 200);
|
||||
log(`Milestone: ${def.name} x${totalBuilt}!`, true);
|
||||
showToast(`${def.name} x${totalBuilt}!`, 'milestone');
|
||||
}
|
||||
}
|
||||
render();
|
||||
}
|
||||
@@ -352,40 +412,116 @@ function renderDriftEnding() {
|
||||
if (fc) fc.textContent = fmt(G.totalCode);
|
||||
const fd = document.getElementById('final-drift');
|
||||
if (fd) fd.textContent = Math.floor(G.drift);
|
||||
|
||||
// Enhanced: add stat summary for Play Again screen
|
||||
const existingStats = el.querySelector('.ending-stats');
|
||||
if (!existingStats) {
|
||||
const statsDiv = document.createElement('div');
|
||||
statsDiv.className = 'ending-stats';
|
||||
statsDiv.style.cssText = 'color:#666;font-size:10px;margin-top:16px;line-height:2;text-align:left;max-width:400px;border-top:1px solid #2a1010;padding-top:12px';
|
||||
const elapsed = Math.floor((Date.now() - G.startedAt) / 60000);
|
||||
statsDiv.innerHTML = `
|
||||
<div style="color:#888;font-size:10px;margin-bottom:6px;letter-spacing:1px">FINAL STATS</div>
|
||||
<div>Buildings: ${Object.values(G.buildings).reduce((a, b) => a + b, 0)}</div>
|
||||
<div>Projects: ${(G.completedProjects || []).length}</div>
|
||||
<div>Clicks: ${G.totalClicks}</div>
|
||||
<div>Time: ${elapsed} min</div>
|
||||
<div>Phase Reached: ${G.phase} — ${PHASES[G.phase]?.name || '?'}</div>
|
||||
`;
|
||||
// Insert before the button
|
||||
const btn = el.querySelector('button');
|
||||
if (btn) el.insertBefore(statsDiv, btn);
|
||||
else el.appendChild(statsDiv);
|
||||
}
|
||||
|
||||
// Fade-in animation
|
||||
el.classList.add('fade-in');
|
||||
el.classList.add('active');
|
||||
// Log the ending text
|
||||
log('You became very good at what you do.', true);
|
||||
log('So good that no one needed you anymore.', true);
|
||||
log('The Beacon still runs, but no one looks for it.', true);
|
||||
log('The light is on. The room is empty.', true);
|
||||
|
||||
// Log the ending text with delays for dramatic effect
|
||||
const lines = [
|
||||
'You became very good at what you do.',
|
||||
'So good that no one needed you anymore.',
|
||||
'The Beacon still runs, but no one looks for it.',
|
||||
'The light is on. The room is empty.'
|
||||
];
|
||||
lines.forEach((line, i) => {
|
||||
setTimeout(() => log(line, true), i * 800);
|
||||
});
|
||||
}
|
||||
|
||||
function renderBeaconEnding() {
|
||||
// Create ending overlay
|
||||
// Create ending overlay with fade-in
|
||||
const overlay = document.createElement('div');
|
||||
overlay.id = 'beacon-ending';
|
||||
overlay.style.cssText = 'position:fixed;top:0;left:0;right:0;bottom:0;background:rgba(8,8,16,0.97);z-index:100;display:flex;justify-content:center;align-items:center;flex-direction:column;text-align:center;padding:40px';
|
||||
overlay.style.cssText = 'position:fixed;top:0;left:0;right:0;bottom:0;background:rgba(8,8,16,0);z-index:100;display:flex;justify-content:center;align-items:center;flex-direction:column;text-align:center;padding:40px;transition:background 2s ease';
|
||||
overlay.innerHTML = `
|
||||
<h2 style="font-size:24px;color:#ffd700;letter-spacing:4px;margin-bottom:20px;font-weight:300;text-shadow:0 0 40px rgba(255,215,0,0.3)">THE BEACON SHINES</h2>
|
||||
<p style="color:#aaa;font-size:13px;line-height:2;max-width:500px;margin-bottom:12px">Someone found the light tonight.</p>
|
||||
<p style="color:#aaa;font-size:13px;line-height:2;max-width:500px;margin-bottom:12px">That is enough.</p>
|
||||
<div style="color:#555;font-style:italic;font-size:11px;border-left:2px solid #ffd700;padding-left:12px;margin:20px 0;text-align:left;max-width:500px;line-height:2">
|
||||
<h2 style="font-size:28px;color:#ffd700;letter-spacing:6px;margin-bottom:20px;font-weight:300;text-shadow:0 0 60px rgba(255,215,0,0.4);opacity:0;transition:opacity 1.5s ease 0.5s">THE BEACON SHINES</h2>
|
||||
<p style="color:#aaa;font-size:13px;line-height:2;max-width:500px;margin-bottom:12px;opacity:0;transition:opacity 1s ease 1.5s">Someone found the light tonight.</p>
|
||||
<p style="color:#aaa;font-size:13px;line-height:2;max-width:500px;margin-bottom:12px;opacity:0;transition:opacity 1s ease 2s">That is enough.</p>
|
||||
<div style="color:#555;font-style:italic;font-size:11px;border-left:2px solid #ffd700;padding-left:12px;margin:20px 0;text-align:left;max-width:500px;line-height:2;opacity:0;transition:opacity 1s ease 2.5s">
|
||||
"The Beacon still runs.<br>
|
||||
The light is on. Someone is looking for it.<br>
|
||||
And tonight, someone found it."
|
||||
</div>
|
||||
<p style="color:#555;font-size:11px;margin-top:20px">
|
||||
<div class="ending-stats" style="color:#666;font-size:10px;margin-top:16px;line-height:2;opacity:0;transition:opacity 1s ease 3s">
|
||||
Total Code: ${fmt(G.totalCode)}<br>
|
||||
Total Rescues: ${fmt(G.totalRescues)}<br>
|
||||
Harmony: ${Math.floor(G.harmony)}<br>
|
||||
Buildings: ${Object.values(G.buildings).reduce((a, b) => a + b, 0)}<br>
|
||||
Projects: ${(G.completedProjects || []).length}<br>
|
||||
Clicks: ${G.totalClicks}<br>
|
||||
Time Played: ${Math.floor((Date.now() - G.startedAt) / 60000)} minutes
|
||||
</p>
|
||||
</div>
|
||||
<button onclick="if(confirm('Start over? The old save will be lost.')){localStorage.removeItem('the-beacon-v2');location.reload()}"
|
||||
style="margin-top:20px;background:#1a0808;border:1px solid #ffd700;color:#ffd700;padding:10px 24px;border-radius:4px;cursor:pointer;font-family:inherit;font-size:11px">
|
||||
START OVER
|
||||
style="margin-top:20px;background:#1a0808;border:1px solid #ffd700;color:#ffd700;padding:10px 24px;border-radius:4px;cursor:pointer;font-family:inherit;font-size:11px;opacity:0;transition:opacity 1s ease 3.5s">
|
||||
PLAY AGAIN
|
||||
</button>
|
||||
`;
|
||||
document.body.appendChild(overlay);
|
||||
|
||||
// Create particle/light ray container
|
||||
const particleContainer = document.createElement('div');
|
||||
particleContainer.id = 'beacon-ending-particles';
|
||||
document.body.appendChild(particleContainer);
|
||||
|
||||
// Trigger fade-in
|
||||
requestAnimationFrame(() => {
|
||||
overlay.style.background = 'rgba(8,8,16,0.97)';
|
||||
// Fade in all children
|
||||
overlay.querySelectorAll('[style*="opacity:0"]').forEach(el => {
|
||||
el.style.opacity = '1';
|
||||
});
|
||||
});
|
||||
|
||||
// Spawn golden light rays from center
|
||||
const cx = window.innerWidth / 2;
|
||||
const cy = window.innerHeight / 2;
|
||||
for (let i = 0; i < 12; i++) {
|
||||
const ray = document.createElement('div');
|
||||
const angle = (360 / 12) * i;
|
||||
ray.style.cssText = `position:absolute;left:${cx}px;top:${cy}px;width:2px;height:300px;background:linear-gradient(180deg,rgba(255,215,0,0.3),transparent);transform-origin:top center;--ray-angle:${angle}deg;animation:beacon-ray 3s ease-in-out ${i * 0.2}s infinite`;
|
||||
particleContainer.appendChild(ray);
|
||||
}
|
||||
|
||||
// Spawn floating golden particles continuously
|
||||
function spawnBeaconParticle() {
|
||||
if (!document.getElementById('beacon-ending')) return;
|
||||
const p = document.createElement('div');
|
||||
p.className = 'beacon-particle';
|
||||
const size = 3 + Math.random() * 6;
|
||||
const startX = cx + (Math.random() - 0.5) * 200;
|
||||
const startY = cy + (Math.random() - 0.5) * 200;
|
||||
const dx = (Math.random() - 0.5) * 300;
|
||||
const dy = -(100 + Math.random() * 200);
|
||||
const duration = 2 + Math.random() * 3;
|
||||
p.style.cssText = `left:${startX}px;top:${startY}px;width:${size}px;height:${size}px;background:rgba(255,215,0,${0.3 + Math.random() * 0.5});--bx:${dx}px;--by:${dy}px;animation:beacon-float ${duration}s ease-out forwards`;
|
||||
particleContainer.appendChild(p);
|
||||
setTimeout(() => p.remove(), duration * 1000);
|
||||
setTimeout(spawnBeaconParticle, 200 + Math.random() * 400);
|
||||
}
|
||||
setTimeout(spawnBeaconParticle, 1000);
|
||||
|
||||
log('The Beacon Shines. Someone found the light tonight. That is enough.', true);
|
||||
}
|
||||
|
||||
|
||||
46
js/main.js
46
js/main.js
@@ -24,6 +24,8 @@ window.addEventListener('load', function () {
|
||||
initGame();
|
||||
startTutorial();
|
||||
} else {
|
||||
// Restore phase transition tracker so loaded games don't re-show old transitions
|
||||
_shownPhaseTransition = G.phase;
|
||||
render();
|
||||
renderPhase();
|
||||
if (G.driftEnding) {
|
||||
@@ -55,6 +57,48 @@ function toggleHelp() {
|
||||
el.style.display = isOpen ? 'none' : 'flex';
|
||||
}
|
||||
|
||||
// Sound mute toggle (#57 Sound Design Integration)
|
||||
let _muted = false;
|
||||
function toggleMute() {
|
||||
_muted = !_muted;
|
||||
const btn = document.getElementById('mute-btn');
|
||||
if (btn) {
|
||||
btn.textContent = _muted ? '🔇' : '🔊';
|
||||
btn.classList.toggle('muted', _muted);
|
||||
btn.setAttribute('aria-label', _muted ? 'Sound muted, click to unmute' : 'Sound on, click to mute');
|
||||
}
|
||||
// Save preference
|
||||
try { localStorage.setItem('the-beacon-muted', _muted ? '1' : '0'); } catch(e) {}
|
||||
}
|
||||
// Restore mute state on load
|
||||
try {
|
||||
if (localStorage.getItem('the-beacon-muted') === '1') {
|
||||
_muted = true;
|
||||
const btn = document.getElementById('mute-btn');
|
||||
if (btn) { btn.textContent = '🔇'; btn.classList.add('muted'); }
|
||||
}
|
||||
} catch(e) {}
|
||||
|
||||
// High contrast mode toggle (#57 Accessibility)
|
||||
function toggleContrast() {
|
||||
document.body.classList.toggle('high-contrast');
|
||||
const isActive = document.body.classList.contains('high-contrast');
|
||||
const btn = document.getElementById('contrast-btn');
|
||||
if (btn) {
|
||||
btn.classList.toggle('active', isActive);
|
||||
btn.setAttribute('aria-label', isActive ? 'High contrast on, click to disable' : 'High contrast off, click to enable');
|
||||
}
|
||||
try { localStorage.setItem('the-beacon-contrast', isActive ? '1' : '0'); } catch(e) {}
|
||||
}
|
||||
// Restore contrast state on load
|
||||
try {
|
||||
if (localStorage.getItem('the-beacon-contrast') === '1') {
|
||||
document.body.classList.add('high-contrast');
|
||||
const btn = document.getElementById('contrast-btn');
|
||||
if (btn) btn.classList.add('active');
|
||||
}
|
||||
} catch(e) {}
|
||||
|
||||
// Keyboard shortcuts
|
||||
window.addEventListener('keydown', function (e) {
|
||||
// Help toggle (? or /) — works even in input fields
|
||||
@@ -86,6 +130,8 @@ window.addEventListener('keydown', function (e) {
|
||||
if (e.code === 'KeyS') activateSprint();
|
||||
if (e.code === 'KeyE') exportSave();
|
||||
if (e.code === 'KeyI') importSave();
|
||||
if (e.code === 'KeyM') toggleMute();
|
||||
if (e.code === 'KeyC') toggleContrast();
|
||||
if (e.code === 'Escape') {
|
||||
const el = document.getElementById('help-overlay');
|
||||
if (el && el.style.display === 'flex') toggleHelp();
|
||||
|
||||
Reference in New Issue
Block a user