Implement Corporate Reputation allocation hub across 3 strategic branches and Boardroom brand legacy perk
This commit is contained in:
parent
221ed97415
commit
8aa76c805a
12 changed files with 757 additions and 27 deletions
|
|
@ -22,7 +22,8 @@ BOARDROOM_PERK_COSTS = {
|
|||
'robotics_overclock': [2, 5, 12, 30, 75],
|
||||
'logistics_efficiency': [2, 6, 15, 40, 100],
|
||||
'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:
|
||||
|
|
@ -45,6 +46,7 @@ def get_initial_state() -> dict:
|
|||
'totalVentureShares': 0,
|
||||
'ipoCount': 0,
|
||||
'reputation': 0,
|
||||
'repAllocations': {'b2b': 0, 'clients': 0, 'brand': 0},
|
||||
'buyMode': '1',
|
||||
'activeTab': 'production',
|
||||
'selectedSector': 'all',
|
||||
|
|
@ -95,9 +97,15 @@ def process_offline_and_ticks(state: dict) -> dict:
|
|||
return state
|
||||
|
||||
perks = state.get('boardroomPerks', {})
|
||||
supply_yield_mult = 1.0 + 0.15 * perks.get('supply_sync', 0)
|
||||
robotics_speed_mult = 1.0 + 0.20 * perks.get('robotics_overclock', 0)
|
||||
logistics_price_mult = 1.0 + 0.25 * perks.get('logistics_efficiency', 0)
|
||||
rep_alloc = state.get('repAllocations', {}) if isinstance(state.get('repAllocations'), dict) else {}
|
||||
brand_rep = rep_alloc.get('brand', 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
|
||||
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', {}):
|
||||
p_cfg = PRODUCT_CONFIGS[pid]
|
||||
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:
|
||||
state['cash'] -= cost
|
||||
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_perks = state.get('boardroomPerks', {})
|
||||
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
|
||||
state = get_initial_state()
|
||||
state['cash'] = new_starting_cash
|
||||
state['ventureShares'] = preserved_shares
|
||||
state['totalVentureShares'] = preserved_total_shares
|
||||
state['boardroomPerks'] = preserved_perks
|
||||
state['ipoCount'] = preserved_ipos
|
||||
state['reputation'] = preserved_rep
|
||||
state['repAllocations'] = {'b2b': 0, 'clients': 0, 'brand': 0}
|
||||
|
||||
elif atype == 'BUY_PERK':
|
||||
perk_id = act.get('perkId')
|
||||
|
|
@ -297,12 +324,51 @@ def apply_actions(state: dict, actions: list) -> dict:
|
|||
state['ventureShares'] -= req_shares
|
||||
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
|
||||
|
||||
def sanitize_state(raw: dict) -> dict:
|
||||
"""Ensures incoming state doesn't have NaN or negative numbers and dynamically populates new products."""
|
||||
if not isinstance(raw, dict):
|
||||
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
|
||||
if 'products' not in raw or not isinstance(raw['products'], dict):
|
||||
|
|
|
|||
|
|
@ -156,6 +156,50 @@ def test_production_target_throttle():
|
|||
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})")
|
||||
|
||||
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__':
|
||||
test_initial_state()
|
||||
test_raw_production_and_logistics()
|
||||
|
|
@ -163,5 +207,6 @@ if __name__ == '__main__':
|
|||
test_chained_microchips_and_boards()
|
||||
test_policy_toggles()
|
||||
test_production_target_throttle()
|
||||
test_reputation_allocation_and_retention()
|
||||
test_ipo_and_shares()
|
||||
print("\nALL BACKEND ENGINE TESTS PASSED SUCCESSFULLY!")
|
||||
|
|
|
|||
|
|
@ -182,3 +182,306 @@
|
|||
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;
|
||||
}
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -430,7 +430,9 @@ class ProductionApp {
|
|||
if (pState && pState.stock >= c.targetAmount) {
|
||||
pState.stock -= c.targetAmount;
|
||||
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.lifetimeRevenue += totalPayout;
|
||||
state.reputation = (state.reputation || 0) + c.rewardRep;
|
||||
|
|
@ -455,6 +457,46 @@ class ProductionApp {
|
|||
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': {
|
||||
this.showConfirmModal({
|
||||
title: 'Authorize Initial Public Offering (IPO)',
|
||||
|
|
|
|||
|
|
@ -29,11 +29,19 @@ export class SimulationEngine {
|
|||
if (dt <= 0) return;
|
||||
const state = this.sm.state;
|
||||
const perks = state.boardroomPerks || {};
|
||||
const repAlloc = state.repAllocations || {};
|
||||
const brandRep = repAlloc.brand || 0;
|
||||
const clientsRep = repAlloc.clients || 0;
|
||||
|
||||
// Boardroom Multipliers
|
||||
const supplyYieldMult = 1.0 + 0.15 * (perks['supply_sync'] || 0);
|
||||
const roboticsSpeedMult = 1.0 + 0.20 * (perks['robotics_overclock'] || 0);
|
||||
const logisticsPriceMult = 1.0 + 0.25 * (perks['logistics_efficiency'] || 0);
|
||||
// Boardroom & Corporate Reputation Multipliers
|
||||
// Apex Synergy: +0.2% raw yield per clients rep point
|
||||
const supplyYieldMult = (1.0 + 0.15 * (perks['supply_sync'] || 0)) * (1.0 + 0.002 * clientsRep);
|
||||
// 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;
|
||||
|
||||
|
|
@ -153,8 +161,8 @@ export class SimulationEngine {
|
|||
|
||||
const availableToSell = Math.max(0, pState.stock - reservedStock);
|
||||
if (availableToSell > 0) {
|
||||
// Logistics fleet capacity: 0.75 items/sec per unit
|
||||
const maxSellCapacity = logisticsUnits * 0.75 * dt;
|
||||
// Logistics fleet capacity: 0.75 items/sec per unit * Vanguard client synergy
|
||||
const maxSellCapacity = logisticsUnits * 0.75 * logisticsThroughputMult * dt;
|
||||
const sold = Math.min(availableToSell, maxSellCapacity);
|
||||
pState.stock -= sold;
|
||||
|
||||
|
|
|
|||
|
|
@ -18,6 +18,11 @@ export function getInitialState() {
|
|||
totalVentureShares: 0,
|
||||
ipoCount: 0,
|
||||
reputation: 0,
|
||||
repAllocations: {
|
||||
b2b: 0,
|
||||
clients: 0,
|
||||
brand: 0
|
||||
},
|
||||
buyMode: '1',
|
||||
activeTab: 'production',
|
||||
floorViewMode: 'standard', // 'standard' | 'compact'
|
||||
|
|
@ -109,6 +114,7 @@ export class StateManager {
|
|||
const merged = { ...fresh, ...parsed };
|
||||
merged.products = { ...fresh.products, ...(parsed.products || {}) };
|
||||
merged.boardroomPerks = { ...fresh.boardroomPerks, ...(parsed.boardroomPerks || {}) };
|
||||
merged.repAllocations = { ...fresh.repAllocations, ...(parsed.repAllocations || {}) };
|
||||
merged.achievements = { ...fresh.achievements, ...(parsed.achievements || {}) };
|
||||
merged.stats = { ...fresh.stats, ...(parsed.stats || {}) };
|
||||
if (!Array.isArray(merged.activeContracts) || merged.activeContracts.length === 0) {
|
||||
|
|
@ -183,6 +189,7 @@ export class StateManager {
|
|||
}
|
||||
|
||||
validated.boardroomPerks = { ...fresh.boardroomPerks, ...(parsed.boardroomPerks || {}) };
|
||||
validated.repAllocations = { ...fresh.repAllocations, ...(parsed.repAllocations || {}) };
|
||||
validated.achievements = { ...fresh.achievements, ...(parsed.achievements || {}) };
|
||||
validated.stats = { ...fresh.stats, ...(parsed.stats || {}) };
|
||||
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 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 preservedTotalShares = this.state.totalVentureShares + earned;
|
||||
const preservedPerks = { ...this.state.boardroomPerks };
|
||||
|
|
@ -221,6 +236,8 @@ export class StateManager {
|
|||
this.state.ipoCount = preservedIpoCount;
|
||||
this.state.boardroomPerks = preservedPerks;
|
||||
this.state.achievements = preservedAchievements;
|
||||
this.state.reputation = preservedRep;
|
||||
this.state.repAllocations = { b2b: 0, clients: 0, brand: 0 };
|
||||
|
||||
this.save();
|
||||
this.notify();
|
||||
|
|
|
|||
|
|
@ -51,6 +51,16 @@ export const BOARDROOM_PERKS = [
|
|||
costShares: [5, 15, 45, 120, 300],
|
||||
maxLevel: 5,
|
||||
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.'
|
||||
}
|
||||
];
|
||||
|
||||
|
|
|
|||
|
|
@ -11,6 +11,22 @@ export function mountContractsBoard(container, state, onAction) {
|
|||
const contracts = state.activeContracts || [];
|
||||
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 = `
|
||||
<div class="contracts-header-pane">
|
||||
<div class="contracts-header-info">
|
||||
|
|
@ -26,7 +42,137 @@ export function mountContractsBoard(container, state, onAction) {
|
|||
<span>Ready to Deliver:</span>
|
||||
<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>
|
||||
<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>
|
||||
|
||||
|
|
@ -115,6 +261,23 @@ export function mountContractsBoard(container, state, onAction) {
|
|||
container.querySelectorAll('.btn-fulfill-contract').forEach(btn => {
|
||||
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) {
|
||||
|
|
@ -139,6 +302,61 @@ export function updateContractsBoard(container, state, onAction) {
|
|||
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) {
|
||||
const currentStock = state.products[c.productId]?.stock || 0;
|
||||
const canFulfill = currentStock >= c.targetAmount;
|
||||
|
|
|
|||
|
|
@ -51,7 +51,9 @@ export function renderCompactFacilityCardHtml(p, pState, state, buyMode) {
|
|||
const nextM = getNextMilestone(pState.lines || 0);
|
||||
const rates = getProductOutputRates(p, pState, state);
|
||||
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 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 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 currentThrottle = pState.productionTarget !== undefined ? pState.productionTarget : 1.0;
|
||||
|
||||
|
|
|
|||
|
|
@ -49,7 +49,9 @@ export function renderFacilityCardHtml(p, pState, state, buyMode) {
|
|||
const nextM = getNextMilestone(pState.lines);
|
||||
const rates = getProductOutputRates(p, pState, state);
|
||||
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 managerCost = p.baseManagerCost;
|
||||
const canAffordManager = state.cash >= managerCost;
|
||||
|
|
@ -420,7 +422,9 @@ export function updateFacilityCardDom(p, pState, state, buyMode) {
|
|||
}
|
||||
|
||||
// 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}`);
|
||||
if (elBtnExpand) {
|
||||
const canAfford = cash >= calc.cost;
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
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;
|
||||
let countToBuy = 1;
|
||||
|
||||
|
|
@ -15,8 +15,9 @@ export function calculateMultiCost(baseCost, currentCount, multiplier) {
|
|||
}
|
||||
|
||||
let totalCost = 0;
|
||||
const factor = Math.max(0.5, 1.0 - discount);
|
||||
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 };
|
||||
}
|
||||
|
|
@ -30,14 +31,21 @@ export function getProductOutputRates(p, pState, state) {
|
|||
}
|
||||
|
||||
const milestoneMult = getUnitMilestoneMultiplier(pState.lines);
|
||||
const roboticsMult = state.upgrades?.automated_robotics ? 2.0 : 1.0;
|
||||
const baseCycleTime = Math.max(0.1, p.baseCycleTime / (milestoneMult * roboticsMult));
|
||||
const perks = state.boardroomPerks || {};
|
||||
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 isBottlenecked = !!pState.isBottlenecked || targetThrottle <= 0;
|
||||
|
||||
// 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 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
|
||||
const logisticsUnits = pState.logistics || 0;
|
||||
|
|
@ -45,9 +53,10 @@ export function getProductOutputRates(p, pState, state) {
|
|||
let revRate = 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 maxSellCapacity = logisticsUnits * 0.75;
|
||||
const maxSellCapacity = logisticsUnits * 0.75 * logisticsThroughputMult;
|
||||
// If stockpiled, sells at max fleet capacity; otherwise bottlenecked by production velocity
|
||||
const effectiveSellRate = Math.min(maxSellCapacity, pState.stock > 0 ? maxSellCapacity : prodRate);
|
||||
revRate = effectiveSellRate * salePrice;
|
||||
|
|
|
|||
|
|
@ -29,7 +29,9 @@ export function mountUnitDetailView(container, state, onAction, engine) {
|
|||
const milestoneMult = rates.milestoneMult || getUnitMilestoneMultiplier(pState.lines || 0);
|
||||
const nextM = getNextMilestone(pState.lines || 0);
|
||||
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 canAffordManager = state.cash >= product.baseManagerCost;
|
||||
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
|
||||
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');
|
||||
if (btnExpand) {
|
||||
btnExpand.disabled = cash < calc.cost;
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue