The game evolves alongside its players. Tracks behavior patterns (click frequency, resource spending, upgrade choices), detects player strategies (hoarder, rusher, optimizer, idle), and generates dynamic events that reward or challenge those strategies. - EmergentMechanics class: track(), detectPatterns(), generateEvent(), getState() - 6 pattern detectors: hoarder, rusher, optimizer, idle_player, clicker, balanced - 16 emergent events across all patterns with meaningful choices - localStorage persistence for cross-session behavior tracking - 25 unit tests, all passing - Hooks into writeCode, buyBuilding, doOps, and tick() - Stats panel shows emergent events count, pattern detections, active strategy - Self-contained: additive system, does not break existing mechanics
435 lines
15 KiB
JavaScript
435 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 () {
|
|
// 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';
|
|
});
|
|
})();
|