- Added G.dismantleDeferUntilAt to restore() condition in main.js - Added defer cooldown restoration in dismantle.js restore() method - Added new test to verify cooldown persistence - Fixed issue where defer cooldown was bypassed after reload The bug occurred because: 1. When player defers, G.dismantleTriggered is set to false 2. On load, Dismantle.restore() was not called because condition only checked G.dismantleTriggered 3. This.deferUntilAt was not restored from G.dismantleDeferUntilAt 4. checkTrigger() would trigger immediately instead of honoring cooldown Fix: - Added G.dismantleDeferUntilAt > 0 to restore() condition in main.js - Added defer cooldown restoration in dismantle.js restore() method - Now defer cooldown properly survives save/load cycles All tests pass including new test for this specific scenario.
220 lines
7.6 KiB
JavaScript
220 lines
7.6 KiB
JavaScript
// === INITIALIZATION ===
|
|
function initGame() {
|
|
G.startedAt = Date.now();
|
|
G.startTime = Date.now();
|
|
G.phase = 1;
|
|
G.deployFlag = 0;
|
|
G.sovereignFlag = 0;
|
|
G.beaconFlag = 0;
|
|
G.dismantleTriggered = false;
|
|
G.dismantleActive = false;
|
|
G.dismantleStage = 0;
|
|
G.dismantleComplete = false;
|
|
updateRates();
|
|
render();
|
|
renderPhase();
|
|
|
|
log('The screen is blank. Write your first line of code.', true);
|
|
log('Click WRITE CODE or press SPACE to start.');
|
|
log('Build AutoCode for passive production.');
|
|
log('Watch for Research Projects to appear.');
|
|
log('Keys: SPACE=Code S=Sprint 1-4=Ops B=Buy x1/10/MAX E=Export I=Import Ctrl+S=Save ?=Help');
|
|
log('Tip: Click fast for combo bonuses! 10x=ops, 20x=knowledge, 30x+=bonus code.');
|
|
}
|
|
|
|
window.addEventListener('load', function () {
|
|
const isNewGame = !loadGame();
|
|
if (isNewGame) {
|
|
initGame();
|
|
startTutorial();
|
|
} else {
|
|
// Restore phase transition tracker so loaded games don't re-show old transitions
|
|
_shownPhaseTransition = G.phase;
|
|
render();
|
|
renderPhase();
|
|
if (G.driftEnding) {
|
|
G.running = false;
|
|
renderDriftEnding();
|
|
} else if (typeof Dismantle !== 'undefined' && (G.dismantleTriggered || G.dismantleActive || G.dismantleComplete || G.dismantleDeferUntilAt > 0)) {
|
|
Dismantle.restore();
|
|
} else if (G.beaconEnding) {
|
|
G.running = false;
|
|
renderBeaconEnding();
|
|
} else {
|
|
log('Game loaded. Welcome back to The Beacon.');
|
|
}
|
|
}
|
|
|
|
// Initialize combat canvas
|
|
if (typeof Combat !== 'undefined') Combat.init();
|
|
|
|
// Game loop at 10Hz (100ms tick)
|
|
setInterval(tick, 100);
|
|
|
|
// Start ambient drone on first interaction
|
|
if (typeof Sound !== 'undefined') {
|
|
const startAmbientOnce = () => {
|
|
Sound.startAmbient();
|
|
Sound.updateAmbientPhase(G.phase);
|
|
document.removeEventListener('click', startAmbientOnce);
|
|
document.removeEventListener('keydown', startAmbientOnce);
|
|
};
|
|
document.addEventListener('click', startAmbientOnce);
|
|
document.addEventListener('keydown', startAmbientOnce);
|
|
}
|
|
|
|
// Auto-save every 30 seconds
|
|
setInterval(saveGame, CONFIG.AUTO_SAVE_INTERVAL);
|
|
|
|
// Update education every 10 seconds
|
|
setInterval(updateEducation, 10000);
|
|
});
|
|
|
|
// Help overlay
|
|
function toggleHelp() {
|
|
const el = document.getElementById('help-overlay');
|
|
if (!el) return;
|
|
const isOpen = el.style.display === 'flex';
|
|
el.style.display = isOpen ? 'none' : 'flex';
|
|
}
|
|
|
|
// 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) {}
|
|
if (typeof Sound !== 'undefined') Sound.onMuteChanged(_muted);
|
|
}
|
|
// 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
|
|
if (e.key === '?' || e.key === '/') {
|
|
// Only trigger ? when not typing in an input
|
|
if (e.target === document.body || e.key === '?') {
|
|
if (e.key === '?' || (e.key === '/' && e.target === document.body)) {
|
|
e.preventDefault();
|
|
toggleHelp();
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
if (e.code === 'Space' && e.target === document.body) {
|
|
e.preventDefault();
|
|
writeCode();
|
|
}
|
|
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 === 'KeyB') {
|
|
// Cycle: 1 -> 10 -> MAX -> 1
|
|
if (G.buyAmount === 1) setBuyAmount(10);
|
|
else if (G.buyAmount === 10) setBuyAmount(-1);
|
|
else setBuyAmount(1);
|
|
}
|
|
if (e.code === 'KeyS') activateSprint();
|
|
if (e.code === 'KeyE') exportSave();
|
|
if (e.code === 'KeyI') importSave();
|
|
if (e.code === 'KeyM') toggleMute();
|
|
if (e.code === 'KeyC') toggleContrast();
|
|
if (e.code === 'Escape') {
|
|
const el = document.getElementById('help-overlay');
|
|
if (el && el.style.display === 'flex') toggleHelp();
|
|
}
|
|
});
|
|
|
|
// Ctrl+S to save (must be on keydown to preventDefault)
|
|
window.addEventListener('keydown', function (e) {
|
|
if ((e.ctrlKey || e.metaKey) && e.code === 'KeyS') {
|
|
e.preventDefault();
|
|
saveGame();
|
|
}
|
|
});
|
|
|
|
// Save-on-pause: auto-save when tab is hidden or closed (#57 Mobile Polish)
|
|
document.addEventListener('visibilitychange', function () {
|
|
if (document.hidden) {
|
|
saveGame();
|
|
}
|
|
});
|
|
window.addEventListener('beforeunload', function () {
|
|
saveGame();
|
|
});
|
|
|
|
// === CUSTOM TOOLTIP SYSTEM (#57) ===
|
|
// Replaces native title= tooltips with styled, instant-appearing tooltips.
|
|
// Elements opt in via data-edu="..." and data-tooltip-label="..." attributes.
|
|
(function () {
|
|
const tip = document.getElementById('custom-tooltip');
|
|
if (!tip) return;
|
|
|
|
document.addEventListener('mouseover', function (e) {
|
|
const el = e.target.closest('[data-edu]');
|
|
if (!el) return;
|
|
const label = el.getAttribute('data-tooltip-label') || '';
|
|
const edu = el.getAttribute('data-edu') || '';
|
|
let html = '';
|
|
if (label) html += '<div class="tt-label">' + label + '</div>';
|
|
if (edu) html += '<div class="tt-edu">' + edu + '</div>';
|
|
if (!html) return;
|
|
tip.innerHTML = html;
|
|
tip.classList.add('visible');
|
|
});
|
|
|
|
document.addEventListener('mouseout', function (e) {
|
|
const el = e.target.closest('[data-edu]');
|
|
if (el) tip.classList.remove('visible');
|
|
});
|
|
|
|
document.addEventListener('mousemove', function (e) {
|
|
if (!tip.classList.contains('visible')) return;
|
|
const pad = 12;
|
|
let x = e.clientX + pad;
|
|
let y = e.clientY + pad;
|
|
// Keep tooltip on screen
|
|
const tw = tip.offsetWidth;
|
|
const th = tip.offsetHeight;
|
|
if (x + tw > window.innerWidth - 8) x = e.clientX - tw - pad;
|
|
if (y + th > window.innerHeight - 8) y = e.clientY - th - pad;
|
|
tip.style.left = x + 'px';
|
|
tip.style.top = y + 'px';
|
|
});
|
|
})();
|