feat(helpers): add Clever Helpers system with bulk buying, real-time ROI advisor and 10/25/50/100% smart cash allocation

This commit is contained in:
Christian 2026-10-15 20:39:18 +02:00
parent aadc21c8c7
commit d62f13ee31
5 changed files with 521 additions and 0 deletions

View file

@ -37,6 +37,12 @@ OFFSHORE_PERK_COSTS = {
'compound_interest': [4, 10, 25, 60, 150] '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 = { ACHIEVEMENT_TIER_REQS = {
'dirty_empire': [10000, 250000, 5000000, 100000000, 1000000000], 'dirty_empire': [10000, 250000, 5000000, 100000000, 1000000000],
'master_synthesizer': [100, 1000, 15000, 200000, 2500000], 'master_synthesizer': [100, 1000, 15000, 200000, 2500000],
@ -87,6 +93,7 @@ def get_initial_state() -> dict:
}, },
'achievements': {}, # { ach_id: claimed_tier_int } 'achievements': {}, # { ach_id: claimed_tier_int }
'offshorePerks': {}, # { perk_id: level_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}, 'skills': {s: False for s in SKILL_COSTS},
'synthesis': {r: 0 for r in SYNTHESIS_RECIPES}, 'synthesis': {r: 0 for r in SYNTHESIS_RECIPES},
'drugs': {}, 'drugs': {},
@ -410,6 +417,14 @@ def apply_actions(state: dict, actions: list) -> dict:
state['cleanBonds'] -= cost state['cleanBonds'] -= cost
state['offshorePerks'][perk_id] = current_lvl + 1 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': elif action_type == 'bribe_police':
base_bribe = max(100.0, state['cash'] * 0.15) base_bribe = max(100.0, state['cash'] * 0.15)
judiciary_lvl = state.get('offshorePerks', {}).get('corrupt_judiciary', 0) judiciary_lvl = state.get('offshorePerks', {}).get('corrupt_judiciary', 0)
@ -479,6 +494,7 @@ def sanitize_state(state: dict) -> dict:
state.setdefault('achievements', {}) state.setdefault('achievements', {})
state.setdefault('offshorePerks', {}) state.setdefault('offshorePerks', {})
state.setdefault('helpers', {h: False for h in HELPER_COSTS})
for d_id, data in state.get('drugs', {}).items(): for d_id, data in state.get('drugs', {}).items():
data['raw'] = clean_round(data.get('raw', 0.0)) data['raw'] = clean_round(data.get('raw', 0.0))

View file

@ -467,6 +467,128 @@ html, body {
cursor: not-allowed; 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 Content */
#main-viewport { #main-viewport {
flex: 1; flex: 1;

View file

@ -111,6 +111,10 @@
<span>Trophies</span> <span>Trophies</span>
<span id="trophy-badge" class="nav-badge" style="display: none;">0</span> <span id="trophy-badge" class="nav-badge" style="display: none;">0</span>
</button> </button>
<button class="nav-tab" data-tab="helpers">
<iconify-icon icon="game-icons:artificial-intelligence"></iconify-icon>
<span>Helpers</span>
</button>
<button class="nav-tab" data-tab="tree"> <button class="nav-tab" data-tab="tree">
<iconify-icon icon="game-icons:spider-web"></iconify-icon> <iconify-icon icon="game-icons:spider-web"></iconify-icon>
<span>Network</span> <span>Network</span>

View file

@ -344,6 +344,37 @@ const SKILL_TREE = [
// --- UNIT MILESTONES (1, 5, 10, 25, 50, 100...) --- // --- UNIT MILESTONES (1, 5, 10, 25, 50, 100...) ---
const UNIT_MILESTONES = [1, 5, 10, 25, 50, 100, 200, 300, 400, 500, 750, 1000, 1500, 2000]; const UNIT_MILESTONES = [1, 5, 10, 25, 50, 100, 200, 300, 400, 500, 750, 1000, 1500, 2000];
// --- CLEVER HELPERS (AI Syndicate Advisors & Automated Assistants) ---
const CLEVER_HELPERS = [
{
id: 'bulk_buyer',
name: 'Logistics Quartermaster',
title: 'Bulk Procurement & Multi-Buying',
icon: 'game-icons:cargo-crate',
cost: 2500,
desc: 'Unlocks advanced bulk purchasing pipelines (10x, 100x, Next Milestone, and Max Affordable) across all raw suppliers, lab technicians, and street dealers.',
feature: 'Multi-Unit Bulk Purchases'
},
{
id: 'roi_advisor',
name: 'Cartel Quant / ROI Advisor',
title: 'Real-Time Capital Efficiency Engine',
icon: 'game-icons:progression',
cost: 15000,
desc: 'Calculates marginal cost per revenue gain ($ / $/s) across raw procurement, production facilities, and street distribution to highlight the optimal investment right now.',
feature: 'Live ROI Analytics & Best Target Recommendation'
},
{
id: 'smart_allocator',
name: 'Chief Syndicate Strategist',
title: 'Portfolio Percentage Allocation',
icon: 'game-icons:chess-king',
cost: 75000,
desc: 'Empowers one-click instant investments of 10%, 25%, 50%, or 100% of available cash into the highest-ROI units automatically.',
feature: '10% / 25% / 50% / 100% Cash Portfolio Injection'
}
];
// --- SWISS OFFSHORE PERKS (Permanent upgrades bought with Clean Bonds) --- // --- SWISS OFFSHORE PERKS (Permanent upgrades bought with Clean Bonds) ---
const OFFSHORE_PERKS = [ const OFFSHORE_PERKS = [
{ {

View file

@ -20,6 +20,7 @@ const INITIAL_GAME_STATE = {
skills: {}, skills: {},
achievements: {}, // { [achId]: claimed_tier_int } achievements: {}, // { [achId]: claimed_tier_int }
offshorePerks: {}, // { [perkId]: level_int } offshorePerks: {}, // { [perkId]: level_int }
helpers: {}, // { [helperId]: bool }
synthesis: {}, synthesis: {},
lastTick: Date.now() lastTick: Date.now()
}; };
@ -48,6 +49,10 @@ OFFSHORE_PERKS.forEach(p => {
INITIAL_GAME_STATE.offshorePerks[p.id] = 0; INITIAL_GAME_STATE.offshorePerks[p.id] = 0;
}); });
CLEVER_HELPERS.forEach(h => {
INITIAL_GAME_STATE.helpers[h.id] = false;
});
SYNTHESIS_RECIPES.forEach(r => { SYNTHESIS_RECIPES.forEach(r => {
INITIAL_GAME_STATE.synthesis[r.id] = 0; INITIAL_GAME_STATE.synthesis[r.id] = 0;
}); });
@ -273,6 +278,108 @@ function getRawPurchaseInfo(drugId) {
return { qty, totalCost }; return { qty, totalCost };
} }
// --- CLEVER HELPERS: ROI CALCULATOR (Across Suppliers, Workers, Dealers) ---
function calculateAllUnitRois() {
const list = [];
const prodMulti = getProductionMultiplier();
const sellPriceMulti = getSellPriceMultiplier();
const supMulti = getSupplierSpeedMultiplier();
const workMulti = getWorkerSpeedMultiplier();
const dealMulti = getDealerSpeedMultiplier();
const allTierSupM = getAllTierMilestoneMultiplier('suppliers');
const allTierWorkM = getAllTierMilestoneMultiplier('workers');
const allTierDealM = getAllTierMilestoneMultiplier('dealers');
DRUG_CATALOG.forEach(d => {
const data = G.drugs[d.id];
if (!data || !data.unlocked) return;
const sellPrice = Math.floor(d.sellPrice * sellPriceMulti);
// 1. RAW SUPPLIER ROI
const supCount = data.suppliers || 0;
const supInfo = getUnitPurchaseInfo(d.id, 'suppliers');
const nextSupCount = supCount + 1;
const curSupM = getUnitMilestoneMultiplier(supCount);
const nextSupM = getUnitMilestoneMultiplier(nextSupCount);
const baseSupplyRate = d.supplyRate || 1.0;
const curSupplyRate = supCount * baseSupplyRate * prodMulti * supMulti * curSupM * allTierSupM;
const newSupplyRate = nextSupCount * baseSupplyRate * prodMulti * supMulti * nextSupM * allTierSupM;
const deltaSupply = Math.max(0.01, newSupplyRate - curSupplyRate);
// Revenue gain: Raw converts to product and sells for sellPrice
const supRevGainPerSec = deltaSupply * sellPrice;
const supCost = getGeometricBulkCost(d.supplierCost || 75, supCount, 1);
const supScore = supRevGainPerSec / Math.max(1, supCost);
list.push({
drugId: d.id,
drugName: d.name,
unitType: 'suppliers',
unitName: d.supplierName,
category: 'Raw Supply',
currentCount: supCount,
unitCost: supCost,
deltaRevenueSec: supRevGainPerSec,
score: supScore,
desc: `+${fmtNum(deltaSupply)} r/s ➔ +$${fmtNum(supRevGainPerSec)}/s value`
});
// 2. WORKER / PRODUCTION ROI
const workCount = data.workers || 0;
const nextWorkCount = workCount + 1;
const curWorkM = getUnitMilestoneMultiplier(workCount);
const nextWorkM = getUnitMilestoneMultiplier(nextWorkCount);
const curProdRate = workCount * d.produceRate * prodMulti * workMulti * curWorkM * allTierWorkM;
const newProdRate = nextWorkCount * d.produceRate * prodMulti * workMulti * nextWorkM * allTierWorkM;
const deltaProd = Math.max(0.01, newProdRate - curProdRate);
const workRevGainPerSec = deltaProd * sellPrice;
const workCost = getGeometricBulkCost(d.workerCost, workCount, 1);
const workScore = workRevGainPerSec / Math.max(1, workCost);
list.push({
drugId: d.id,
drugName: d.name,
unitType: 'workers',
unitName: d.workerName,
category: 'Lab Production',
currentCount: workCount,
unitCost: workCost,
deltaRevenueSec: workRevGainPerSec,
score: workScore,
desc: `+${fmtNum(deltaProd)}g/s ➔ +$${fmtNum(workRevGainPerSec)}/s yield`
});
// 3. DEALER / DISTRIBUTION ROI
const dealCount = data.dealers || 0;
const nextDealCount = dealCount + 1;
const curDealM = getUnitMilestoneMultiplier(dealCount);
const nextDealM = getUnitMilestoneMultiplier(nextDealCount);
const curDealRate = dealCount * d.sellRate * dealMulti * curDealM * allTierDealM;
const newDealRate = nextDealCount * d.sellRate * dealMulti * nextDealM * allTierDealM;
const deltaDeal = Math.max(0.01, newDealRate - curDealRate);
const dealRevGainPerSec = deltaDeal * sellPrice;
const dealCost = getGeometricBulkCost(d.dealerCost, dealCount, 1);
const dealScore = dealRevGainPerSec / Math.max(1, dealCost);
list.push({
drugId: d.id,
drugName: d.name,
unitType: 'dealers',
unitName: d.dealerName,
category: 'Street Distribution',
currentCount: dealCount,
unitCost: dealCost,
deltaRevenueSec: dealRevGainPerSec,
score: dealScore,
desc: `+${fmtNum(deltaDeal)}g/s ➔ +$${fmtNum(dealRevGainPerSec)}/s cashflow`
});
});
list.sort((a, b) => b.score - a.score);
return list;
}
// --- MULTIPLIERS & BONUSES --- // --- MULTIPLIERS & BONUSES ---
function getProductionMultiplier() { function getProductionMultiplier() {
let m = 1.0; let m = 1.0;
@ -375,6 +482,8 @@ function renderViewport() {
renderTrophiesView(); renderTrophiesView();
} else if (G.activeTab === 'tree') { } else if (G.activeTab === 'tree') {
renderSkillTreeView(); renderSkillTreeView();
} else if (G.activeTab === 'helpers') {
renderHelpersView();
} else if (G.activeTab === 'offshore') { } else if (G.activeTab === 'offshore') {
renderOffshoreView(); renderOffshoreView();
} }
@ -840,6 +949,173 @@ function renderOffshoreView() {
dom.viewport.innerHTML = html; dom.viewport.innerHTML = html;
} }
// 5. CLEVER HELPERS VIEW (AI Advisors & Automation)
function renderHelpersView() {
const hasBulkBuyer = !!(G.helpers && G.helpers['bulk_buyer']);
const hasRoiAdvisor = !!(G.helpers && G.helpers['roi_advisor']);
const hasSmartAlloc = !!(G.helpers && G.helpers['smart_allocator']);
const rois = hasRoiAdvisor ? calculateAllUnitRois() : [];
const bestTarget = rois.length > 0 ? rois[0] : null;
let html = `
<div style="font-family: var(--font-mono); font-size: 0.72rem; color: var(--text-secondary); margin-bottom: 6px; display: flex; justify-content: space-between; align-items: center;">
<span>SYNDICATE CLEVER HELPERS & ADVISORS</span>
<span style="color: var(--brand-gold);">VAULT: $${fmtNum(G.cash)}</span>
</div>
`;
// 1. SMART ALLOCATOR LIVE DASHBOARD (If unlocked)
if (hasSmartAlloc && bestTarget) {
html += `
<div class="card" style="border: 2px solid var(--neon-cyan); background: #0c141d;">
<div class="card-header" style="margin-bottom: 6px;">
<div class="card-title-group">
<div class="drug-icon-box" style="border-color: var(--neon-cyan); color: var(--neon-cyan);">
<iconify-icon icon="game-icons:chess-king"></iconify-icon>
</div>
<div>
<div class="card-title" style="color: var(--neon-cyan);">CHIEF STRATEGIST DASHBOARD</div>
<div style="font-size: 0.68rem; color: var(--text-secondary); font-family: var(--font-mono);">
Smart Portfolio Capital Allocation
</div>
</div>
</div>
<span class="helper-status-badge active">ONLINE</span>
</div>
<div class="roi-target-highlight">
<div>
<div style="font-family: var(--font-mono); font-size: 0.7rem; color: var(--text-secondary);">TOP ROI INVESTMENT:</div>
<div style="font-weight: 800; font-size: 0.92rem; color: #fff;">
${bestTarget.drugName} • ${bestTarget.unitName}
</div>
<div style="font-family: var(--font-mono); font-size: 0.68rem; color: #38bdf8;">
${bestTarget.category} • Cost: $${fmtNum(bestTarget.unitCost)} / unit
</div>
</div>
<div style="text-align: right; font-family: var(--font-mono);">
<div style="font-size: 0.65rem; color: var(--text-muted);">MARGINAL IMPACT:</div>
<div style="font-size: 0.8rem; font-weight: bold; color: #22c55e;">${bestTarget.desc}</div>
</div>
</div>
<div style="margin-top: 6px;">
<div style="font-family: var(--font-mono); font-size: 0.68rem; color: var(--text-muted); margin-bottom: 4px;">
INJECT CASH PORTFOLIO INTO TOP ROI UNIT:
</div>
<div class="alloc-btn-grid">
<button class="btn-alloc" onclick="investPortfolioPercent(0.10)" ${G.cash < bestTarget.unitCost ? 'disabled' : ''}>
Buy 10% Cash
<div style="font-size: 0.62rem; color: var(--text-secondary);">$${fmtNum(G.cash * 0.10)}</div>
</button>
<button class="btn-alloc" onclick="investPortfolioPercent(0.25)" ${G.cash < bestTarget.unitCost ? 'disabled' : ''}>
Buy 25% Cash
<div style="font-size: 0.62rem; color: var(--text-secondary);">$${fmtNum(G.cash * 0.25)}</div>
</button>
<button class="btn-alloc" onclick="investPortfolioPercent(0.50)" ${G.cash < bestTarget.unitCost ? 'disabled' : ''}>
Buy 50% Cash
<div style="font-size: 0.62rem; color: var(--text-secondary);">$${fmtNum(G.cash * 0.50)}</div>
</button>
<button class="btn-alloc" style="border-color: #38bdf8; color: #38bdf8;" onclick="investPortfolioPercent(1.0)" ${G.cash < bestTarget.unitCost ? 'disabled' : ''}>
Buy 100% Cash
<div style="font-size: 0.62rem; color: var(--text-secondary);">$${fmtNum(G.cash)}</div>
</button>
</div>
</div>
</div>
`;
}
// 2. ROI ADVISOR LIVE TABLE (If unlocked)
if (hasRoiAdvisor) {
html += `
<div class="roi-dashboard">
<div style="display: flex; justify-content: space-between; align-items: center;">
<div style="font-family: var(--font-mono); font-size: 0.75rem; font-weight: bold; color: var(--brand-gold); display: flex; align-items: center; gap: 6px;">
<iconify-icon icon="game-icons:progression"></iconify-icon>
LIVE CAPITAL EFFICIENCY RANKINGS (RAW, PROD & SALES)
</div>
<span style="font-family: var(--font-mono); font-size: 0.68rem; color: var(--text-secondary);">Score: Δ Rev / Cost</span>
</div>
<div style="display: flex; flex-direction: column; gap: 4px;">
`;
rois.slice(0, 6).forEach((r, idx) => {
const isAffordable = G.cash >= r.unitCost;
html += `
<div class="roi-rank-item" style="background: ${idx === 0 ? '#101a24' : 'transparent'}; border-radius: 6px;">
<div>
<span style="color: ${idx === 0 ? 'var(--brand-gold)' : 'var(--text-muted)'}; font-weight: bold; margin-right: 6px;">#${idx + 1}</span>
<span style="font-weight: 600; color: #fff;">${r.drugName} ${r.unitName}</span>
<span style="color: var(--text-muted); font-size: 0.65rem;">(${r.category})</span>
</div>
<div style="display: flex; align-items: center; gap: 10px;">
<span style="color: ${isAffordable ? '#4ade80' : '#8b949e'};">$${fmtNum(r.unitCost)}</span>
<span style="color: #38bdf8; font-size: 0.68rem;">+${fmtNum(r.deltaRevenueSec)}/s</span>
<button class="btn-tactical" style="padding: 4px 8px; font-size: 0.65rem;" onclick="quickBuyRoiUnit('${r.drugId}', '${r.unitType}')" ${!isAffordable ? 'disabled' : ''}>
Buy +1
</button>
</div>
</div>
`;
});
html += `
</div>
</div>
`;
}
// 3. CLEVER HELPERS RECRUITMENT ROSTER
html += `
<div style="font-family: var(--font-mono); font-size: 0.72rem; color: var(--text-secondary); margin: 12px 0 6px;">
CLEVER HELPER OPERATIVES & AUTOMATION
</div>
<div class="helper-grid">
`;
CLEVER_HELPERS.forEach(h => {
const isHired = !!(G.helpers && G.helpers[h.id]);
const canAfford = G.cash >= h.cost;
html += `
<div class="helper-card ${isHired ? 'unlocked' : ''}">
<div class="helper-card-header">
<div style="display: flex; align-items: center; gap: 10px;">
<iconify-icon icon="${h.icon}" style="font-size: 1.8rem; color: ${isHired ? 'var(--neon-cyan)' : 'var(--brand-gold)'};"></iconify-icon>
<div>
<div style="font-weight: bold; font-size: 0.88rem; color: #fff;">${h.name}</div>
<div style="font-size: 0.68rem; color: var(--neon-cyan); font-family: var(--font-mono);">${h.title}</div>
</div>
</div>
<span class="helper-status-badge ${isHired ? 'active' : 'locked'}">
${isHired ? 'HIRED & ACTIVE' : `$${h.cost.toLocaleString()}`}
</span>
</div>
<p style="font-size: 0.74rem; color: var(--text-secondary); line-height: 1.4; margin: 0;">
${h.desc}
</p>
<div style="display: flex; justify-content: space-between; align-items: center; background: #080c10; padding: 8px 10px; border-radius: 6px; font-family: var(--font-mono); font-size: 0.7rem;">
<span style="color: #94a3b8;">Feature: <span style="color: #fff;">${h.feature}</span></span>
${!isHired ? `
<button class="btn-tactical btn-sell-action" style="padding: 6px 14px;" onclick="buyHelper('${h.id}')" ${!canAfford ? 'disabled' : ''}>
Hire Operative ($${h.cost.toLocaleString()})
</button>
` : `
<span style="color: #22c55e; font-weight: bold;">OPERATIONAL ✓</span>
`}
</div>
</div>
`;
});
html += `</div>`;
dom.viewport.innerHTML = html;
}
// --- OPTIMISTIC CLIENT ACTIONS (Debounced Aggregation & Server Reconciliation) --- // --- OPTIMISTIC CLIENT ACTIONS (Debounced Aggregation & Server Reconciliation) ---
function queueAction(action) { function queueAction(action) {
// Action aggregation: consolidate rapid repetitive clicks // 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() { async function launderMoney() {
const totalLifetimeBonds = Math.max(G.totalCleanBonds || 0, G.cleanBonds || 0); const totalLifetimeBonds = Math.max(G.totalCleanBonds || 0, G.cleanBonds || 0);
const bondsToEarn = Math.floor(Math.sqrt(Math.max(0, G.totalEarned) / 25000)); 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; state.offshorePerks[act.perk_id] = curLvl + 1;
return; 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; if (!state.drugs || !state.drugs[dId]) return;