// EL PURO - SilkNet OS Core Engine (Authoritative Server Synchronization)
const INITIAL_GAME_STATE = {
cash: 25.0,
cleanBonds: 0,
heat: 0.0,
totalEarned: 0.0,
activeTab: 'lab',
drugs: {},
skills: {},
achievements: {},
synthesis: {},
lastTick: Date.now()
};
// Initialize drugs state
DRUG_CATALOG.forEach(d => {
INITIAL_GAME_STATE.drugs[d.id] = {
unlocked: d.unlockedDefault,
raw: 0.0,
product: 0.0,
workers: 0,
dealers: 0
};
});
SKILL_TREE.forEach(s => {
INITIAL_GAME_STATE.skills[s.id] = false;
});
ACHIEVEMENTS.forEach(a => {
INITIAL_GAME_STATE.achievements[a.id] = false;
});
SYNTHESIS_RECIPES.forEach(r => {
INITIAL_GAME_STATE.synthesis[r.id] = 0;
});
let G = JSON.parse(JSON.stringify(INITIAL_GAME_STATE));
let cashRateTracker = 0.0;
let pendingActions = []; // Actions queued between 6-second authoritative server ticks
let isSyncing = false;
// --- NUMBER FORMATTING HELPER (Max 1 decimal, graceful fallback) ---
function fmtNum(val) {
if (val === undefined || val === null || isNaN(val) || !isFinite(val)) return '0';
const rounded = Math.round(Number(val) * 10) / 10;
// If it's an integer, show with .0 for uniform currency or clean integer for items
return rounded.toLocaleString(undefined, { minimumFractionDigits: 1, maximumFractionDigits: 1 });
}
function fmtInt(val) {
if (val === undefined || val === null || isNaN(val) || !isFinite(val)) return '0';
return Math.floor(Math.max(0, Number(val))).toLocaleString();
}
// --- DOM CACHE ---
const dom = {
cash: document.getElementById('hud-cash'),
rate: document.getElementById('hud-rate'),
bonds: document.getElementById('hud-bonds'),
multiplier: document.getElementById('hud-multiplier'),
heatPct: document.getElementById('heat-pct'),
heatBar: document.getElementById('heat-bar-fill'),
viewport: document.getElementById('main-viewport'),
clock: document.getElementById('system-clock'),
userBadge: document.getElementById('user-badge'),
authModal: document.getElementById('auth-modal'),
raidModal: document.getElementById('raid-modal'),
raidBribeCost: document.getElementById('raid-bribe-cost'),
toastContainer: document.getElementById('toast-container'),
syncTimer: document.getElementById('sync-timer-text'),
syncBar: document.getElementById('sync-bar-fill'),
quickStash: document.getElementById('quick-stash-items')
};
// --- MULTIPLIERS & BONUSES ---
function getProductionMultiplier() {
let m = 1.0;
m += G.cleanBonds * 0.03;
if (G.skills['chem_1']) m += 0.15;
if (G.achievements['millionaire']) m += 0.20;
return m;
}
function getSellPriceMultiplier() {
let m = 1.0;
m += G.cleanBonds * 0.04;
if (G.skills['king_2']) m += 0.25;
if (G.achievements['first_hit']) m += 0.05;
if (G.achievements['cartel_tier']) m += 0.15;
return m;
}
function getRawCostMultiplier() {
let m = 1.0;
if (G.skills['chem_2']) m *= 0.85;
return m;
}
function getDealerSpeedMultiplier() {
let m = 1.0;
if (G.skills['king_1']) m += 0.20;
if (G.skills['king_3']) m += 0.50;
return m;
}
// Cost scaling formulas
function getWorkerCost(drugId) {
const config = DRUG_CATALOG.find(d => d.id === drugId);
const count = G.drugs[drugId].workers;
return Math.floor(config.workerCost * Math.pow(1.15, count));
}
function getDealerCost(drugId) {
const config = DRUG_CATALOG.find(d => d.id === drugId);
const count = G.drugs[drugId].dealers;
return Math.floor(config.dealerCost * Math.pow(1.15, count));
}
function getUnlockCost(drugId) {
return DRUG_CATALOG.find(d => d.id === drugId).unlockCost;
}
// --- NOTIFICATIONS (TOASTS) ---
function showToast(msg, isDanger = false) {
const toast = document.createElement('div');
toast.className = `toast ${isDanger ? 'danger' : ''}`;
toast.textContent = msg;
dom.toastContainer.appendChild(toast);
setTimeout(() => {
toast.style.opacity = '0';
setTimeout(() => toast.remove(), 400);
}, 2800);
}
// --- TAB SWITCHING ---
function setTab(tabId) {
G.activeTab = tabId;
document.querySelectorAll('.nav-tab').forEach(b => {
b.classList.toggle('active', b.dataset.tab === tabId);
});
renderViewport();
}
// --- RENDER VIEWPORT ---
function renderViewport() {
if (G.activeTab === 'lab') {
renderLabView();
} else if (G.activeTab === 'streets') {
renderStreetsView();
} else if (G.activeTab === 'tree') {
renderSkillTreeView();
} else if (G.activeTab === 'offshore') {
renderOffshoreView();
}
}
// 1. LAB VIEW
function renderLabView() {
let html = `
ACTIVE LABORATORIES
SYNTHESIS LEVEL: ${G.cleanBonds}
`;
DRUG_CATALOG.forEach(d => {
const data = G.drugs[d.id] || { raw: 0, product: 0, workers: 0, dealers: 0, unlocked: false };
const rawCost = Math.floor(d.rawCost * getRawCostMultiplier());
const workerCost = getWorkerCost(d.id);
if (!data.unlocked) {
const unlockCost = getUnlockCost(d.id);
html += `
Acquire Formula ($${unlockCost.toLocaleString()})
`;
return;
}
html += `
${d.rawName}
${fmtNum(data.raw)}
${d.productName}
${fmtNum(data.product)}g
Buy Raw
$${rawCost.toLocaleString()}
Synthesize
1 Raw -> 1g
Quick Deal
+$${Math.floor(d.sellPrice * getSellPriceMultiplier()).toLocaleString()}
Hire ${d.workerName}
$${workerCost.toLocaleString()}
Manage Dealers ->
`;
});
dom.viewport.innerHTML = html;
}
// 2. STREETS VIEW (Dealers & Blends)
function renderStreetsView() {
let html = `
STREET DISTRIBUTION & DEALER NETWORK
`;
DRUG_CATALOG.filter(d => G.drugs[d.id] && G.drugs[d.id].unlocked).forEach(d => {
const data = G.drugs[d.id];
const dealerCost = getDealerCost(d.id);
const unitPrice = Math.floor(d.sellPrice * getSellPriceMultiplier());
html += `
In Stash
${fmtNum(data.product)}g
Deal Speed
${fmtNum(data.dealers * d.sellRate * getDealerSpeedMultiplier())}/s
Hire ${d.dealerName}
$${dealerCost.toLocaleString()}
Sell 10x Stash
+$${(unitPrice * Math.min(10, Math.floor(data.product))).toLocaleString()}
`;
});
// RECIPE SYNTHESIS SECTION
html += `
DESIGNER COCKTAILS & BLENDS
`;
SYNTHESIS_RECIPES.forEach(r => {
const canCook = Object.entries(r.ingredients).every(([drugId, reqAmount]) => {
return G.drugs[drugId] && G.drugs[drugId].product >= reqAmount;
});
const ingrLabels = Object.entries(r.ingredients).map(([id, amt]) => {
const has = G.drugs[id] ? G.drugs[id].product : 0;
const drugDef = DRUG_CATALOG.find(d => d.id === id);
return `${amt}g ${drugDef.name} (${fmtNum(has)}g) `;
}).join(' + ');
html += `
Recipe: ${ingrLabels}
Cook & Distribute Blend
Instant Payout: +$${r.sellPrice.toLocaleString()}
`;
});
dom.viewport.innerHTML = html;
}
// 3. SKILL TREE (SPIDERWEB)
function renderSkillTreeView() {
let html = `
UNDERWORLD TALENT SPIDERWEB
`;
const branches = [
{ id: 'chemist', name: 'Path of the Chemist', icon: 'game-icons:chemical-drop', color: '#00e5ff' },
{ id: 'kingpin', name: 'Path of the Kingpin', icon: 'game-icons:crown', color: '#eab308' },
{ id: 'smuggler', name: 'Path of the Smuggler', icon: 'game-icons:stealth-bomber', color: '#a855f7' }
];
branches.forEach(b => {
html += `
${b.name.toUpperCase()}
`;
const skillsInBranch = SKILL_TREE.filter(s => s.branch === b.id);
skillsInBranch.forEach(s => {
const isUnlocked = G.skills[s.id];
const prereqsMet = s.prereqs.every(req => G.skills[req]);
html += `
${s.name} ${isUnlocked ? '✓' : ''}
${s.desc}
${isUnlocked ? 'ACQUIRED ' : '$' + s.cost.toLocaleString()}
${!isUnlocked ? `
${!prereqsMet ? 'Requires Prior Link' : 'Unlock Node ($' + s.cost.toLocaleString() + ')'}
` : ''}
`;
});
});
dom.viewport.innerHTML = html;
}
// 4. OFFSHORE (PRESTIGE & MONEY LAUNDERING)
function renderOffshoreView() {
const bondsToEarn = Math.floor(Math.sqrt(Math.max(0, G.totalEarned) / 25000));
const nextBonds = Math.max(0, bondsToEarn - G.cleanBonds);
let html = `
SWISS ACCOUNTS & MONEY LAUNDERING
Launder Dirty Syndicate Cash
Reset dirty cash, lab equipment, and street dealers in exchange for permanent Clean Offshore Bonds .
Each Bond permanently boosts production (+3%) and sale values (+4%).
CLEAN BONDS UPON LAUNDERING:
+${nextBonds} Bonds
Career Volume: $${fmtNum(G.totalEarned)}
${nextBonds >= 1 ? 'Launder Cash & Reset Syndicate' : 'Need More Volume to Launder'}
GLOBAL SYNDICATE ACHIEVEMENTS
`;
ACHIEVEMENTS.forEach(a => {
const has = G.achievements[a.id];
html += `
${has ? '🏆 ' : '🔒 '}${a.name}
${a.desc}
${a.reward}
`;
});
dom.viewport.innerHTML = html;
}
// --- OPTIMISTIC CLIENT ACTIONS (Queued for Server Authority) ---
function queueAction(action) {
pendingActions.push(action);
}
function buyRaw(drugId) {
const d = DRUG_CATALOG.find(x => x.id === drugId);
const cost = Math.floor(d.rawCost * getRawCostMultiplier());
if (G.cash >= cost) {
// Optimistic local UI update
G.cash -= cost;
G.drugs[drugId].raw += 1;
queueAction({ type: 'buy_raw', drug_id: drugId, amount: 1 });
updateLiveInventory();
}
}
function manufacture(drugId) {
const data = G.drugs[drugId];
if (data.raw >= 1) {
data.raw -= 1;
let yieldCount = 1;
if (G.skills['chem_3'] && Math.random() < 0.10) {
yieldCount = 3;
showToast('99.1% Purity Proc! 3x Yield!', false);
}
data.product += yieldCount;
addHeat(0.5);
queueAction({ type: 'manufacture', drug_id: drugId, amount: 1 });
updateLiveInventory();
}
}
function sellManual(drugId, amount = 1) {
const d = DRUG_CATALOG.find(x => x.id === drugId);
const data = G.drugs[drugId];
const actual = Math.min(amount, Math.floor(data.product));
if (actual > 0) {
data.product -= actual;
const profit = actual * Math.floor(d.sellPrice * getSellPriceMultiplier());
G.cash += profit;
G.totalEarned += profit;
addHeat(actual * 0.8);
queueAction({ type: 'sell_manual', drug_id: drugId, amount: actual });
checkAchievements();
updateLiveInventory();
}
}
function hireWorker(drugId) {
const cost = getWorkerCost(drugId);
if (G.cash >= cost) {
G.cash -= cost;
G.drugs[drugId].workers += 1;
queueAction({ type: 'hire_worker', drug_id: drugId });
checkAchievements();
renderViewport();
updateLiveInventory();
}
}
function hireDealer(drugId) {
const cost = getDealerCost(drugId);
if (G.cash >= cost) {
G.cash -= cost;
G.drugs[drugId].dealers += 1;
queueAction({ type: 'hire_dealer', drug_id: drugId });
renderViewport();
updateLiveInventory();
}
}
function unlockDrug(drugId) {
const cost = getUnlockCost(drugId);
if (G.cash >= cost && !G.drugs[drugId].unlocked) {
G.cash -= cost;
G.drugs[drugId].unlocked = true;
showToast(`Unlocked formula for ${DRUG_CATALOG.find(d => d.id === drugId).name}!`);
queueAction({ type: 'unlock_drug', drug_id: drugId });
checkAchievements();
renderViewport();
updateLiveInventory();
}
}
function blendRecipe(recipeId) {
const r = SYNTHESIS_RECIPES.find(x => x.id === recipeId);
const canCook = Object.entries(r.ingredients).every(([drugId, amt]) => G.drugs[drugId].product >= amt);
if (canCook) {
Object.entries(r.ingredients).forEach(([drugId, amt]) => {
G.drugs[drugId].product -= amt;
});
G.cash += r.sellPrice;
G.totalEarned += r.sellPrice;
addHeat(5);
queueAction({ type: 'blend_recipe', recipe_id: recipeId });
showToast(`Synthesized & Sold ${r.name} for +$${r.sellPrice.toLocaleString()}!`);
renderViewport();
updateLiveInventory();
}
}
function buySkill(skillId) {
const s = SKILL_TREE.find(x => x.id === skillId);
if (G.cash >= s.cost && !G.skills[skillId]) {
G.cash -= s.cost;
G.skills[skillId] = true;
queueAction({ type: 'buy_skill', skill_id: skillId });
showToast(`Skill Activated: ${s.name}`);
renderViewport();
updateLiveInventory();
}
}
function launderMoney() {
const bondsToEarn = Math.floor(Math.sqrt(Math.max(0, G.totalEarned) / 25000));
const gain = Math.max(0, bondsToEarn - G.cleanBonds);
if (gain < 1) return;
if (confirm(`Launder all assets for +${gain} Clean Offshore Bonds? Your syndicate operations will reset.`)) {
queueAction({ type: 'launder_prestige' });
G.cleanBonds += gain;
G.cash = 50.0;
G.heat = 0.0;
DRUG_CATALOG.forEach(d => {
G.drugs[d.id] = {
unlocked: d.unlockedDefault,
raw: 0.0,
product: 0.0,
workers: 0,
dealers: 0
};
});
checkAchievements();
showToast(`Laundered! You now have ${G.cleanBonds} Clean Bonds.`);
renderViewport();
updateLiveInventory();
}
}
// --- POLICE HEAT & RAIDS ---
function addHeat(amount) {
let riseMod = G.skills['smug_1'] ? 0.65 : 1.0;
G.heat = Math.min(100.0, G.heat + (amount * riseMod));
}
function checkRaidEvent() {
if (G.heat >= 85 && Math.random() < 0.25) {
triggerRaidModal();
}
}
function triggerRaidModal() {
const bribeCost = Math.max(100, Math.floor(G.cash * 0.15 * (G.skills['smug_2'] ? 0.5 : 1.0)));
dom.raidBribeCost.textContent = bribeCost.toLocaleString();
dom.raidModal.classList.add('active');
document.getElementById('btn-raid-bribe').onclick = () => {
if (G.cash >= bribeCost) {
G.cash -= bribeCost;
G.heat = 0.0;
queueAction({ type: 'bribe_police' });
dom.raidModal.classList.remove('active');
showToast('Sheriff bribed. Heat cleared.');
updateLiveInventory();
} else {
showToast('Insufficient cash for bribe!', true);
}
};
document.getElementById('btn-raid-stash').onclick = () => {
G.heat = 20.0;
queueAction({ type: 'flush_stash' });
dom.raidModal.classList.remove('active');
showToast('Stash flushed & secured. Heat diminished.');
updateLiveInventory();
};
document.getElementById('btn-raid-shootout').onclick = () => {
dom.raidModal.classList.remove('active');
if (Math.random() < 0.5) {
const reward = Math.floor(G.cash * 0.5);
G.cash += reward;
G.heat = 0.0;
showToast(`Broke through blockade! Seized +$${reward.toLocaleString()}!`);
} else {
DRUG_CATALOG.forEach(d => {
G.drugs[d.id].product = Math.floor(G.drugs[d.id].product * 0.7);
});
G.heat = 30.0;
showToast('Agents seized 30% of stored product!', true);
}
updateLiveInventory();
};
}
// --- ACHIEVEMENTS CHECK ---
function checkAchievements() {
if (G.totalEarned > 0 && !G.achievements['first_hit']) {
G.achievements['first_hit'] = true;
showToast('Achievement: First Batch!');
}
if (G.drugs['weed'] && G.drugs['weed'].workers >= 10 && !G.achievements['grower_op']) {
G.achievements['grower_op'] = true;
showToast('Achievement: Green Thumb!');
}
if (G.drugs['meth'] && G.drugs['meth'].unlocked && !G.achievements['blue_sky']) {
G.achievements['blue_sky'] = true;
showToast('Achievement: Say My Name!');
}
if (G.drugs['cocaine'] && G.drugs['cocaine'].unlocked && !G.achievements['cartel_tier']) {
G.achievements['cartel_tier'] = true;
showToast('Achievement: El Patrón!');
}
if (G.cash >= 1000000 && !G.achievements['millionaire']) {
G.achievements['millionaire'] = true;
showToast('Achievement: Seven Figures!');
}
if (G.cleanBonds >= 1 && !G.achievements['laundered']) {
G.achievements['laundered'] = true;
showToast('Achievement: Clean Hands!');
}
}
// --- LIGHTWEIGHT LOCAL TICK (Smooth 60FPS UI feedback between server syncs) ---
function clientTick(deltaSec) {
let earnedThisTick = 0;
const prodMulti = getProductionMultiplier();
const sellMulti = getSellPriceMultiplier();
const dealerSpeed = getDealerSpeedMultiplier();
DRUG_CATALOG.forEach(d => {
const data = G.drugs[d.id];
if (!data || !data.unlocked) return;
// Auto Production
if (data.workers > 0 && data.raw > 0) {
const prodPerSec = data.workers * d.produceRate * prodMulti;
const toMake = Math.min(data.raw, prodPerSec * deltaSec);
data.raw = Math.max(0, data.raw - toMake);
data.product = data.product + toMake;
}
// Auto Sales
if (data.dealers > 0 && data.product > 0) {
const sellPerSec = data.dealers * d.sellRate * dealerSpeed;
const toSell = Math.min(data.product, sellPerSec * deltaSec);
data.product = Math.max(0, data.product - toSell);
const revenue = toSell * Math.floor(d.sellPrice * sellMulti);
G.cash += revenue;
G.totalEarned += revenue;
earnedThisTick += revenue;
}
});
if (G.heat > 0) {
G.heat = Math.max(0.0, G.heat - (0.5 * deltaSec));
}
if (deltaSec > 0) {
cashRateTracker = cashRateTracker * 0.85 + (earnedThisTick / deltaSec) * 0.15;
}
// Direct, surgical DOM text updates every tick (no lag, no re-rendering entire card HTML)
updateLiveInventory();
updateSyncCountdown();
}
const TICK_INTERVAL_MS = 6000;
let lastSyncTimestamp = Date.now();
function updateSyncCountdown() {
const elapsed = Date.now() - lastSyncTimestamp;
const remainingMs = Math.max(0, TICK_INTERVAL_MS - elapsed);
const remainingSec = (remainingMs / 1000).toFixed(1);
const pct = Math.min(100, Math.max(0, (remainingMs / TICK_INTERVAL_MS) * 100));
if (dom.syncTimer) {
dom.syncTimer.textContent = `${remainingSec}s`;
}
if (dom.syncBar) {
dom.syncBar.style.width = `${pct}%`;
}
}
function updateLiveInventory() {
dom.cash.textContent = fmtNum(G.cash);
dom.rate.textContent = fmtNum(cashRateTracker);
dom.bonds.textContent = fmtInt(G.cleanBonds);
dom.multiplier.textContent = fmtInt(G.cleanBonds * 4);
dom.heatPct.textContent = `${Math.round(G.heat)}%`;
dom.heatBar.style.width = `${Math.min(100, Math.round(G.heat))}%`;
const rawCostMulti = getRawCostMultiplier();
let stashChipsHtml = '';
// Surgically update raw, product, buttons, and stash chips
DRUG_CATALOG.forEach(d => {
const drugData = G.drugs[d.id];
if (!drugData) return;
// Populate quick stash overview for owned/unlocked products
if (drugData.unlocked || drugData.product > 0 || drugData.raw > 0) {
stashChipsHtml += `
${d.name.split(' ')[0]}:
${fmtNum(drugData.product)}g
`;
}
// Live text update for raw & product
const rawEl = document.getElementById(`val-raw-${d.id}`);
if (rawEl) rawEl.textContent = fmtNum(drugData.raw);
const prodEl = document.getElementById(`val-prod-${d.id}`);
if (prodEl) prodEl.textContent = `${fmtNum(drugData.product)}g`;
const streetProdEl = document.getElementById(`val-prod-street-${d.id}`);
if (streetProdEl) streetProdEl.textContent = `${fmtNum(drugData.product)}g`;
// Live button state updates (enable/disable as resources change)
const rawCost = Math.floor(d.rawCost * rawCostMulti);
const workerCost = getWorkerCost(d.id);
const dealerCost = getDealerCost(d.id);
const btnBuyRaw = document.getElementById(`btn-buy-raw-${d.id}`);
if (btnBuyRaw) btnBuyRaw.disabled = (G.cash < rawCost);
const btnMake = document.getElementById(`btn-make-${d.id}`);
if (btnMake) btnMake.disabled = (drugData.raw < 1);
const btnSell = document.getElementById(`btn-sell-${d.id}`);
if (btnSell) btnSell.disabled = (drugData.product < 1);
const btnHireWorker = document.getElementById(`btn-hire-worker-${d.id}`);
if (btnHireWorker) btnHireWorker.disabled = (G.cash < workerCost);
const btnHireDealer = document.getElementById(`btn-hire-dealer-${d.id}`);
if (btnHireDealer) btnHireDealer.disabled = (G.cash < dealerCost);
const btnSell10x = document.getElementById(`btn-sell-10x-${d.id}`);
if (btnSell10x) btnSell10x.disabled = (drugData.product < 10);
});
if (dom.quickStash) {
dom.quickStash.innerHTML = stashChipsHtml || 'No active stash ';
}
}
// --- AUTHORITATIVE SERVER TICK (Every 6 seconds) ---
async function authoritativeServerTick() {
if (isSyncing) return;
isSyncing = true;
lastSyncTimestamp = Date.now();
// Grab queued actions and reset local queue
const actionsToSend = [...pendingActions];
pendingActions = [];
if (API.getToken()) {
const serverState = await API.syncTick(actionsToSend);
if (serverState) {
// Reconcile authoritative server state (protects against browser devtools tampering)
G = {
...INITIAL_GAME_STATE,
...serverState,
activeTab: G.activeTab // Preserve UI view tab
};
checkAchievements();
updateLiveInventory();
}
} else {
// Guest mode / offline fallback: persist locally
localStorage.setItem('el_puro_local_save', JSON.stringify(G));
}
isSyncing = false;
}
// Game loop with requestAnimationFrame
let lastFrameTime = performance.now();
function gameLoop(now) {
const delta = (now - lastFrameTime) / 1000;
lastFrameTime = now;
if (delta > 0 && delta < 5) {
clientTick(delta);
}
requestAnimationFrame(gameLoop);
}
// Server tick: exactly every 6 seconds to prevent flooding server
setInterval(authoritativeServerTick, TICK_INTERVAL_MS);
// Clock updates
setInterval(() => {
const d = new Date();
dom.clock.textContent = d.toTimeString().substring(0, 5);
}, 1000);
// Periodic raid check (every 15 seconds)
setInterval(checkRaidEvent, 15000);
// --- INITIALIZATION & EVENTS ---
document.addEventListener('DOMContentLoaded', async () => {
const username = API.getUsername();
if (username) {
dom.userBadge.textContent = `[${username}]`;
}
// Load initial authoritative save from server if logged in
let loaded = false;
if (API.getToken()) {
const remote = await API.loadRemoteSave();
if (remote) {
G = { ...INITIAL_GAME_STATE, ...remote, activeTab: 'lab' };
loaded = true;
}
}
if (!loaded) {
const local = localStorage.getItem('el_puro_local_save');
if (local) {
try {
G = { ...INITIAL_GAME_STATE, ...JSON.parse(local), activeTab: 'lab' };
} catch (e) {
console.warn('Corrupt local save, using initial state');
}
}
}
// Setup navigation tabs
document.querySelectorAll('.nav-tab').forEach(b => {
b.addEventListener('click', () => setTab(b.dataset.tab));
});
// Auth Modal handling
dom.userBadge.addEventListener('click', () => dom.authModal.classList.add('active'));
document.getElementById('btn-close-auth').addEventListener('click', () => dom.authModal.classList.remove('active'));
document.getElementById('btn-login-submit').addEventListener('click', async () => {
const u = document.getElementById('auth-username').value.trim();
const p = document.getElementById('auth-password').value;
if (!u || !p) return showToast('Enter username and password', true);
try {
const res = await API.login(u, p);
dom.userBadge.textContent = `[${res.username}]`;
dom.authModal.classList.remove('active');
showToast(`Connected: Welcome ${res.username}`);
// Fetch server authoritative state
const remote = await API.loadRemoteSave();
if (remote) {
G = { ...INITIAL_GAME_STATE, ...remote, activeTab: G.activeTab };
renderViewport();
}
} catch (e) {
showToast(e.message, true);
}
});
document.getElementById('btn-register-submit').addEventListener('click', async () => {
const u = document.getElementById('auth-username').value.trim();
const p = document.getElementById('auth-password').value;
const hp = document.getElementById('auth-website-hp').value;
if (!u || !p) return showToast('Enter username and password', true);
try {
const res = await API.register(u, p, hp);
dom.userBadge.textContent = `[${res.username}]`;
dom.authModal.classList.remove('active');
showToast(`Syndicate registered: ${res.username}`);
// Fetch clean server initialized state
const remote = await API.loadRemoteSave();
if (remote) {
G = { ...INITIAL_GAME_STATE, ...remote, activeTab: G.activeTab };
renderViewport();
}
} catch (e) {
showToast(e.message, true);
}
});
renderViewport();
updateLiveInventory();
requestAnimationFrame(gameLoop);
});