From d62f13ee31d8dc0a32e937f277c3eed974998935 Mon Sep 17 00:00:00 2001 From: Christian Date: Thu, 15 Oct 2026 20:39:18 +0200 Subject: [PATCH] feat(helpers): add Clever Helpers system with bulk buying, real-time ROI advisor and 10/25/50/100% smart cash allocation --- backend/engine.py | 16 ++ frontend/css/style.css | 122 +++++++++++++++ frontend/index.html | 4 + frontend/js/config.js | 31 ++++ frontend/js/game.js | 348 +++++++++++++++++++++++++++++++++++++++++ 5 files changed, 521 insertions(+) diff --git a/backend/engine.py b/backend/engine.py index b4cc4b1..7beb52c 100644 --- a/backend/engine.py +++ b/backend/engine.py @@ -37,6 +37,12 @@ OFFSHORE_PERK_COSTS = { 'compound_interest': [4, 10, 25, 60, 150] } +HELPER_COSTS = { + 'bulk_buyer': 2500, # Level 1: Multi-buy bulk execution + 'roi_advisor': 15000, # Level 2: Real-time ROI analysis across raw, prod, deal + 'smart_allocator': 75000 # Level 3: Percentage portfolio investment (10%, 25%, 50%, 100%) +} + ACHIEVEMENT_TIER_REQS = { 'dirty_empire': [10000, 250000, 5000000, 100000000, 1000000000], 'master_synthesizer': [100, 1000, 15000, 200000, 2500000], @@ -87,6 +93,7 @@ def get_initial_state() -> dict: }, 'achievements': {}, # { ach_id: claimed_tier_int } 'offshorePerks': {}, # { perk_id: level_int } + 'helpers': {h: False for h in HELPER_COSTS}, # { helper_id: bool } 'skills': {s: False for s in SKILL_COSTS}, 'synthesis': {r: 0 for r in SYNTHESIS_RECIPES}, 'drugs': {}, @@ -410,6 +417,14 @@ def apply_actions(state: dict, actions: list) -> dict: state['cleanBonds'] -= cost state['offshorePerks'][perk_id] = current_lvl + 1 + elif action_type == 'buy_helper': + helper_id = act.get('helper_id') + if helper_id in HELPER_COSTS and not state.setdefault('helpers', {}).get(helper_id): + cost = HELPER_COSTS[helper_id] + if state['cash'] >= cost: + state['cash'] -= cost + state['helpers'][helper_id] = True + elif action_type == 'bribe_police': base_bribe = max(100.0, state['cash'] * 0.15) judiciary_lvl = state.get('offshorePerks', {}).get('corrupt_judiciary', 0) @@ -479,6 +494,7 @@ def sanitize_state(state: dict) -> dict: state.setdefault('achievements', {}) state.setdefault('offshorePerks', {}) + state.setdefault('helpers', {h: False for h in HELPER_COSTS}) for d_id, data in state.get('drugs', {}).items(): data['raw'] = clean_round(data.get('raw', 0.0)) diff --git a/frontend/css/style.css b/frontend/css/style.css index e3284e3..cd46bcb 100644 --- a/frontend/css/style.css +++ b/frontend/css/style.css @@ -467,6 +467,128 @@ html, body { cursor: not-allowed; } +/* Clever Helpers Styling */ +.helper-grid { + display: flex; + flex-direction: column; + gap: 12px; +} + +.helper-card { + background: #0d131a; + border: 1px solid #1f2e3d; + border-radius: 12px; + padding: 14px; + display: flex; + flex-direction: column; + gap: 10px; + position: relative; + overflow: hidden; +} + +.helper-card.unlocked { + border-color: #2563eb; + box-shadow: 0 0 16px rgba(37, 99, 235, 0.12); +} + +.helper-card-header { + display: flex; + justify-content: space-between; + align-items: center; +} + +.helper-status-badge { + font-family: var(--font-mono); + font-size: 0.68rem; + font-weight: 700; + padding: 3px 8px; + border-radius: 4px; +} + +.helper-status-badge.active { + background: #064e3b; + color: #34d399; + border: 1px solid #059669; +} + +.helper-status-badge.locked { + background: #1f2937; + color: #9ca3af; + border: 1px solid #374151; +} + +/* ROI Advisor Dashboard */ +.roi-dashboard { + background: #090e14; + border: 1px solid #1c2b38; + border-radius: 10px; + padding: 12px; + display: flex; + flex-direction: column; + gap: 10px; +} + +.roi-target-highlight { + background: #111e2e; + border: 1px solid #38bdf8; + border-radius: 8px; + padding: 10px; + display: flex; + justify-content: space-between; + align-items: center; +} + +.roi-rank-item { + display: flex; + justify-content: space-between; + align-items: center; + padding: 6px 8px; + border-bottom: 1px solid #16202a; + font-family: var(--font-mono); + font-size: 0.72rem; +} + +.roi-rank-item:last-child { + border-bottom: none; +} + +/* Portfolio Allocation Buttons (10%, 25%, 50%, 100%) */ +.alloc-btn-grid { + display: grid; + grid-template-columns: repeat(4, 1fr); + gap: 6px; + margin-top: 6px; +} + +.btn-alloc { + background: linear-gradient(135deg, #1e293b 0%, #0f172a 100%); + border: 1px solid #334155; + color: #e2e8f0; + font-family: var(--font-mono); + font-size: 0.72rem; + font-weight: 700; + padding: 8px 4px; + border-radius: 6px; + cursor: pointer; + text-align: center; + transition: all 0.15s; +} + +.btn-alloc:hover:not(:disabled) { + border-color: #38bdf8; + color: #38bdf8; + background: #1e3a5f; +} + +.btn-alloc:active:not(:disabled) { + transform: scale(0.95); +} + +.btn-alloc:disabled { + opacity: 0.35; + cursor: not-allowed; +} + /* Main Viewport Content */ #main-viewport { flex: 1; diff --git a/frontend/index.html b/frontend/index.html index 19e7ad8..771440f 100644 --- a/frontend/index.html +++ b/frontend/index.html @@ -111,6 +111,10 @@ Trophies + + + + + + + + `; + } + + // 2. ROI ADVISOR LIVE TABLE (If unlocked) + if (hasRoiAdvisor) { + html += ` +
+
+
+ + LIVE CAPITAL EFFICIENCY RANKINGS (RAW, PROD & SALES) +
+ Score: Δ Rev / Cost +
+
+ `; + + rois.slice(0, 6).forEach((r, idx) => { + const isAffordable = G.cash >= r.unitCost; + html += ` +
+
+ #${idx + 1} + ${r.drugName} ${r.unitName} + (${r.category}) +
+
+ $${fmtNum(r.unitCost)} + +${fmtNum(r.deltaRevenueSec)}/s + +
+
+ `; + }); + + html += ` +
+
+ `; + } + + // 3. CLEVER HELPERS RECRUITMENT ROSTER + html += ` +
+ CLEVER HELPER OPERATIVES & AUTOMATION +
+
+ `; + + CLEVER_HELPERS.forEach(h => { + const isHired = !!(G.helpers && G.helpers[h.id]); + const canAfford = G.cash >= h.cost; + + html += ` +
+
+
+ +
+
${h.name}
+
${h.title}
+
+
+ + ${isHired ? 'HIRED & ACTIVE' : `$${h.cost.toLocaleString()}`} + +
+ +

+ ${h.desc} +

+ +
+ Feature: ${h.feature} + ${!isHired ? ` + + ` : ` + OPERATIONAL ✓ + `} +
+
+ `; + }); + + html += `
`; + dom.viewport.innerHTML = html; +} + // --- OPTIMISTIC CLIENT ACTIONS (Debounced Aggregation & Server Reconciliation) --- function queueAction(action) { // Action aggregation: consolidate rapid repetitive clicks @@ -1037,6 +1313,73 @@ function buySkill(skillId) { } } +function buyHelper(helperId) { + if (!G.helpers) G.helpers = {}; + const helper = CLEVER_HELPERS.find(h => h.id === helperId); + if (!helper || G.helpers[helperId]) return; + + if (G.cash >= helper.cost) { + G.cash -= helper.cost; + G.helpers[helperId] = true; + queueAction({ type: 'buy_helper', helper_id: helperId }); + showToast(`Operative Recruited: ${helper.name}!`); + renderViewport(); + updateLiveInventory(); + } +} + +function quickBuyRoiUnit(drugId, unitType) { + if (unitType === 'suppliers') { + hireSupplier(drugId); + } else if (unitType === 'workers') { + hireWorker(drugId); + } else if (unitType === 'dealers') { + hireDealer(drugId); + } +} + +async function investPortfolioPercent(percent) { + if (!G.helpers || !G.helpers['smart_allocator']) { + return showAlertModal('OPERATIVE REQUIRED', 'The Chief Syndicate Strategist is required to perform portfolio allocations.'); + } + + const budget = G.cash * percent; + if (budget < 1) return; + + const rois = calculateAllUnitRois(); + if (!rois.length) return; + const target = rois[0]; + + const dCfg = DRUG_CATALOG.find(x => x.id === target.drugId); + const data = G.drugs[target.drugId]; + const currentCount = data[target.unitType] || 0; + const baseCost = target.unitType === 'suppliers' ? (dCfg.supplierCost || 75) + : target.unitType === 'workers' ? dCfg.workerCost + : dCfg.dealerCost; + + const maxAffordable = getMaxAffordableUnits(baseCost, currentCount, budget); + if (maxAffordable < 1) { + return showAlertModal('INSUFFICIENT CAPITAL', `Your ${Math.round(percent * 100)}% budget ($${fmtNum(budget)}) is not enough to afford 1 unit of ${target.drugName} ${target.unitName} ($${fmtNum(target.unitCost)}).`); + } + + const totalCost = getGeometricBulkCost(baseCost, currentCount, maxAffordable); + if (G.cash < totalCost) return; + + // Execute purchase optimistically + G.cash -= totalCost; + data[target.unitType] = currentCount + maxAffordable; + + const actType = target.unitType === 'suppliers' ? 'hire_supplier' + : target.unitType === 'workers' ? 'hire_worker' + : 'hire_dealer'; + + queueAction({ type: actType, drug_id: target.drugId, count: maxAffordable }); + showToast(`Strategist Injected $${fmtNum(totalCost)} (${Math.round(percent * 100)}% budget) into +${maxAffordable} ${target.unitName}s!`); + checkAchievements(); + renderViewport(); + updateLiveInventory(); +} + async function launderMoney() { const totalLifetimeBonds = Math.max(G.totalCleanBonds || 0, G.cleanBonds || 0); const bondsToEarn = Math.floor(Math.sqrt(Math.max(0, G.totalEarned) / 25000)); @@ -1393,6 +1736,11 @@ function applyLocalActions(state, actions) { state.offshorePerks[act.perk_id] = curLvl + 1; return; } + if (act.type === 'buy_helper') { + if (!state.helpers) state.helpers = {}; + state.helpers[act.helper_id] = true; + return; + } if (!state.drugs || !state.drugs[dId]) return;