Add Distribution Fleet toggle, Downstream Supply toggle, and enforce 3x2 grid on All Sectors

This commit is contained in:
Christian 2026-10-16 11:35:29 +02:00
parent 21cb0aeb50
commit ff8983273e
11 changed files with 328 additions and 54 deletions

View file

@ -272,9 +272,17 @@ def process_offline_and_ticks(state: dict) -> dict:
# Manufacturing logic with clean batch cycle state # Manufacturing logic with clean batch cycle state
needed = [(inp['id'], inp['amount'] * lines) for inp in inputs] needed = [(inp['id'], inp['amount'] * lines) for inp in inputs]
def can_access_inputs():
for item_id, req in needed:
upstream_state = state.get('products', {}).get(item_id, {})
if upstream_state.get('supplyDownstreamEnabled') is False:
return False
if upstream_state.get('stock', 0.0) < req:
return False
return True
if p_state.get('cycleProgress', 0.0) <= 0.0: if p_state.get('cycleProgress', 0.0) <= 0.0:
can_start = all(state['products'].get(item_id, {}).get('stock', 0.0) >= req for item_id, req in needed) if can_access_inputs():
if can_start:
p_state['isBottlenecked'] = False p_state['isBottlenecked'] = False
for item_id, req in needed: for item_id, req in needed:
state['products'][item_id]['stock'] = max(0.0, state['products'][item_id].get('stock', 0.0) - req) state['products'][item_id]['stock'] = max(0.0, state['products'][item_id].get('stock', 0.0) - req)
@ -292,8 +300,7 @@ def process_offline_and_ticks(state: dict) -> dict:
p_state['totalProduced'] = p_state.get('totalProduced', 0.0) + produced p_state['totalProduced'] = p_state.get('totalProduced', 0.0) + produced
# Check next batch # Check next batch
can_start_next = all(state['products'].get(item_id, {}).get('stock', 0.0) >= req for item_id, req in needed) if can_access_inputs():
if can_start_next:
for item_id, req in needed: for item_id, req in needed:
state['products'][item_id]['stock'] = max(0.0, state['products'][item_id].get('stock', 0.0) - req) state['products'][item_id]['stock'] = max(0.0, state['products'][item_id].get('stock', 0.0) - req)
p_state['isBottlenecked'] = False p_state['isBottlenecked'] = False
@ -302,15 +309,18 @@ def process_offline_and_ticks(state: dict) -> dict:
# Passive Logistics Sales with stock reservation for downstream factories # Passive Logistics Sales with stock reservation for downstream factories
logistics = p_state.get('logistics', 0) logistics = p_state.get('logistics', 0)
if logistics > 0 and p_state.get('stock', 0.0) > 0: is_logistics_active = p_state.get('logisticsEnabled', True) is not False
if is_logistics_active and logistics > 0 and p_state.get('stock', 0.0) > 0:
reserved_stock = 0.0 reserved_stock = 0.0
for other_pid, other_cfg in PRODUCT_CONFIGS.items(): if p_state.get('supplyDownstreamEnabled', True) is not False:
other_inputs = other_cfg.get('inputs', []) for other_pid, other_cfg in PRODUCT_CONFIGS.items():
if any(inp['id'] == pid for inp in other_inputs): other_inputs = other_cfg.get('inputs', [])
other_state = state.get('products', {}).get(other_pid) if any(inp['id'] == pid for inp in other_inputs):
if other_state and other_state.get('unlocked') and other_state.get('lines', 0) > 0: other_state = state.get('products', {}).get(other_pid)
inp_match = next(inp for inp in other_inputs if inp['id'] == pid) if other_state and other_state.get('unlocked') and other_state.get('lines', 0) > 0:
reserved_stock = max(reserved_stock, float(inp_match['amount'] * other_state['lines'])) inp_match = next(inp for inp in other_inputs if inp['id'] == pid)
reserved_stock = max(reserved_stock, float(inp_match['amount'] * other_state['lines']))
avail = max(0.0, p_state.get('stock', 0.0) - reserved_stock) avail = max(0.0, p_state.get('stock', 0.0) - reserved_stock)
if avail > 0: if avail > 0:
@ -374,6 +384,18 @@ def apply_actions(state: dict, actions: list) -> dict:
state['cash'] -= cost state['cash'] -= cost
state['products'][pid]['logistics'] = current_logistics + 1 state['products'][pid]['logistics'] = current_logistics + 1
elif atype == 'TOGGLE_LOGISTICS':
pid = act.get('productId')
if pid in state.get('products', {}):
current = state['products'][pid].get('logisticsEnabled', True) is not False
state['products'][pid]['logisticsEnabled'] = not current
elif atype == 'TOGGLE_DOWNSTREAM_SUPPLY':
pid = act.get('productId')
if pid in state.get('products', {}):
current = state['products'][pid].get('supplyDownstreamEnabled', True) is not False
state['products'][pid]['supplyDownstreamEnabled'] = not current
elif atype == 'TRIGGER_IPO': elif atype == 'TRIGGER_IPO':
earned = calculate_shares_earned(state.get('lifetimeRevenue', 0.0)) earned = calculate_shares_earned(state.get('lifetimeRevenue', 0.0))
if earned > 0: if earned > 0:

