Implement Corporate Reputation allocation hub across 3 strategic branches and Boardroom brand legacy perk

This commit is contained in:
Christian 2026-10-16 21:31:02 +02:00
parent 221ed97415
commit 8aa76c805a
12 changed files with 757 additions and 27 deletions

View file

@ -22,7 +22,8 @@ BOARDROOM_PERK_COSTS = {
'robotics_overclock': [2, 5, 12, 30, 75], 'robotics_overclock': [2, 5, 12, 30, 75],
'logistics_efficiency': [2, 6, 15, 40, 100], 'logistics_efficiency': [2, 6, 15, 40, 100],
'government_subsidies': [3, 8, 20, 55, 120], 'government_subsidies': [3, 8, 20, 55, 120],
'angel_syndicate': [5, 15, 45, 120, 300] 'angel_syndicate': [5, 15, 45, 120, 300],
'brand_legacy': [4, 10, 25, 70, 150]
} }
def clean_round(val: float) -> float: def clean_round(val: float) -> float:
@ -45,6 +46,7 @@ def get_initial_state() -> dict:
'totalVentureShares': 0, 'totalVentureShares': 0,
'ipoCount': 0, 'ipoCount': 0,
'reputation': 0, 'reputation': 0,
'repAllocations': {'b2b': 0, 'clients': 0, 'brand': 0},
'buyMode': '1', 'buyMode': '1',
'activeTab': 'production', 'activeTab': 'production',
'selectedSector': 'all', 'selectedSector': 'all',
@ -95,9 +97,15 @@ def process_offline_and_ticks(state: dict) -> dict:
return state return state
perks = state.get('boardroomPerks', {}) perks = state.get('boardroomPerks', {})
supply_yield_mult = 1.0 + 0.15 * perks.get('supply_sync', 0) rep_alloc = state.get('repAllocations', {}) if isinstance(state.get('repAllocations'), dict) else {}
robotics_speed_mult = 1.0 + 0.20 * perks.get('robotics_overclock', 0) brand_rep = rep_alloc.get('brand', 0)
logistics_price_mult = 1.0 + 0.25 * perks.get('logistics_efficiency', 0) clients_rep = rep_alloc.get('clients', 0)
# Multipliers
supply_yield_mult = (1.0 + 0.15 * perks.get('supply_sync', 0)) * (1.0 + 0.002 * clients_rep)
robotics_speed_mult = (1.0 + 0.20 * perks.get('robotics_overclock', 0)) * (1.0 + 0.0015 * clients_rep)
# Brand Premium: +0.25% wholesale price per point
logistics_price_mult = (1.0 + 0.25 * perks.get('logistics_efficiency', 0)) * (1.0 + 0.0025 * brand_rep)
# Simulate in reasonable sub-steps for chained DAG dependency # Simulate in reasonable sub-steps for chained DAG dependency
sub_steps = max(1, min(int(elapsed), 120)) sub_steps = max(1, min(int(elapsed), 120))
@ -225,7 +233,9 @@ def apply_actions(state: dict, actions: list) -> dict:
if pid in PRODUCT_CONFIGS and pid in state.get('products', {}): if pid in PRODUCT_CONFIGS and pid in state.get('products', {}):
p_cfg = PRODUCT_CONFIGS[pid] p_cfg = PRODUCT_CONFIGS[pid]
current_lines = state['products'][pid].get('lines', 0) current_lines = state['products'][pid].get('lines', 0)
cost = p_cfg['baseFacilityCost'] * math.pow(1.15, current_lines) client_rep = state.get('repAllocations', {}).get('clients', 0) if isinstance(state.get('repAllocations'), dict) else 0
line_discount = min(0.50, 0.002 * client_rep)
cost = p_cfg['baseFacilityCost'] * math.pow(1.15, current_lines) * (1.0 - line_discount)
if state.get('cash', 0.0) >= cost: if state.get('cash', 0.0) >= cost:
state['cash'] -= cost state['cash'] -= cost
state['products'][pid]['lines'] = current_lines + 1 state['products'][pid]['lines'] = current_lines + 1
@ -278,13 +288,30 @@ def apply_actions(state: dict, actions: list) -> dict:
preserved_total_shares = state.get('totalVentureShares', 0) + earned preserved_total_shares = state.get('totalVentureShares', 0) + earned
preserved_perks = state.get('boardroomPerks', {}) preserved_perks = state.get('boardroomPerks', {})
preserved_ipos = state.get('ipoCount', 0) + 1 preserved_ipos = state.get('ipoCount', 0) + 1
# Calculate preserved reputation from brand_legacy perk (10% to 50%)
brand_lvl = preserved_perks.get('brand_legacy', 0)
rep_retention_ratios = [0.0, 0.10, 0.20, 0.30, 0.40, 0.50]
ratio = rep_retention_ratios[min(brand_lvl, len(rep_retention_ratios) - 1)]
total_current_rep = state.get('reputation', 0)
allocs = state.get('repAllocations', {})
total_rep_accumulated = total_current_rep + sum(allocs.values()) if isinstance(allocs, dict) else total_current_rep
preserved_rep = int(math.floor(total_rep_accumulated * ratio))
# Starting cash from angel_syndicate
angel_lvl = preserved_perks.get('angel_syndicate', 0)
starting_cash_bonuses = [30.0, 500.0, 5000.0, 50000.0, 500000.0, 5000000.0]
new_starting_cash = starting_cash_bonuses[min(angel_lvl, len(starting_cash_bonuses) - 1)]
# Reset to clean initial state # Reset to clean initial state
state = get_initial_state() state = get_initial_state()
state['cash'] = new_starting_cash
state['ventureShares'] = preserved_shares state['ventureShares'] = preserved_shares
state['totalVentureShares'] = preserved_total_shares state['totalVentureShares'] = preserved_total_shares
state['boardroomPerks'] = preserved_perks state['boardroomPerks'] = preserved_perks
state['ipoCount'] = preserved_ipos state['ipoCount'] = preserved_ipos
state['reputation'] = preserved_rep
state['repAllocations'] = {'b2b': 0, 'clients': 0, 'brand': 0}
elif atype == 'BUY_PERK': elif atype == 'BUY_PERK':
perk_id = act.get('perkId') perk_id = act.get('perkId')
@ -297,12 +324,51 @@ def apply_actions(state: dict, actions: list) -> dict:
state['ventureShares'] -= req_shares state['ventureShares'] -= req_shares
state['boardroomPerks'][perk_id] = current_lvl + 1 state['boardroomPerks'][perk_id] = current_lvl + 1
elif atype == 'ALLOCATE_REP':
dept = act.get('department')
delta = act.get('delta', 0)
if 'repAllocations' not in state or not isinstance(state['repAllocations'], dict):
state['repAllocations'] = {'b2b': 0, 'clients': 0, 'brand': 0}
allocs = state['repAllocations']
if delta == 'reset':
# Return all points to available pool
freed = sum(allocs.values())
state['reputation'] = state.get('reputation', 0) + freed
state['repAllocations'] = {'b2b': 0, 'clients': 0, 'brand': 0}
elif dept in ('b2b', 'clients', 'brand'):
try:
delta_int = int(delta)
if delta_int > 0:
# Allocating points from pool
to_alloc = min(delta_int, state.get('reputation', 0))
if to_alloc > 0:
state['reputation'] -= to_alloc
allocs[dept] = allocs.get(dept, 0) + to_alloc
elif delta_int < 0:
# De-allocating points back to pool
to_free = min(abs(delta_int), allocs.get(dept, 0))
if to_free > 0:
allocs[dept] -= to_free
state['reputation'] = state.get('reputation', 0) + to_free
except (ValueError, TypeError):
pass
return state return state
def sanitize_state(raw: dict) -> dict: def sanitize_state(raw: dict) -> dict:
"""Ensures incoming state doesn't have NaN or negative numbers and dynamically populates new products.""" """Ensures incoming state doesn't have NaN or negative numbers and dynamically populates new products."""
if not isinstance(raw, dict): if not isinstance(raw, dict):
return get_initial_state() return get_initial_state()
if 'repAllocations' not in raw or not isinstance(raw['repAllocations'], dict):
raw['repAllocations'] = {'b2b': 0, 'clients': 0, 'brand': 0}
else:
for k in ('b2b', 'clients', 'brand'):
if k not in raw['repAllocations'] or not isinstance(raw['repAllocations'][k], (int, float)):
raw['repAllocations'][k] = 0
else:
raw['repAllocations'][k] = max(0, int(raw['repAllocations'][k]))
# Dynamic re-hydration: Ensure any new products added to the catalog exist in the player's saved state # Dynamic re-hydration: Ensure any new products added to the catalog exist in the player's saved state
if 'products' not in raw or not isinstance(raw['products'], dict): if 'products' not in raw or not isinstance(raw['products'], dict):

View file

@ -156,6 +156,50 @@ def test_production_target_throttle():
assert state['products']['wheat']['stock'] == stock_before # Zero production when throttled to 0% assert state['products']['wheat']['stock'] == stock_before # Zero production when throttled to 0%
print(f"PASS: test_production_target_throttle (50%: {produced_at_50}, 100%: {produced_at_100})") print(f"PASS: test_production_target_throttle (50%: {produced_at_50}, 100%: {produced_at_100})")
def test_reputation_allocation_and_retention():
state = engine.get_initial_state()
state['reputation'] = 100
# Allocate 30 to B2B, 40 to clients, 20 to brand
actions = [
{'type': 'ALLOCATE_REP', 'department': 'b2b', 'delta': 30},
{'type': 'ALLOCATE_REP', 'department': 'clients', 'delta': 40},
{'type': 'ALLOCATE_REP', 'department': 'brand', 'delta': 20}
]
state = engine.apply_actions(state, actions)
assert state['reputation'] == 10
assert state['repAllocations']['b2b'] == 30
assert state['repAllocations']['clients'] == 40
assert state['repAllocations']['brand'] == 20
# Test Titan discount on lines (40 * 0.2% = 8% discount)
p_cfg = engine.PRODUCT_CONFIGS['flour']
cost_base = p_cfg['baseFacilityCost']
expected_cost = cost_base * 0.92
state['cash'] = expected_cost + 10.0
cash_before = state['cash']
state = engine.apply_actions(state, [{'type': 'BUY_LINES', 'productId': 'flour'}])
assert abs(state['cash'] - (cash_before - expected_cost)) < 0.01
# Reset test
state = engine.apply_actions(state, [{'type': 'ALLOCATE_REP', 'delta': 'reset'}])
assert state['reputation'] == 100
assert state['repAllocations']['b2b'] == 0
assert state['repAllocations']['clients'] == 0
assert state['repAllocations']['brand'] == 0
# IPO without brand_legacy perk -> 0 rep preserved
state['lifetimeRevenue'] = 400000.0
state_no_perk = engine.apply_actions(dict(state), [{'type': 'TRIGGER_IPO'}])
assert state_no_perk['reputation'] == 0
# IPO with brand_legacy level 3 -> 30% rep preserved (30 Rep)
state['boardroomPerks']['brand_legacy'] = 3
state_with_perk = engine.apply_actions(dict(state), [{'type': 'TRIGGER_IPO'}])
assert state_with_perk['reputation'] == 30
assert state_with_perk['repAllocations'] == {'b2b': 0, 'clients': 0, 'brand': 0}
print("PASS: test_reputation_allocation_and_retention")
if __name__ == '__main__': if __name__ == '__main__':
test_initial_state() test_initial_state()
test_raw_production_and_logistics() test_raw_production_and_logistics()
@ -163,5 +207,6 @@ if __name__ == '__main__':
test_chained_microchips_and_boards() test_chained_microchips_and_boards()
test_policy_toggles() test_policy_toggles()
test_production_target_throttle() test_production_target_throttle()
test_reputation_allocation_and_retention()
test_ipo_and_shares() test_ipo_and_shares()
print("\nALL BACKEND ENGINE TESTS PASSED SUCCESSFULLY!") print("\nALL BACKEND ENGINE TESTS PASSED SUCCESSFULLY!")

View file

