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:
parent
aadc21c8c7
commit
d62f13ee31
5 changed files with 521 additions and 0 deletions
|
|
@ -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))
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -111,6 +111,10 @@
|
|||
<span>Trophies</span>
|
||||
<span id="trophy-badge" class="nav-badge" style="display: none;">0</span>
|
||||
</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">
|
||||
<iconify-icon icon="game-icons:spider-web"></iconify-icon>
|
||||
<span>Network</span>
|
||||
|
|
|
|||
|
|
@ -344,6 +344,37 @@ const SKILL_TREE = [
|
|||
// --- 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];
|
||||
|
||||
// --- 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) ---
|
||||
const OFFSHORE_PERKS = [
|
||||
{
|
||||
|
|
|
|||
|
|
@ -20,6 +20,7 @@ const INITIAL_GAME_STATE = {
|
|||
skills: {},
|
||||
achievements: {}, // { [achId]: claimed_tier_int }
|
||||
offshorePerks: {}, // { [perkId]: level_int }
|
||||
helpers: {}, // { [helperId]: bool }
|
||||
synthesis: {},
|
||||
lastTick: Date.now()
|
||||
};
|
||||
|
|
@ -48,6 +49,10 @@ OFFSHORE_PERKS.forEach(p => {
|
|||
INITIAL_GAME_STATE.offshorePerks[p.id] = 0;
|
||||
});
|
||||
|
||||
CLEVER_HELPERS.forEach(h => {
|
||||
INITIAL_GAME_STATE.helpers[h.id] = false;
|
||||
});
|
||||
|
||||
SYNTHESIS_RECIPES.forEach(r => {
|
||||
INITIAL_GAME_STATE.synthesis[r.id] = 0;
|
||||
});
|
||||
|
|
@ -273,6 +278,108 @@ function getRawPurchaseInfo(drugId) {
|
|||
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 ---
|
||||
function getProductionMultiplier() {
|
||||
let m = 1.0;
|
||||
|
|
@ -375,6 +482,8 @@ function renderViewport() {
|
|||
renderTrophiesView();
|
||||
} else if (G.activeTab === 'tree') {
|
||||
renderSkillTreeView();
|
||||
} else if (G.activeTab === 'helpers') {
|
||||
renderHelpersView();
|
||||
} else if (G.activeTab === 'offshore') {
|
||||
renderOffshoreView();
|
||||
}
|
||||
|
|
@ -840,6 +949,173 @@ function renderOffshoreView() {
|
|||
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) ---
|
||||
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;
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue