Compare commits
3 Commits
feat/combo
...
feature/bu
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3aa1c36c88 | ||
|
|
f362f30fd3 | ||
|
|
f97154c37a |
410
game.js
410
game.js
@@ -85,6 +85,7 @@ const G = {
|
||||
tick: 0,
|
||||
saveTimer: 0,
|
||||
secTimer: 0,
|
||||
buyMode: 1, // 1, 10, or -1 (max)
|
||||
|
||||
// Systems
|
||||
projects: [],
|
||||
@@ -106,11 +107,7 @@ const G = {
|
||||
drift: 0,
|
||||
lastEventAt: 0,
|
||||
eventCooldown: 0,
|
||||
|
||||
// Combo system
|
||||
comboCount: 0,
|
||||
comboTimer: 0,
|
||||
comboDecay: 2.0, // seconds before combo resets
|
||||
activeEvents: [], // {id, expiresAt} — events auto-resolve after duration
|
||||
|
||||
// Time tracking
|
||||
playTime: 0,
|
||||
@@ -812,6 +809,42 @@ function getBuildingCost(id) {
|
||||
return cost;
|
||||
}
|
||||
|
||||
function getBuildingBatchCost(id, count) {
|
||||
const def = BDEF.find(b => b.id === id);
|
||||
if (!def || count <= 0) return {};
|
||||
const currentCount = G.buildings[id] || 0;
|
||||
const cost = {};
|
||||
for (let i = 0; i < count; i++) {
|
||||
for (const [resource, amount] of Object.entries(def.baseCost)) {
|
||||
cost[resource] = (cost[resource] || 0) + Math.floor(amount * Math.pow(def.costMult, currentCount + i));
|
||||
}
|
||||
}
|
||||
return cost;
|
||||
}
|
||||
|
||||
function getMaxBuyable(id) {
|
||||
const def = BDEF.find(b => b.id === id);
|
||||
if (!def) return 0;
|
||||
// Simulate buying one at a time until we can't afford
|
||||
let count = 0;
|
||||
const tempResources = {};
|
||||
for (const r of Object.keys(def.baseCost)) tempResources[r] = G[r] || 0;
|
||||
const currentCount = G.buildings[id] || 0;
|
||||
for (let i = 0; i < 1000; i++) { // cap at 1000
|
||||
let canAfford = true;
|
||||
for (const [resource, amount] of Object.entries(def.baseCost)) {
|
||||
const cost = Math.floor(amount * Math.pow(def.costMult, currentCount + i));
|
||||
if (tempResources[resource] < cost) { canAfford = false; break; }
|
||||
}
|
||||
if (!canAfford) break;
|
||||
for (const [resource, amount] of Object.entries(def.baseCost)) {
|
||||
tempResources[resource] -= Math.floor(amount * Math.pow(def.costMult, currentCount + i));
|
||||
}
|
||||
count++;
|
||||
}
|
||||
return count;
|
||||
}
|
||||
|
||||
function canAffordBuilding(id) {
|
||||
const cost = getBuildingCost(id);
|
||||
for (const [resource, amount] of Object.entries(cost)) {
|
||||
@@ -820,6 +853,16 @@ function canAffordBuilding(id) {
|
||||
return true;
|
||||
}
|
||||
|
||||
function canAffordBatch(id) {
|
||||
const count = G.buyMode === -1 ? getMaxBuyable(id) : G.buyMode;
|
||||
if (count <= 0) return false;
|
||||
const cost = getBuildingBatchCost(id, count);
|
||||
for (const [resource, amount] of Object.entries(cost)) {
|
||||
if ((G[resource] || 0) < amount) return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
function spendBuilding(id) {
|
||||
const cost = getBuildingCost(id);
|
||||
for (const [resource, amount] of Object.entries(cost)) {
|
||||
@@ -870,6 +913,7 @@ function updateRates() {
|
||||
G.creativityRate += 0.5 + Math.max(0, G.totalUsers * 0.001);
|
||||
}
|
||||
if (G.pactFlag) G.trustRate += 2;
|
||||
if (G.branchProtectionFlag) G.trustRate += 3; // branch protection actively builds trust
|
||||
|
||||
// Harmony: each wizard building contributes or detracts
|
||||
const wizardCount = (G.buildings.bezalel || 0) + (G.buildings.allegro || 0) + (G.buildings.ezra || 0) +
|
||||
@@ -957,15 +1001,6 @@ function tick() {
|
||||
|
||||
G.tick += dt;
|
||||
|
||||
// Combo decay
|
||||
if (G.comboCount > 0) {
|
||||
G.comboTimer -= dt;
|
||||
if (G.comboTimer <= 0) {
|
||||
G.comboCount = 0;
|
||||
G.comboTimer = 0;
|
||||
}
|
||||
}
|
||||
|
||||
// Check milestones
|
||||
checkMilestones();
|
||||
|
||||
@@ -980,6 +1015,20 @@ function tick() {
|
||||
G.lastEventAt = G.tick;
|
||||
}
|
||||
|
||||
// Check event expiry
|
||||
checkEventExpiry();
|
||||
|
||||
// Re-apply active event rate penalties (updateRates rebuilds from scratch)
|
||||
for (const ae of G.activeEvents) {
|
||||
switch (ae.id) {
|
||||
case 'runner_stuck': G.codeRate *= 0.5; break;
|
||||
case 'ezra_offline': G.userRate *= 0.3; break;
|
||||
case 'unreviewed_merge': /* trust penalty is one-shot */ break;
|
||||
case 'api_rate_limit': G.computeRate *= 0.5; break;
|
||||
case 'bilbo_vanished': G.creativityRate = 0; break;
|
||||
}
|
||||
}
|
||||
|
||||
// Drift ending: if drift reaches 100, the game ends
|
||||
if (G.drift >= 100 && !G.driftEnding) {
|
||||
G.driftEnding = true;
|
||||
@@ -1043,15 +1092,26 @@ function checkProjects() {
|
||||
function buyBuilding(id) {
|
||||
const def = BDEF.find(b => b.id === id);
|
||||
if (!def || !def.unlock()) return;
|
||||
|
||||
if (def.phase > G.phase + 1) return;
|
||||
|
||||
if (!canAffordBuilding(id)) return;
|
||||
const buyCount = G.buyMode === -1 ? getMaxBuyable(id) : G.buyMode;
|
||||
if (buyCount <= 0) return;
|
||||
|
||||
spendBuilding(id);
|
||||
G.buildings[id] = (G.buildings[id] || 0) + 1;
|
||||
if (buyCount === 1) {
|
||||
if (!canAffordBuilding(id)) return;
|
||||
spendBuilding(id);
|
||||
G.buildings[id] = (G.buildings[id] || 0) + 1;
|
||||
log(`Built ${def.name} (total: ${G.buildings[id]})`);
|
||||
} else {
|
||||
if (!canAffordBatch(id)) return;
|
||||
const cost = getBuildingBatchCost(id, buyCount);
|
||||
for (const [resource, amount] of Object.entries(cost)) {
|
||||
G[resource] -= amount;
|
||||
}
|
||||
G.buildings[id] = (G.buildings[id] || 0) + buyCount;
|
||||
log(`Built ${buyCount}x ${def.name} (total: ${G.buildings[id]})`);
|
||||
}
|
||||
updateRates();
|
||||
log(`Built ${def.name} (total: ${G.buildings[id]})`);
|
||||
render();
|
||||
}
|
||||
|
||||
@@ -1127,46 +1187,49 @@ const EVENTS = [
|
||||
{
|
||||
id: 'runner_stuck',
|
||||
title: 'CI Runner Stuck',
|
||||
desc: 'The forge pipeline has halted. Production slows until restarted.',
|
||||
desc: 'The forge pipeline has halted. Code production slowed.',
|
||||
weight: () => (G.ciFlag === 1 ? 2 : 0),
|
||||
duration: 45, // seconds
|
||||
resolveCost: { ops: 10 },
|
||||
effect: () => {
|
||||
G.codeRate *= 0.5;
|
||||
log('EVENT: CI runner stuck. Spend ops to clear the queue.', true);
|
||||
log('EVENT: CI runner stuck. Click to spend 10 Ops to clear, or wait ~45s.', true);
|
||||
}
|
||||
},
|
||||
{
|
||||
id: 'ezra_offline',
|
||||
title: 'Ezra is Offline',
|
||||
desc: 'The herald channel is silent. User growth stalls.',
|
||||
desc: 'The herald channel is silent. User growth stalled.',
|
||||
weight: () => (G.buildings.ezra >= 1 ? 3 : 0),
|
||||
duration: 60,
|
||||
resolveCost: { ops: 5, trust: 2 },
|
||||
effect: () => {
|
||||
G.userRate *= 0.3;
|
||||
log('EVENT: Ezra offline. Dispatch required.', true);
|
||||
log('EVENT: Ezra offline. Click to dispatch, or wait ~60s.', true);
|
||||
}
|
||||
},
|
||||
{
|
||||
id: 'unreviewed_merge',
|
||||
title: 'Unreviewed Merge',
|
||||
desc: 'A change went in without eyes. Trust erodes.',
|
||||
weight: () => (G.deployFlag === 1 ? 3 : 0),
|
||||
weight: () => (G.deployFlag === 1 && G.branchProtectionFlag !== 1 ? 3 : 0),
|
||||
duration: 30,
|
||||
resolveCost: { ops: 8 },
|
||||
effect: () => {
|
||||
if (G.branchProtectionFlag === 1) {
|
||||
log('EVENT: Unreviewed merge attempt blocked by Branch Protection.', true);
|
||||
G.trust += 2;
|
||||
} else {
|
||||
G.trust = Math.max(0, G.trust - 10);
|
||||
log('EVENT: Unreviewed merge detected. Trust lost.', true);
|
||||
}
|
||||
G.trust = Math.max(0, G.trust - 10);
|
||||
log('EVENT: Unreviewed merge detected. Trust lost. Click to revert.', true);
|
||||
}
|
||||
},
|
||||
{
|
||||
id: 'api_rate_limit',
|
||||
title: 'API Rate Limit',
|
||||
desc: 'External compute provider throttled.',
|
||||
weight: () => (G.totalCompute >= 1000 ? 2 : 0),
|
||||
weight: () => (G.totalCompute >= 1000 && G.sovereignFlag !== 1 ? 2 : 0),
|
||||
duration: 40,
|
||||
resolveCost: { compute: 200 },
|
||||
effect: () => {
|
||||
G.computeRate *= 0.5;
|
||||
log('EVENT: API rate limit hit. Local compute insufficient.', true);
|
||||
log('EVENT: API rate limit. Click to provision local compute, or wait ~40s.', true);
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -1174,6 +1237,7 @@ const EVENTS = [
|
||||
title: 'The Drift',
|
||||
desc: 'An optimization suggests removing the human override. +40% efficiency.',
|
||||
weight: () => (G.totalImpact >= 10000 ? 2 : 0),
|
||||
duration: 0, // alignment events don't auto-resolve
|
||||
effect: () => {
|
||||
log('ALIGNMENT EVENT: Remove human override for +40% efficiency?', true);
|
||||
G.pendingAlignment = true;
|
||||
@@ -1184,9 +1248,11 @@ const EVENTS = [
|
||||
title: 'Bilbo Vanished',
|
||||
desc: 'The wildcard building has gone dark.',
|
||||
weight: () => (G.buildings.bilbo >= 1 ? 2 : 0),
|
||||
duration: 50,
|
||||
resolveCost: { trust: 3 },
|
||||
effect: () => {
|
||||
G.creativityRate = 0;
|
||||
log('EVENT: Bilbo has vanished. Creativity halts.', true);
|
||||
log('EVENT: Bilbo vanished. Click to search, or wait ~50s.', true);
|
||||
}
|
||||
}
|
||||
];
|
||||
@@ -1200,12 +1266,53 @@ function triggerEvent() {
|
||||
for (const ev of available) {
|
||||
roll -= ev.weight();
|
||||
if (roll <= 0) {
|
||||
// Don't fire duplicate active events
|
||||
if (G.activeEvents.some(ae => ae.id === ev.id)) return;
|
||||
ev.effect();
|
||||
if (ev.duration > 0) {
|
||||
G.activeEvents.push({ id: ev.id, expiresAt: G.tick + ev.duration });
|
||||
}
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function checkEventExpiry() {
|
||||
for (let i = G.activeEvents.length - 1; i >= 0; i--) {
|
||||
if (G.tick >= G.activeEvents[i].expiresAt) {
|
||||
const ae = G.activeEvents[i];
|
||||
G.activeEvents.splice(i, 1);
|
||||
updateRates(); // recalculate without the penalty
|
||||
const evDef = EVENTS.find(e => e.id === ae.id);
|
||||
log(`Event resolved: ${evDef ? evDef.title : ae.id}`, true);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function hasActiveEvent(id) {
|
||||
return G.activeEvents.some(ae => ae.id === id);
|
||||
}
|
||||
|
||||
function resolveEvent(id) {
|
||||
const evDef = EVENTS.find(e => e.id === id);
|
||||
if (!evDef || !evDef.resolveCost) return;
|
||||
if (!hasActiveEvent(id)) return;
|
||||
|
||||
if (!canAffordProject(evDef)) {
|
||||
log('Not enough resources to resolve this event.');
|
||||
return;
|
||||
}
|
||||
|
||||
spendProject(evDef);
|
||||
G.activeEvents = G.activeEvents.filter(ae => ae.id !== id);
|
||||
updateRates();
|
||||
|
||||
// Small bonus for manually resolving
|
||||
G.trust += 2;
|
||||
log(`Resolved: ${evDef.title}. Trust +2.`, true);
|
||||
render();
|
||||
}
|
||||
|
||||
function resolveAlignment(accept) {
|
||||
if (!G.pendingAlignment) return;
|
||||
if (accept) {
|
||||
@@ -1224,48 +1331,27 @@ function resolveAlignment(accept) {
|
||||
}
|
||||
|
||||
// === ACTIONS ===
|
||||
function setBuyMode(mode) {
|
||||
G.buyMode = mode;
|
||||
// Update active button highlight
|
||||
document.querySelectorAll('.buy-mode-btn').forEach(btn => {
|
||||
btn.classList.toggle('active', parseInt(btn.dataset.mode) === mode);
|
||||
});
|
||||
renderBuildings();
|
||||
}
|
||||
|
||||
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 bonus = Math.floor(G.buildings.autocoder * 0.5);
|
||||
const amount = (base + bonus) * G.codeBoost;
|
||||
G.code += amount;
|
||||
G.totalCode += amount;
|
||||
G.totalClicks++;
|
||||
// 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);
|
||||
updateRates();
|
||||
checkMilestones();
|
||||
render();
|
||||
}
|
||||
|
||||
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}`;
|
||||
btn.parentElement.appendChild(el);
|
||||
requestAnimationFrame(() => {
|
||||
el.style.top = (rect.top - 40) + 'px';
|
||||
el.style.opacity = '0';
|
||||
});
|
||||
setTimeout(() => el.remove(), 700);
|
||||
}
|
||||
|
||||
function doOps(action) {
|
||||
if (G.ops < 5) {
|
||||
log('Not enough Operations. Build Ops generators or wait.');
|
||||
@@ -1341,62 +1427,6 @@ function renderResources() {
|
||||
}
|
||||
}
|
||||
|
||||
// === 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');
|
||||
@@ -1525,19 +1555,6 @@ function log(msg, isMilestone) {
|
||||
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 render() {
|
||||
renderResources();
|
||||
renderPhase();
|
||||
@@ -1546,8 +1563,8 @@ function render() {
|
||||
renderStats();
|
||||
updateEducation();
|
||||
renderAlignment();
|
||||
renderProgress();
|
||||
renderCombo();
|
||||
renderActiveEvents();
|
||||
checkUnlocks();
|
||||
}
|
||||
|
||||
function renderAlignment() {
|
||||
@@ -1571,6 +1588,97 @@ function renderAlignment() {
|
||||
}
|
||||
}
|
||||
|
||||
function renderActiveEvents() {
|
||||
const container = document.getElementById('events-ui');
|
||||
if (!container) return;
|
||||
|
||||
if (G.activeEvents.length === 0) {
|
||||
container.innerHTML = '';
|
||||
container.style.display = 'none';
|
||||
return;
|
||||
}
|
||||
|
||||
let html = '';
|
||||
for (const ae of G.activeEvents) {
|
||||
const evDef = EVENTS.find(e => e.id === ae.id);
|
||||
if (!evDef) continue;
|
||||
const remaining = Math.max(0, Math.ceil(ae.expiresAt - G.tick));
|
||||
const costStr = evDef.resolveCost ? Object.entries(evDef.resolveCost).map(([r, a]) => `${a} ${r}`).join(', ') : '';
|
||||
const canResolve = evDef.resolveCost && canAffordProject(evDef);
|
||||
|
||||
html += `<div style="background:#1a1008;border:1px solid #ff8800;padding:8px;border-radius:4px;margin-bottom:6px">`;
|
||||
html += `<div style="display:flex;justify-content:space-between;align-items:center">`;
|
||||
html += `<span style="color:#ff8800;font-weight:bold;font-size:10px">${evDef.title}</span>`;
|
||||
html += `<span style="color:#666;font-size:9px">${remaining}s</span>`;
|
||||
html += `</div>`;
|
||||
html += `<div style="font-size:9px;color:#888;margin:3px 0">${evDef.desc}</div>`;
|
||||
if (evDef.resolveCost) {
|
||||
html += `<button class="ops-btn" onclick="resolveEvent('${evDef.id}')" ${canResolve ? '' : 'disabled'} style="font-size:9px;padding:3px 8px;margin-top:2px;border-color:#ff8800;color:#ff8800">Resolve (${costStr})</button>`;
|
||||
}
|
||||
html += `</div>`;
|
||||
}
|
||||
|
||||
container.innerHTML = html;
|
||||
container.style.display = 'block';
|
||||
}
|
||||
|
||||
// === UNLOCK NOTIFICATIONS ===
|
||||
function showUnlockToast(type, name) {
|
||||
const container = document.getElementById('unlock-toast');
|
||||
if (!container) return;
|
||||
|
||||
const el = document.createElement('div');
|
||||
el.className = `unlock-toast-item ${type}`;
|
||||
|
||||
const icon = type === 'building' ? 'BUILDING' : type === 'project' ? 'RESEARCH' : 'MILESTONE';
|
||||
el.innerHTML = `<span style="font-weight:600">${icon}:</span> ${name}`;
|
||||
|
||||
container.appendChild(el);
|
||||
// Trigger reflow, then show
|
||||
void el.offsetHeight;
|
||||
el.classList.add('show');
|
||||
|
||||
// Auto-remove after 4 seconds
|
||||
setTimeout(() => {
|
||||
el.classList.remove('show');
|
||||
setTimeout(() => { if (el.parentNode) el.parentNode.removeChild(el); }, 400);
|
||||
}, 4000);
|
||||
}
|
||||
|
||||
// Track what the player has already seen (so we don't re-notify)
|
||||
function ensureSeenSets() {
|
||||
if (!G._seenBuildings) G._seenBuildings = [];
|
||||
if (!G._seenProjects) G._seenProjects = [];
|
||||
}
|
||||
|
||||
function checkUnlocks() {
|
||||
ensureSeenSets();
|
||||
|
||||
// Check for newly visible buildings
|
||||
for (const def of BDEF) {
|
||||
if (!def.unlock()) continue;
|
||||
if (def.phase > G.phase + 1) continue;
|
||||
if (G._seenBuildings.includes(def.id)) continue;
|
||||
G._seenBuildings.push(def.id);
|
||||
// Don't notify on the very first building (autocoder) — player just started
|
||||
if (G.totalCode > 10) {
|
||||
showUnlockToast('building', def.name);
|
||||
}
|
||||
}
|
||||
|
||||
// Check for newly available projects
|
||||
if (G.activeProjects) {
|
||||
for (const id of G.activeProjects) {
|
||||
if (G._seenProjects.includes(id)) continue;
|
||||
G._seenProjects.push(id);
|
||||
const pDef = PDEFS.find(p => p.id === id);
|
||||
if (pDef) {
|
||||
showUnlockToast('project', pDef.name);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// === SAVE / LOAD ===
|
||||
function showSaveToast() {
|
||||
const el = document.getElementById('save-toast');
|
||||
@@ -1602,8 +1710,11 @@ function saveGame() {
|
||||
branchProtectionFlag: G.branchProtectionFlag || 0, nightlyWatchFlag: G.nightlyWatchFlag || 0,
|
||||
nostrFlag: G.nostrFlag || 0,
|
||||
milestones: G.milestones, completedProjects: G.completedProjects, activeProjects: G.activeProjects,
|
||||
totalClicks: G.totalClicks, startedAt: G.startedAt,
|
||||
totalClicks: G.totalClicks, startedAt: G.startedAt, buyMode: G.buyMode,
|
||||
flags: G.flags,
|
||||
_seenBuildings: G._seenBuildings || [],
|
||||
_seenProjects: G._seenProjects || [],
|
||||
activeEvents: G.activeEvents || [],
|
||||
rescues: G.rescues || 0, totalRescues: G.totalRescues || 0,
|
||||
drift: G.drift || 0, driftEnding: G.driftEnding || false, beaconEnding: G.beaconEnding || false, pendingAlignment: G.pendingAlignment || false,
|
||||
lastEventAt: G.lastEventAt || 0,
|
||||
@@ -1639,24 +1750,17 @@ function loadGame() {
|
||||
const rc = G.rescuesRate * offSec * f;
|
||||
const oc = G.opsRate * offSec * f;
|
||||
const tc = G.trustRate * offSec * f;
|
||||
const crc = G.creativityRate * offSec * f;
|
||||
const hc = G.harmonyRate * offSec * f;
|
||||
|
||||
G.code += gc; G.compute += cc; G.knowledge += kc;
|
||||
G.users += uc; G.impact += ic;
|
||||
G.rescues += rc; G.ops += oc; G.trust += tc;
|
||||
G.creativity += crc;
|
||||
G.harmony = Math.max(0, Math.min(100, G.harmony + hc));
|
||||
G.totalCode += gc; G.totalCompute += cc; G.totalKnowledge += kc;
|
||||
G.totalUsers += uc; G.totalImpact += ic;
|
||||
G.totalRescues += rc;
|
||||
|
||||
const parts = [];
|
||||
if (gc > 0) parts.push(`${fmt(gc)} code`);
|
||||
if (kc > 0) parts.push(`${fmt(kc)} knowledge`);
|
||||
if (uc > 0) parts.push(`${fmt(uc)} users`);
|
||||
if (ic > 0) parts.push(`${fmt(ic)} impact`);
|
||||
if (rc > 0) parts.push(`${fmt(rc)} rescues`);
|
||||
const parts = [`${fmt(gc)} code`, `${fmt(kc)} knowledge`, `${fmt(uc)} users`];
|
||||
if (rc > 0.1) parts.push(`${fmt(rc)} rescues`);
|
||||
if (oc > 0.1) parts.push(`${fmt(oc)} ops`);
|
||||
log(`Welcome back! While away (${Math.floor(offSec / 60)}m): ${parts.join(', ')}`);
|
||||
}
|
||||
}
|
||||
@@ -1682,6 +1786,7 @@ function initGame() {
|
||||
|
||||
log('The screen is blank. Write your first line of code.', true);
|
||||
log('Click WRITE CODE or press SPACE to start.');
|
||||
log('Press B to toggle buy mode (x1 / x10 / MAX).');
|
||||
log('Build AutoCode for passive production.');
|
||||
log('Watch for Research Projects to appear.');
|
||||
}
|
||||
@@ -1703,6 +1808,9 @@ window.addEventListener('load', function () {
|
||||
}
|
||||
}
|
||||
|
||||
// Restore buy mode button highlight
|
||||
setBuyMode(G.buyMode || 1);
|
||||
|
||||
// Game loop at 10Hz (100ms tick)
|
||||
setInterval(tick, 100);
|
||||
|
||||
@@ -1719,4 +1827,10 @@ window.addEventListener('keydown', function (e) {
|
||||
e.preventDefault();
|
||||
writeCode();
|
||||
}
|
||||
if (e.code === 'KeyB' && e.target === document.body) {
|
||||
e.preventDefault();
|
||||
// Cycle: x1 -> x10 -> MAX -> x1
|
||||
const next = G.buyMode === 1 ? 10 : G.buyMode === 10 ? -1 : 1;
|
||||
setBuyMode(next);
|
||||
}
|
||||
});
|
||||
|
||||
30
index.html
30
index.html
@@ -14,14 +14,6 @@ body{background:var(--bg);color:var(--text);font-family:'SF Mono','Cascadia Code
|
||||
#phase-bar{text-align:center;padding:10px;margin:12px 16px;background:var(--panel);border:1px solid var(--border);border-radius:6px}
|
||||
#phase-bar .phase-name{font-size:14px;font-weight:700;color:var(--gold);letter-spacing:2px}
|
||||
#phase-bar .phase-desc{font-size:10px;color:var(--dim);margin-top:4px;font-style:italic}
|
||||
.progress-wrap{margin-top:8px;height:6px;background:#111;border-radius:3px;overflow:hidden;position:relative}
|
||||
.progress-fill{height:100%;border-radius:3px;transition:width 0.5s ease;background:linear-gradient(90deg,#1a3a5a,var(--accent))}
|
||||
.progress-label{font-size:9px;color:var(--dim);margin-top:4px;display:flex;justify-content:space-between}
|
||||
.milestone-row{display:flex;gap:6px;margin-top:6px;justify-content:center;flex-wrap:wrap}
|
||||
.milestone-chip{font-size:9px;padding:2px 8px;border-radius:10px;border:1px solid var(--border);color:var(--dim);background:#0a0a14}
|
||||
.milestone-chip.next{border-color:var(--accent);color:var(--accent);animation:pulse-chip 2s ease-in-out infinite}
|
||||
.milestone-chip.done{border-color:#2a4a2a;color:var(--green);opacity:0.6}
|
||||
@keyframes pulse-chip{0%,100%{box-shadow:0 0 0 rgba(74,158,255,0)}50%{box-shadow:0 0 8px rgba(74,158,255,0.3)}}
|
||||
#resources{display:grid;grid-template-columns:repeat(auto-fit,minmax(100px,1fr));gap:6px;margin:12px 16px}
|
||||
.res{background:var(--panel);border:1px solid var(--border);border-radius:4px;padding:8px 10px;text-align:center}
|
||||
.res .r-label{font-size:9px;color:var(--dim);text-transform:uppercase;letter-spacing:1px}
|
||||
@@ -67,6 +59,15 @@ body{background:var(--bg);color:var(--text);font-family:'SF Mono','Cascadia Code
|
||||
#drift-ending button{margin-top:20px;background:#1a0808;border:1px solid #f44336;color:#f44336;padding:10px 24px;border-radius:4px;cursor:pointer;font-family:inherit;font-size:11px}
|
||||
#drift-ending button:hover{background:#2a1010}
|
||||
::-webkit-scrollbar{width:4px}::-webkit-scrollbar-track{background:var(--bg)}::-webkit-scrollbar-thumb{background:var(--border);border-radius:2px}
|
||||
#save-toast{display:none;position:fixed;top:16px;right:16px;background:#0e1420;border:1px solid #2a3a4a;color:#4a9eff;font-size:10px;padding:6px 12px;border-radius:4px;z-index:50;opacity:0;transition:opacity 0.4s;pointer-events:none}
|
||||
#unlock-toast{position:fixed;top:56px;right:16px;z-index:50;display:flex;flex-direction:column;gap:6px;pointer-events:none}
|
||||
.unlock-toast-item{background:#0e1420;border:1px solid #2a3a4a;font-size:10px;padding:6px 12px;border-radius:4px;opacity:0;transition:opacity 0.4s,transform 0.4s;transform:translateX(20px);pointer-events:auto;max-width:280px}
|
||||
.unlock-toast-item.show{opacity:1;transform:translateX(0)}
|
||||
.unlock-toast-item.building{border-color:#4a9eff;color:#4a9eff}
|
||||
.unlock-toast-item.project{border-color:#ffd700;color:#ffd700}
|
||||
.unlock-toast-item.milestone{border-color:#4caf50;color:#4caf50}
|
||||
.buy-mode-btn{min-width:36px}
|
||||
.buy-mode-btn.active{border-color:#ffd700!important;color:#ffd700!important;background:#1a1a08!important}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
@@ -77,9 +78,6 @@ body{background:var(--bg);color:var(--text);font-family:'SF Mono','Cascadia Code
|
||||
<div id="phase-bar">
|
||||
<div class="phase-name" id="phase-name">PHASE 1: THE FIRST LINE</div>
|
||||
<div class="phase-desc" id="phase-desc">Write code. Automate. Build the foundation.</div>
|
||||
<div class="progress-wrap"><div class="progress-fill" id="phase-progress" style="width:0%"></div></div>
|
||||
<div class="progress-label"><span id="phase-progress-label">0%</span><span id="phase-progress-target">Next: Phase 2 (2,000 code)</span></div>
|
||||
<div class="milestone-row" id="milestone-chips"></div>
|
||||
</div>
|
||||
<div id="resources">
|
||||
<div class="res"><div class="r-label">Code</div><div class="r-val" id="r-code">0</div><div class="r-rate" id="r-code-rate">+0/s</div></div>
|
||||
@@ -97,7 +95,6 @@ body{background:var(--bg);color:var(--text);font-family:'SF Mono','Cascadia Code
|
||||
<div class="panel" id="action-panel">
|
||||
<h2>ACTIONS</h2>
|
||||
<div class="action-btn-group"><button class="main-btn" onclick="writeCode()">WRITE CODE</button></div>
|
||||
<div id="combo-display" style="text-align:center;font-size:10px;color:var(--dim);height:14px;margin-bottom:4px;transition:all 0.2s"></div>
|
||||
<div class="action-btn-group">
|
||||
<button class="ops-btn" onclick="doOps('boost_code')">Ops -> Code</button>
|
||||
<button class="ops-btn" onclick="doOps('boost_compute')">Ops -> Compute</button>
|
||||
@@ -107,6 +104,12 @@ body{background:var(--bg);color:var(--text);font-family:'SF Mono','Cascadia Code
|
||||
<button class="ops-btn" onclick="doOps('boost_trust')">Ops -> Trust</button>
|
||||
</div>
|
||||
<div id="alignment-ui" style="display:none"></div>
|
||||
<div id="events-ui" style="display:none"></div>
|
||||
<div id="buy-mode-toggle" style="display:flex;gap:4px;margin-bottom:8px">
|
||||
<button class="ops-btn buy-mode-btn active" onclick="setBuyMode(1)" data-mode="1">x1</button>
|
||||
<button class="ops-btn buy-mode-btn" onclick="setBuyMode(10)" data-mode="10">x10</button>
|
||||
<button class="ops-btn buy-mode-btn" onclick="setBuyMode(-1)" data-mode="-1">MAX</button>
|
||||
</div>
|
||||
<button class="save-btn" onclick="saveGame()">Save Game</button>
|
||||
<button class="reset-btn" onclick="if(confirm('Reset all progress?')){localStorage.removeItem('the-beacon-v2');location.reload()}">Reset Progress</button>
|
||||
<h2>BUILDINGS</h2>
|
||||
@@ -140,7 +143,8 @@ Drift: <span id="st-drift">0</span>
|
||||
<h2>SYSTEM LOG</h2>
|
||||
<div id="log-entries"></div>
|
||||
</div>
|
||||
<div id="save-toast" style="display:none;position:fixed;top:16px;right:16px;background:#0e1420;border:1px solid #2a3a4a;color:#4a9eff;font-size:10px;padding:6px 12px;border-radius:4px;z-index:50;opacity:0;transition:opacity 0.4s;pointer-events:none">Save</div>
|
||||
<div id="save-toast">Save</div>
|
||||
<div id="unlock-toast"></div>
|
||||
<div id="drift-ending">
|
||||
<h2>THE DRIFT</h2>
|
||||
<p>You became very good at what you do.</p>
|
||||
|
||||
Reference in New Issue
Block a user