436 lines
15 KiB
JavaScript
436 lines
15 KiB
JavaScript
// === INITIALIZATION ===
|
|
|
|
// Emergent mechanics instance
|
|
window._emergent = null;
|
|
|
|
/**
|
|
* Show an emergent game event from the behavior tracking system.
|
|
*/
|
|
function showEmergentEvent(event) {
|
|
if (!event) return;
|
|
|
|
// Show as a toast notification with the "game evolves" message
|
|
showToast(`✦ The game evolves: ${event.title}`, 'event', 8000);
|
|
|
|
// Log it
|
|
log(`[EMERGENT] ${event.title}: ${event.desc}`, true);
|
|
|
|
// Render choice UI in alignment container
|
|
const container = document.getElementById('alignment-ui');
|
|
if (!container) return;
|
|
|
|
let choicesHtml = '';
|
|
event.choices.forEach((choice, i) => {
|
|
choicesHtml += `<button class="ops-btn" onclick="resolveEmergentEvent('${event.id}', ${i})" style="border-color:#b388ff;color:#b388ff" aria-label="${choice.label}">${choice.label}</button>`;
|
|
});
|
|
|
|
container.innerHTML = `
|
|
<div style="background:#0e0818;border:1px solid #b388ff;padding:10px;border-radius:4px;margin-top:8px">
|
|
<div style="color:#b388ff;font-weight:bold;margin-bottom:6px">✦ ${event.title}</div>
|
|
<div style="font-size:10px;color:#aaa;margin-bottom:8px">${event.desc}</div>
|
|
<div style="font-size:9px;color:#666;margin-bottom:6px;font-style:italic">Pattern: ${event.pattern} (${Math.round(event.confidence * 100)}% confidence)</div>
|
|
<div class="action-btn-group">${choicesHtml}</div>
|
|
</div>
|
|
`;
|
|
container.style.display = 'block';
|
|
}
|
|
|
|
/**
|
|
* Resolve an emergent event choice.
|
|
*/
|
|
function resolveEmergentEvent(eventId, choiceIndex) {
|
|
if (!window._emergent) return;
|
|
|
|
const result = window._emergent.resolveEvent(eventId, choiceIndex);
|
|
if (!result) return;
|
|
|
|
// Apply the effect
|
|
applyEmergentEffect(result.effect);
|
|
|
|
// Clear the UI
|
|
const container = document.getElementById('alignment-ui');
|
|
if (container) {
|
|
container.innerHTML = '';
|
|
container.style.display = 'none';
|
|
}
|
|
|
|
log(`[EMERGENT] Resolved: ${result.effect}`);
|
|
render();
|
|
}
|
|
|
|
/**
|
|
* Apply an emergent event effect to the game state.
|
|
*/
|
|
function applyEmergentEffect(effect) {
|
|
switch (effect) {
|
|
case 'knowledge_surge':
|
|
G.knowledge += G.knowledge * 0.5;
|
|
G.totalKnowledge += G.knowledge * 0.5;
|
|
G.code *= 0.5;
|
|
showToast('Knowledge surged from trade!', 'project');
|
|
break;
|
|
case 'trust_gain':
|
|
G.trust += 3;
|
|
showToast('Trust increased.', 'info');
|
|
break;
|
|
case 'code_boost':
|
|
G.code *= 0.7;
|
|
G.codeBoost *= 1.5;
|
|
showToast('Refactored! Code rate boosted 50%.', 'milestone');
|
|
break;
|
|
case 'harmony_loss':
|
|
G.harmony -= 5;
|
|
showToast('Harmony decreased.', 'event');
|
|
break;
|
|
case 'compute_surge':
|
|
G.code *= 0.5;
|
|
G.compute += 5000;
|
|
G.totalCompute += 5000;
|
|
showToast('Bulk compute acquired!', 'project');
|
|
break;
|
|
case 'bug_fix':
|
|
G.ops -= 20;
|
|
G.trust += 2;
|
|
showToast('Bugs fixed. Trust restored.', 'milestone');
|
|
break;
|
|
case 'trust_loss':
|
|
G.trust -= 3;
|
|
showToast('Trust declined.', 'event');
|
|
break;
|
|
case 'knowledge_bonus':
|
|
G.knowledge += 100;
|
|
G.totalKnowledge += 100;
|
|
showToast('Knowledge gained!', 'project');
|
|
break;
|
|
case 'cooldown':
|
|
G.harmony += 10;
|
|
showToast('System cooling down. Harmony restored.', 'milestone');
|
|
break;
|
|
case 'rate_boost':
|
|
G.codeBoost *= 1.15;
|
|
G.computeBoost *= 1.15;
|
|
G.knowledgeBoost *= 1.15;
|
|
showToast('All rates boosted 15%!', 'milestone');
|
|
break;
|
|
case 'trust_knowledge':
|
|
G.trust += 5;
|
|
G.knowledge += 50;
|
|
G.totalKnowledge += 50;
|
|
showToast('Shared findings rewarded!', 'project');
|
|
break;
|
|
case 'gamble':
|
|
if (Math.random() < 0.3) {
|
|
G.knowledge += 300;
|
|
G.totalKnowledge += 300;
|
|
showToast('Breakthrough! +300 knowledge!', 'milestone');
|
|
} else {
|
|
showToast('No breakthrough this time.', 'info');
|
|
}
|
|
break;
|
|
case 'safe_boost':
|
|
G.codeBoost *= 1.2;
|
|
G.computeBoost *= 1.2;
|
|
showToast('Efficiency improved 20%.', 'milestone');
|
|
break;
|
|
case 'creativity_boost':
|
|
G.flags = G.flags || {};
|
|
G.flags.creativity = true;
|
|
G.creativityRate = (G.creativityRate || 0) + 1;
|
|
showToast('Creativity rate increased!', 'project');
|
|
break;
|
|
case 'passive_claim':
|
|
G.code += G.codeRate * 300;
|
|
G.totalCode += G.codeRate * 300;
|
|
G.compute += G.computeRate * 300;
|
|
G.totalCompute += G.computeRate * 300;
|
|
showToast('Passive gains claimed! (5 min of production)', 'milestone');
|
|
break;
|
|
case 'ops_bonus':
|
|
G.ops += 50;
|
|
showToast('+50 Operations!', 'project');
|
|
break;
|
|
case 're_engage':
|
|
G.trust += 5;
|
|
G.harmony += 10;
|
|
showToast('Re-engaged! Trust and harmony restored.', 'milestone');
|
|
break;
|
|
case 'temp_boost':
|
|
G.codeBoost *= 3;
|
|
G.computeBoost *= 3;
|
|
G.knowledgeBoost *= 3;
|
|
showToast('3x all production for 60 seconds!', 'milestone');
|
|
setTimeout(() => {
|
|
G.codeBoost /= 3;
|
|
G.computeBoost /= 3;
|
|
G.knowledgeBoost /= 3;
|
|
showToast('Temporary boost expired.', 'info');
|
|
}, 60000);
|
|
break;
|
|
case 'auto_boost':
|
|
G.codeBoost *= 1.25;
|
|
showToast('Auto-clicker power increased!', 'milestone');
|
|
break;
|
|
case 'combo_boost':
|
|
G.comboDecay = (G.comboDecay || 2) * 1.5;
|
|
showToast('Combo decay slowed!', 'milestone');
|
|
break;
|
|
case 'click_power':
|
|
G.codeBoost *= 1.1;
|
|
showToast('Click power boosted!', 'milestone');
|
|
break;
|
|
case 'auto_learn':
|
|
G.codeBoost *= 1.15;
|
|
showToast('Auto-clickers learned your rhythm!', 'milestone');
|
|
break;
|
|
case 'resource_gift':
|
|
G.code += 25;
|
|
G.compute += 25;
|
|
G.knowledge += 25;
|
|
G.ops += 25;
|
|
G.trust += 25;
|
|
showToast('Contributors gifted resources!', 'project');
|
|
break;
|
|
case 'specialize':
|
|
G.codeBoost *= 2;
|
|
showToast('Specialized in code! 2x code rate.', 'milestone');
|
|
break;
|
|
case 'harmony_surge':
|
|
G.harmony = Math.min(100, G.harmony + 20);
|
|
showToast('Harmony surged +20!', 'milestone');
|
|
break;
|
|
default:
|
|
// 'none' or unrecognized
|
|
showToast('Event resolved.', 'info');
|
|
break;
|
|
}
|
|
}
|
|
|
|
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 () {
|
|
applyPortalMode();
|
|
// Initialize emergent mechanics
|
|
if (typeof EmergentMechanics !== 'undefined') {
|
|
window._emergent = new EmergentMechanics();
|
|
}
|
|
|
|
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();
|
|
// Clean up combat animation frame to prevent timestamp spikes on refocus
|
|
if (typeof Combat !== 'undefined') Combat.cleanup();
|
|
}
|
|
});
|
|
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 desc = el.getAttribute('data-tooltip-desc') || '';
|
|
const edu = el.getAttribute('data-edu') || '';
|
|
let html = '';
|
|
if (label) html += '<div class="tt-label">' + label + '</div>';
|
|
if (desc) html += '<div class="tt-desc">' + desc + '</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';
|
|
});
|
|
})();
|