diff --git a/backend/engine.py b/backend/engine.py index 0cefe96..16eb04b 100644 --- a/backend/engine.py +++ b/backend/engine.py @@ -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): diff --git a/backend/test_engine.py b/backend/test_engine.py index dbfd16d..92c449b 100644 --- a/backend/test_engine.py +++ b/backend/test_engine.py @@ -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!") diff --git a/frontend/css/views/contracts.css b/frontend/css/views/contracts.css index 30b0d87..e6cd425 100644 --- a/frontend/css/views/contracts.css +++ b/frontend/css/views/contracts.css @@ -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; +} + + diff --git a/frontend/js/app.js b/frontend/js/app.js index af0bd77..bbc8fa1 100644 --- a/frontend/js/app.js +++ b/frontend/js/app.js @@ -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)', diff --git a/frontend/js/core/engine.js b/frontend/js/core/engine.js index d7a1e0f..af105ec 100644 --- a/frontend/js/core/engine.js +++ b/frontend/js/core/engine.js @@ -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; diff --git a/frontend/js/core/state.js b/frontend/js/core/state.js index 7122301..86171cc 100644 --- a/frontend/js/core/state.js +++ b/frontend/js/core/state.js @@ -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(); diff --git a/frontend/js/data/boardroom.js b/frontend/js/data/boardroom.js index 80faaa4..b87153d 100644 --- a/frontend/js/data/boardroom.js +++ b/frontend/js/data/boardroom.js @@ -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.' } ]; diff --git a/frontend/js/ui/contractsView.js b/frontend/js/ui/contractsView.js index 538ebc3..3748bc5 100644 --- a/frontend/js/ui/contractsView.js +++ b/frontend/js/ui/contractsView.js @@ -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 = `
Distribute corporate reputation earned from fulfilled contracts across 3 strategic branches. Points can be reallocated at any time.
+Increases cash payouts on all delivered B2B tenders by establishing premier vendor standing.
+Deepens industrial partnerships with the 4 MegaCorps to unlock multifaceted operational synergies.
+Builds prestigious global brand equity, boosting open-market wholesale prices across all logistics fleets.
+