View file

@ -92,10 +92,44 @@ def test_chained_microchips_and_boards():
assert state['products']['circuit_boards']['totalProduced'] > 0 assert state['products']['circuit_boards']['totalProduced'] > 0
print(f"PASS: test_chained_microchips_and_boards (Chips: {state['products']['microchips']['totalProduced']}, Boards: {state['products']['circuit_boards']['totalProduced']})") print(f"PASS: test_chained_microchips_and_boards (Chips: {state['products']['microchips']['totalProduced']}, Boards: {state['products']['circuit_boards']['totalProduced']})")
def test_policy_toggles():
state = engine.get_initial_state()
# Unlock wheat and flour
state['products']['wheat']['stock'] = 20.0
state['products']['flour']['unlocked'] = True
state['products']['flour']['lines'] = 1
# 1. Turn off downstream supply on wheat
state = engine.apply_actions(state, [{'type': 'TOGGLE_DOWNSTREAM_SUPPLY', 'productId': 'wheat'}])
assert state['products']['wheat']['supplyDownstreamEnabled'] is False
# Simulate 5 seconds: flour should be bottlenecked because wheat supply to downstream is blocked
state['lastTick'] = time.time() - 5.0
state = engine.process_offline_and_ticks(state)
assert state['products']['flour']['stock'] == 0.0
assert state['products']['flour']['isBottlenecked'] is True
# 2. Turn downstream supply back on: flour consumes wheat and produces
state = engine.apply_actions(state, [{'type': 'TOGGLE_DOWNSTREAM_SUPPLY', 'productId': 'wheat'}])
assert state['products']['wheat']['supplyDownstreamEnabled'] is True
# 3. Turn off logistics fleet on wheat
state = engine.apply_actions(state, [{'type': 'TOGGLE_LOGISTICS', 'productId': 'wheat'}])
assert state['products']['wheat']['logisticsEnabled'] is False
# Simulate 5 seconds: wheat stock will NOT be sold by logistics fleet
cash_before = state['cash']
state['lastTick'] = time.time() - 5.0
state = engine.process_offline_and_ticks(state)
assert state['cash'] == cash_before # Wheat logistics paused, no wholesale sales
assert state['products']['flour']['totalProduced'] > 0 # Flour received wheat!
print("PASS: test_policy_toggles")
if __name__ == '__main__': if __name__ == '__main__':
test_initial_state() test_initial_state()
test_raw_production_and_logistics() test_raw_production_and_logistics()
test_chained_dependency_and_bottleneck() test_chained_dependency_and_bottleneck()
test_chained_microchips_and_boards() test_chained_microchips_and_boards()
test_policy_toggles()
test_ipo_and_shares() test_ipo_and_shares()
print("\nALL BACKEND ENGINE TESTS PASSED SUCCESSFULLY!") print("\nALL BACKEND ENGINE TESTS PASSED SUCCESSFULLY!")