@ -182,3 +182,306 @@
color: var(--emerald-dark); color: var(--emerald-dark);
} }
/* ==========================================================================
CORPORATE REPUTATION ALLOCATION HUB
========================================================================== */
.rep-hub-container {
background: linear-gradient(135deg, var(--bg-surface) 0%, var(--bg-surface-sub) 100%);
border: 1px solid var(--border-color);
border-radius: var(--radius-lg);
padding: 20px;
display: flex;
flex-direction: column;
gap: 16px;
box-shadow: var(--shadow-sm);
margin-bottom: 8px;
}
.rep-hub-header {
display: flex;
justify-content: space-between;
align-items: center;
gap: 16px;
flex-wrap: wrap;
border-bottom: 1px solid var(--border-color);
padding-bottom: 16px;
}
.rep-hub-title-group {
display: flex;
align-items: center;
gap: 14px;
}
.rep-hub-icon {
width: 44px;
height: 44px;
border-radius: var(--radius-md);
background: rgba(245, 158, 11, 0.15);
color: #f59e0b;
display: flex;
align-items: center;
justify-content: center;
font-size: 1.5rem;
flex-shrink: 0;
}
.rep-hub-title {
font-size: 1.15rem;
font-weight: 800;
color: var(--text-primary);
margin: 0;
}
.rep-hub-desc {
font-size: 0.8rem;
color: var(--text-secondary);
margin-top: 3px;
max-width: 620px;
}
.rep-pool-card {
display: flex;
align-items: center;
gap: 18px;
background: var(--bg-surface);
border: 1px solid var(--border-color);
padding: 10px 16px;
border-radius: var(--radius-md);
}
.rep-pool-info {
display: flex;
flex-direction: column;
}
.rep-pool-label {
font-size: 0.68rem;
font-weight: 700;
text-transform: uppercase;
letter-spacing: 0.05em;
color: var(--text-muted);
}
.rep-pool-val {
font-size: 1.45rem;
font-weight: 900;
color: #f59e0b;
}
.rep-pool-meta {
display: flex;
flex-direction: column;
gap: 6px;
font-size: 0.72rem;
color: var(--text-secondary);
border-left: 1px solid var(--border-color);
padding-left: 14px;
}
.btn-rep-reset {
display: inline-flex;
align-items: center;
gap: 5px;
background: transparent;
border: 1px solid var(--border-color);
border-radius: var(--radius-sm);
padding: 3px 8px;
font-size: 0.72rem;
font-weight: 700;
color: var(--text-secondary);
cursor: pointer;
transition: all 0.15s ease;
}
.btn-rep-reset:hover:not(:disabled) {
background: rgba(239, 68, 68, 0.1);
border-color: #ef4444;
color: #ef4444;
}
.btn-rep-reset:disabled {
opacity: 0.4;
cursor: not-allowed;
}
/* Models Grid */
.rep-models-grid {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 16px;
}
@media (max-width: 1024px) {
.rep-models-grid {
grid-template-columns: 1fr;
}
}
.rep-model-card {
background: var(--bg-surface);
border: 1px solid var(--border-color);
border-radius: var(--radius-md);
padding: 16px;
display: flex;
flex-direction: column;
justify-content: space-between;
gap: 12px;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.04);
}
.rep-model-head {
display: flex;
align-items: center;
gap: 10px;
}
.rep-model-icon {
width: 34px;
height: 34px;
border-radius: var(--radius-sm);
display: flex;
align-items: center;
justify-content: center;
font-size: 1.15rem;
flex-shrink: 0;
}
.rep-icon-b2b {
background: rgba(37, 99, 235, 0.12);
color: #2563eb;
}
.rep-icon-clients {
background: rgba(147, 51, 234, 0.12);
color: #9333ea;
}
.rep-icon-brand {
background: rgba(16, 185, 129, 0.12);
color: #10b981;
}
.rep-model-meta {
flex: 1;
}
.rep-model-meta h4 {
font-size: 0.88rem;
font-weight: 800;
color: var(--text-primary);
margin: 0;
}
.rep-model-sub {
font-size: 0.68rem;
color: var(--text-muted);
}
.rep-allocated-badge {
background: var(--bg-surface-sub);
border: 1px solid var(--border-color);
padding: 4px 8px;
border-radius: var(--radius-sm);
font-size: 0.78rem;
font-weight: 800;
color: #f59e0b;
}
.rep-model-body {
display: flex;
flex-direction: column;
gap: 10px;
flex: 1;
}
.rep-benefit-desc {
font-size: 0.76rem;
color: var(--text-secondary);
line-height: 1.4;
margin: 0;
}
.rep-bonus-stat {
display: flex;
justify-content: space-between;
align-items: center;
background: var(--bg-surface-sub);
padding: 8px 12px;
border-radius: var(--radius-sm);
font-size: 0.78rem;
border: 1px solid var(--border-color);
}
.rep-bonus-multi-grid {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 6px;
}
.rep-multi-item {
background: var(--bg-surface-sub);
padding: 6px 8px;
border-radius: var(--radius-sm);
font-size: 0.72rem;
display: flex;
flex-direction: column;
gap: 2px;
border: 1px solid var(--border-color);
}
.client-name-tag {
font-size: 0.65rem;
font-weight: 700;
}
.text-amber { color: #f59e0b; }
.text-purple { color: #a855f7; }
.text-cyan { color: #06b6d4; }
.rep-model-actions {
display: flex;
justify-content: flex-end;
border-top: 1px solid var(--border-color);
padding-top: 10px;
}
.rep-btn-group {
display: flex;
align-items: center;
gap: 4px;
width: 100%;
}
.btn-rep-ctrl {
flex: 1;
height: 30px;
background: var(--bg-surface-sub);
border: 1px solid var(--border-color);
border-radius: var(--radius-sm);
font-size: 0.76rem;
font-weight: 800;
color: var(--text-primary);
cursor: pointer;
transition: all 0.15s ease;
}
.btn-rep-ctrl:hover:not(:disabled) {
background: var(--primary-blue);
color: #fff;
border-color: var(--primary-blue);
}
.btn-rep-sub:hover:not(:disabled) {
background: rgba(239, 68, 68, 0.15);
color: #ef4444;
border-color: #ef4444;
}
.btn-rep-ctrl:disabled {
opacity: 0.35;
cursor: not-allowed;
}

View file

@ -430,7 +430,9 @@ class ProductionApp {
if (pState && pState.stock >= c.targetAmount) { if (pState && pState.stock >= c.targetAmount) {
pState.stock -= c.targetAmount; pState.stock -= c.targetAmount;
const subsidyBonus = 1.0 + 0.35 * (state.boardroomPerks['government_subsidies'] || 0); const subsidyBonus = 1.0 + 0.35 * (state.boardroomPerks['government_subsidies'] || 0);
const totalPayout = c.rewardCash * subsidyBonus; const b2bRep = state.repAllocations?.b2b || 0;
const b2bBonus = 1.0 + 0.005 * b2bRep; // +0.5% per point
const totalPayout = c.rewardCash * subsidyBonus * b2bBonus;
state.cash += totalPayout; state.cash += totalPayout;
state.lifetimeRevenue += totalPayout; state.lifetimeRevenue += totalPayout;
state.reputation = (state.reputation || 0) + c.rewardRep; state.reputation = (state.reputation || 0) + c.rewardRep;
@ -455,6 +457,46 @@ class ProductionApp {
break; break;
} }
case 'ALLOCATE_REP': {
if (!state.repAllocations) {
state.repAllocations = { b2b: 0, clients: 0, brand: 0 };
}
const allocs = state.repAllocations;
const dept = action.department;
const delta = action.delta;
if (delta === 'reset') {
const freed = (allocs.b2b || 0) + (allocs.clients || 0) + (allocs.brand || 0);
state.reputation = (state.reputation || 0) + freed;
state.repAllocations = { b2b: 0, clients: 0, brand: 0 };
this.showToast(`All Corporate Rep Points Recalled to Reserve Pool!`, 'info');
this.sync.queueAction(action);
this.mountActiveView();
} else if (dept && allocs[dept] !== undefined) {
const deltaInt = parseInt(delta, 10);
if (deltaInt > 0) {
const toAlloc = Math.min(deltaInt, state.reputation || 0);
if (toAlloc > 0) {
state.reputation -= toAlloc;
allocs[dept] += toAlloc;
this.showToast(`Allocated +${toAlloc} Rep to ${dept.toUpperCase()}!`, 'success');
this.sync.queueAction(action);
this.mountActiveView();
}
} else if (deltaInt < 0) {
const toFree = Math.min(Math.abs(deltaInt), allocs[dept] || 0);
if (toFree > 0) {
allocs[dept] -= toFree;
state.reputation = (state.reputation || 0) + toFree;
this.showToast(`Recalled -${toFree} Rep from ${dept.toUpperCase()}!`, 'info');
this.sync.queueAction(action);
this.mountActiveView();
}
}
}
break;
}
case 'TRIGGER_IPO': { case 'TRIGGER_IPO': {
this.showConfirmModal({ this.showConfirmModal({
title: 'Authorize Initial Public Offering (IPO)', title: 'Authorize Initial Public Offering (IPO)',

View file

@ -29,11 +29,19 @@ export class SimulationEngine {
if (dt <= 0) return; if (dt <= 0) return;
const state = this.sm.state; const state = this.sm.state;
const perks = state.boardroomPerks || {}; const perks = state.boardroomPerks || {};
const repAlloc = state.repAllocations || {};
const brandRep = repAlloc.brand || 0;
const clientsRep = repAlloc.clients || 0;
// Boardroom Multipliers // Boardroom & Corporate Reputation Multipliers
const supplyYieldMult = 1.0 + 0.15 * (perks['supply_sync'] || 0); // Apex Synergy: +0.2% raw yield per clients rep point
const roboticsSpeedMult = 1.0 + 0.20 * (perks['robotics_overclock'] || 0); const supplyYieldMult = (1.0 + 0.15 * (perks['supply_sync'] || 0)) * (1.0 + 0.002 * clientsRep);
const logisticsPriceMult = 1.0 + 0.25 * (perks['logistics_efficiency'] || 0); // Omni Dynamics: +0.15% cycle speed per clients rep point
const roboticsSpeedMult = (1.0 + 0.20 * (perks['robotics_overclock'] || 0)) * (1.0 + 0.0015 * clientsRep);
// Brand Premium: +0.25% wholesale sales price per brand rep point
const logisticsPriceMult = (1.0 + 0.25 * (perks['logistics_efficiency'] || 0)) * (1.0 + 0.0025 * brandRep);
// Vanguard Shipping: +0.25% logistics throughput capacity per clients rep point
const logisticsThroughputMult = 1.0 + 0.0025 * clientsRep;
let tickRevenue = 0.0; let tickRevenue = 0.0;
@ -153,8 +161,8 @@ export class SimulationEngine {
const availableToSell = Math.max(0, pState.stock - reservedStock); const availableToSell = Math.max(0, pState.stock - reservedStock);
if (availableToSell > 0) { if (availableToSell > 0) {
// Logistics fleet capacity: 0.75 items/sec per unit // Logistics fleet capacity: 0.75 items/sec per unit * Vanguard client synergy
const maxSellCapacity = logisticsUnits * 0.75 * dt; const maxSellCapacity = logisticsUnits * 0.75 * logisticsThroughputMult * dt;
const sold = Math.min(availableToSell, maxSellCapacity); const sold = Math.min(availableToSell, maxSellCapacity);
pState.stock -= sold; pState.stock -= sold;

View file

@ -18,6 +18,11 @@ export function getInitialState() {
totalVentureShares: 0, totalVentureShares: 0,
ipoCount: 0, ipoCount: 0,
reputation: 0, reputation: 0,
repAllocations: {
b2b: 0,
clients: 0,
brand: 0
},
buyMode: '1', buyMode: '1',
activeTab: 'production', activeTab: 'production',
floorViewMode: 'standard', // 'standard' | 'compact' floorViewMode: 'standard', // 'standard' | 'compact'
@ -109,6 +114,7 @@ export class StateManager {
const merged = { ...fresh, ...parsed }; const merged = { ...fresh, ...parsed };
merged.products = { ...fresh.products, ...(parsed.products || {}) }; merged.products = { ...fresh.products, ...(parsed.products || {}) };
merged.boardroomPerks = { ...fresh.boardroomPerks, ...(parsed.boardroomPerks || {}) }; merged.boardroomPerks = { ...fresh.boardroomPerks, ...(parsed.boardroomPerks || {}) };
merged.repAllocations = { ...fresh.repAllocations, ...(parsed.repAllocations || {}) };
merged.achievements = { ...fresh.achievements, ...(parsed.achievements || {}) }; merged.achievements = { ...fresh.achievements, ...(parsed.achievements || {}) };
merged.stats = { ...fresh.stats, ...(parsed.stats || {}) }; merged.stats = { ...fresh.stats, ...(parsed.stats || {}) };
if (!Array.isArray(merged.activeContracts) || merged.activeContracts.length === 0) { if (!Array.isArray(merged.activeContracts) || merged.activeContracts.length === 0) {
@ -183,6 +189,7 @@ export class StateManager {
} }
validated.boardroomPerks = { ...fresh.boardroomPerks, ...(parsed.boardroomPerks || {}) }; validated.boardroomPerks = { ...fresh.boardroomPerks, ...(parsed.boardroomPerks || {}) };
validated.repAllocations = { ...fresh.repAllocations, ...(parsed.repAllocations || {}) };
validated.achievements = { ...fresh.achievements, ...(parsed.achievements || {}) }; validated.achievements = { ...fresh.achievements, ...(parsed.achievements || {}) };
validated.stats = { ...fresh.stats, ...(parsed.stats || {}) }; validated.stats = { ...fresh.stats, ...(parsed.stats || {}) };
if (!Array.isArray(validated.activeContracts) || validated.activeContracts.length === 0) { if (!Array.isArray(validated.activeContracts) || validated.activeContracts.length === 0) {
@ -207,6 +214,14 @@ export class StateManager {
const startingCashBonuses = [30.0, 500.0, 5000.0, 50000.0, 500000.0, 5000000.0]; const startingCashBonuses = [30.0, 500.0, 5000.0, 50000.0, 500000.0, 5000000.0];
const newStartingCash = startingCashBonuses[Math.min(startingBonusPerk, startingCashBonuses.length - 1)]; const newStartingCash = startingCashBonuses[Math.min(startingBonusPerk, startingCashBonuses.length - 1)];
// Brand Legacy: Retain 10% - 50% of total accumulated Reputation across IPOs
const brandLegacyLvl = this.state.boardroomPerks['brand_legacy'] || 0;
const repRetentionRatios = [0.0, 0.10, 0.20, 0.30, 0.40, 0.50];
const ratio = repRetentionRatios[Math.min(brandLegacyLvl, repRetentionRatios.length - 1)];
const allocs = this.state.repAllocations || {};
const totalRep = (this.state.reputation || 0) + (allocs.b2b || 0) + (allocs.clients || 0) + (allocs.brand || 0);
const preservedRep = Math.floor(totalRep * ratio);
const preservedShares = this.state.ventureShares + earned; const preservedShares = this.state.ventureShares + earned;
const preservedTotalShares = this.state.totalVentureShares + earned; const preservedTotalShares = this.state.totalVentureShares + earned;
const preservedPerks = { ...this.state.boardroomPerks }; const preservedPerks = { ...this.state.boardroomPerks };
@ -221,6 +236,8 @@ export class StateManager {
this.state.ipoCount = preservedIpoCount; this.state.ipoCount = preservedIpoCount;
this.state.boardroomPerks = preservedPerks; this.state.boardroomPerks = preservedPerks;
this.state.achievements = preservedAchievements; this.state.achievements = preservedAchievements;
this.state.reputation = preservedRep;
this.state.repAllocations = { b2b: 0, clients: 0, brand: 0 };
this.save(); this.save();
this.notify(); this.notify();

View file

@ -51,6 +51,16 @@ export const BOARDROOM_PERKS = [
costShares: [5, 15, 45, 120, 300], costShares: [5, 15, 45, 120, 300],
maxLevel: 5, maxLevel: 5,
effectDesc: 'Increases starting capital by $500, $5k, $50k, $500k, $5M after IPO.' effectDesc: 'Increases starting capital by $500, $5k, $50k, $500k, $5M after IPO.'
},
{
id: 'brand_legacy',
name: 'Brand Legacy & Heritage',
icon: 'lucide:award',
color: '#0284c7',
description: 'Enduring reputation and corporate goodwill carry forward into your next enterprise.',
costShares: [4, 10, 25, 70, 150],
maxLevel: 5,
effectDesc: 'Preserves 10%, 20%, 30%, 40%, 50% of Corporate Reputation across IPOs.'
} }
]; ];

View file

@ -11,6 +11,22 @@ export function mountContractsBoard(container, state, onAction) {
const contracts = state.activeContracts || []; const contracts = state.activeContracts || [];
currentMountedContractUids = contracts.map(c => c.uid).join(','); currentMountedContractUids = contracts.map(c => c.uid).join(',');
const repPool = state.reputation || 0;
const allocs = state.repAllocations || { b2b: 0, clients: 0, brand: 0 };
const b2bRep = allocs.b2b || 0;
const clientsRep = allocs.clients || 0;
const brandRep = allocs.brand || 0;
const totalAllocated = b2bRep + clientsRep + brandRep;
const totalAccumulatedRep = repPool + totalAllocated;
// Computed bonus percentages
const b2bPayoutPct = (b2bRep * 0.5).toFixed(1);
const apexYieldPct = (clientsRep * 0.2).toFixed(1);
const titanDiscountPct = Math.min(50, clientsRep * 0.2).toFixed(1);
const omniSpeedPct = (clientsRep * 0.15).toFixed(1);
const vanguardCapPct = (clientsRep * 0.25).toFixed(1);
const brandPricePct = (brandRep * 0.25).toFixed(1);
let html = ` let html = `
<div class="contracts-header-pane"> <div class="contracts-header-pane">
<div class="contracts-header-info"> <div class="contracts-header-info">
@ -26,7 +42,137 @@ export function mountContractsBoard(container, state, onAction) {
<span>Ready to Deliver:</span> <span>Ready to Deliver:</span>
<strong id="contract-stats-ready" class="font-mono text-profit">${state.readyContractsCount || 0}</strong> <strong id="contract-stats-ready" class="font-mono text-profit">${state.readyContractsCount || 0}</strong>
<span style="margin-left: 8px; border-left: 1px solid var(--border-color); padding-left: 8px;">Completed:</span> <span style="margin-left: 8px; border-left: 1px solid var(--border-color); padding-left: 8px;">Completed:</span>
<strong id="contract-stats-completed" class="font-mono">${state.stats.contractsCompleted || 0}</strong> <strong id="contract-stats-completed" class="font-mono">${state.stats?.contractsCompleted || 0}</strong>
</div>
</div>
<!-- Corporate Reputation Allocation Hub -->
<div class="rep-hub-container">
<div class="rep-hub-header">
<div class="rep-hub-title-group">
<div class="rep-hub-icon">
<iconify-icon icon="lucide:award"></iconify-icon>
</div>
<div>
<h3 class="rep-hub-title">Corporate Standing & Reputation Hub</h3>
<p class="rep-hub-desc">Distribute corporate reputation earned from fulfilled contracts across 3 strategic branches. Points can be reallocated at any time.</p>
</div>
</div>
<div class="rep-pool-card">
<div class="rep-pool-info">
<span class="rep-pool-label">Available Rep Pool</span>
<span class="rep-pool-val font-mono" id="rep-pool-val">${fmtNum(repPool, 0)}</span>
</div>
<div class="rep-pool-meta">
<span title="Total Reputation Earned This IPO Cycle">Accumulated: <strong>${fmtNum(totalAccumulatedRep, 0)}</strong></span>
<button class="btn-rep-reset" id="btn-rep-reset" ${totalAllocated > 0 ? '' : 'disabled'} title="Recall all allocated points back to available pool">
<iconify-icon icon="lucide:rotate-ccw"></iconify-icon> Reset Allocations
</button>
</div>
</div>
</div>
<div class="rep-models-grid">
<!-- Model 1: B2B Standing -->
<div class="rep-model-card">
<div class="rep-model-head">
<div class="rep-model-icon rep-icon-b2b">
<iconify-icon icon="lucide:handshake"></iconify-icon>
</div>
<div class="rep-model-meta">
<h4>B2B Procurement Standing</h4>
<span class="rep-model-sub">Model 1: Tender Optimization</span>
</div>
<div class="rep-allocated-badge font-mono" id="rep-alloc-badge-b2b">${fmtNum(b2bRep, 0)} Rep</div>
</div>
<div class="rep-model-body">
<p class="rep-benefit-desc">Increases cash payouts on all delivered B2B tenders by establishing premier vendor standing.</p>
<div class="rep-bonus-stat">
<span>Contract Cash Bonus:</span>
<strong class="font-mono text-profit" id="rep-bonus-b2b">+${b2bPayoutPct}%</strong>
</div>
</div>
<div class="rep-model-actions">
<div class="rep-btn-group">
<button class="btn-rep-ctrl btn-rep-sub" data-dept="b2b" data-delta="-10" ${b2bRep >= 10 ? '' : 'disabled'}>-10</button>
<button class="btn-rep-ctrl btn-rep-sub" data-dept="b2b" data-delta="-1" ${b2bRep >= 1 ? '' : 'disabled'}>-1</button>
<button class="btn-rep-ctrl btn-rep-add" data-dept="b2b" data-delta="1" ${repPool >= 1 ? '' : 'disabled'}>+1</button>
<button class="btn-rep-ctrl btn-rep-add" data-dept="b2b" data-delta="10" ${repPool >= 10 ? '' : 'disabled'}>+10</button>
</div>
</div>
</div>
<!-- Model 2: Client Strategic Relations -->
<div class="rep-model-card">
<div class="rep-model-head">
<div class="rep-model-icon rep-icon-clients">
<iconify-icon icon="lucide:building-2"></iconify-icon>
</div>
<div class="rep-model-meta">
<h4>Client Strategic Relations</h4>
<span class="rep-model-sub">Model 2: MegaCorp Synergies</span>
</div>
<div class="rep-allocated-badge font-mono" id="rep-alloc-badge-clients">${fmtNum(clientsRep, 0)} Rep</div>
</div>
<div class="rep-model-body">
<p class="rep-benefit-desc">Deepens industrial partnerships with the 4 MegaCorps to unlock multifaceted operational synergies.</p>
<div class="rep-bonus-multi-grid">
<div class="rep-multi-item" title="Apex Resources Synergy">
<span class="client-name-tag text-amber">Apex Raw Yield</span>
<strong class="font-mono text-profit" id="rep-bonus-apex">+${apexYieldPct}%</strong>
</div>
<div class="rep-multi-item" title="Titan Heavy Industries Synergy">
<span class="client-name-tag text-blue">Titan Line Discount</span>
<strong class="font-mono text-profit" id="rep-bonus-titan">-${titanDiscountPct}%</strong>
</div>
<div class="rep-multi-item" title="Omni Dynamics Synergy">
<span class="client-name-tag text-purple">Omni Cycle Speed</span>
<strong class="font-mono text-profit" id="rep-bonus-omni">+${omniSpeedPct}%</strong>
</div>
<div class="rep-multi-item" title="Vanguard Shipping Synergy">
<span class="client-name-tag text-cyan">Vanguard Fleet Flow</span>
<strong class="font-mono text-profit" id="rep-bonus-vanguard">+${vanguardCapPct}%</strong>
</div>
</div>
</div>
<div class="rep-model-actions">
<div class="rep-btn-group">
<button class="btn-rep-ctrl btn-rep-sub" data-dept="clients" data-delta="-10" ${clientsRep >= 10 ? '' : 'disabled'}>-10</button>
<button class="btn-rep-ctrl btn-rep-sub" data-dept="clients" data-delta="-1" ${clientsRep >= 1 ? '' : 'disabled'}>-1</button>
<button class="btn-rep-ctrl btn-rep-add" data-dept="clients" data-delta="1" ${repPool >= 1 ? '' : 'disabled'}>+1</button>
<button class="btn-rep-ctrl btn-rep-add" data-dept="clients" data-delta="10" ${repPool >= 10 ? '' : 'disabled'}>+10</button>
</div>
</div>
</div>
<!-- Model 3: Wholesale Brand Premium -->
<div class="rep-model-card">
<div class="rep-model-head">
<div class="rep-model-icon rep-icon-brand">
<iconify-icon icon="lucide:sparkles"></iconify-icon>
</div>
<div class="rep-model-meta">
<h4>Wholesale Brand Premium</h4>
<span class="rep-model-sub">Model 3: Market Margin Power</span>
</div>
<div class="rep-allocated-badge font-mono" id="rep-alloc-badge-brand">${fmtNum(brandRep, 0)} Rep</div>
</div>
<div class="rep-model-body">
<p class="rep-benefit-desc">Builds prestigious global brand equity, boosting open-market wholesale prices across all logistics fleets.</p>
<div class="rep-bonus-stat">
<span>Wholesale Price Premium:</span>
<strong class="font-mono text-profit" id="rep-bonus-brand">+${brandPricePct}%</strong>
</div>
</div>
<div class="rep-model-actions">
<div class="rep-btn-group">
<button class="btn-rep-ctrl btn-rep-sub" data-dept="brand" data-delta="-10" ${brandRep >= 10 ? '' : 'disabled'}>-10</button>
<button class="btn-rep-ctrl btn-rep-sub" data-dept="brand" data-delta="-1" ${brandRep >= 1 ? '' : 'disabled'}>-1</button>
<button class="btn-rep-ctrl btn-rep-add" data-dept="brand" data-delta="1" ${repPool >= 1 ? '' : 'disabled'}>+1</button>
<button class="btn-rep-ctrl btn-rep-add" data-dept="brand" data-delta="10" ${repPool >= 10 ? '' : 'disabled'}>+10</button>
</div>
</div>
</div>
</div> </div>
</div> </div>
@ -115,6 +261,23 @@ export function mountContractsBoard(container, state, onAction) {
container.querySelectorAll('.btn-fulfill-contract').forEach(btn => { container.querySelectorAll('.btn-fulfill-contract').forEach(btn => {
btn.onclick = () => onAction({ type: 'FULFILL_CONTRACT', uid: btn.dataset.uid }); btn.onclick = () => onAction({ type: 'FULFILL_CONTRACT', uid: btn.dataset.uid });
}); });
// Rep Hub Allocation Control Listeners
container.querySelectorAll('.btn-rep-ctrl').forEach(btn => {
btn.onclick = () => onAction({
type: 'ALLOCATE_REP',
department: btn.dataset.dept,
delta: btn.dataset.delta
});
});
const btnReset = container.querySelector('#btn-rep-reset');
if (btnReset) {
btnReset.onclick = () => onAction({
type: 'ALLOCATE_REP',
delta: 'reset'
});
}
} }
export function updateContractsBoard(container, state, onAction) { export function updateContractsBoard(container, state, onAction) {
@ -139,6 +302,61 @@ export function updateContractsBoard(container, state, onAction) {
if (elStatsCompleted.textContent !== val) elStatsCompleted.textContent = val; if (elStatsCompleted.textContent !== val) elStatsCompleted.textContent = val;
} }
// Update Rep Pool & Allocations Live DOM
const repPool = state.reputation || 0;
const allocs = state.repAllocations || { b2b: 0, clients: 0, brand: 0 };
const b2bRep = allocs.b2b || 0;
const clientsRep = allocs.clients || 0;
const brandRep = allocs.brand || 0;
const totalAllocated = b2bRep + clientsRep + brandRep;
const elRepPool = document.getElementById('rep-pool-val');
if (elRepPool) {
const poolTxt = fmtNum(repPool, 0);
if (elRepPool.textContent !== poolTxt) elRepPool.textContent = poolTxt;
}
const elResetBtn = document.getElementById('btn-rep-reset');
if (elResetBtn) {
elResetBtn.disabled = totalAllocated <= 0;
}
// Model 1: B2B
const elBadgeB2B = document.getElementById('rep-alloc-badge-b2b');
if (elBadgeB2B) elBadgeB2B.textContent = `${fmtNum(b2bRep, 0)} Rep`;
const elBonusB2B = document.getElementById('rep-bonus-b2b');
if (elBonusB2B) elBonusB2B.textContent = `+${(b2bRep * 0.5).toFixed(1)}%`;
// Model 2: Clients
const elBadgeClients = document.getElementById('rep-alloc-badge-clients');
if (elBadgeClients) elBadgeClients.textContent = `${fmtNum(clientsRep, 0)} Rep`;
const elBonusApex = document.getElementById('rep-bonus-apex');
if (elBonusApex) elBonusApex.textContent = `+${(clientsRep * 0.2).toFixed(1)}%`;
const elBonusTitan = document.getElementById('rep-bonus-titan');
if (elBonusTitan) elBonusTitan.textContent = `-${Math.min(50, clientsRep * 0.2).toFixed(1)}%`;
const elBonusOmni = document.getElementById('rep-bonus-omni');
if (elBonusOmni) elBonusOmni.textContent = `+${(clientsRep * 0.15).toFixed(1)}%`;
const elBonusVanguard = document.getElementById('rep-bonus-vanguard');
if (elBonusVanguard) elBonusVanguard.textContent = `+${(clientsRep * 0.25).toFixed(1)}%`;
// Model 3: Brand
const elBadgeBrand = document.getElementById('rep-alloc-badge-brand');
if (elBadgeBrand) elBadgeBrand.textContent = `${fmtNum(brandRep, 0)} Rep`;
const elBonusBrand = document.getElementById('rep-bonus-brand');
if (elBonusBrand) elBonusBrand.textContent = `+${(brandRep * 0.25).toFixed(1)}%`;
// Rep Control Buttons Disabled State
container.querySelectorAll('.btn-rep-ctrl').forEach(btn => {
const dept = btn.dataset.dept;
const delta = parseInt(btn.dataset.delta, 10);
const currentDeptRep = allocs[dept] || 0;
if (delta > 0) {
btn.disabled = repPool < delta;
} else if (delta < 0) {
btn.disabled = currentDeptRep < Math.abs(delta);
}
});
for (const c of contracts) { for (const c of contracts) {
const currentStock = state.products[c.productId]?.stock || 0; const currentStock = state.products[c.productId]?.stock || 0;
const canFulfill = currentStock >= c.targetAmount; const canFulfill = currentStock >= c.targetAmount;

View file

@ -51,7 +51,9 @@ export function renderCompactFacilityCardHtml(p, pState, state, buyMode) {
const nextM = getNextMilestone(pState.lines || 0); const nextM = getNextMilestone(pState.lines || 0);
const rates = getProductOutputRates(p, pState, state); const rates = getProductOutputRates(p, pState, state);
const milestoneMult = rates.milestoneMult || getUnitMilestoneMultiplier(pState.lines || 0); const milestoneMult = rates.milestoneMult || getUnitMilestoneMultiplier(pState.lines || 0);
const calc = calculateMultiCost(p.baseFacilityCost, pState.lines || 0, buyMode); const clientRep = state.repAllocations?.clients || 0;
const lineDiscount = Math.min(0.50, 0.002 * clientRep);
const calc = calculateMultiCost(p.baseFacilityCost, pState.lines || 0, buyMode, lineDiscount);
const canAffordBuy = state.cash >= calc.cost; const canAffordBuy = state.cash >= calc.cost;
const currentThrottle = pState.productionTarget !== undefined ? pState.productionTarget : 1.0; const currentThrottle = pState.productionTarget !== undefined ? pState.productionTarget : 1.0;
@ -146,7 +148,9 @@ export function updateCompactFacilityCardDom(p, pState, state, engine) {
const buyMode = state.buyMode || '1'; const buyMode = state.buyMode || '1';
const rates = getProductOutputRates(p, pState, state); const rates = getProductOutputRates(p, pState, state);
const calc = calculateMultiCost(p.baseFacilityCost, pState.lines || 0, buyMode); const clientRep = state.repAllocations?.clients || 0;
const lineDiscount = Math.min(0.50, 0.002 * clientRep);
const calc = calculateMultiCost(p.baseFacilityCost, pState.lines || 0, buyMode, lineDiscount);
const canAffordBuy = state.cash >= calc.cost; const canAffordBuy = state.cash >= calc.cost;
const currentThrottle = pState.productionTarget !== undefined ? pState.productionTarget : 1.0; const currentThrottle = pState.productionTarget !== undefined ? pState.productionTarget : 1.0;

View file

@ -49,7 +49,9 @@ export function renderFacilityCardHtml(p, pState, state, buyMode) {
const nextM = getNextMilestone(pState.lines); const nextM = getNextMilestone(pState.lines);
const rates = getProductOutputRates(p, pState, state); const rates = getProductOutputRates(p, pState, state);
const milestoneMult = rates.milestoneMult || getUnitMilestoneMultiplier(pState.lines); const milestoneMult = rates.milestoneMult || getUnitMilestoneMultiplier(pState.lines);
const calc = calculateMultiCost(p.baseFacilityCost, pState.lines, buyMode); const clientRep = state.repAllocations?.clients || 0;
const lineDiscount = Math.min(0.50, 0.002 * clientRep);
const calc = calculateMultiCost(p.baseFacilityCost, pState.lines, buyMode, lineDiscount);
const canAffordBuy = state.cash >= calc.cost; const canAffordBuy = state.cash >= calc.cost;
const managerCost = p.baseManagerCost; const managerCost = p.baseManagerCost;
const canAffordManager = state.cash >= managerCost; const canAffordManager = state.cash >= managerCost;
@ -420,7 +422,9 @@ export function updateFacilityCardDom(p, pState, state, buyMode) {
} }
// Expand Button disabled state // Expand Button disabled state
const calc = calculateMultiCost(p.baseFacilityCost, pState.lines, buyMode); const clientRep = state.repAllocations?.clients || 0;
const lineDiscount = Math.min(0.50, 0.002 * clientRep);
const calc = calculateMultiCost(p.baseFacilityCost, pState.lines, buyMode, lineDiscount);
const elBtnExpand = document.getElementById(`btn-expand-${p.id}`); const elBtnExpand = document.getElementById(`btn-expand-${p.id}`);
if (elBtnExpand) { if (elBtnExpand) {
const canAfford = cash >= calc.cost; const canAfford = cash >= calc.cost;

View file

@ -1,6 +1,6 @@
import { PRODUCTS, getNextMilestone, getUnitMilestoneMultiplier } from '../../data/products.js'; import { PRODUCTS, getNextMilestone, getUnitMilestoneMultiplier } from '../../data/products.js';
export function calculateMultiCost(baseCost, currentCount, multiplier) { export function calculateMultiCost(baseCost, currentCount, multiplier, discount = 0.0) {
const growth = 1.15; const growth = 1.15;
let countToBuy = 1; let countToBuy = 1;
@ -15,8 +15,9 @@ export function calculateMultiCost(baseCost, currentCount, multiplier) {
} }
let totalCost = 0; let totalCost = 0;
const factor = Math.max(0.5, 1.0 - discount);
for (let i = 0; i < countToBuy; i++) { for (let i = 0; i < countToBuy; i++) {
totalCost += baseCost * Math.pow(growth, currentCount + i); totalCost += baseCost * Math.pow(growth, currentCount + i) * factor;
} }
return { count: countToBuy, cost: totalCost }; return { count: countToBuy, cost: totalCost };
} }
@ -30,14 +31,21 @@ export function getProductOutputRates(p, pState, state) {
} }
const milestoneMult = getUnitMilestoneMultiplier(pState.lines); const milestoneMult = getUnitMilestoneMultiplier(pState.lines);
const roboticsMult = state.upgrades?.automated_robotics ? 2.0 : 1.0; const perks = state.boardroomPerks || {};
const baseCycleTime = Math.max(0.1, p.baseCycleTime / (milestoneMult * roboticsMult)); const repAlloc = state.repAllocations || {};
const brandRep = repAlloc.brand || 0;
const clientsRep = repAlloc.clients || 0;
const roboticsSpeedMult = (1.0 + 0.20 * (perks['robotics_overclock'] || 0)) * (1.0 + 0.0015 * clientsRep);
const baseCycleTime = Math.max(0.05, p.baseCycleTime / (milestoneMult * roboticsSpeedMult));
const targetThrottle = pState.productionTarget !== undefined ? pState.productionTarget : 1.0; const targetThrottle = pState.productionTarget !== undefined ? pState.productionTarget : 1.0;
const isBottlenecked = !!pState.isBottlenecked || targetThrottle <= 0; const isBottlenecked = !!pState.isBottlenecked || targetThrottle <= 0;
// Output velocity: units per second // Output velocity: units per second
const isRaw = !p.inputs || p.inputs.length === 0;
const supplyYieldMult = isRaw ? (1.0 + 0.15 * (perks['supply_sync'] || 0)) * (1.0 + 0.002 * clientsRep) : 1.0;
const cyclesPerSec = (1 / baseCycleTime) * targetThrottle; const cyclesPerSec = (1 / baseCycleTime) * targetThrottle;
const prodRate = isBottlenecked ? 0 : cyclesPerSec * p.outputAmount * pState.lines; const prodRate = isBottlenecked ? 0 : cyclesPerSec * p.outputAmount * pState.lines * supplyYieldMult;
// Logistics revenue rate: items sold passively per second * price // Logistics revenue rate: items sold passively per second * price
const logisticsUnits = pState.logistics || 0; const logisticsUnits = pState.logistics || 0;
@ -45,9 +53,10 @@ export function getProductOutputRates(p, pState, state) {
let revRate = 0; let revRate = 0;
if (isLogisticsActive && logisticsUnits > 0) { if (isLogisticsActive && logisticsUnits > 0) {
const logisticsPriceMult = state.upgrades?.global_supply_chain ? 1.5 : 1.0; const logisticsPriceMult = (1.0 + 0.25 * (perks['logistics_efficiency'] || 0)) * (1.0 + 0.0025 * brandRep);
const logisticsThroughputMult = 1.0 + 0.0025 * clientsRep;
const salePrice = p.basePrice * logisticsPriceMult; const salePrice = p.basePrice * logisticsPriceMult;
const maxSellCapacity = logisticsUnits * 0.75; const maxSellCapacity = logisticsUnits * 0.75 * logisticsThroughputMult;
// If stockpiled, sells at max fleet capacity; otherwise bottlenecked by production velocity // If stockpiled, sells at max fleet capacity; otherwise bottlenecked by production velocity
const effectiveSellRate = Math.min(maxSellCapacity, pState.stock > 0 ? maxSellCapacity : prodRate); const effectiveSellRate = Math.min(maxSellCapacity, pState.stock > 0 ? maxSellCapacity : prodRate);
revRate = effectiveSellRate * salePrice; revRate = effectiveSellRate * salePrice;

View file

@ -29,7 +29,9 @@ export function mountUnitDetailView(container, state, onAction, engine) {
const milestoneMult = rates.milestoneMult || getUnitMilestoneMultiplier(pState.lines || 0); const milestoneMult = rates.milestoneMult || getUnitMilestoneMultiplier(pState.lines || 0);
const nextM = getNextMilestone(pState.lines || 0); const nextM = getNextMilestone(pState.lines || 0);
const buyMode = state.buyMode || '1'; const buyMode = state.buyMode || '1';
const calc = calculateMultiCost(product.baseFacilityCost, pState.lines || 0, buyMode); const clientRep = state.repAllocations?.clients || 0;
const lineDiscount = Math.min(0.50, 0.002 * clientRep);
const calc = calculateMultiCost(product.baseFacilityCost, pState.lines || 0, buyMode, lineDiscount);
const canAffordBuy = state.cash >= calc.cost; const canAffordBuy = state.cash >= calc.cost;
const canAffordManager = state.cash >= product.baseManagerCost; const canAffordManager = state.cash >= product.baseManagerCost;
const logisticsCost = product.baseLogisticsCost * Math.pow(1.15, pState.logistics || 0); const logisticsCost = product.baseLogisticsCost * Math.pow(1.15, pState.logistics || 0);
@ -666,7 +668,9 @@ export function updateUnitDetailView(container, state, engine) {
// Expand Button disabled state // Expand Button disabled state
const buyMode = state.buyMode || '1'; const buyMode = state.buyMode || '1';
const calc = calculateMultiCost(product.baseFacilityCost, pState.lines || 0, buyMode); const clientRep = state.repAllocations?.clients || 0;
const lineDiscount = Math.min(0.50, 0.002 * clientRep);
const calc = calculateMultiCost(product.baseFacilityCost, pState.lines || 0, buyMode, lineDiscount);
const btnExpand = document.getElementById('detail-btn-expand'); const btnExpand = document.getElementById('detail-btn-expand');
if (btnExpand) { if (btnExpand) {
btnExpand.disabled = cash < calc.cost; btnExpand.disabled = cash < calc.cost;