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]
|
'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))
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
|
||||||
|
|
@ -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 = [
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue