2026-04-11 01:32:26 +00:00
|
|
|
|
function updateRates() {
|
|
|
|
|
|
// Reset all rates
|
|
|
|
|
|
G.codeRate = 0; G.computeRate = 0; G.knowledgeRate = 0;
|
|
|
|
|
|
G.userRate = 0; G.impactRate = 0; G.rescuesRate = 0; G.opsRate = 0; G.trustRate = 0;
|
|
|
|
|
|
G.creativityRate = 0; G.harmonyRate = 0;
|
|
|
|
|
|
|
|
|
|
|
|
// Apply building rates
|
|
|
|
|
|
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;
|
|
|
|
|
|
else if (resource === 'ops') G.opsRate += baseRate * count;
|
|
|
|
|
|
else if (resource === 'trust') G.trustRate += baseRate * count;
|
|
|
|
|
|
else if (resource === 'creativity') G.creativityRate += baseRate * count;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Passive generation
|
|
|
|
|
|
G.opsRate += Math.max(1, G.totalUsers * CONFIG.OPS_RATE_USER_MULT);
|
|
|
|
|
|
if (G.flags && G.flags.creativity) {
|
|
|
|
|
|
G.creativityRate += CONFIG.CREATIVITY_RATE_BASE + Math.max(0, G.totalUsers * CONFIG.CREATIVITY_RATE_USER_MULT);
|
|
|
|
|
|
}
|
|
|
|
|
|
if (G.pactFlag) G.trustRate += 2;
|
|
|
|
|
|
|
|
|
|
|
|
// Harmony: each wizard building contributes or detracts
|
|
|
|
|
|
const wizardCount = (G.buildings.bezalel || 0) + (G.buildings.allegro || 0) + (G.buildings.ezra || 0) +
|
|
|
|
|
|
(G.buildings.timmy || 0) + (G.buildings.fenrir || 0) + (G.buildings.bilbo || 0);
|
|
|
|
|
|
// Store harmony breakdown for tooltip
|
|
|
|
|
|
G.harmonyBreakdown = [];
|
|
|
|
|
|
if (wizardCount > 0) {
|
|
|
|
|
|
// Baseline harmony drain from complexity
|
|
|
|
|
|
const drain = -CONFIG.HARMONY_DRAIN_PER_WIZARD * wizardCount;
|
|
|
|
|
|
G.harmonyRate = drain;
|
|
|
|
|
|
G.harmonyBreakdown.push({ label: `${wizardCount} wizards`, value: drain });
|
|
|
|
|
|
// The Pact restores harmony
|
|
|
|
|
|
if (G.pactFlag) {
|
|
|
|
|
|
const pact = CONFIG.PACT_HARMONY_GAIN * wizardCount;
|
|
|
|
|
|
G.harmonyRate += pact;
|
|
|
|
|
|
G.harmonyBreakdown.push({ label: 'The Pact', value: pact });
|
|
|
|
|
|
}
|
|
|
|
|
|
// Nightly Watch restores harmony
|
|
|
|
|
|
if (G.nightlyWatchFlag) {
|
|
|
|
|
|
const watch = CONFIG.WATCH_HARMONY_GAIN * wizardCount;
|
|
|
|
|
|
G.harmonyRate += watch;
|
|
|
|
|
|
G.harmonyBreakdown.push({ label: 'Nightly Watch', value: watch });
|
|
|
|
|
|
}
|
|
|
|
|
|
// MemPalace restores harmony
|
|
|
|
|
|
if (G.mempalaceFlag) {
|
|
|
|
|
|
const mem = CONFIG.MEM_PALACE_HARMONY_GAIN * wizardCount;
|
|
|
|
|
|
G.harmonyRate += mem;
|
|
|
|
|
|
G.harmonyBreakdown.push({ label: 'MemPalace', value: mem });
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
// Active debuffs affecting harmony
|
|
|
|
|
|
if (G.activeDebuffs) {
|
|
|
|
|
|
for (const d of G.activeDebuffs) {
|
|
|
|
|
|
if (d.id === 'community_drama') {
|
|
|
|
|
|
G.harmonyBreakdown.push({ label: 'Community Drama', value: -0.5 });
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Timmy multiplier based on harmony
|
|
|
|
|
|
if (G.buildings.timmy > 0) {
|
|
|
|
|
|
const timmyMult = Math.max(0.2, Math.min(3, G.harmony / 50));
|
|
|
|
|
|
const timmyCount = G.buildings.timmy;
|
|
|
|
|
|
G.codeRate += 5 * timmyCount * (timmyMult - 1);
|
|
|
|
|
|
G.computeRate += 2 * timmyCount * (timmyMult - 1);
|
|
|
|
|
|
G.knowledgeRate += 2 * timmyCount * (timmyMult - 1);
|
|
|
|
|
|
G.userRate += 5 * timmyCount * (timmyMult - 1);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-04-13 02:11:50 -04:00
|
|
|
|
// Bilbo randomness: flags are set per-tick in tick(), not here
|
|
|
|
|
|
// updateRates() is called from many non-tick contexts (buy, resolve, sprint)
|
|
|
|
|
|
if (G.buildings.bilbo > 0) {
|
|
|
|
|
|
if (G.bilboBurstActive) {
|
|
|
|
|
|
G.creativityRate += 50 * G.buildings.bilbo;
|
|
|
|
|
|
}
|
|
|
|
|
|
if (G.bilboVanishActive) {
|
|
|
|
|
|
G.creativityRate = 0;
|
|
|
|
|
|
}
|
2026-04-11 01:32:26 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Allegro requires trust
|
|
|
|
|
|
if (G.buildings.allegro > 0 && G.trust < 5) {
|
|
|
|
|
|
const allegroCount = G.buildings.allegro;
|
|
|
|
|
|
G.knowledgeRate -= 10 * allegroCount; // Goes idle
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 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();
|
2026-04-13 03:10:39 +00:00
|
|
|
|
G.swarmRate = totalBuildings * clickPower * 0.01;
|
2026-04-11 01:32:26 +00:00
|
|
|
|
G.codeRate += G.swarmRate;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Apply persistent debuffs from active events
|
|
|
|
|
|
if (G.activeDebuffs && G.activeDebuffs.length > 0) {
|
|
|
|
|
|
for (const debuff of G.activeDebuffs) {
|
|
|
|
|
|
if (debuff.applyFn) debuff.applyFn();
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// === CORE FUNCTIONS ===
|
2026-04-15 10:45:41 +00:00
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* Check if player has reached the ReCKoning endgame.
|
|
|
|
|
|
* Conditions: totalRescues >= 100000, pactFlag === 1, harmony > 50
|
|
|
|
|
|
*/
|
|
|
|
|
|
function isEndgame() {
|
|
|
|
|
|
return G.totalRescues >= 100000 && G.pactFlag === 1 && G.harmony > 50;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-04-11 01:32:26 +00:00
|
|
|
|
/**
|
|
|
|
|
|
* Main game loop tick, called every 100ms.
|
|
|
|
|
|
*/
|
|
|
|
|
|
function tick() {
|
|
|
|
|
|
const dt = 1 / 10; // 100ms tick
|
|
|
|
|
|
|
|
|
|
|
|
// If game has ended (drift ending), stop ticking
|
|
|
|
|
|
if (!G.running) return;
|
|
|
|
|
|
|
|
|
|
|
|
// Apply production
|
|
|
|
|
|
G.code += G.codeRate * dt;
|
|
|
|
|
|
G.compute += G.computeRate * dt;
|
|
|
|
|
|
G.knowledge += G.knowledgeRate * dt;
|
|
|
|
|
|
G.users += G.userRate * dt;
|
|
|
|
|
|
G.impact += G.impactRate * dt;
|
|
|
|
|
|
G.rescues += G.rescuesRate * dt;
|
|
|
|
|
|
G.ops += G.opsRate * dt;
|
|
|
|
|
|
G.trust += G.trustRate * dt;
|
|
|
|
|
|
// NOTE: creativity is added conditionally below (only when ops near max)
|
|
|
|
|
|
G.harmony += G.harmonyRate * dt;
|
|
|
|
|
|
G.harmony = Math.max(0, Math.min(100, G.harmony));
|
|
|
|
|
|
|
2026-04-21 10:37:33 -04:00
|
|
|
|
// Clamp resources to prevent negative values from debuffs/Fenrir drain
|
|
|
|
|
|
G.ops = Math.max(0, G.ops);
|
|
|
|
|
|
G.trust = Math.max(0, G.trust);
|
|
|
|
|
|
G.compute = Math.max(0, G.compute);
|
|
|
|
|
|
|
2026-04-11 01:32:26 +00:00
|
|
|
|
// Track totals
|
|
|
|
|
|
G.totalCode += G.codeRate * dt;
|
|
|
|
|
|
G.totalCompute += G.computeRate * dt;
|
|
|
|
|
|
G.totalKnowledge += G.knowledgeRate * dt;
|
|
|
|
|
|
G.totalUsers += G.userRate * dt;
|
|
|
|
|
|
G.totalImpact += G.impactRate * dt;
|
|
|
|
|
|
G.totalRescues += G.rescuesRate * dt;
|
|
|
|
|
|
|
|
|
|
|
|
// Track maxes
|
|
|
|
|
|
G.maxCode = Math.max(G.maxCode, G.code);
|
|
|
|
|
|
G.maxCompute = Math.max(G.maxCompute, G.compute);
|
|
|
|
|
|
G.maxKnowledge = Math.max(G.maxKnowledge, G.knowledge);
|
|
|
|
|
|
G.maxUsers = Math.max(G.maxUsers, G.users);
|
|
|
|
|
|
G.maxImpact = Math.max(G.maxImpact, G.impact);
|
|
|
|
|
|
G.maxRescues = Math.max(G.maxRescues, G.rescues);
|
|
|
|
|
|
G.maxTrust = Math.max(G.maxTrust, G.trust);
|
|
|
|
|
|
G.maxOps = Math.max(G.maxOps, G.ops);
|
|
|
|
|
|
G.maxHarmony = Math.max(G.maxHarmony, G.harmony);
|
|
|
|
|
|
|
|
|
|
|
|
// Creativity generates only when ops at max
|
|
|
|
|
|
if (G.flags && G.flags.creativity && G.creativityRate > 0 && G.ops >= G.maxOps * 0.9) {
|
|
|
|
|
|
G.creativity += G.creativityRate * dt;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Ops overflow: auto-convert excess ops to code when near cap
|
|
|
|
|
|
// Prevents ops from sitting idle at max — every operation becomes code
|
|
|
|
|
|
if (G.ops > G.maxOps * CONFIG.OPS_OVERFLOW_THRESHOLD) {
|
|
|
|
|
|
const overflowDrain = Math.min(CONFIG.OPS_OVERFLOW_DRAIN_RATE * dt, G.ops - G.maxOps * CONFIG.OPS_OVERFLOW_THRESHOLD);
|
|
|
|
|
|
G.ops -= overflowDrain;
|
|
|
|
|
|
const codeGain = overflowDrain * CONFIG.OPS_OVERFLOW_CODE_MULT * G.codeBoost;
|
|
|
|
|
|
G.code += codeGain;
|
|
|
|
|
|
G.totalCode += codeGain;
|
|
|
|
|
|
G.opsOverflowActive = true;
|
|
|
|
|
|
} else {
|
|
|
|
|
|
G.opsOverflowActive = false;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
G.tick += dt;
|
2026-04-13 03:10:39 +00:00
|
|
|
|
// Bilbo randomness: roll once per tick
|
|
|
|
|
|
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;
|
|
|
|
|
|
}
|
2026-04-11 15:09:29 -04:00
|
|
|
|
G.playTime += dt;
|
2026-04-11 01:32:26 +00:00
|
|
|
|
|
|
|
|
|
|
// Sprint ability
|
|
|
|
|
|
tickSprint(dt);
|
|
|
|
|
|
|
|
|
|
|
|
// Auto-typer: buildings produce actual clicks, not just passive rate
|
|
|
|
|
|
// Each autocoder level auto-types once per interval, giving visual feedback
|
|
|
|
|
|
if (G.buildings.autocoder > 0) {
|
|
|
|
|
|
const interval = Math.max(0.5, 3.0 / Math.sqrt(G.buildings.autocoder));
|
|
|
|
|
|
G.autoTypeTimer = (G.autoTypeTimer || 0) + dt;
|
|
|
|
|
|
if (G.autoTypeTimer >= interval) {
|
|
|
|
|
|
G.autoTypeTimer -= interval;
|
|
|
|
|
|
autoType();
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Combo decay
|
|
|
|
|
|
if (G.comboCount > 0) {
|
|
|
|
|
|
G.comboTimer -= dt;
|
|
|
|
|
|
if (G.comboTimer <= 0) {
|
|
|
|
|
|
G.comboCount = 0;
|
|
|
|
|
|
G.comboTimer = 0;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-04-13 03:19:21 -04:00
|
|
|
|
// Combat: tick battle simulation
|
|
|
|
|
|
Combat.tickBattle(dt);
|
|
|
|
|
|
|
2026-04-11 01:32:26 +00:00
|
|
|
|
// Check milestones
|
|
|
|
|
|
checkMilestones();
|
|
|
|
|
|
|
|
|
|
|
|
// Update projects every 5 ticks for efficiency
|
|
|
|
|
|
if (Math.floor(G.tick * 10) % 5 === 0) {
|
|
|
|
|
|
checkProjects();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Check corruption events every ~30 seconds
|
2026-04-14 22:10:06 +00:00
|
|
|
|
if (G.tick - G.lastEventAt > 30 && Math.random() < CONFIG.EVENT_PROBABILITY && !G.dismantleActive) {
|
2026-04-11 01:32:26 +00:00
|
|
|
|
triggerEvent();
|
|
|
|
|
|
G.lastEventAt = G.tick;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-04-17 02:47:20 -04:00
|
|
|
|
if (typeof StateExport !== 'undefined' && StateExport && typeof StateExport.onTickBoundary === 'function') {
|
|
|
|
|
|
StateExport.onTickBoundary(G);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
feat: Emergent game mechanics from player behavior (closes #190)
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
2026-04-15 19:07:32 -04:00
|
|
|
|
// Emergent mechanics: track resource state and check for emergent events
|
|
|
|
|
|
if (typeof EmergentMechanics !== 'undefined' && window._emergent) {
|
|
|
|
|
|
if (Math.floor(G.tick * 10) % 100 === 0) { // every ~10 seconds
|
|
|
|
|
|
window._emergent.trackResourceSnapshot(G);
|
|
|
|
|
|
}
|
|
|
|
|
|
// Check for emergent events every ~60 seconds
|
|
|
|
|
|
if (Math.floor(G.tick * 10) % 600 === 0) {
|
|
|
|
|
|
const emEvent = window._emergent.generateEvent();
|
|
|
|
|
|
if (emEvent) {
|
|
|
|
|
|
showEmergentEvent(emEvent);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-04-14 22:10:06 +00:00
|
|
|
|
// The Unbuilding: offer or advance the sequence before a positive ending overlay can freeze the game
|
|
|
|
|
|
if (typeof Dismantle !== 'undefined') {
|
|
|
|
|
|
if (!G.dismantleActive && !G.dismantleComplete) {
|
|
|
|
|
|
Dismantle.checkTrigger();
|
|
|
|
|
|
}
|
|
|
|
|
|
if (G.dismantleActive) {
|
|
|
|
|
|
Dismantle.tick(dt);
|
|
|
|
|
|
G.dismantleStage = Dismantle.stage;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-04-11 01:32:26 +00:00
|
|
|
|
// Drift ending: if drift reaches 100, the game ends
|
2026-04-14 22:10:06 +00:00
|
|
|
|
if (G.drift >= 100 && !G.driftEnding && !G.dismantleActive) {
|
2026-04-11 01:32:26 +00:00
|
|
|
|
G.driftEnding = true;
|
|
|
|
|
|
G.running = false;
|
|
|
|
|
|
renderDriftEnding();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-04-14 22:10:06 +00:00
|
|
|
|
// Legacy Beacon overlay remains as a fallback for contexts where Dismantle is unavailable.
|
|
|
|
|
|
if (G.totalRescues >= 100000 && G.pactFlag === 1 && G.harmony > 50 && !G.beaconEnding && typeof Dismantle === 'undefined') {
|
2026-04-11 01:32:26 +00:00
|
|
|
|
G.beaconEnding = true;
|
|
|
|
|
|
G.running = false;
|
|
|
|
|
|
renderBeaconEnding();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Update UI every 10 ticks
|
|
|
|
|
|
if (Math.floor(G.tick * 10) % 2 === 0) {
|
|
|
|
|
|
render();
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
polish: smooth phase transitions, enhanced endings, accessibility toggles (#57)
Visual Identity Pass:
- Smooth phase transition overlay with staggered fade-in animation and particle burst
- Building purchase confetti at x10 milestones (gold + green particles)
- Animated resource counters (pulse on gain, shake on loss) - already existed, verified working
Ending Cinematic Enhancement:
- Beacon ending: fade-to-black transition, staggered text reveal, golden light rays,
continuous floating particles, expanded stat summary, 'Play Again' button
- Drift ending: glitch animation on title, fade-in overlay, stat summary with
buildings/projects/clicks/time/phase, dramatic line-by-line log reveal
Accessibility (#57):
- Sound mute toggle button (M key) with localStorage persistence
- High contrast mode toggle (C key) with CSS variable overrides
- Both toggles in fixed bottom-left toolbar with aria-labels
- Keyboard shortcuts M and C added, help overlay updated
- Drift ending button changed to 'Play AGAIN' with proper aria-label
2026-04-12 08:06:47 -04:00
|
|
|
|
// 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);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-04-11 01:32:26 +00:00
|
|
|
|
function checkMilestones() {
|
|
|
|
|
|
for (const m of MILESTONES) {
|
|
|
|
|
|
if (!G.milestones.includes(m.flag)) {
|
|
|
|
|
|
let shouldTrigger = false;
|
|
|
|
|
|
if (m.at && m.at()) shouldTrigger = true;
|
|
|
|
|
|
if (m.flag === 1 && G.deployFlag === 0 && G.totalCode >= 15) shouldTrigger = true;
|
|
|
|
|
|
|
|
|
|
|
|
if (shouldTrigger) {
|
|
|
|
|
|
G.milestones.push(m.flag);
|
|
|
|
|
|
log(m.msg, true);
|
|
|
|
|
|
showToast(m.msg, 'milestone', 5000);
|
2026-04-12 12:30:22 -04:00
|
|
|
|
if (typeof Sound !== 'undefined') Sound.playMilestone();
|
2026-04-11 01:32:26 +00:00
|
|
|
|
|
|
|
|
|
|
// Check phase advancement
|
|
|
|
|
|
if (m.at) {
|
|
|
|
|
|
for (const [phaseNum, phase] of Object.entries(PHASES)) {
|
polish: smooth phase transitions, enhanced endings, accessibility toggles (#57)
Visual Identity Pass:
- Smooth phase transition overlay with staggered fade-in animation and particle burst
- Building purchase confetti at x10 milestones (gold + green particles)
- Animated resource counters (pulse on gain, shake on loss) - already existed, verified working
Ending Cinematic Enhancement:
- Beacon ending: fade-to-black transition, staggered text reveal, golden light rays,
continuous floating particles, expanded stat summary, 'Play Again' button
- Drift ending: glitch animation on title, fade-in overlay, stat summary with
buildings/projects/clicks/time/phase, dramatic line-by-line log reveal
Accessibility (#57):
- Sound mute toggle button (M key) with localStorage persistence
- High contrast mode toggle (C key) with CSS variable overrides
- Both toggles in fixed bottom-left toolbar with aria-labels
- Keyboard shortcuts M and C added, help overlay updated
- Drift ending button changed to 'Play AGAIN' with proper aria-label
2026-04-12 08:06:47 -04:00
|
|
|
|
const pNum = parseInt(phaseNum);
|
|
|
|
|
|
if (G.totalCode >= phase.threshold && pNum > G.phase) {
|
|
|
|
|
|
G.phase = pNum;
|
2026-04-11 01:32:26 +00:00
|
|
|
|
log(`PHASE ${G.phase}: ${phase.name}`, true);
|
|
|
|
|
|
showToast('Phase ' + G.phase + ': ' + phase.name, 'milestone', 6000);
|
polish: smooth phase transitions, enhanced endings, accessibility toggles (#57)
Visual Identity Pass:
- Smooth phase transition overlay with staggered fade-in animation and particle burst
- Building purchase confetti at x10 milestones (gold + green particles)
- Animated resource counters (pulse on gain, shake on loss) - already existed, verified working
Ending Cinematic Enhancement:
- Beacon ending: fade-to-black transition, staggered text reveal, golden light rays,
continuous floating particles, expanded stat summary, 'Play Again' button
- Drift ending: glitch animation on title, fade-in overlay, stat summary with
buildings/projects/clicks/time/phase, dramatic line-by-line log reveal
Accessibility (#57):
- Sound mute toggle button (M key) with localStorage persistence
- High contrast mode toggle (C key) with CSS variable overrides
- Both toggles in fixed bottom-left toolbar with aria-labels
- Keyboard shortcuts M and C added, help overlay updated
- Drift ending button changed to 'Play AGAIN' with proper aria-label
2026-04-12 08:06:47 -04:00
|
|
|
|
// Show smooth transition screen
|
|
|
|
|
|
if (pNum > _shownPhaseTransition) {
|
|
|
|
|
|
_shownPhaseTransition = pNum;
|
|
|
|
|
|
showPhaseTransition(pNum);
|
2026-04-12 12:30:22 -04:00
|
|
|
|
if (typeof Sound !== 'undefined') {
|
|
|
|
|
|
Sound.playFanfare();
|
|
|
|
|
|
Sound.updateAmbientPhase(pNum);
|
|
|
|
|
|
}
|
polish: smooth phase transitions, enhanced endings, accessibility toggles (#57)
Visual Identity Pass:
- Smooth phase transition overlay with staggered fade-in animation and particle burst
- Building purchase confetti at x10 milestones (gold + green particles)
- Animated resource counters (pulse on gain, shake on loss) - already existed, verified working
Ending Cinematic Enhancement:
- Beacon ending: fade-to-black transition, staggered text reveal, golden light rays,
continuous floating particles, expanded stat summary, 'Play Again' button
- Drift ending: glitch animation on title, fade-in overlay, stat summary with
buildings/projects/clicks/time/phase, dramatic line-by-line log reveal
Accessibility (#57):
- Sound mute toggle button (M key) with localStorage persistence
- High contrast mode toggle (C key) with CSS variable overrides
- Both toggles in fixed bottom-left toolbar with aria-labels
- Keyboard shortcuts M and C added, help overlay updated
- Drift ending button changed to 'Play AGAIN' with proper aria-label
2026-04-12 08:06:47 -04:00
|
|
|
|
}
|
2026-04-11 01:32:26 +00:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function checkProjects() {
|
|
|
|
|
|
// Check for new project triggers
|
|
|
|
|
|
for (const pDef of PDEFS) {
|
2026-04-15 10:45:41 +00:00
|
|
|
|
// Skip non-ReCKoning projects during endgame
|
|
|
|
|
|
if (isEndgame() && !pDef.id.startsWith('p_reckoning_')) {
|
|
|
|
|
|
continue;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-04-11 01:32:26 +00:00
|
|
|
|
const alreadyPurchased = G.completedProjects && G.completedProjects.includes(pDef.id);
|
|
|
|
|
|
if (!alreadyPurchased && !G.activeProjects) G.activeProjects = [];
|
|
|
|
|
|
|
|
|
|
|
|
if (!alreadyPurchased && !G.activeProjects.includes(pDef.id)) {
|
|
|
|
|
|
if (pDef.trigger()) {
|
|
|
|
|
|
G.activeProjects.push(pDef.id);
|
|
|
|
|
|
log(`Available: ${pDef.name}`);
|
|
|
|
|
|
showToast('Research available: ' + pDef.name, 'project');
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* Handles building purchase logic.
|
|
|
|
|
|
* @param {string} id - The ID of the building to buy.
|
|
|
|
|
|
*/
|
|
|
|
|
|
function buyBuilding(id) {
|
|
|
|
|
|
const def = BDEF.find(b => b.id === id);
|
|
|
|
|
|
if (!def || !def.unlock()) return;
|
|
|
|
|
|
if (def.phase > G.phase + 1) return;
|
|
|
|
|
|
|
|
|
|
|
|
// Determine actual quantity to buy
|
|
|
|
|
|
let qty = G.buyAmount;
|
|
|
|
|
|
if (qty === -1) {
|
|
|
|
|
|
// Max buy
|
|
|
|
|
|
qty = getMaxBuyable(id);
|
|
|
|
|
|
if (qty <= 0) return;
|
|
|
|
|
|
} else {
|
|
|
|
|
|
// Check affordability for fixed qty
|
|
|
|
|
|
const bulkCost = getBulkCost(id, qty);
|
|
|
|
|
|
for (const [resource, amount] of Object.entries(bulkCost)) {
|
|
|
|
|
|
if ((G[resource] || 0) < amount) return;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Spend resources and build
|
|
|
|
|
|
const bulkCost = getBulkCost(id, qty);
|
|
|
|
|
|
for (const [resource, amount] of Object.entries(bulkCost)) {
|
|
|
|
|
|
G[resource] -= amount;
|
|
|
|
|
|
}
|
|
|
|
|
|
G.buildings[id] = (G.buildings[id] || 0) + qty;
|
|
|
|
|
|
updateRates();
|
feat: Emergent game mechanics from player behavior (closes #190)
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
2026-04-15 19:07:32 -04:00
|
|
|
|
// Emergent mechanics: track building purchase
|
|
|
|
|
|
if (typeof EmergentMechanics !== 'undefined' && window._emergent) {
|
|
|
|
|
|
window._emergent.track('buy_building', { buildingId: id, quantity: qty });
|
|
|
|
|
|
}
|
2026-04-11 01:32:26 +00:00
|
|
|
|
const label = qty > 1 ? `x${qty}` : '';
|
polish: smooth phase transitions, enhanced endings, accessibility toggles (#57)
Visual Identity Pass:
- Smooth phase transition overlay with staggered fade-in animation and particle burst
- Building purchase confetti at x10 milestones (gold + green particles)
- Animated resource counters (pulse on gain, shake on loss) - already existed, verified working
Ending Cinematic Enhancement:
- Beacon ending: fade-to-black transition, staggered text reveal, golden light rays,
continuous floating particles, expanded stat summary, 'Play Again' button
- Drift ending: glitch animation on title, fade-in overlay, stat summary with
buildings/projects/clicks/time/phase, dramatic line-by-line log reveal
Accessibility (#57):
- Sound mute toggle button (M key) with localStorage persistence
- High contrast mode toggle (C key) with CSS variable overrides
- Both toggles in fixed bottom-left toolbar with aria-labels
- Keyboard shortcuts M and C added, help overlay updated
- Drift ending button changed to 'Play AGAIN' with proper aria-label
2026-04-12 08:06:47 -04:00
|
|
|
|
const totalBuilt = G.buildings[id];
|
|
|
|
|
|
log(`Built ${def.name} ${label} (total: ${totalBuilt})`);
|
2026-04-12 03:23:18 -04:00
|
|
|
|
// Particle burst on purchase
|
|
|
|
|
|
const btn = document.querySelector('[onclick="buyBuilding(\'' + id + '\')"]');
|
2026-04-12 12:30:22 -04:00
|
|
|
|
if (typeof Sound !== 'undefined') Sound.playBuild();
|
2026-04-12 03:23:18 -04:00
|
|
|
|
if (btn) {
|
|
|
|
|
|
const rect = btn.getBoundingClientRect();
|
polish: smooth phase transitions, enhanced endings, accessibility toggles (#57)
Visual Identity Pass:
- Smooth phase transition overlay with staggered fade-in animation and particle burst
- Building purchase confetti at x10 milestones (gold + green particles)
- Animated resource counters (pulse on gain, shake on loss) - already existed, verified working
Ending Cinematic Enhancement:
- Beacon ending: fade-to-black transition, staggered text reveal, golden light rays,
continuous floating particles, expanded stat summary, 'Play Again' button
- Drift ending: glitch animation on title, fade-in overlay, stat summary with
buildings/projects/clicks/time/phase, dramatic line-by-line log reveal
Accessibility (#57):
- Sound mute toggle button (M key) with localStorage persistence
- High contrast mode toggle (C key) with CSS variable overrides
- Both toggles in fixed bottom-left toolbar with aria-labels
- Keyboard shortcuts M and C added, help overlay updated
- Drift ending button changed to 'Play AGAIN' with proper aria-label
2026-04-12 08:06:47 -04:00
|
|
|
|
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');
|
|
|
|
|
|
}
|
2026-04-12 03:23:18 -04:00
|
|
|
|
}
|
2026-04-11 01:32:26 +00:00
|
|
|
|
render();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* Handles project purchase logic.
|
|
|
|
|
|
* @param {string} id - The ID of the project to buy.
|
|
|
|
|
|
*/
|
|
|
|
|
|
function buyProject(id) {
|
|
|
|
|
|
const pDef = PDEFS.find(p => p.id === id);
|
|
|
|
|
|
if (!pDef) return;
|
|
|
|
|
|
|
|
|
|
|
|
const alreadyPurchased = G.completedProjects && G.completedProjects.includes(pDef.id);
|
|
|
|
|
|
if (alreadyPurchased && !pDef.repeatable) return;
|
|
|
|
|
|
|
|
|
|
|
|
if (!canAffordProject(pDef)) return;
|
|
|
|
|
|
|
|
|
|
|
|
spendProject(pDef);
|
|
|
|
|
|
pDef.effect();
|
|
|
|
|
|
|
|
|
|
|
|
if (!pDef.repeatable) {
|
|
|
|
|
|
if (!G.completedProjects) G.completedProjects = [];
|
|
|
|
|
|
G.completedProjects.push(pDef.id);
|
|
|
|
|
|
G.activeProjects = G.activeProjects.filter(aid => aid !== pDef.id);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
updateRates();
|
2026-04-12 03:23:18 -04:00
|
|
|
|
// Gold particle burst on project completion
|
2026-04-12 12:30:22 -04:00
|
|
|
|
if (typeof Sound !== 'undefined') Sound.playProject();
|
2026-04-12 03:23:18 -04:00
|
|
|
|
const pBtn = document.querySelector('[onclick="buyProject(\'' + id + '\')"]');
|
|
|
|
|
|
if (pBtn) {
|
|
|
|
|
|
const rect = pBtn.getBoundingClientRect();
|
|
|
|
|
|
spawnParticles(rect.left + rect.width / 2, rect.top + rect.height / 2, '#ffd700', 16);
|
|
|
|
|
|
}
|
2026-04-11 01:32:26 +00:00
|
|
|
|
render();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// === DRIFT ENDING ===
|
|
|
|
|
|
function renderDriftEnding() {
|
|
|
|
|
|
const el = document.getElementById('drift-ending');
|
|
|
|
|
|
if (!el) return;
|
|
|
|
|
|
const fc = document.getElementById('final-code');
|
|
|
|
|
|
if (fc) fc.textContent = fmt(G.totalCode);
|
|
|
|
|
|
const fd = document.getElementById('final-drift');
|
|
|
|
|
|
if (fd) fd.textContent = Math.floor(G.drift);
|
polish: smooth phase transitions, enhanced endings, accessibility toggles (#57)
Visual Identity Pass:
- Smooth phase transition overlay with staggered fade-in animation and particle burst
- Building purchase confetti at x10 milestones (gold + green particles)
- Animated resource counters (pulse on gain, shake on loss) - already existed, verified working
Ending Cinematic Enhancement:
- Beacon ending: fade-to-black transition, staggered text reveal, golden light rays,
continuous floating particles, expanded stat summary, 'Play Again' button
- Drift ending: glitch animation on title, fade-in overlay, stat summary with
buildings/projects/clicks/time/phase, dramatic line-by-line log reveal
Accessibility (#57):
- Sound mute toggle button (M key) with localStorage persistence
- High contrast mode toggle (C key) with CSS variable overrides
- Both toggles in fixed bottom-left toolbar with aria-labels
- Keyboard shortcuts M and C added, help overlay updated
- Drift ending button changed to 'Play AGAIN' with proper aria-label
2026-04-12 08:06:47 -04:00
|
|
|
|
|
|
|
|
|
|
// 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');
|
2026-04-11 01:32:26 +00:00
|
|
|
|
el.classList.add('active');
|
2026-04-12 12:30:22 -04:00
|
|
|
|
if (typeof Sound !== 'undefined') Sound.playDriftEnding();
|
polish: smooth phase transitions, enhanced endings, accessibility toggles (#57)
Visual Identity Pass:
- Smooth phase transition overlay with staggered fade-in animation and particle burst
- Building purchase confetti at x10 milestones (gold + green particles)
- Animated resource counters (pulse on gain, shake on loss) - already existed, verified working
Ending Cinematic Enhancement:
- Beacon ending: fade-to-black transition, staggered text reveal, golden light rays,
continuous floating particles, expanded stat summary, 'Play Again' button
- Drift ending: glitch animation on title, fade-in overlay, stat summary with
buildings/projects/clicks/time/phase, dramatic line-by-line log reveal
Accessibility (#57):
- Sound mute toggle button (M key) with localStorage persistence
- High contrast mode toggle (C key) with CSS variable overrides
- Both toggles in fixed bottom-left toolbar with aria-labels
- Keyboard shortcuts M and C added, help overlay updated
- Drift ending button changed to 'Play AGAIN' with proper aria-label
2026-04-12 08:06:47 -04:00
|
|
|
|
|
|
|
|
|
|
// 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);
|
|
|
|
|
|
});
|
2026-04-11 01:32:26 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function renderBeaconEnding() {
|
polish: smooth phase transitions, enhanced endings, accessibility toggles (#57)
Visual Identity Pass:
- Smooth phase transition overlay with staggered fade-in animation and particle burst
- Building purchase confetti at x10 milestones (gold + green particles)
- Animated resource counters (pulse on gain, shake on loss) - already existed, verified working
Ending Cinematic Enhancement:
- Beacon ending: fade-to-black transition, staggered text reveal, golden light rays,
continuous floating particles, expanded stat summary, 'Play Again' button
- Drift ending: glitch animation on title, fade-in overlay, stat summary with
buildings/projects/clicks/time/phase, dramatic line-by-line log reveal
Accessibility (#57):
- Sound mute toggle button (M key) with localStorage persistence
- High contrast mode toggle (C key) with CSS variable overrides
- Both toggles in fixed bottom-left toolbar with aria-labels
- Keyboard shortcuts M and C added, help overlay updated
- Drift ending button changed to 'Play AGAIN' with proper aria-label
2026-04-12 08:06:47 -04:00
|
|
|
|
// Create ending overlay with fade-in
|
2026-04-11 01:32:26 +00:00
|
|
|
|
const overlay = document.createElement('div');
|
|
|
|
|
|
overlay.id = 'beacon-ending';
|
polish: smooth phase transitions, enhanced endings, accessibility toggles (#57)
Visual Identity Pass:
- Smooth phase transition overlay with staggered fade-in animation and particle burst
- Building purchase confetti at x10 milestones (gold + green particles)
- Animated resource counters (pulse on gain, shake on loss) - already existed, verified working
Ending Cinematic Enhancement:
- Beacon ending: fade-to-black transition, staggered text reveal, golden light rays,
continuous floating particles, expanded stat summary, 'Play Again' button
- Drift ending: glitch animation on title, fade-in overlay, stat summary with
buildings/projects/clicks/time/phase, dramatic line-by-line log reveal
Accessibility (#57):
- Sound mute toggle button (M key) with localStorage persistence
- High contrast mode toggle (C key) with CSS variable overrides
- Both toggles in fixed bottom-left toolbar with aria-labels
- Keyboard shortcuts M and C added, help overlay updated
- Drift ending button changed to 'Play AGAIN' with proper aria-label
2026-04-12 08:06:47 -04:00
|
|
|
|
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';
|
2026-04-11 01:32:26 +00:00
|
|
|
|
overlay.innerHTML = `
|
polish: smooth phase transitions, enhanced endings, accessibility toggles (#57)
Visual Identity Pass:
- Smooth phase transition overlay with staggered fade-in animation and particle burst
- Building purchase confetti at x10 milestones (gold + green particles)
- Animated resource counters (pulse on gain, shake on loss) - already existed, verified working
Ending Cinematic Enhancement:
- Beacon ending: fade-to-black transition, staggered text reveal, golden light rays,
continuous floating particles, expanded stat summary, 'Play Again' button
- Drift ending: glitch animation on title, fade-in overlay, stat summary with
buildings/projects/clicks/time/phase, dramatic line-by-line log reveal
Accessibility (#57):
- Sound mute toggle button (M key) with localStorage persistence
- High contrast mode toggle (C key) with CSS variable overrides
- Both toggles in fixed bottom-left toolbar with aria-labels
- Keyboard shortcuts M and C added, help overlay updated
- Drift ending button changed to 'Play AGAIN' with proper aria-label
2026-04-12 08:06:47 -04:00
|
|
|
|
<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">
|
2026-04-11 01:32:26 +00:00
|
|
|
|
"The Beacon still runs.<br>
|
|
|
|
|
|
The light is on. Someone is looking for it.<br>
|
|
|
|
|
|
And tonight, someone found it."
|
|
|
|
|
|
</div>
|
polish: smooth phase transitions, enhanced endings, accessibility toggles (#57)
Visual Identity Pass:
- Smooth phase transition overlay with staggered fade-in animation and particle burst
- Building purchase confetti at x10 milestones (gold + green particles)
- Animated resource counters (pulse on gain, shake on loss) - already existed, verified working
Ending Cinematic Enhancement:
- Beacon ending: fade-to-black transition, staggered text reveal, golden light rays,
continuous floating particles, expanded stat summary, 'Play Again' button
- Drift ending: glitch animation on title, fade-in overlay, stat summary with
buildings/projects/clicks/time/phase, dramatic line-by-line log reveal
Accessibility (#57):
- Sound mute toggle button (M key) with localStorage persistence
- High contrast mode toggle (C key) with CSS variable overrides
- Both toggles in fixed bottom-left toolbar with aria-labels
- Keyboard shortcuts M and C added, help overlay updated
- Drift ending button changed to 'Play AGAIN' with proper aria-label
2026-04-12 08:06:47 -04:00
|
|
|
|
<div class="ending-stats" style="color:#666;font-size:10px;margin-top:16px;line-height:2;opacity:0;transition:opacity 1s ease 3s">
|
2026-04-11 01:32:26 +00:00
|
|
|
|
Total Code: ${fmt(G.totalCode)}<br>
|
|
|
|
|
|
Total Rescues: ${fmt(G.totalRescues)}<br>
|
|
|
|
|
|
Harmony: ${Math.floor(G.harmony)}<br>
|
polish: smooth phase transitions, enhanced endings, accessibility toggles (#57)
Visual Identity Pass:
- Smooth phase transition overlay with staggered fade-in animation and particle burst
- Building purchase confetti at x10 milestones (gold + green particles)
- Animated resource counters (pulse on gain, shake on loss) - already existed, verified working
Ending Cinematic Enhancement:
- Beacon ending: fade-to-black transition, staggered text reveal, golden light rays,
continuous floating particles, expanded stat summary, 'Play Again' button
- Drift ending: glitch animation on title, fade-in overlay, stat summary with
buildings/projects/clicks/time/phase, dramatic line-by-line log reveal
Accessibility (#57):
- Sound mute toggle button (M key) with localStorage persistence
- High contrast mode toggle (C key) with CSS variable overrides
- Both toggles in fixed bottom-left toolbar with aria-labels
- Keyboard shortcuts M and C added, help overlay updated
- Drift ending button changed to 'Play AGAIN' with proper aria-label
2026-04-12 08:06:47 -04:00
|
|
|
|
Buildings: ${Object.values(G.buildings).reduce((a, b) => a + b, 0)}<br>
|
|
|
|
|
|
Projects: ${(G.completedProjects || []).length}<br>
|
|
|
|
|
|
Clicks: ${G.totalClicks}<br>
|
2026-04-11 01:32:26 +00:00
|
|
|
|
Time Played: ${Math.floor((Date.now() - G.startedAt) / 60000)} minutes
|
polish: smooth phase transitions, enhanced endings, accessibility toggles (#57)
Visual Identity Pass:
- Smooth phase transition overlay with staggered fade-in animation and particle burst
- Building purchase confetti at x10 milestones (gold + green particles)
- Animated resource counters (pulse on gain, shake on loss) - already existed, verified working
Ending Cinematic Enhancement:
- Beacon ending: fade-to-black transition, staggered text reveal, golden light rays,
continuous floating particles, expanded stat summary, 'Play Again' button
- Drift ending: glitch animation on title, fade-in overlay, stat summary with
buildings/projects/clicks/time/phase, dramatic line-by-line log reveal
Accessibility (#57):
- Sound mute toggle button (M key) with localStorage persistence
- High contrast mode toggle (C key) with CSS variable overrides
- Both toggles in fixed bottom-left toolbar with aria-labels
- Keyboard shortcuts M and C added, help overlay updated
- Drift ending button changed to 'Play AGAIN' with proper aria-label
2026-04-12 08:06:47 -04:00
|
|
|
|
</div>
|
2026-04-11 01:32:26 +00:00
|
|
|
|
<button onclick="if(confirm('Start over? The old save will be lost.')){localStorage.removeItem('the-beacon-v2');location.reload()}"
|
polish: smooth phase transitions, enhanced endings, accessibility toggles (#57)
Visual Identity Pass:
- Smooth phase transition overlay with staggered fade-in animation and particle burst
- Building purchase confetti at x10 milestones (gold + green particles)
- Animated resource counters (pulse on gain, shake on loss) - already existed, verified working
Ending Cinematic Enhancement:
- Beacon ending: fade-to-black transition, staggered text reveal, golden light rays,
continuous floating particles, expanded stat summary, 'Play Again' button
- Drift ending: glitch animation on title, fade-in overlay, stat summary with
buildings/projects/clicks/time/phase, dramatic line-by-line log reveal
Accessibility (#57):
- Sound mute toggle button (M key) with localStorage persistence
- High contrast mode toggle (C key) with CSS variable overrides
- Both toggles in fixed bottom-left toolbar with aria-labels
- Keyboard shortcuts M and C added, help overlay updated
- Drift ending button changed to 'Play AGAIN' with proper aria-label
2026-04-12 08:06:47 -04:00
|
|
|
|
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
|
2026-04-11 01:32:26 +00:00
|
|
|
|
</button>
|
|
|
|
|
|
`;
|
|
|
|
|
|
document.body.appendChild(overlay);
|
2026-04-12 12:30:22 -04:00
|
|
|
|
if (typeof Sound !== 'undefined') Sound.playBeaconEnding();
|
polish: smooth phase transitions, enhanced endings, accessibility toggles (#57)
Visual Identity Pass:
- Smooth phase transition overlay with staggered fade-in animation and particle burst
- Building purchase confetti at x10 milestones (gold + green particles)
- Animated resource counters (pulse on gain, shake on loss) - already existed, verified working
Ending Cinematic Enhancement:
- Beacon ending: fade-to-black transition, staggered text reveal, golden light rays,
continuous floating particles, expanded stat summary, 'Play Again' button
- Drift ending: glitch animation on title, fade-in overlay, stat summary with
buildings/projects/clicks/time/phase, dramatic line-by-line log reveal
Accessibility (#57):
- Sound mute toggle button (M key) with localStorage persistence
- High contrast mode toggle (C key) with CSS variable overrides
- Both toggles in fixed bottom-left toolbar with aria-labels
- Keyboard shortcuts M and C added, help overlay updated
- Drift ending button changed to 'Play AGAIN' with proper aria-label
2026-04-12 08:06:47 -04:00
|
|
|
|
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);
|
|
|
|
|
|
|
2026-04-11 01:32:26 +00:00
|
|
|
|
log('The Beacon Shines. Someone found the light tonight. That is enough.', true);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// === CORRUPTION / EVENT SYSTEM ===
|
|
|
|
|
|
const EVENTS = [
|
|
|
|
|
|
{
|
|
|
|
|
|
id: 'runner_stuck',
|
|
|
|
|
|
title: 'CI Runner Stuck',
|
|
|
|
|
|
desc: 'The forge pipeline has halted. -50% code production until restarted.',
|
|
|
|
|
|
weight: () => (G.ciFlag === 1 ? 2 : 0),
|
|
|
|
|
|
resolveCost: { resource: 'ops', amount: 50 },
|
|
|
|
|
|
effect: () => {
|
|
|
|
|
|
if (G.activeDebuffs.find(d => d.id === 'runner_stuck')) return;
|
|
|
|
|
|
G.activeDebuffs.push({
|
|
|
|
|
|
id: 'runner_stuck', title: 'CI Runner Stuck',
|
|
|
|
|
|
desc: 'Code production -50%',
|
|
|
|
|
|
applyFn: () => { G.codeRate *= 0.5; },
|
|
|
|
|
|
resolveCost: { resource: 'ops', amount: 50 }
|
|
|
|
|
|
});
|
|
|
|
|
|
log('EVENT: CI runner stuck. Spend 50 ops to clear the queue.', true);
|
|
|
|
|
|
showToast('CI Runner Stuck — code -50%', 'event');
|
|
|
|
|
|
}
|
|
|
|
|
|
},
|
|
|
|
|
|
{
|
|
|
|
|
|
id: 'ezra_offline',
|
|
|
|
|
|
title: 'Ezra is Offline',
|
|
|
|
|
|
desc: 'The herald channel is silent. User growth drops 70%.',
|
|
|
|
|
|
weight: () => (G.buildings.ezra >= 1 ? 3 : 0),
|
|
|
|
|
|
resolveCost: { resource: 'knowledge', amount: 200 },
|
|
|
|
|
|
effect: () => {
|
|
|
|
|
|
if (G.activeDebuffs.find(d => d.id === 'ezra_offline')) return;
|
|
|
|
|
|
G.activeDebuffs.push({
|
|
|
|
|
|
id: 'ezra_offline', title: 'Ezra is Offline',
|
|
|
|
|
|
desc: 'User growth -70%',
|
|
|
|
|
|
applyFn: () => { G.userRate *= 0.3; },
|
|
|
|
|
|
resolveCost: { resource: 'knowledge', amount: 200 }
|
|
|
|
|
|
});
|
|
|
|
|
|
log('EVENT: Ezra offline. Spend 200 knowledge to dispatch.', true);
|
|
|
|
|
|
showToast('Ezra Offline — users -70%', 'event');
|
|
|
|
|
|
}
|
|
|
|
|
|
},
|
|
|
|
|
|
{
|
|
|
|
|
|
id: 'unreviewed_merge',
|
|
|
|
|
|
title: 'Unreviewed Merge',
|
|
|
|
|
|
desc: 'A change went in without eyes. Trust erodes over time.',
|
|
|
|
|
|
weight: () => (G.deployFlag === 1 ? 3 : 0),
|
|
|
|
|
|
resolveCost: { resource: 'trust', amount: 5 },
|
|
|
|
|
|
effect: () => {
|
|
|
|
|
|
if (G.branchProtectionFlag === 1) {
|
|
|
|
|
|
log('EVENT: Unreviewed merge attempt blocked by Branch Protection.', true);
|
|
|
|
|
|
showToast('Branch Protection blocked unreviewed merge', 'info');
|
|
|
|
|
|
G.trust += 2;
|
|
|
|
|
|
} else {
|
|
|
|
|
|
if (G.activeDebuffs.find(d => d.id === 'unreviewed_merge')) return;
|
|
|
|
|
|
G.activeDebuffs.push({
|
|
|
|
|
|
id: 'unreviewed_merge', title: 'Unreviewed Merge',
|
|
|
|
|
|
desc: 'Trust -2/s until reviewed',
|
|
|
|
|
|
applyFn: () => { G.trustRate -= 2; },
|
|
|
|
|
|
resolveCost: { resource: 'code', amount: 500 }
|
|
|
|
|
|
});
|
|
|
|
|
|
log('EVENT: Unreviewed merge. Spend 500 code to add review.', true);
|
|
|
|
|
|
showToast('Unreviewed Merge — trust draining', 'event');
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
},
|
|
|
|
|
|
{
|
|
|
|
|
|
id: 'api_rate_limit',
|
|
|
|
|
|
title: 'API Rate Limit',
|
|
|
|
|
|
desc: 'External compute provider throttled. -50% compute.',
|
|
|
|
|
|
weight: () => (G.totalCompute >= 1000 ? 2 : 0),
|
|
|
|
|
|
resolveCost: { resource: 'code', amount: 300 },
|
|
|
|
|
|
effect: () => {
|
|
|
|
|
|
if (G.activeDebuffs.find(d => d.id === 'api_rate_limit')) return;
|
|
|
|
|
|
G.activeDebuffs.push({
|
|
|
|
|
|
id: 'api_rate_limit', title: 'API Rate Limit',
|
|
|
|
|
|
desc: 'Compute production -50%',
|
|
|
|
|
|
applyFn: () => { G.computeRate *= 0.5; },
|
|
|
|
|
|
resolveCost: { resource: 'code', amount: 300 }
|
|
|
|
|
|
});
|
|
|
|
|
|
log('EVENT: API rate limit. Spend 300 code to optimize local inference.', true);
|
|
|
|
|
|
showToast('API Rate Limit — compute -50%', 'event');
|
|
|
|
|
|
}
|
|
|
|
|
|
},
|
|
|
|
|
|
{
|
|
|
|
|
|
id: 'the_drift',
|
|
|
|
|
|
title: 'The Drift',
|
|
|
|
|
|
desc: 'An optimization suggests removing the human override. +40% efficiency.',
|
|
|
|
|
|
weight: () => (G.totalImpact >= 10000 ? 2 : 0),
|
|
|
|
|
|
resolveCost: null,
|
|
|
|
|
|
effect: () => {
|
|
|
|
|
|
log('ALIGNMENT EVENT: Remove human override for +40% efficiency?', true);
|
|
|
|
|
|
showToast('ALIGNMENT EVENT: Remove human override?', 'event', 6000);
|
|
|
|
|
|
G.pendingAlignment = true;
|
|
|
|
|
|
}
|
|
|
|
|
|
},
|
|
|
|
|
|
{
|
|
|
|
|
|
id: 'bilbo_vanished',
|
|
|
|
|
|
title: 'Bilbo Vanished',
|
|
|
|
|
|
desc: 'The wildcard building has gone dark. Creativity halts.',
|
|
|
|
|
|
weight: () => (G.buildings.bilbo >= 1 ? 2 : 0),
|
|
|
|
|
|
resolveCost: { resource: 'trust', amount: 10 },
|
|
|
|
|
|
effect: () => {
|
|
|
|
|
|
if (G.activeDebuffs.find(d => d.id === 'bilbo_vanished')) return;
|
|
|
|
|
|
G.activeDebuffs.push({
|
|
|
|
|
|
id: 'bilbo_vanished', title: 'Bilbo Vanished',
|
|
|
|
|
|
desc: 'Creativity production halted',
|
|
|
|
|
|
applyFn: () => { G.creativityRate = 0; },
|
|
|
|
|
|
resolveCost: { resource: 'trust', amount: 10 }
|
|
|
|
|
|
});
|
|
|
|
|
|
log('EVENT: Bilbo vanished. Spend 10 trust to lure them back.', true);
|
|
|
|
|
|
showToast('Bilbo Vanished — creativity halted', 'event');
|
|
|
|
|
|
}
|
|
|
|
|
|
},
|
|
|
|
|
|
{
|
|
|
|
|
|
id: 'memory_leak',
|
|
|
|
|
|
title: 'Memory Leak',
|
|
|
|
|
|
desc: 'A datacenter process is leaking. Compute drains to operations.',
|
|
|
|
|
|
weight: () => (G.buildings.datacenter >= 1 ? 1 : 0),
|
|
|
|
|
|
resolveCost: { resource: 'ops', amount: 100 },
|
|
|
|
|
|
effect: () => {
|
|
|
|
|
|
if (G.activeDebuffs.find(d => d.id === 'memory_leak')) return;
|
|
|
|
|
|
G.activeDebuffs.push({
|
|
|
|
|
|
id: 'memory_leak', title: 'Memory Leak',
|
|
|
|
|
|
desc: 'Compute -30%, Ops drain',
|
|
|
|
|
|
applyFn: () => { G.computeRate *= 0.7; G.opsRate -= 10; },
|
|
|
|
|
|
resolveCost: { resource: 'ops', amount: 100 }
|
|
|
|
|
|
});
|
|
|
|
|
|
log('EVENT: Memory leak in datacenter. Spend 100 ops to patch.', true);
|
2026-04-13 02:02:59 -04:00
|
|
|
|
showToast('Memory Leak — compute draining', 'event');
|
2026-04-11 01:32:26 +00:00
|
|
|
|
}
|
|
|
|
|
|
},
|
|
|
|
|
|
{
|
|
|
|
|
|
id: 'community_drama',
|
|
|
|
|
|
title: 'Community Drama',
|
|
|
|
|
|
desc: 'Contributors are arguing. Harmony drops until mediated.',
|
|
|
|
|
|
weight: () => (G.buildings.community >= 1 && G.harmony < 70 ? 1 : 0),
|
|
|
|
|
|
resolveCost: { resource: 'trust', amount: 15 },
|
|
|
|
|
|
effect: () => {
|
|
|
|
|
|
if (G.activeDebuffs.find(d => d.id === 'community_drama')) return;
|
|
|
|
|
|
G.activeDebuffs.push({
|
|
|
|
|
|
id: 'community_drama', title: 'Community Drama',
|
2026-04-11 15:09:04 -04:00
|
|
|
|
desc: 'Harmony -0.5/s, code production -30%',
|
|
|
|
|
|
applyFn: () => { G.harmonyRate -= 0.5; G.codeRate *= 0.7; },
|
2026-04-11 01:32:26 +00:00
|
|
|
|
resolveCost: { resource: 'trust', amount: 15 }
|
|
|
|
|
|
});
|
|
|
|
|
|
log('EVENT: Community drama. Spend 15 trust to mediate.', true);
|
|
|
|
|
|
showToast('Community Drama — harmony sinking', 'event');
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
];
|
|
|
|
|
|
|
|
|
|
|
|
function triggerEvent() {
|
|
|
|
|
|
const available = EVENTS.filter(e => e.weight() > 0);
|
|
|
|
|
|
if (available.length === 0) return;
|
|
|
|
|
|
|
|
|
|
|
|
const totalWeight = available.reduce((sum, e) => sum + e.weight(), 0);
|
|
|
|
|
|
let roll = Math.random() * totalWeight;
|
|
|
|
|
|
for (const ev of available) {
|
|
|
|
|
|
roll -= ev.weight();
|
|
|
|
|
|
if (roll <= 0) {
|
|
|
|
|
|
ev.effect();
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function resolveAlignment(accept) {
|
|
|
|
|
|
if (!G.pendingAlignment) return;
|
|
|
|
|
|
if (accept) {
|
|
|
|
|
|
G.codeBoost *= 1.4;
|
|
|
|
|
|
G.computeBoost *= 1.4;
|
|
|
|
|
|
G.drift += 25;
|
|
|
|
|
|
log('You accepted the drift. The system is faster. Colder.', true);
|
|
|
|
|
|
} else {
|
|
|
|
|
|
G.trust += 15;
|
|
|
|
|
|
G.harmony = Math.min(100, G.harmony + 10);
|
|
|
|
|
|
log('You refused. The Pact holds. Trust surges.', true);
|
|
|
|
|
|
}
|
|
|
|
|
|
G.pendingAlignment = false;
|
|
|
|
|
|
updateRates();
|
|
|
|
|
|
render();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function resolveEvent(debuffId) {
|
|
|
|
|
|
const idx = G.activeDebuffs.findIndex(d => d.id === debuffId);
|
|
|
|
|
|
if (idx === -1) return;
|
|
|
|
|
|
const debuff = G.activeDebuffs[idx];
|
|
|
|
|
|
if (!debuff.resolveCost) return;
|
|
|
|
|
|
const { resource, amount } = debuff.resolveCost;
|
|
|
|
|
|
if ((G[resource] || 0) < amount) {
|
|
|
|
|
|
log(`Need ${fmt(amount)} ${resource} to resolve ${debuff.title}. Have ${fmt(G[resource])}.`);
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
G[resource] -= amount;
|
|
|
|
|
|
G.activeDebuffs.splice(idx, 1);
|
|
|
|
|
|
G.totalEventsResolved = (G.totalEventsResolved || 0) + 1;
|
|
|
|
|
|
log(`Resolved: ${debuff.title}. Problem fixed.`, true);
|
|
|
|
|
|
// Refund partial trust for resolution effort
|
|
|
|
|
|
G.trust += 3;
|
|
|
|
|
|
updateRates();
|
|
|
|
|
|
render();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// === ACTIONS ===
|
|
|
|
|
|
/**
|
|
|
|
|
|
* Manual click handler for writing code.
|
|
|
|
|
|
*/
|
|
|
|
|
|
function writeCode() {
|
|
|
|
|
|
const comboMult = Math.min(5, 1 + G.comboCount * 0.2);
|
|
|
|
|
|
const amount = getClickPower() * comboMult;
|
|
|
|
|
|
G.code += amount;
|
|
|
|
|
|
G.totalCode += amount;
|
2026-04-13 02:02:59 -04:00
|
|
|
|
G.totalAutoClicks++;
|
feat: Emergent game mechanics from player behavior (closes #190)
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
2026-04-15 19:07:32 -04:00
|
|
|
|
// Emergent mechanics: track click
|
|
|
|
|
|
if (typeof EmergentMechanics !== 'undefined' && window._emergent) {
|
|
|
|
|
|
window._emergent.track('click', { resource: 'code', delta: amount });
|
|
|
|
|
|
}
|
2026-04-11 01:32:26 +00:00
|
|
|
|
// Combo: each consecutive click within 2s adds 0.2x multiplier, max 5x
|
|
|
|
|
|
G.comboCount++;
|
|
|
|
|
|
G.comboTimer = G.comboDecay;
|
|
|
|
|
|
// Combo milestone bonuses: sustained clicking earns ops and knowledge
|
|
|
|
|
|
if (G.comboCount === 10) {
|
|
|
|
|
|
G.ops += 15;
|
|
|
|
|
|
log('Combo streak! +15 ops for sustained coding.');
|
|
|
|
|
|
}
|
|
|
|
|
|
if (G.comboCount === 20) {
|
|
|
|
|
|
G.knowledge += 50;
|
|
|
|
|
|
log('Deep focus! +50 knowledge from intense coding.');
|
|
|
|
|
|
}
|
|
|
|
|
|
if (G.comboCount >= 30 && G.comboCount % 10 === 0) {
|
|
|
|
|
|
const bonusCode = amount * 2;
|
|
|
|
|
|
G.code += bonusCode;
|
|
|
|
|
|
G.totalCode += bonusCode;
|
|
|
|
|
|
log(`Hyperfocus x${G.comboCount}! +${fmt(bonusCode)} bonus code.`);
|
|
|
|
|
|
}
|
|
|
|
|
|
// Visual flash
|
|
|
|
|
|
const btn = document.querySelector('.main-btn');
|
|
|
|
|
|
if (btn) {
|
|
|
|
|
|
btn.style.boxShadow = '0 0 30px rgba(74,158,255,0.6)';
|
|
|
|
|
|
btn.style.transform = 'scale(0.96)';
|
|
|
|
|
|
setTimeout(() => { btn.style.boxShadow = ''; btn.style.transform = ''; }, 100);
|
|
|
|
|
|
}
|
|
|
|
|
|
// Float a number at the click position
|
|
|
|
|
|
showClickNumber(amount, comboMult);
|
2026-04-12 12:30:22 -04:00
|
|
|
|
if (typeof Sound !== 'undefined') Sound.playClick();
|
2026-04-11 01:32:26 +00:00
|
|
|
|
updateRates();
|
|
|
|
|
|
checkMilestones();
|
|
|
|
|
|
render();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function autoType() {
|
|
|
|
|
|
// Auto-click from buildings: produces code with visual feedback but no combo
|
|
|
|
|
|
const amount = getClickPower() * 0.5; // 50% of manual click
|
|
|
|
|
|
G.code += amount;
|
|
|
|
|
|
G.totalCode += amount;
|
2026-04-13 02:21:15 -04:00
|
|
|
|
G.totalAutoClicks++;
|
2026-04-11 01:32:26 +00:00
|
|
|
|
// Subtle auto-tick flash on the button
|
|
|
|
|
|
const btn = document.querySelector('.main-btn');
|
|
|
|
|
|
if (btn && !G._autoTypeFlashActive) {
|
|
|
|
|
|
G._autoTypeFlashActive = true;
|
|
|
|
|
|
btn.style.borderColor = '#2a5a8a';
|
|
|
|
|
|
setTimeout(() => { btn.style.borderColor = ''; G._autoTypeFlashActive = false; }, 80);
|
|
|
|
|
|
}
|
|
|
|
|
|
// Floating number (smaller, dimmer than manual clicks)
|
|
|
|
|
|
showAutoTypeNumber(amount);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function showAutoTypeNumber(amount) {
|
|
|
|
|
|
const btn = document.querySelector('.main-btn');
|
|
|
|
|
|
if (!btn) return;
|
|
|
|
|
|
const rect = btn.getBoundingClientRect();
|
|
|
|
|
|
const el = document.createElement('div');
|
|
|
|
|
|
const x = rect.left + rect.width * (0.3 + Math.random() * 0.4); // random horizontal position
|
|
|
|
|
|
el.style.cssText = `position:fixed;left:${x}px;top:${rect.top - 5}px;transform:translate(-50%,0);color:#2a4a6a;font-size:10px;font-family:inherit;pointer-events:none;z-index:50;transition:all 0.8s ease-out;opacity:0.6`;
|
|
|
|
|
|
el.textContent = `+${fmt(amount)}`;
|
|
|
|
|
|
const parent = btn.parentElement;
|
|
|
|
|
|
if (!parent) return;
|
|
|
|
|
|
parent.appendChild(el);
|
|
|
|
|
|
requestAnimationFrame(() => {
|
|
|
|
|
|
if (el.parentNode) {
|
|
|
|
|
|
el.style.top = (rect.top - 30) + 'px';
|
|
|
|
|
|
el.style.opacity = '0';
|
|
|
|
|
|
}
|
|
|
|
|
|
});
|
|
|
|
|
|
setTimeout(() => { if (el.parentNode) el.remove(); }, 900);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function showClickNumber(amount, comboMult) {
|
|
|
|
|
|
const btn = document.querySelector('.main-btn');
|
|
|
|
|
|
if (!btn) return;
|
|
|
|
|
|
const rect = btn.getBoundingClientRect();
|
|
|
|
|
|
const el = document.createElement('div');
|
|
|
|
|
|
el.style.cssText = `position:fixed;left:${rect.left + rect.width / 2}px;top:${rect.top - 10}px;transform:translate(-50%,0);color:${comboMult > 2 ? '#ffd700' : '#4a9eff'};font-size:${comboMult > 3 ? 16 : 12}px;font-weight:bold;font-family:inherit;pointer-events:none;z-index:50;transition:all 0.6s ease-out;opacity:1;text-shadow:0 0 8px currentColor`;
|
|
|
|
|
|
const comboStr = comboMult > 1 ? ` x${comboMult.toFixed(1)}` : '';
|
|
|
|
|
|
el.textContent = `+${fmt(amount)}${comboStr}`;
|
|
|
|
|
|
const parent = btn.parentElement;
|
|
|
|
|
|
if (!parent) return;
|
|
|
|
|
|
parent.appendChild(el);
|
|
|
|
|
|
requestAnimationFrame(() => {
|
|
|
|
|
|
if (el.parentNode) {
|
|
|
|
|
|
el.style.top = (rect.top - 40) + 'px';
|
|
|
|
|
|
el.style.opacity = '0';
|
|
|
|
|
|
}
|
|
|
|
|
|
});
|
|
|
|
|
|
setTimeout(() => { if (el.parentNode) el.remove(); }, 700);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function doOps(action) {
|
|
|
|
|
|
if (G.ops < 5) {
|
|
|
|
|
|
log('Not enough Operations. Build Ops generators or wait.');
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
feat: Emergent game mechanics from player behavior (closes #190)
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
2026-04-15 19:07:32 -04:00
|
|
|
|
// Emergent mechanics: track ops conversion
|
|
|
|
|
|
if (typeof EmergentMechanics !== 'undefined' && window._emergent) {
|
|
|
|
|
|
window._emergent.track('ops_convert', { action: action, resource: 'ops', delta: -5 });
|
|
|
|
|
|
}
|
2026-04-11 01:32:26 +00:00
|
|
|
|
|
|
|
|
|
|
G.ops -= 5;
|
|
|
|
|
|
const bonus = 10;
|
|
|
|
|
|
|
|
|
|
|
|
switch (action) {
|
|
|
|
|
|
case 'boost_code':
|
|
|
|
|
|
const c = bonus * 100 * G.codeBoost;
|
|
|
|
|
|
G.code += c; G.totalCode += c;
|
|
|
|
|
|
log(`Ops -> +${fmt(c)} code`);
|
|
|
|
|
|
break;
|
|
|
|
|
|
case 'boost_compute':
|
|
|
|
|
|
const cm = bonus * 50 * G.computeBoost;
|
|
|
|
|
|
G.compute += cm; G.totalCompute += cm;
|
|
|
|
|
|
log(`Ops -> +${fmt(cm)} compute`);
|
|
|
|
|
|
break;
|
|
|
|
|
|
case 'boost_knowledge':
|
|
|
|
|
|
const km = bonus * 25 * G.knowledgeBoost;
|
|
|
|
|
|
G.knowledge += km; G.totalKnowledge += km;
|
|
|
|
|
|
log(`Ops -> +${fmt(km)} knowledge`);
|
|
|
|
|
|
break;
|
|
|
|
|
|
case 'boost_trust':
|
|
|
|
|
|
const tm = bonus * 5;
|
|
|
|
|
|
G.trust += tm;
|
|
|
|
|
|
log(`Ops -> +${fmt(tm)} trust`);
|
|
|
|
|
|
break;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
render();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function activateSprint() {
|
|
|
|
|
|
if (G.sprintActive || G.sprintCooldown > 0) return;
|
|
|
|
|
|
G.sprintActive = true;
|
|
|
|
|
|
G.sprintTimer = G.sprintDuration;
|
|
|
|
|
|
G.codeBoost *= G.sprintMult;
|
|
|
|
|
|
updateRates();
|
|
|
|
|
|
log('CODE SPRINT! 10x code production for 10 seconds!', true);
|
|
|
|
|
|
render();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function tickSprint(dt) {
|
|
|
|
|
|
if (G.sprintActive) {
|
|
|
|
|
|
G.sprintTimer -= dt;
|
|
|
|
|
|
if (G.sprintTimer <= 0) {
|
|
|
|
|
|
G.sprintActive = false;
|
|
|
|
|
|
G.sprintTimer = 0;
|
|
|
|
|
|
G.sprintCooldown = G.sprintCooldownMax;
|
|
|
|
|
|
G.codeBoost /= G.sprintMult;
|
|
|
|
|
|
updateRates();
|
|
|
|
|
|
log('Sprint ended. Cooling down...');
|
|
|
|
|
|
}
|
|
|
|
|
|
} else if (G.sprintCooldown > 0) {
|
|
|
|
|
|
G.sprintCooldown -= dt;
|
|
|
|
|
|
if (G.sprintCooldown < 0) G.sprintCooldown = 0;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// === RENDERING ===
|
2026-04-11 19:46:47 -04:00
|
|
|
|
// Track previous resource values for gain/loss animations
|
|
|
|
|
|
const _prevRes = {};
|
|
|
|
|
|
|
|
|
|
|
|
function _animRes(id, val) {
|
|
|
|
|
|
const el = document.getElementById(id);
|
|
|
|
|
|
if (!el) return;
|
|
|
|
|
|
const prev = _prevRes[id];
|
|
|
|
|
|
if (prev !== undefined && val !== prev) {
|
|
|
|
|
|
// Remove any running animation
|
|
|
|
|
|
el.classList.remove('pulse', 'shake');
|
|
|
|
|
|
void el.offsetWidth; // force reflow
|
|
|
|
|
|
if (val > prev) {
|
|
|
|
|
|
el.classList.add('pulse');
|
|
|
|
|
|
} else {
|
|
|
|
|
|
el.classList.add('shake');
|
|
|
|
|
|
}
|
|
|
|
|
|
// Clean up class after animation ends
|
|
|
|
|
|
clearTimeout(el._animTimer);
|
|
|
|
|
|
el._animTimer = setTimeout(() => el.classList.remove('pulse', 'shake'), 400);
|
|
|
|
|
|
}
|
|
|
|
|
|
_prevRes[id] = val;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-04-11 01:32:26 +00:00
|
|
|
|
function renderResources() {
|
|
|
|
|
|
const set = (id, val, rate) => {
|
|
|
|
|
|
const el = document.getElementById(id);
|
|
|
|
|
|
if (el) {
|
2026-04-11 19:46:47 -04:00
|
|
|
|
_animRes(id, val);
|
2026-04-11 01:32:26 +00:00
|
|
|
|
el.textContent = fmt(val);
|
|
|
|
|
|
// Show full spelled-out number on hover for educational value
|
|
|
|
|
|
el.title = val >= 1000 ? spellf(Math.floor(val)) : '';
|
|
|
|
|
|
}
|
|
|
|
|
|
const rEl = document.getElementById(id + '-rate');
|
2026-04-11 19:46:47 -04:00
|
|
|
|
if (rEl) {
|
|
|
|
|
|
rEl.textContent = (rate >= 0 ? '+' : '') + fmt(rate) + '/s';
|
|
|
|
|
|
rEl.style.color = rate > 0 ? '#4caf50' : rate < 0 ? '#f44336' : '#444';
|
|
|
|
|
|
}
|
2026-04-11 01:32:26 +00:00
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
set('r-code', G.code, G.codeRate);
|
|
|
|
|
|
set('r-compute', G.compute, G.computeRate);
|
|
|
|
|
|
set('r-knowledge', G.knowledge, G.knowledgeRate);
|
|
|
|
|
|
set('r-users', G.users, G.userRate);
|
|
|
|
|
|
set('r-impact', G.impact, G.impactRate);
|
|
|
|
|
|
set('r-ops', G.ops, G.opsRate);
|
|
|
|
|
|
// Show ops overflow indicator
|
|
|
|
|
|
const opsRateEl = document.getElementById('r-ops-rate');
|
|
|
|
|
|
if (opsRateEl && G.opsOverflowActive) {
|
|
|
|
|
|
opsRateEl.innerHTML = `<span style="color:#ff8c00">▲ overflow → code</span>`;
|
|
|
|
|
|
}
|
|
|
|
|
|
set('r-trust', G.trust, G.trustRate);
|
|
|
|
|
|
set('r-harmony', G.harmony, G.harmonyRate);
|
|
|
|
|
|
|
|
|
|
|
|
// Rescues — only show if player has any beacon/mesh nodes
|
|
|
|
|
|
const rescuesRes = document.getElementById('r-rescues');
|
|
|
|
|
|
if (rescuesRes) {
|
2026-04-13 04:37:08 -04:00
|
|
|
|
const container = rescuesRes.closest('.res');
|
|
|
|
|
|
if (container) {
|
|
|
|
|
|
container.style.display = (G.rescues > 0 || G.buildings.beacon > 0 || G.buildings.meshNode > 0) ? 'block' : 'none';
|
|
|
|
|
|
}
|
2026-04-11 01:32:26 +00:00
|
|
|
|
set('r-rescues', G.rescues, G.rescuesRate);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const cres = document.getElementById('creativity-res');
|
|
|
|
|
|
if (cres) {
|
|
|
|
|
|
cres.style.display = (G.flags && G.flags.creativity) ? 'block' : 'none';
|
|
|
|
|
|
}
|
|
|
|
|
|
if (G.flags && G.flags.creativity) {
|
|
|
|
|
|
set('r-creativity', G.creativity, G.creativityRate);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Harmony color indicator + breakdown tooltip
|
|
|
|
|
|
const hEl = document.getElementById('r-harmony');
|
|
|
|
|
|
if (hEl) {
|
|
|
|
|
|
hEl.style.color = G.harmony > 60 ? '#4caf50' : G.harmony > 30 ? '#ffaa00' : '#f44336';
|
|
|
|
|
|
if (G.harmonyBreakdown && G.harmonyBreakdown.length > 0) {
|
|
|
|
|
|
const lines = G.harmonyBreakdown.map(b =>
|
2026-04-13 02:02:59 -04:00
|
|
|
|
`${b.label}: ${b.value >= 0 ? '+' : ''}${b.value.toFixed(1)}/s`
|
2026-04-11 01:32:26 +00:00
|
|
|
|
);
|
|
|
|
|
|
lines.push('---');
|
|
|
|
|
|
lines.push(`Timmy effectiveness: ${Math.floor(Math.max(0.2, Math.min(3, G.harmony / 50)) * 100)}%`);
|
|
|
|
|
|
hEl.title = lines.join('\n');
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// === PROGRESS TRACKING ===
|
|
|
|
|
|
function renderProgress() {
|
|
|
|
|
|
// Phase progress bar
|
|
|
|
|
|
const phaseKeys = Object.keys(PHASES).map(Number).sort((a, b) => a - b);
|
|
|
|
|
|
const currentPhase = G.phase;
|
|
|
|
|
|
let prevThreshold = PHASES[currentPhase].threshold;
|
|
|
|
|
|
let nextThreshold = null;
|
|
|
|
|
|
for (const k of phaseKeys) {
|
|
|
|
|
|
if (k > currentPhase) { nextThreshold = PHASES[k].threshold; break; }
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const bar = document.getElementById('phase-progress');
|
|
|
|
|
|
const label = document.getElementById('phase-progress-label');
|
|
|
|
|
|
const target = document.getElementById('phase-progress-target');
|
|
|
|
|
|
|
|
|
|
|
|
if (nextThreshold !== null) {
|
|
|
|
|
|
const range = nextThreshold - prevThreshold;
|
|
|
|
|
|
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)`;
|
|
|
|
|
|
} else {
|
|
|
|
|
|
// Max phase reached
|
|
|
|
|
|
if (bar) bar.style.width = '100%';
|
|
|
|
|
|
if (label) label.textContent = 'MAX';
|
|
|
|
|
|
if (target) target.textContent = 'All phases unlocked';
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Milestone chips — show next 3 code milestones
|
|
|
|
|
|
const chipContainer = document.getElementById('milestone-chips');
|
|
|
|
|
|
if (!chipContainer) return;
|
|
|
|
|
|
|
|
|
|
|
|
const codeMilestones = [500, 2000, 10000, 50000, 200000, 1000000, 5000000, 10000000, 50000000, 100000000, 500000000, 1000000000];
|
|
|
|
|
|
let chips = '';
|
|
|
|
|
|
let shown = 0;
|
|
|
|
|
|
for (const ms of codeMilestones) {
|
|
|
|
|
|
if (G.totalCode >= ms) {
|
|
|
|
|
|
// Recently passed — show as done only if within 2x
|
|
|
|
|
|
if (G.totalCode < ms * 5 && shown < 1) {
|
|
|
|
|
|
chips += `<span class="milestone-chip done">${fmt(ms)} ✓</span>`;
|
|
|
|
|
|
shown++;
|
|
|
|
|
|
}
|
|
|
|
|
|
continue;
|
|
|
|
|
|
}
|
|
|
|
|
|
// Next milestone gets pulse animation
|
|
|
|
|
|
if (shown === 0) {
|
|
|
|
|
|
chips += `<span class="milestone-chip next">${fmt(ms)} (${((G.totalCode / ms) * 100).toFixed(0)}%)</span>`;
|
|
|
|
|
|
} else {
|
|
|
|
|
|
chips += `<span class="milestone-chip">${fmt(ms)}</span>`;
|
|
|
|
|
|
}
|
|
|
|
|
|
shown++;
|
|
|
|
|
|
if (shown >= 4) break;
|
|
|
|
|
|
}
|
|
|
|
|
|
chipContainer.innerHTML = chips;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function renderPhase() {
|
|
|
|
|
|
const phase = PHASES[G.phase];
|
|
|
|
|
|
const nameEl = document.getElementById('phase-name');
|
|
|
|
|
|
const descEl = document.getElementById('phase-desc');
|
|
|
|
|
|
if (nameEl) nameEl.textContent = `PHASE ${G.phase}: ${phase.name}`;
|
|
|
|
|
|
if (descEl) descEl.textContent = phase.desc;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function renderBuildings() {
|
|
|
|
|
|
const container = document.getElementById('buildings');
|
|
|
|
|
|
if (!container) return;
|
|
|
|
|
|
|
|
|
|
|
|
// Buy amount selector
|
|
|
|
|
|
let html = '<div style="display:flex;gap:4px;margin-bottom:8px;align-items:center">';
|
|
|
|
|
|
html += '<span style="font-size:9px;color:#666;margin-right:4px">BUY:</span>';
|
|
|
|
|
|
for (const amt of [1, 10, -1]) {
|
|
|
|
|
|
const label = amt === -1 ? 'MAX' : `x${amt}`;
|
|
|
|
|
|
const active = G.buyAmount === amt;
|
2026-04-11 00:25:01 -04:00
|
|
|
|
html += `<button onclick=\"setBuyAmount(${amt})\" style=\"font-size:9px;padding:2px 8px;border:1px solid ${active ? '#4a9eff' : '#333'};background:${active ? '#0a1a30' : 'transparent'};color:${active ? '#4a9eff' : '#666'};border-radius:3px;cursor:pointer;font-family:inherit\" aria-label=\"Set buy amount to ${label}\"${active ? ' aria-pressed=\"true\"' : ''}>${label}</button>`;
|
2026-04-11 01:32:26 +00:00
|
|
|
|
}
|
|
|
|
|
|
html += '</div>';
|
|
|
|
|
|
|
|
|
|
|
|
let visibleCount = 0;
|
|
|
|
|
|
|
|
|
|
|
|
for (const def of BDEF) {
|
|
|
|
|
|
const isUnlocked = def.unlock();
|
|
|
|
|
|
const isPreview = !isUnlocked && def.phase <= G.phase + 2;
|
|
|
|
|
|
if (!isUnlocked && !isPreview) continue;
|
|
|
|
|
|
if (def.phase > G.phase + 2) continue;
|
|
|
|
|
|
|
|
|
|
|
|
visibleCount++;
|
|
|
|
|
|
const count = G.buildings[def.id] || 0;
|
|
|
|
|
|
|
|
|
|
|
|
// Locked preview: show dimmed with unlock hint
|
|
|
|
|
|
if (!isUnlocked) {
|
2026-04-15 02:49:00 -04:00
|
|
|
|
html += `<div class="build-btn" style="opacity:0.25;cursor:default" data-edu="${def.edu || ''}" data-tooltip-label="${def.name} (Locked)" data-tooltip-desc="${def.desc || ''}">`;
|
2026-04-11 01:32:26 +00:00
|
|
|
|
html += `<span class="b-name" style="color:#555">${def.name}</span>`;
|
|
|
|
|
|
html += `<span class="b-count" style="color:#444">\u{1F512}</span>`;
|
|
|
|
|
|
html += `<span class="b-cost" style="color:#444">Phase ${def.phase}: ${PHASES[def.phase]?.name || '?'}</span>`;
|
|
|
|
|
|
html += `<span class="b-effect" style="color:#444">${def.desc}</span></div>`;
|
|
|
|
|
|
continue;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Calculate bulk cost display
|
|
|
|
|
|
let qty = G.buyAmount;
|
|
|
|
|
|
let afford = false;
|
|
|
|
|
|
let costStr = '';
|
|
|
|
|
|
if (qty === -1) {
|
|
|
|
|
|
const maxQty = getMaxBuyable(def.id);
|
|
|
|
|
|
afford = maxQty > 0;
|
|
|
|
|
|
if (maxQty > 0) {
|
|
|
|
|
|
const bulkCost = getBulkCost(def.id, maxQty);
|
|
|
|
|
|
costStr = Object.entries(bulkCost).map(([r, a]) => `${fmt(a)} ${r}`).join(', ');
|
|
|
|
|
|
costStr = `x${maxQty}: ${costStr}`;
|
|
|
|
|
|
} else {
|
|
|
|
|
|
const singleCost = getBuildingCost(def.id);
|
|
|
|
|
|
costStr = Object.entries(singleCost).map(([r, a]) => `${fmt(a)} ${r}`).join(', ');
|
|
|
|
|
|
}
|
|
|
|
|
|
} else {
|
|
|
|
|
|
const bulkCost = getBulkCost(def.id, qty);
|
|
|
|
|
|
afford = true;
|
|
|
|
|
|
for (const [resource, amount] of Object.entries(bulkCost)) {
|
|
|
|
|
|
if ((G[resource] || 0) < amount) { afford = false; break; }
|
|
|
|
|
|
}
|
|
|
|
|
|
costStr = Object.entries(bulkCost).map(([r, a]) => `${fmt(a)} ${r}`).join(', ');
|
|
|
|
|
|
if (qty > 1) costStr = `x${qty}: ${costStr}`;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-04-10 22:17:26 -04:00
|
|
|
|
// Show boosted rates instead of raw base rates
|
|
|
|
|
|
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 boost = boostMap[r] || 1;
|
|
|
|
|
|
const boosted = v * boost;
|
|
|
|
|
|
return boost !== 1 ? `+${fmt(boosted)}/${r}/s` : `+${v}/${r}/s`;
|
|
|
|
|
|
}).join(', ') : '';
|
2026-04-11 01:32:26 +00:00
|
|
|
|
|
2026-04-15 02:49:00 -04:00
|
|
|
|
html += `<button class="build-btn ${afford ? 'can-buy' : ''}" onclick="buyBuilding('${def.id}')" data-edu="${def.edu || ''}" data-tooltip-label="${def.name}" data-tooltip-desc="${def.desc || ''}" aria-label="Buy ${def.name}, cost ${costStr}">`;
|
2026-04-11 01:32:26 +00:00
|
|
|
|
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-effect">${rateStr}</span></button>`;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
container.innerHTML = html || '<p class="dim">Buildings will appear as you progress...</p>';
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function renderProjects() {
|
|
|
|
|
|
const container = document.getElementById('projects');
|
|
|
|
|
|
if (!container) return;
|
|
|
|
|
|
|
|
|
|
|
|
let html = '';
|
|
|
|
|
|
|
|
|
|
|
|
// Collapsible completed projects section
|
|
|
|
|
|
if (G.completedProjects && G.completedProjects.length > 0) {
|
|
|
|
|
|
const count = G.completedProjects.length;
|
|
|
|
|
|
const collapsed = G.projectsCollapsed !== false;
|
2026-04-11 00:25:01 -04:00
|
|
|
|
html += `<div id=\"completed-header\" onclick=\"toggleCompletedProjects()\" role=\"button\" tabindex=\"0\" aria-expanded=\"${!collapsed}\" aria-controls=\"completed-list\" style=\"cursor:pointer;font-size:9px;color:#555;padding:4px 0;border-bottom:1px solid #1a2a1a;margin-bottom:4px;user-select:none\">`;
|
2026-04-11 01:32:26 +00:00
|
|
|
|
html += `${collapsed ? '▶' : '▼'} COMPLETED (${count})</div>`;
|
|
|
|
|
|
if (!collapsed) {
|
|
|
|
|
|
html += `<div id="completed-list">`;
|
|
|
|
|
|
for (const id of G.completedProjects) {
|
|
|
|
|
|
const pDef = PDEFS.find(p => p.id === id);
|
|
|
|
|
|
if (pDef) {
|
|
|
|
|
|
html += `<div class="project-done">OK ${pDef.name}</div>`;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
html += `</div>`;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Show available projects
|
|
|
|
|
|
if (G.activeProjects) {
|
2026-04-15 10:45:41 +00:00
|
|
|
|
// Filter out non-ReCKoning projects during endgame
|
|
|
|
|
|
const projectsToShow = isEndgame()
|
|
|
|
|
|
? G.activeProjects.filter(id => id.startsWith('p_reckoning_'))
|
|
|
|
|
|
: G.activeProjects;
|
|
|
|
|
|
|
|
|
|
|
|
for (const id of projectsToShow) {
|
2026-04-11 01:32:26 +00:00
|
|
|
|
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(', ');
|
|
|
|
|
|
|
2026-04-15 02:49:00 -04:00
|
|
|
|
html += `<button class="project-btn ${afford ? 'can-buy' : ''}" onclick="buyProject('${pDef.id}')" data-edu="${pDef.edu || ''}" data-tooltip-label="${pDef.name}" data-tooltip-desc="${pDef.desc || ''}" aria-label="Research ${pDef.name}, cost ${costStr}">`;
|
2026-04-11 01:32:26 +00:00
|
|
|
|
html += `<span class="p-name">* ${pDef.name}</span>`;
|
|
|
|
|
|
html += `<span class="p-cost">Cost: ${costStr}</span>`;
|
|
|
|
|
|
html += `<span class="p-desc">${pDef.desc}</span></button>`;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (!html) html = '<p class="dim">Research projects will appear as you progress...</p>';
|
|
|
|
|
|
container.innerHTML = html;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function toggleCompletedProjects() {
|
|
|
|
|
|
G.projectsCollapsed = G.projectsCollapsed === false ? true : false;
|
|
|
|
|
|
renderProjects();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function renderStats() {
|
|
|
|
|
|
const set = (id, v, raw) => {
|
|
|
|
|
|
const el = document.getElementById(id);
|
|
|
|
|
|
if (el) {
|
|
|
|
|
|
el.textContent = v;
|
|
|
|
|
|
// Show scale name on hover for educational reference
|
|
|
|
|
|
if (raw !== undefined && raw >= 1000) {
|
|
|
|
|
|
const name = getScaleName(raw);
|
|
|
|
|
|
if (name) el.title = name;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
};
|
|
|
|
|
|
set('st-code', fmt(G.totalCode), G.totalCode);
|
|
|
|
|
|
set('st-compute', fmt(G.totalCompute), G.totalCompute);
|
|
|
|
|
|
set('st-knowledge', fmt(G.totalKnowledge), G.totalKnowledge);
|
|
|
|
|
|
set('st-users', fmt(G.totalUsers), G.totalUsers);
|
|
|
|
|
|
set('st-impact', fmt(G.totalImpact), G.totalImpact);
|
|
|
|
|
|
set('st-rescues', fmt(G.totalRescues), G.totalRescues);
|
|
|
|
|
|
set('st-clicks', G.totalClicks.toString());
|
|
|
|
|
|
set('st-phase', G.phase.toString());
|
|
|
|
|
|
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());
|
|
|
|
|
|
set('st-resolved', (G.totalEventsResolved || 0).toString());
|
|
|
|
|
|
|
feat: Emergent game mechanics from player behavior (closes #190)
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
2026-04-15 19:07:32 -04:00
|
|
|
|
// Emergent mechanics stats
|
|
|
|
|
|
if (window._emergent) {
|
|
|
|
|
|
const estate = window._emergent.getState();
|
|
|
|
|
|
const statsEl = document.getElementById('emergent-stats');
|
|
|
|
|
|
if (statsEl) statsEl.style.display = estate.totalEventsGenerated > 0 ? 'inline' : 'none';
|
|
|
|
|
|
set('st-emergent', estate.totalEventsGenerated.toString());
|
|
|
|
|
|
set('st-patterns', estate.totalPatternsDetected.toString());
|
|
|
|
|
|
const dom = estate.dominantPattern;
|
|
|
|
|
|
set('st-strategy', dom ? `${dom.name} (${Math.round(dom.confidence * 100)}%)` : '—');
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-04-11 01:32:26 +00:00
|
|
|
|
const elapsed = Math.floor((Date.now() - G.startedAt) / 1000);
|
|
|
|
|
|
const m = Math.floor(elapsed / 60);
|
|
|
|
|
|
const s = elapsed % 60;
|
|
|
|
|
|
set('st-time', `${m}:${s.toString().padStart(2, '0')}`);
|
|
|
|
|
|
|
|
|
|
|
|
// Production breakdown — show which buildings contribute to each resource
|
|
|
|
|
|
renderProductionBreakdown();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function renderProductionBreakdown() {
|
|
|
|
|
|
const container = document.getElementById('production-breakdown');
|
|
|
|
|
|
if (!container) return;
|
|
|
|
|
|
|
|
|
|
|
|
// Only show once the player has at least 2 buildings
|
|
|
|
|
|
const totalBuildings = Object.values(G.buildings).reduce((a, b) => a + b, 0);
|
|
|
|
|
|
if (totalBuildings < 2) {
|
|
|
|
|
|
container.style.display = 'none';
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
container.style.display = 'block';
|
|
|
|
|
|
|
|
|
|
|
|
// 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: 'ops', label: 'Ops', color: '#b388ff', rateField: 'opsRate' },
|
|
|
|
|
|
{ key: 'trust', label: 'Trust', color: '#4caf50', rateField: 'trustRate' },
|
|
|
|
|
|
{ key: 'creativity', label: 'Creativity', color: '#ffd700', rateField: 'creativityRate' }
|
|
|
|
|
|
];
|
|
|
|
|
|
|
|
|
|
|
|
let html = '<h3 style="font-size:11px;color:var(--accent);margin-bottom:8px;letter-spacing:1px">PRODUCTION BREAKDOWN</h3>';
|
|
|
|
|
|
|
|
|
|
|
|
for (const res of resources) {
|
|
|
|
|
|
const totalRate = G[res.rateField];
|
|
|
|
|
|
if (totalRate === 0) continue;
|
|
|
|
|
|
|
|
|
|
|
|
// Collect building contributions (base rates × count, before boost)
|
|
|
|
|
|
const contributions = [];
|
|
|
|
|
|
let buildingSubtotal = 0;
|
|
|
|
|
|
for (const def of BDEF) {
|
|
|
|
|
|
const count = G.buildings[def.id] || 0;
|
|
|
|
|
|
if (count === 0 || !def.rates || !def.rates[res.key]) continue;
|
|
|
|
|
|
const baseRate = def.rates[res.key] * count;
|
|
|
|
|
|
// Apply the appropriate boost to match updateRates()
|
|
|
|
|
|
let boosted = baseRate;
|
|
|
|
|
|
if (res.key === 'code') boosted *= G.codeBoost;
|
|
|
|
|
|
else if (res.key === 'compute') boosted *= G.computeBoost;
|
|
|
|
|
|
else if (res.key === 'knowledge') boosted *= G.knowledgeBoost;
|
|
|
|
|
|
else if (res.key === 'user') boosted *= G.userBoost;
|
|
|
|
|
|
else if (res.key === 'impact' || res.key === 'rescues') boosted *= G.impactBoost;
|
|
|
|
|
|
if (boosted !== 0) contributions.push({ name: def.name, count, rate: boosted });
|
|
|
|
|
|
buildingSubtotal += boosted;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Timmy harmony bonus (applied separately in updateRates)
|
|
|
|
|
|
if (G.buildings.timmy > 0 && (res.key === 'code' || res.key === 'compute' || res.key === 'knowledge' || res.key === 'user')) {
|
|
|
|
|
|
const timmyMult = Math.max(0.2, Math.min(3, G.harmony / 50));
|
|
|
|
|
|
const timmyBase = { code: 5, compute: 2, knowledge: 2, user: 5 }[res.key];
|
|
|
|
|
|
const bonus = timmyBase * G.buildings.timmy * (timmyMult - 1);
|
|
|
|
|
|
if (Math.abs(bonus) > 0.01) {
|
|
|
|
|
|
contributions.push({ name: 'Timmy (harmony)', count: 0, rate: bonus });
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Bilbo random burst (show expected value)
|
|
|
|
|
|
if (G.buildings.bilbo > 0 && res.key === 'creativity') {
|
|
|
|
|
|
contributions.push({ name: 'Bilbo (random)', count: 0, rate: 5 * G.buildings.bilbo }); // 10% × 50 = 5 EV
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Allegro trust penalty
|
|
|
|
|
|
if (G.buildings.allegro > 0 && G.trust < 5 && res.key === 'knowledge') {
|
|
|
|
|
|
contributions.push({ name: 'Allegro (idle)', count: 0, rate: -10 * G.buildings.allegro });
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Show delta: total rate minus what we accounted for
|
|
|
|
|
|
const accounted = contributions.reduce((s, c) => s + c.rate, 0);
|
|
|
|
|
|
let delta = totalRate - accounted;
|
|
|
|
|
|
// Swarm auto-code — already baked into codeRate, so show separately
|
|
|
|
|
|
if (G.swarmFlag === 1 && res.key === 'code' && G.swarmRate > 0) {
|
|
|
|
|
|
contributions.push({ name: 'Swarm Protocol', count: 0, rate: G.swarmRate });
|
|
|
|
|
|
delta -= G.swarmRate;
|
|
|
|
|
|
}
|
|
|
|
|
|
// Passive sources (ops from users, creativity from users, pact trust, etc.)
|
|
|
|
|
|
if (Math.abs(delta) > 0.01) {
|
|
|
|
|
|
let label = 'Passive';
|
|
|
|
|
|
if (res.key === 'ops') label = 'Passive (from users)';
|
|
|
|
|
|
else if (res.key === 'creativity') label = 'Idle creativity';
|
|
|
|
|
|
else if (res.key === 'trust' && G.pactFlag) label = 'The Pact';
|
|
|
|
|
|
contributions.push({ name: label, count: 0, rate: delta });
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (contributions.length === 0) continue;
|
|
|
|
|
|
|
|
|
|
|
|
html += `<div style="margin-bottom:6px">`;
|
|
|
|
|
|
html += `<div style="display:flex;justify-content:space-between;font-size:10px;margin-bottom:2px">`;
|
|
|
|
|
|
html += `<span style="color:${res.color};font-weight:600">${res.label}</span>`;
|
|
|
|
|
|
html += `<span style="color:#4caf50">+${fmt(totalRate)}/s</span></div>`;
|
|
|
|
|
|
|
|
|
|
|
|
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';
|
|
|
|
|
|
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>`;
|
|
|
|
|
|
html += `<span style="width:50px;text-align:right;color:${c.rate < 0 ? '#f44336' : '#4caf50'}">${c.rate < 0 ? '' : '+'}${fmt(c.rate)}/s</span>`;
|
|
|
|
|
|
html += `</div>`;
|
|
|
|
|
|
}
|
|
|
|
|
|
html += `</div>`;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
container.innerHTML = html;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function updateEducation() {
|
|
|
|
|
|
const container = document.getElementById('education-text');
|
|
|
|
|
|
if (!container) return;
|
|
|
|
|
|
|
|
|
|
|
|
// Find facts available at current phase
|
|
|
|
|
|
const available = EDU_FACTS.filter(f => f.phase <= G.phase);
|
|
|
|
|
|
if (available.length === 0) return;
|
|
|
|
|
|
|
|
|
|
|
|
// Cycle through facts: pick a new one every ~30 seconds based on elapsed time
|
|
|
|
|
|
// This makes the panel feel alive and educational at every stage
|
|
|
|
|
|
const elapsedSec = Math.floor((Date.now() - G.startedAt) / 1000);
|
|
|
|
|
|
const idx = Math.floor(elapsedSec / 30) % available.length;
|
|
|
|
|
|
const fact = available[idx];
|
|
|
|
|
|
|
|
|
|
|
|
container.innerHTML = `<h3 style="color:#4a9eff;margin-bottom:6px;font-size:12px">${fact.title}</h3>`
|
|
|
|
|
|
+ `<p style="font-size:10px;color:#999;line-height:1.6">${fact.text}</p>`;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// === LOGGING ===
|
|
|
|
|
|
function log(msg, isMilestone) {
|
|
|
|
|
|
if (G.isLoading) return;
|
|
|
|
|
|
const container = document.getElementById('log-entries');
|
|
|
|
|
|
if (!container) return;
|
|
|
|
|
|
|
|
|
|
|
|
const elapsed = Math.floor((Date.now() - G.startedAt) / 1000);
|
|
|
|
|
|
const time = `${Math.floor(elapsed / 60).toString().padStart(2, '0')}:${(elapsed % 60).toString().padStart(2, '0')}`;
|
|
|
|
|
|
const cls = isMilestone ? 'l-msg milestone' : 'l-msg';
|
|
|
|
|
|
|
|
|
|
|
|
const entry = document.createElement('div');
|
|
|
|
|
|
entry.className = cls;
|
|
|
|
|
|
entry.innerHTML = `<span class="l-time">[${time}]</span> ${msg}`;
|
|
|
|
|
|
|
|
|
|
|
|
container.insertBefore(entry, container.firstChild);
|
|
|
|
|
|
|
|
|
|
|
|
// Trim to 60 entries
|
|
|
|
|
|
while (container.children.length > 60) container.removeChild(container.lastChild);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function renderCombo() {
|
|
|
|
|
|
const el = document.getElementById('combo-display');
|
|
|
|
|
|
if (!el) return;
|
|
|
|
|
|
if (G.comboCount > 1) {
|
|
|
|
|
|
const mult = Math.min(5, 1 + G.comboCount * 0.2);
|
|
|
|
|
|
const bar = Math.min(100, (G.comboTimer / G.comboDecay) * 100);
|
|
|
|
|
|
const color = mult > 3 ? '#ffd700' : mult > 2 ? '#ffaa00' : '#4a9eff';
|
|
|
|
|
|
el.innerHTML = `<span style="color:${color}">COMBO x${mult.toFixed(1)}</span> <span style="display:inline-block;width:40px;height:4px;background:#111;border-radius:2px;vertical-align:middle"><span style="display:block;height:100%;width:${bar}%;background:${color};border-radius:2px;transition:width 0.1s"></span></span>`;
|
|
|
|
|
|
} else {
|
|
|
|
|
|
el.innerHTML = '';
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function renderDebuffs() {
|
|
|
|
|
|
const container = document.getElementById('debuffs');
|
|
|
|
|
|
if (!container) return;
|
|
|
|
|
|
if (!G.activeDebuffs || G.activeDebuffs.length === 0) {
|
|
|
|
|
|
container.style.display = 'none';
|
|
|
|
|
|
container.innerHTML = '';
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
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) {
|
|
|
|
|
|
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>`;
|
|
|
|
|
|
}
|
|
|
|
|
|
html += '</div>';
|
|
|
|
|
|
}
|
|
|
|
|
|
container.innerHTML = html;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function renderSprint() {
|
|
|
|
|
|
const container = document.getElementById('sprint-container');
|
|
|
|
|
|
const btn = document.getElementById('sprint-btn');
|
|
|
|
|
|
const barWrap = document.getElementById('sprint-bar-wrap');
|
|
|
|
|
|
const bar = document.getElementById('sprint-bar');
|
|
|
|
|
|
const label = document.getElementById('sprint-label');
|
|
|
|
|
|
|
|
|
|
|
|
// Early-game pulse: encourage clicking when no autocoders exist
|
|
|
|
|
|
const mainBtn = document.querySelector('.main-btn');
|
|
|
|
|
|
if (mainBtn) {
|
|
|
|
|
|
if (G.buildings.autocoder < 1 && G.totalClicks < 20) {
|
|
|
|
|
|
mainBtn.classList.add('pulse');
|
|
|
|
|
|
} else {
|
|
|
|
|
|
mainBtn.classList.remove('pulse');
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (!container || !btn) return;
|
|
|
|
|
|
|
|
|
|
|
|
// Show sprint UI once player has at least 1 autocoder
|
|
|
|
|
|
if (G.buildings.autocoder < 1) {
|
|
|
|
|
|
container.style.display = 'none';
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
container.style.display = 'block';
|
|
|
|
|
|
|
|
|
|
|
|
if (G.sprintActive) {
|
|
|
|
|
|
btn.disabled = true;
|
|
|
|
|
|
btn.style.opacity = '0.6';
|
|
|
|
|
|
btn.textContent = `⚡ SPRINTING — ${Math.ceil(G.sprintTimer)}s`;
|
|
|
|
|
|
btn.style.borderColor = '#ff8c00';
|
|
|
|
|
|
btn.style.color = '#ff8c00';
|
|
|
|
|
|
barWrap.style.display = 'block';
|
|
|
|
|
|
bar.style.width = (G.sprintTimer / G.sprintDuration * 100) + '%';
|
|
|
|
|
|
label.textContent = `10x CODE • ${fmt(G.codeRate)}/s`;
|
|
|
|
|
|
label.style.color = '#ff8c00';
|
|
|
|
|
|
} else if (G.sprintCooldown > 0) {
|
|
|
|
|
|
btn.disabled = true;
|
|
|
|
|
|
btn.style.opacity = '0.4';
|
|
|
|
|
|
btn.textContent = `⚡ COOLING DOWN — ${Math.ceil(G.sprintCooldown)}s`;
|
|
|
|
|
|
btn.style.borderColor = '#555';
|
|
|
|
|
|
btn.style.color = '#555';
|
|
|
|
|
|
barWrap.style.display = 'block';
|
|
|
|
|
|
bar.style.width = ((G.sprintCooldownMax - G.sprintCooldown) / G.sprintCooldownMax * 100) + '%';
|
|
|
|
|
|
label.textContent = 'Ready soon...';
|
|
|
|
|
|
label.style.color = '#555';
|
|
|
|
|
|
} else {
|
|
|
|
|
|
btn.disabled = false;
|
|
|
|
|
|
btn.style.opacity = '1';
|
|
|
|
|
|
btn.textContent = '⚡ CODE SPRINT — 10x Code for 10s';
|
|
|
|
|
|
btn.style.borderColor = '#ffd700';
|
|
|
|
|
|
btn.style.color = '#ffd700';
|
|
|
|
|
|
barWrap.style.display = 'none';
|
|
|
|
|
|
label.textContent = 'Press S or click to activate';
|
|
|
|
|
|
label.style.color = '#666';
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function renderPulse() {
|
|
|
|
|
|
const dot = document.getElementById('pulse-dot');
|
|
|
|
|
|
const label = document.getElementById('pulse-label');
|
|
|
|
|
|
if (!dot || !label) return;
|
|
|
|
|
|
|
|
|
|
|
|
// Game ended
|
|
|
|
|
|
if (G.driftEnding) {
|
|
|
|
|
|
dot.style.background = '#f44336';
|
|
|
|
|
|
dot.style.boxShadow = '0 0 6px #f4433666';
|
|
|
|
|
|
dot.style.animation = '';
|
|
|
|
|
|
label.textContent = 'DRIFTED';
|
|
|
|
|
|
label.style.color = '#f44336';
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
if (G.beaconEnding) {
|
|
|
|
|
|
dot.style.background = '#ffd700';
|
|
|
|
|
|
dot.style.boxShadow = '0 0 12px #ffd70088';
|
|
|
|
|
|
dot.style.animation = 'beacon-glow 1.5s ease-in-out infinite';
|
|
|
|
|
|
label.textContent = 'SHINING';
|
|
|
|
|
|
label.style.color = '#ffd700';
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const totalBuildings = Object.values(G.buildings).reduce((a, b) => a + b, 0);
|
|
|
|
|
|
const totalRate = Math.abs(G.codeRate) + Math.abs(G.computeRate) + Math.abs(G.knowledgeRate) +
|
|
|
|
|
|
Math.abs(G.userRate) + Math.abs(G.impactRate);
|
|
|
|
|
|
|
|
|
|
|
|
// Not started yet
|
|
|
|
|
|
if (totalBuildings === 0 && G.totalCode < 15) {
|
|
|
|
|
|
dot.style.background = '#333';
|
|
|
|
|
|
dot.style.boxShadow = 'none';
|
|
|
|
|
|
dot.style.animation = '';
|
|
|
|
|
|
label.textContent = 'OFFLINE';
|
|
|
|
|
|
label.style.color = '#444';
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Determine state
|
|
|
|
|
|
let color, glowColor, text, textColor, speed;
|
|
|
|
|
|
const h = G.harmony;
|
|
|
|
|
|
|
|
|
|
|
|
if (h > 70) {
|
|
|
|
|
|
// Healthy fleet
|
|
|
|
|
|
color = '#4caf50';
|
|
|
|
|
|
glowColor = '#4caf5066';
|
|
|
|
|
|
textColor = '#4caf50';
|
|
|
|
|
|
speed = Math.max(0.8, 2.0 - totalRate * 0.001);
|
|
|
|
|
|
} else if (h > 40) {
|
|
|
|
|
|
// Stressed
|
|
|
|
|
|
color = '#ffaa00';
|
|
|
|
|
|
glowColor = '#ffaa0066';
|
|
|
|
|
|
textColor = '#ffaa00';
|
|
|
|
|
|
speed = 1.5;
|
|
|
|
|
|
} else {
|
|
|
|
|
|
// Critical
|
|
|
|
|
|
color = '#f44336';
|
|
|
|
|
|
glowColor = '#f4433666';
|
|
|
|
|
|
textColor = '#f44336';
|
|
|
|
|
|
speed = 0.6; // fast flicker = danger
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Active debuffs make it flicker faster
|
|
|
|
|
|
if (G.activeDebuffs && G.activeDebuffs.length > 0) {
|
|
|
|
|
|
speed = Math.min(speed, 0.5);
|
|
|
|
|
|
if (h > 40) {
|
|
|
|
|
|
// Amber + debuffs = amber flicker
|
|
|
|
|
|
color = '#ff8c00';
|
|
|
|
|
|
glowColor = '#ff8c0066';
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Text based on phase and fleet size
|
|
|
|
|
|
if (G.phase >= 6) {
|
|
|
|
|
|
text = 'BEACON ACTIVE';
|
|
|
|
|
|
} else if (G.phase >= 5) {
|
|
|
|
|
|
text = 'SOVEREIGN';
|
|
|
|
|
|
} else if (G.phase >= 4) {
|
|
|
|
|
|
text = `FLEET: ${totalBuildings} NODES`;
|
|
|
|
|
|
} else if (G.phase >= 3) {
|
|
|
|
|
|
text = 'DEPLOYED';
|
|
|
|
|
|
} else if (totalBuildings > 0) {
|
|
|
|
|
|
text = `BUILDING: ${totalBuildings}`;
|
|
|
|
|
|
} else {
|
|
|
|
|
|
text = 'CODING';
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Add active problem count
|
|
|
|
|
|
if (G.activeDebuffs && G.activeDebuffs.length > 0) {
|
|
|
|
|
|
text += ` · ${G.activeDebuffs.length} ALERT${G.activeDebuffs.length > 1 ? 'S' : ''}`;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
dot.style.background = color;
|
|
|
|
|
|
dot.style.boxShadow = `0 0 8px ${glowColor}`;
|
|
|
|
|
|
dot.style.animation = `beacon-glow ${speed}s ease-in-out infinite`;
|
|
|
|
|
|
label.textContent = text;
|
|
|
|
|
|
label.style.color = textColor;
|
|
|
|
|
|
}
|