View file

@ -216,20 +216,14 @@
color: var(--emerald-dark); color: var(--emerald-dark);
} }
/* All Sectors Overview Grid (Strict 6, 3, or 2 grid for 6 sectors) */ /* All Sectors Overview Grid (Strict 3 columns on desktop, 2 columns on mobile/tablet) */
.all-sectors-summary-grid { .all-sectors-summary-grid {
display: grid; display: grid;
grid-template-columns: repeat(3, 1fr); grid-template-columns: repeat(3, 1fr);
gap: 12px; gap: 12px;
} }
@media (min-width: 1400px) { @media (max-width: 900px) {
.all-sectors-summary-grid {
grid-template-columns: repeat(6, 1fr);
}
}
@media (max-width: 768px) {
.all-sectors-summary-grid { .all-sectors-summary-grid {
grid-template-columns: repeat(2, 1fr); grid-template-columns: repeat(2, 1fr);
} }
@ -243,6 +237,7 @@
} }
.sector-mini-card { .sector-mini-card {
background: var(--bg-surface); background: var(--bg-surface);
border: 1px solid var(--border-color); border: 1px solid var(--border-color);

View file

@ -300,6 +300,62 @@
color: var(--amber-dark); color: var(--amber-dark);
} }
/* Facility Logistics & Downstream Policy Bar */
.facility-policy-bar {
display: flex;
gap: 6px;
align-items: center;
background: #f8fafc;
border: 1px solid var(--border-color);
border-radius: var(--radius-sm);
padding: 4px 6px;
}
.policy-label {
font-size: 0.68rem;
font-weight: 700;
color: var(--text-muted);
text-transform: uppercase;
letter-spacing: 0.03em;
margin-right: 2px;
}
.btn-policy-toggle {
display: inline-flex;
align-items: center;
gap: 4px;
background: var(--bg-surface);
border: 1px solid var(--border-color);
border-radius: var(--radius-sm);
padding: 3px 7px;
font-size: 0.72rem;
font-weight: 600;
color: var(--text-secondary);
cursor: pointer;
transition: all 0.15s;
user-select: none;
}
.btn-policy-toggle:hover {
border-color: var(--border-subtle);
background: var(--bg-surface-sub);
}
.btn-policy-toggle.active {
background: #ecfdf5;
border-color: #6ee7b7;
color: var(--emerald-dark);
font-weight: 700;
}
.btn-policy-toggle.inactive {
background: #fef2f2;
border-color: #fecaca;
color: #b91c1c;
font-weight: 700;
}
/* Action Buttons */ /* Action Buttons */
.facility-actions-row { .facility-actions-row {
display: flex; display: flex;

View file

@ -213,6 +213,34 @@ class ProductionApp {
break; break;
} }
case 'TOGGLE_LOGISTICS': {
const p = getProduct(action.productId);
const pState = state.products[action.productId];
if (p && pState) {
const nextVal = pState.logisticsEnabled === false ? true : false;
pState.logisticsEnabled = nextVal;
this.showToast(`${p.name} Wholesale Fleet: ${nextVal ? 'ENABLED' : 'PAUSED'}`, nextVal ? 'success' : 'info');
this.sync.queueAction(action);
this.updateActiveView();
this.updateInspectorPane();
}
break;
}
case 'TOGGLE_DOWNSTREAM_SUPPLY': {
const p = getProduct(action.productId);
const pState = state.products[action.productId];
if (p && pState) {
const nextVal = pState.supplyDownstreamEnabled === false ? true : false;
pState.supplyDownstreamEnabled = nextVal;
this.showToast(`${p.name} Downstream Supply: ${nextVal ? 'ALLOWED' : 'BLOCKED'}`, nextVal ? 'success' : 'warning');
this.sync.queueAction(action);
this.updateActiveView();
this.updateInspectorPane();
}
break;
}
case 'MANUAL_CYCLE': { case 'MANUAL_CYCLE': {
const p = getProduct(action.productId); const p = getProduct(action.productId);
const pState = state.products[action.productId]; const pState = state.products[action.productId];

View file

@ -68,16 +68,21 @@ export class SimulationEngine {
} }
} else { } else {
// Manufacturing logic: // Manufacturing logic:
// If cycle is ready to start (cycleProgress <= 0.0), check and reserve inputs. // Check if inputs can be consumed (only if upstream producer has supplyDownstreamEnabled !== false)
// If already in progress, advance cycle until completion.
const neededInputs = p.inputs.map(inp => ({ const neededInputs = p.inputs.map(inp => ({
id: inp.id, id: inp.id,
amount: inp.amount * lines amount: inp.amount * lines
})); }));
const canAccessAllInputs = neededInputs.every(inp => {
const upstreamState = state.products[inp.id];
// If upstream has explicitly disabled downstream delivery, downstream cannot pull
if (upstreamState && upstreamState.supplyDownstreamEnabled === false) return false;
return (upstreamState?.stock || 0) >= inp.amount;
});
if (pState.cycleProgress <= 0.0) { if (pState.cycleProgress <= 0.0) {
const canStart = neededInputs.every(inp => (state.products[inp.id]?.stock || 0) >= inp.amount); if (canAccessAllInputs) {
if (canStart) {
for (const inp of neededInputs) { for (const inp of neededInputs) {
state.products[inp.id].stock = Math.max(0, state.products[inp.id].stock - inp.amount); state.products[inp.id].stock = Math.max(0, state.products[inp.id].stock - inp.amount);
} }
@ -102,7 +107,12 @@ export class SimulationEngine {
} }
// Check if another batch can immediately start this tick // Check if another batch can immediately start this tick
const canStartNext = neededInputs.every(inp => (state.products[inp.id]?.stock || 0) >= inp.amount); const canStartNext = neededInputs.every(inp => {
const upstreamState = state.products[inp.id];
if (upstreamState && upstreamState.supplyDownstreamEnabled === false) return false;
return (upstreamState?.stock || 0) >= inp.amount;
});
if (canStartNext) { if (canStartNext) {
for (const inp of neededInputs) { for (const inp of neededInputs) {
state.products[inp.id].stock = Math.max(0, state.products[inp.id].stock - inp.amount); state.products[inp.id].stock = Math.max(0, state.products[inp.id].stock - inp.amount);
@ -115,17 +125,21 @@ export class SimulationEngine {
} }
// Continuous Logistics Distribution (Wholesale Sales) // Continuous Logistics Distribution (Wholesale Sales)
// Intermediate goods required by active downstream lines have a reserved buffer so logistics won't starve them // Respect logisticsEnabled flag (can be toggled on/off by player)
const logisticsUnits = pState.logistics || 0; const logisticsUnits = pState.logistics || 0;
if (logisticsUnits > 0 && pState.stock > 0) { const isLogisticsActive = pState.logisticsEnabled !== false;
// Check if any active unlocked facility needs this product as an input
if (isLogisticsActive && logisticsUnits > 0 && pState.stock > 0) {
// If supply downstream is enabled, reserve stock for active downstream lines
let reservedStock = 0; let reservedStock = 0;
for (const otherP of PRODUCTS) { if (pState.supplyDownstreamEnabled !== false) {
if (otherP.inputs && otherP.inputs.some(inp => inp.id === p.id)) { for (const otherP of PRODUCTS) {
const otherState = state.products[otherP.id]; if (otherP.inputs && otherP.inputs.some(inp => inp.id === p.id)) {
if (otherState && otherState.unlocked && otherState.lines > 0) { const otherState = state.products[otherP.id];
const inpDef = otherP.inputs.find(inp => inp.id === p.id); if (otherState && otherState.unlocked && otherState.lines > 0) {
reservedStock = Math.max(reservedStock, inpDef.amount * otherState.lines); const inpDef = otherP.inputs.find(inp => inp.id === p.id);
reservedStock = Math.max(reservedStock, inpDef.amount * otherState.lines);
}
} }
} }
} }

View file

@ -42,12 +42,15 @@ export function getInitialState() {
lines: p.unlockedDefault ? 1 : 0, lines: p.unlockedDefault ? 1 : 0,
manager: false, manager: false,
logistics: p.unlockedDefault ? 1 : 0, logistics: p.unlockedDefault ? 1 : 0,
logisticsEnabled: true,
supplyDownstreamEnabled: true,
cycleProgress: 0.0, cycleProgress: 0.0,
isBottlenecked: false, isBottlenecked: false,
totalProduced: 0.0 totalProduced: 0.0
}; };
}); });
// Initialize boardroom perks // Initialize boardroom perks
BOARDROOM_PERKS.forEach(perk => { BOARDROOM_PERKS.forEach(perk => {
state.boardroomPerks[perk.id] = 0; state.boardroomPerks[perk.id] = 0;

View file

@ -53,6 +53,31 @@ export function mountInspector(container, state, onAction) {
</div> </div>
</div> </div>
<!-- Distribution Fleet & Downstream Supply Policies -->
<div class="inspector-section">
<h4 class="section-heading">
<iconify-icon icon="lucide:sliders"></iconify-icon> Supply & Distribution Policies
</h4>
<div class="liquidation-buttons">
<button class="btn-corp-secondary" id="insp-btn-toggle-logistics" data-product="${product.id}" title="Toggle Wholesale Fleet on/off">
<span class="liquidation-btn-label">
<iconify-icon icon="${(pState.logisticsEnabled !== false) ? 'lucide:truck' : 'lucide:truck-x'}"></iconify-icon> Wholesale Fleet
</span>
<span class="liquidation-btn-val" id="insp-toggle-log-status" style="color: ${(pState.logisticsEnabled !== false) ? 'var(--emerald-dark)' : 'var(--amber-dark)'};">
${(pState.logisticsEnabled !== false) ? 'ACTIVE' : 'PAUSED'}
</span>
</button>
<button class="btn-corp-secondary" id="insp-btn-toggle-downstream" data-product="${product.id}" title="Toggle Raw Delivery to Downstream Consumers on/off">
<span class="liquidation-btn-label">
<iconify-icon icon="${(pState.supplyDownstreamEnabled !== false) ? 'lucide:git-pull-request' : 'lucide:git-pull-request-closed'}"></iconify-icon> Downstream
</span>
<span class="liquidation-btn-val" id="insp-toggle-down-status" style="color: ${(pState.supplyDownstreamEnabled !== false) ? 'var(--emerald-dark)' : 'var(--amber-dark)'};">
${(pState.supplyDownstreamEnabled !== false) ? 'ALLOWED' : 'BLOCKED'}
</span>
</button>
</div>
</div>
<!-- Instant Wholesale Market Liquidation --> <!-- Instant Wholesale Market Liquidation -->
<div class="inspector-section"> <div class="inspector-section">
<h4 class="section-heading"> <h4 class="section-heading">
@ -145,6 +170,16 @@ export function mountInspector(container, state, onAction) {
allBtn.onclick = () => onAction({ type: 'LIQUIDATE_STOCK', productId: product.id, ratio: 1.0 }); allBtn.onclick = () => onAction({ type: 'LIQUIDATE_STOCK', productId: product.id, ratio: 1.0 });
} }
const toggleLogBtn = container.querySelector('#insp-btn-toggle-logistics');
if (toggleLogBtn) {
toggleLogBtn.onclick = () => onAction({ type: 'TOGGLE_LOGISTICS', productId: product.id });
}
const toggleDownBtn = container.querySelector('#insp-btn-toggle-downstream');
if (toggleDownBtn) {
toggleDownBtn.onclick = () => onAction({ type: 'TOGGLE_DOWNSTREAM_SUPPLY', productId: product.id });
}
container.querySelectorAll('.chain-item-row').forEach(row => { container.querySelectorAll('.chain-item-row').forEach(row => {
row.onclick = () => onAction({ type: 'SELECT_PRODUCT', productId: row.dataset.product }); row.onclick = () => onAction({ type: 'SELECT_PRODUCT', productId: row.dataset.product });
}); });
@ -166,6 +201,21 @@ export function updateInspector(container, state) {
elStock.textContent = `${fmtNum(pState.stock, 0)} ${product.unit}`; elStock.textContent = `${fmtNum(pState.stock, 0)} ${product.unit}`;
} }
// Update policy statuses
const isLogActive = pState.logisticsEnabled !== false;
const elLogStatus = document.getElementById('insp-toggle-log-status');
if (elLogStatus) {
elLogStatus.textContent = isLogActive ? 'ACTIVE' : 'PAUSED';
elLogStatus.style.color = isLogActive ? 'var(--emerald-dark)' : 'var(--amber-dark)';
}
const isDownActive = pState.supplyDownstreamEnabled !== false;
const elDownStatus = document.getElementById('insp-toggle-down-status');
if (elDownStatus) {
elDownStatus.textContent = isDownActive ? 'ALLOWED' : 'BLOCKED';
elDownStatus.style.color = isDownActive ? 'var(--emerald-dark)' : 'var(--amber-dark)';
}
// Update liquidation values & buttons // Update liquidation values & buttons
const totalLiquidationValue = (pState.stock || 0) * product.basePrice; const totalLiquidationValue = (pState.stock || 0) * product.basePrice;
const halfLiquidationValue = totalLiquidationValue * 0.5; const halfLiquidationValue = totalLiquidationValue * 0.5;

View file

@ -127,12 +127,29 @@ export function renderFacilityCardHtml(p, pState, state, buyMode) {
</div> </div>
<div class="stat-cell" title="Logistics fleets automatically sell stockpiled goods continuously onto the wholesale market (0.75 units/sec per fleet)"> <div class="stat-cell" title="Logistics fleets automatically sell stockpiled goods continuously onto the wholesale market (0.75 units/sec per fleet)">
<span class="stat-label">Logistics Auto-Sell:</span> <span class="stat-label">Logistics Auto-Sell:</span>
<span class="stat-value font-mono" id="logistics-${p.id}">${pState.logistics || 0} fleets (${fmtNum((pState.logistics || 0) * 0.75, 2)}/s)</span> <span class="stat-value font-mono" id="logistics-${p.id}">
${pState.logistics || 0} fleets (${(pState.logisticsEnabled !== false) ? `${fmtNum((pState.logistics || 0) * 0.75, 2)}/s` : 'Paused'})
</span>
</div> </div>
</div> </div>
${inputsHtml} ${inputsHtml}
<!-- Facility Supply & Distribution Policies -->
<div class="facility-policy-bar">
<span class="policy-label">Policy:</span>
<button class="btn-policy-toggle ${(pState.logisticsEnabled !== false) ? 'active' : 'inactive'} btn-toggle-logistics" id="btn-toggle-logistics-${p.id}" data-product="${p.id}" title="Toggle Wholesale Distribution Fleet on/off">
<iconify-icon icon="${(pState.logisticsEnabled !== false) ? 'lucide:truck' : 'lucide:truck-x'}"></iconify-icon>
<span id="btn-toggle-logistics-text-${p.id}">Wholesale Fleet: ${(pState.logisticsEnabled !== false) ? 'ON' : 'OFF'}</span>
</button>
<button class="btn-policy-toggle ${(pState.supplyDownstreamEnabled !== false) ? 'active' : 'inactive'} btn-toggle-downstream" id="btn-toggle-downstream-${p.id}" data-product="${p.id}" title="Toggle raw material supply to downstream manufacturing factories on/off">
<iconify-icon icon="${(pState.supplyDownstreamEnabled !== false) ? 'lucide:git-pull-request' : 'lucide:git-pull-request-closed'}"></iconify-icon>
<span id="btn-toggle-downstream-text-${p.id}">Downstream Supply: ${(pState.supplyDownstreamEnabled !== false) ? 'ON' : 'OFF'}</span>
</button>
</div>
<!-- Facility Actions Bar --> <!-- Facility Actions Bar -->
<div class="facility-actions-row"> <div class="facility-actions-row">
<button class="btn-corp-action btn-cycle" data-product="${p.id}" title="Run 1 manual production cycle"> <button class="btn-corp-action btn-cycle" data-product="${p.id}" title="Run 1 manual production cycle">
@ -223,12 +240,45 @@ export function updateFacilityCardDom(p, pState, state, buyMode) {
const elLogistics = document.getElementById(`logistics-${p.id}`); const elLogistics = document.getElementById(`logistics-${p.id}`);
if (elLogistics) { if (elLogistics) {
const count = pState.logistics || 0; const count = pState.logistics || 0;
const logText = `${count} fleets (${fmtNum(count * 0.75, 2)}/s)`; const isLogActive = pState.logisticsEnabled !== false;
const ratePart = isLogActive ? `${fmtNum(count * 0.75, 2)}/s` : 'Paused';
const logText = `${count} fleets (${ratePart})`;
if (elLogistics.textContent !== logText) { if (elLogistics.textContent !== logText) {
elLogistics.textContent = logText; elLogistics.textContent = logText;
} }
} }
// Policy Button Toggles DOM State
const btnToggleLog = document.getElementById(`btn-toggle-logistics-${p.id}`);
if (btnToggleLog) {
const isLogActive = pState.logisticsEnabled !== false;
btnToggleLog.className = `btn-policy-toggle ${isLogActive ? 'active' : 'inactive'} btn-toggle-logistics`;
const textSpan = document.getElementById(`btn-toggle-logistics-text-${p.id}`);
if (textSpan) {
const tText = `Wholesale Fleet: ${isLogActive ? 'ON' : 'OFF'}`;
if (textSpan.textContent !== tText) textSpan.textContent = tText;
}
const iconEl = btnToggleLog.querySelector('iconify-icon');
if (iconEl) {
iconEl.setAttribute('icon', isLogActive ? 'lucide:truck' : 'lucide:truck-x');
}
}
const btnToggleDown = document.getElementById(`btn-toggle-downstream-${p.id}`);
if (btnToggleDown) {
const isDownActive = pState.supplyDownstreamEnabled !== false;
btnToggleDown.className = `btn-policy-toggle ${isDownActive ? 'active' : 'inactive'} btn-toggle-downstream`;
const textSpan = document.getElementById(`btn-toggle-downstream-text-${p.id}`);
if (textSpan) {
const tText = `Downstream Supply: ${isDownActive ? 'ON' : 'OFF'}`;
if (textSpan.textContent !== tText) textSpan.textContent = tText;
}
const iconEl = btnToggleDown.querySelector('iconify-icon');
if (iconEl) {
iconEl.setAttribute('icon', isDownActive ? 'lucide:git-pull-request' : 'lucide:git-pull-request-closed');
}
}
const elProgress = document.getElementById(`progress-${p.id}`); const elProgress = document.getElementById(`progress-${p.id}`);
if (elProgress) { if (elProgress) {
const progressPct = Math.min(100, Math.floor((pState.cycleProgress || 0) * 100)); const progressPct = Math.min(100, Math.floor((pState.cycleProgress || 0) * 100));

View file

@ -39,12 +39,17 @@ export function getProductOutputRates(p, pState, state) {
// 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;
const logisticsPriceMult = state.upgrades?.global_supply_chain ? 1.5 : 1.0; const isLogisticsActive = pState.logisticsEnabled !== false;
const salePrice = p.basePrice * logisticsPriceMult; let revRate = 0;
const maxSellCapacity = logisticsUnits * 0.75;
// If stockpiled, sells at max fleet capacity; otherwise bottlenecked by production velocity if (isLogisticsActive && logisticsUnits > 0) {
const effectiveSellRate = Math.min(maxSellCapacity, pState.stock > 0 ? maxSellCapacity : prodRate); const logisticsPriceMult = state.upgrades?.global_supply_chain ? 1.5 : 1.0;
const revRate = effectiveSellRate * salePrice; const salePrice = p.basePrice * logisticsPriceMult;
const maxSellCapacity = logisticsUnits * 0.75;
// 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;
}
return { prodRate, revRate, isBottlenecked, milestoneMult }; return { prodRate, revRate, isBottlenecked, milestoneMult };
} }
@ -63,23 +68,32 @@ export function computeProductNetFlow(product, state) {
// Calculate how much active downstream factories consume per second // Calculate how much active downstream factories consume per second
let consumedRate = 0; let consumedRate = 0;
for (const otherP of PRODUCTS) { const isSupplyDownstreamActive = pState.supplyDownstreamEnabled !== false;
if (!otherP.inputs) continue;
const inpMatch = otherP.inputs.find(inp => inp.id === product.id); if (isSupplyDownstreamActive) {
if (inpMatch) { for (const otherP of PRODUCTS) {
const otherState = state.products[otherP.id]; if (!otherP.inputs) continue;
if (otherState && otherState.unlocked && otherState.lines > 0 && !otherState.isBottlenecked) { const inpMatch = otherP.inputs.find(inp => inp.id === product.id);
const otherRates = getProductOutputRates(otherP, otherState, state); if (inpMatch) {
const cyclesPerSec = otherRates.prodRate / (otherP.outputAmount * otherState.lines); const otherState = state.products[otherP.id];
consumedRate += inpMatch.amount * otherState.lines * cyclesPerSec; if (otherState && otherState.unlocked && otherState.lines > 0 && !otherState.isBottlenecked) {
const otherRates = getProductOutputRates(otherP, otherState, state);
const cyclesPerSec = otherRates.prodRate / (otherP.outputAmount * otherState.lines);
consumedRate += inpMatch.amount * otherState.lines * cyclesPerSec;
}
} }
} }
} }
const logisticsUnits = pState.logistics || 0; const logisticsUnits = pState.logistics || 0;
const maxSellCapacity = logisticsUnits * 0.75; const isLogisticsActive = pState.logisticsEnabled !== false;
const availableForSale = Math.max(0, effectiveProd - consumedRate); let soldRate = 0;
const soldRate = Math.min(maxSellCapacity, pState.stock > 0 ? maxSellCapacity : availableForSale);
if (isLogisticsActive && logisticsUnits > 0) {
const maxSellCapacity = logisticsUnits * 0.75;
const availableForSale = Math.max(0, effectiveProd - consumedRate);
soldRate = Math.min(maxSellCapacity, pState.stock > 0 ? maxSellCapacity : availableForSale);
}
const netRate = effectiveProd - consumedRate - soldRate; const netRate = effectiveProd - consumedRate - soldRate;

View file

@ -64,6 +64,14 @@ export function mountProductionFloor(container, state, onAction, engine) {
container.querySelectorAll('.btn-expand-logistics').forEach(btn => { container.querySelectorAll('.btn-expand-logistics').forEach(btn => {
btn.onclick = () => onAction({ type: 'BUY_LOGISTICS', productId: btn.dataset.product }); btn.onclick = () => onAction({ type: 'BUY_LOGISTICS', productId: btn.dataset.product });
}); });
container.querySelectorAll('.btn-toggle-logistics').forEach(btn => {
btn.onclick = () => onAction({ type: 'TOGGLE_LOGISTICS', productId: btn.dataset.product });
});
container.querySelectorAll('.btn-toggle-downstream').forEach(btn => {
btn.onclick = () => onAction({ type: 'TOGGLE_DOWNSTREAM_SUPPLY', productId: btn.dataset.product });
});
} }
/** /**