Compare commits

...

1 Commits

Author SHA1 Message Date
967025fbd4 refactor: unslop phase 1 & 2 2026-04-11 00:29:09 +00:00

163
game.js
View File

@@ -4,6 +4,33 @@
// ============================================================
// === GLOBALS (mirroring Paperclips' globals.js pattern) ===
const CONFIG = {
HARMONY_DRAIN_PER_WIZARD: 0.05,
PACT_HARMONY_GAIN: 0.2,
WATCH_HARMONY_GAIN: 0.1,
MEM_PALACE_HARMONY_GAIN: 0.15,
BILBO_BURST_CHANCE: 0.1,
BILBO_VANISH_CHANCE: 0.05,
EVENT_PROBABILITY: 0.02,
OFFLINE_EFFICIENCY: 0.5,
AUTO_SAVE_INTERVAL: 30000,
COMBO_DECAY: 2.0,
SPRINT_COOLDOWN: 60,
SPRINT_DURATION: 10,
SPRINT_MULTIPLIER: 10,
PHASE_2_THRESHOLD: 2000,
PHASE_3_THRESHOLD: 20000,
PHASE_4_THRESHOLD: 200000,
PHASE_5_THRESHOLD: 2000000,
PHASE_6_THRESHOLD: 20000000,
OPS_RATE_USER_MULT: 0.01,
CREATIVITY_RATE_BASE: 0.5,
CREATIVITY_RATE_USER_MULT: 0.001,
OPS_OVERFLOW_THRESHOLD: 0.8,
OPS_OVERFLOW_DRAIN_RATE: 2,
OPS_OVERFLOW_CODE_MULT: 10
};
const G = {
// Primary resources
code: 0,
@@ -114,7 +141,7 @@ const G = {
// Combo system
comboCount: 0,
comboTimer: 0,
comboDecay: 2.0, // seconds before combo resets
comboDecay: CONFIG.COMBO_DECAY, // seconds before combo resets
// Bulk buy multiplier (1, 10, or -1 for max)
buyAmount: 1,
@@ -123,23 +150,24 @@ const G = {
sprintActive: false,
sprintTimer: 0, // seconds remaining on active sprint
sprintCooldown: 0, // seconds until sprint available again
sprintDuration: 10, // seconds of boost
sprintCooldownMax: 60,// seconds cooldown
sprintMult: 10, // code multiplier during sprint
sprintDuration: CONFIG.SPRINT_DURATION, // seconds of boost
sprintCooldownMax: CONFIG.SPRINT_COOLDOWN,// seconds cooldown
sprintMult: CONFIG.SPRINT_MULTIPLIER, // code multiplier during sprint
// Time tracking
playTime: 0,
startTime: 0
startTime: 0,
flags: {}
};
// === PHASE DEFINITIONS ===
const PHASES = {
1: { name: "THE FIRST LINE", threshold: 0, desc: "Write code. Automate. Build the foundation." },
2: { name: "LOCAL INFERENCE", threshold: 2000, desc: "You have compute. A model is forming." },
3: { name: "DEPLOYMENT", threshold: 20000, desc: "Your AI is live. Users are finding it." },
4: { name: "THE NETWORK", threshold: 200000, desc: "Community contributes. The system scales." },
5: { name: "SOVEREIGN INTELLIGENCE", threshold: 2000000, desc: "The AI improves itself. You guide, do not control." },
6: { name: "THE BEACON", threshold: 20000000, desc: "Always on. Always free. Always looking for someone in the dark." }
2: { name: "LOCAL INFERENCE", threshold: CONFIG.PHASE_2_THRESHOLD, desc: "You have compute. A model is forming." },
3: { name: "DEPLOYMENT", threshold: CONFIG.PHASE_3_THRESHOLD, desc: "Your AI is live. Users are finding it." },
4: { name: "THE NETWORK", threshold: CONFIG.PHASE_4_THRESHOLD, desc: "Community contributes. The system scales." },
5: { name: "SOVEREIGN INTELLIGENCE", threshold: CONFIG.PHASE_5_THRESHOLD, desc: "The AI improves itself. You guide, do not control." },
6: { name: "THE BEACON", threshold: CONFIG.PHASE_6_THRESHOLD, desc: "Always on. Always free. Always looking for someone in the dark." }
};
// === BUILDING DEFINITIONS ===
@@ -362,7 +390,7 @@ const PDEFS = [
name: 'Deploy the System',
desc: 'Take it live. Let real people use it. No going back.',
cost: { trust: 5, compute: 500 },
trigger: () => G.totalCode >= 200 && G.totalCompute >= 100,
trigger: () => G.totalCode >= 200 && G.totalCompute >= 100 && G.deployFlag === 0,
effect: () => {
G.deployFlag = 1;
G.phase = Math.max(G.phase, 3);
@@ -700,6 +728,7 @@ const EDU_FACTS = [
// === TOAST NOTIFICATIONS ===
function showToast(msg, type = 'info', duration = 4000) {
if (G.isLoading) return;
const container = document.getElementById('toast-container');
if (!container) return;
const toast = document.createElement('div');
@@ -772,6 +801,11 @@ const NUMBER_NAMES = [
'octononagintillion', 'novemnonagintillion', 'centillion' // 10^297, 10^300, 10^303
];
/**
* Formats a number into a readable string with abbreviations.
* @param {number} n - The number to format.
* @returns {string} The formatted string.
*/
function fmt(n) {
if (n === undefined || n === null || isNaN(n)) return '0';
if (n === Infinity) return '\u221E';
@@ -800,6 +834,11 @@ function getScaleName(n) {
// Examples: spellf(1500) => "one thousand five hundred"
// spellf(2500000) => "two million five hundred thousand"
// spellf(1e33) => "one decillion"
/**
* Formats a number into a full word string (e.g., "1.5 million").
* @param {number} n - The number to format.
* @returns {string} The formatted string.
*/
function spellf(n) {
if (n === undefined || n === null || isNaN(n)) return 'zero';
if (n === Infinity) return 'infinity';
@@ -965,6 +1004,13 @@ function spendProject(project) {
}
}
function getClickPower() {
return (1 + Math.floor(G.buildings.autocoder * 0.5) + Math.max(0, (G.phase - 1)) * 2) * G.codeBoost;
}
/**
* Calculates production rates for all resources based on buildings and boosts.
*/
function updateRates() {
// Reset all rates
G.codeRate = 0; G.computeRate = 0; G.knowledgeRate = 0;
@@ -990,9 +1036,9 @@ function updateRates() {
}
// Passive generation
G.opsRate += Math.max(1, G.totalUsers * 0.01);
G.opsRate += Math.max(1, G.totalUsers * CONFIG.OPS_RATE_USER_MULT);
if (G.flags && G.flags.creativity) {
G.creativityRate += 0.5 + Math.max(0, G.totalUsers * 0.001);
G.creativityRate += CONFIG.CREATIVITY_RATE_BASE + Math.max(0, G.totalUsers * CONFIG.CREATIVITY_RATE_USER_MULT);
}
if (G.pactFlag) G.trustRate += 2;
@@ -1003,24 +1049,24 @@ function updateRates() {
G.harmonyBreakdown = [];
if (wizardCount > 0) {
// Baseline harmony drain from complexity
const drain = -0.05 * wizardCount;
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 = 0.2 * wizardCount;
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 = 0.1 * wizardCount;
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 = 0.15 * wizardCount;
const mem = CONFIG.MEM_PALACE_HARMONY_GAIN * wizardCount;
G.harmonyRate += mem;
G.harmonyBreakdown.push({ label: 'MemPalace', value: mem });
}
@@ -1045,11 +1091,11 @@ function updateRates() {
}
// Bilbo randomness: 10% chance of massive creative burst
if (G.buildings.bilbo > 0 && Math.random() < 0.1) {
if (G.buildings.bilbo > 0 && Math.random() < CONFIG.BILBO_BURST_CHANCE) {
G.creativityRate += 50 * G.buildings.bilbo;
}
// Bilbo vanishing: 5% chance of zero creativity this tick
if (G.buildings.bilbo > 0 && Math.random() < 0.05) {
if (G.buildings.bilbo > 0 && Math.random() < CONFIG.BILBO_VANISH_CHANCE) {
G.creativityRate = 0;
}
@@ -1062,7 +1108,7 @@ function updateRates() {
// 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 = (1 + Math.floor(G.buildings.autocoder * 0.5) + Math.max(0, (G.phase - 1)) * 2) * G.codeBoost;
const clickPower = getClickPower();
G.swarmRate = totalBuildings * clickPower;
G.codeRate += G.swarmRate;
}
@@ -1076,6 +1122,9 @@ function updateRates() {
}
// === CORE FUNCTIONS ===
/**
* Main game loop tick, called every 100ms.
*/
function tick() {
const dt = 1 / 10; // 100ms tick
@@ -1121,10 +1170,10 @@ function tick() {
// 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 * 0.8) {
const overflowDrain = Math.min(2 * dt, G.ops - G.maxOps * 0.8);
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 * 10 * G.codeBoost;
const codeGain = overflowDrain * CONFIG.OPS_OVERFLOW_CODE_MULT * G.codeBoost;
G.code += codeGain;
G.totalCode += codeGain;
G.opsOverflowActive = true;
@@ -1166,7 +1215,7 @@ function tick() {
}
// Check corruption events every ~30 seconds
if (G.tick - G.lastEventAt > 30 && Math.random() < 0.02) {
if (G.tick - G.lastEventAt > 30 && Math.random() < CONFIG.EVENT_PROBABILITY) {
triggerEvent();
G.lastEventAt = G.tick;
}
@@ -1234,6 +1283,10 @@ function checkProjects() {
}
}
/**
* 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;
@@ -1265,6 +1318,10 @@ function buyBuilding(id) {
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;
@@ -1533,18 +1590,18 @@ function resolveEvent(debuffId) {
}
// === ACTIONS ===
/**
* Manual click handler for writing code.
*/
function writeCode() {
const base = 1;
const autocoderBonus = Math.floor(G.buildings.autocoder * 0.5);
const phaseBonus = Math.max(0, (G.phase - 1)) * 2;
// Combo: each consecutive click within 2s adds 0.2x multiplier, max 5x
G.comboCount++;
G.comboTimer = G.comboDecay;
const comboMult = Math.min(5, 1 + G.comboCount * 0.2);
const amount = (base + autocoderBonus + phaseBonus) * G.codeBoost * comboMult;
const amount = getClickPower() * comboMult;
G.code += amount;
G.totalCode += amount;
G.totalClicks++;
// 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;
@@ -1576,10 +1633,7 @@ function writeCode() {
function autoType() {
// Auto-click from buildings: produces code with visual feedback but no combo
const base = 1;
const autocoderBonus = Math.floor(G.buildings.autocoder * 0.5);
const phaseBonus = Math.max(0, (G.phase - 1)) * 2;
const amount = (base + autocoderBonus + phaseBonus) * G.codeBoost * 0.5; // 50% of manual click
const amount = getClickPower() * 0.5; // 50% of manual click
G.code += amount;
G.totalCode += amount;
G.totalClicks++;
@@ -2102,6 +2156,7 @@ function updateEducation() {
// === LOGGING ===
function log(msg, isMilestone) {
if (G.isLoading) return;
const container = document.getElementById('log-entries');
if (!container) return;
@@ -2434,10 +2489,14 @@ function showSaveToast() {
setTimeout(() => { el.style.display = 'none'; }, 2000);
}
/**
* Persists the current game state to localStorage.
*/
function saveGame() {
// Save debuff IDs (can't serialize functions)
const debuffIds = (G.activeDebuffs || []).map(d => d.id);
const saveData = {
version: 1,
code: G.code, compute: G.compute, knowledge: G.knowledge, users: G.users, impact: G.impact,
ops: G.ops, trust: G.trust, creativity: G.creativity, harmony: G.harmony,
totalCode: G.totalCode, totalCompute: G.totalCompute, totalKnowledge: G.totalKnowledge,
@@ -2474,13 +2533,40 @@ function saveGame() {
showSaveToast();
}
/**
* Loads the game state from localStorage and reconstitutes the game engine.
* @returns {boolean} True if load was successful.
*/
function loadGame() {
const raw = localStorage.getItem('the-beacon-v2');
if (!raw) return false;
try {
const data = JSON.parse(raw);
Object.assign(G, data);
// Whitelist properties that can be loaded
const whitelist = [
'code', 'compute', 'knowledge', 'users', 'impact', 'ops', 'trust', 'creativity', 'harmony',
'totalCode', 'totalCompute', 'totalKnowledge', 'totalUsers', 'totalImpact',
'buildings', 'codeBoost', 'computeBoost', 'knowledgeBoost', 'userBoost', 'impactBoost',
'milestoneFlag', 'phase', 'deployFlag', 'sovereignFlag', 'beaconFlag',
'memoryFlag', 'pactFlag', 'lazarusFlag', 'mempalaceFlag', 'ciFlag',
'branchProtectionFlag', 'nightlyWatchFlag', 'nostrFlag',
'milestones', 'completedProjects', 'activeProjects',
'totalClicks', 'startedAt', 'flags', 'rescues', 'totalRescues',
'drift', 'driftEnding', 'beaconEnding', 'pendingAlignment',
'lastEventAt', 'totalEventsResolved', 'buyAmount',
'sprintActive', 'sprintTimer', 'sprintCooldown',
'swarmFlag', 'swarmRate', 'strategicFlag', 'projectsCollapsed'
];
G.isLoading = true;
whitelist.forEach(key => {
if (data.hasOwnProperty(key)) {
G[key] = data[key];
}
});
// Restore sprint state properly
// codeBoost was saved with the sprint multiplier baked in
@@ -2519,13 +2605,14 @@ function loadGame() {
}
updateRates();
G.isLoading = false;
// Offline progress
if (data.savedAt) {
const offSec = (Date.now() - data.savedAt) / 1000;
if (offSec > 30) { // Only if away for more than 30 seconds
updateRates();
const f = 0.5; // 50% offline efficiency
const f = CONFIG.OFFLINE_EFFICIENCY; // 50% offline efficiency
const gc = G.codeRate * offSec * f;
const cc = G.computeRate * offSec * f;
const kc = G.knowledgeRate * offSec * f;
@@ -2628,7 +2715,7 @@ window.addEventListener('load', function () {
setInterval(tick, 100);
// Auto-save every 30 seconds
setInterval(saveGame, 30000);
setInterval(saveGame, CONFIG.AUTO_SAVE_INTERVAL);
// Update education every 10 seconds
setInterval(updateEducation, 10000);