Add Distribution Fleet toggle, Downstream Supply toggle, and enforce 3x2 grid on All Sectors
This commit is contained in:
parent
21cb0aeb50
commit
ff8983273e
11 changed files with 328 additions and 54 deletions
|
|
@ -272,9 +272,17 @@ def process_offline_and_ticks(state: dict) -> dict:
|
|||
# Manufacturing logic with clean batch cycle state
|
||||
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:
|
||||
can_start = all(state['products'].get(item_id, {}).get('stock', 0.0) >= req for item_id, req in needed)
|
||||
if can_start:
|
||||
if can_access_inputs():
|
||||
p_state['isBottlenecked'] = False
|
||||
for item_id, req in needed:
|
||||
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
|
||||
|
||||
# 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_start_next:
|
||||
if can_access_inputs():
|
||||
for item_id, req in needed:
|
||||
state['products'][item_id]['stock'] = max(0.0, state['products'][item_id].get('stock', 0.0) - req)
|
||||
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
|
||||
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
|
||||
for other_pid, other_cfg in PRODUCT_CONFIGS.items():
|
||||
other_inputs = other_cfg.get('inputs', [])
|
||||
if any(inp['id'] == pid for inp in other_inputs):
|
||||
other_state = state.get('products', {}).get(other_pid)
|
||||
if other_state and other_state.get('unlocked') and other_state.get('lines', 0) > 0:
|
||||
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']))
|
||||
if p_state.get('supplyDownstreamEnabled', True) is not False:
|
||||
for other_pid, other_cfg in PRODUCT_CONFIGS.items():
|
||||
other_inputs = other_cfg.get('inputs', [])
|
||||
if any(inp['id'] == pid for inp in other_inputs):
|
||||
other_state = state.get('products', {}).get(other_pid)
|
||||
if other_state and other_state.get('unlocked') and other_state.get('lines', 0) > 0:
|
||||
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)
|
||||
if avail > 0:
|
||||
|
|
@ -374,6 +384,18 @@ def apply_actions(state: dict, actions: list) -> dict:
|
|||
state['cash'] -= cost
|
||||
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':
|
||||
earned = calculate_shares_earned(state.get('lifetimeRevenue', 0.0))
|
||||
if earned > 0:
|
||||
|
|
|
|||
|
|
@ -92,10 +92,44 @@ def test_chained_microchips_and_boards():
|
|||
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']})")
|
||||
|
||||
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__':
|
||||
test_initial_state()
|
||||
test_raw_production_and_logistics()
|
||||
test_chained_dependency_and_bottleneck()
|
||||
test_chained_microchips_and_boards()
|
||||
test_policy_toggles()
|
||||
test_ipo_and_shares()
|
||||
print("\nALL BACKEND ENGINE TESTS PASSED SUCCESSFULLY!")
|
||||
|
|
|
|||
|
|
@ -216,20 +216,14 @@
|
|||
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 {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, 1fr);
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
@media (min-width: 1400px) {
|
||||
.all-sectors-summary-grid {
|
||||
grid-template-columns: repeat(6, 1fr);
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
@media (max-width: 900px) {
|
||||
.all-sectors-summary-grid {
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
}
|
||||
|
|
@ -243,6 +237,7 @@
|
|||
}
|
||||
|
||||
|
||||
|
||||
.sector-mini-card {
|
||||
background: var(--bg-surface);
|
||||
border: 1px solid var(--border-color);
|
||||
|
|
|
|||
|
|
@ -300,6 +300,62 @@
|
|||
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 */
|
||||
.facility-actions-row {
|
||||
display: flex;
|
||||
|
|
|
|||
|
|
@ -213,6 +213,34 @@ class ProductionApp {
|
|||
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': {
|
||||
const p = getProduct(action.productId);
|
||||
const pState = state.products[action.productId];
|
||||
|
|
|
|||
|
|
@ -68,16 +68,21 @@ export class SimulationEngine {
|
|||
}
|
||||
} else {
|
||||
// Manufacturing logic:
|
||||
// If cycle is ready to start (cycleProgress <= 0.0), check and reserve inputs.
|
||||
// If already in progress, advance cycle until completion.
|
||||
// Check if inputs can be consumed (only if upstream producer has supplyDownstreamEnabled !== false)
|
||||
const neededInputs = p.inputs.map(inp => ({
|
||||
id: inp.id,
|
||||
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) {
|
||||
const canStart = neededInputs.every(inp => (state.products[inp.id]?.stock || 0) >= inp.amount);
|
||||
if (canStart) {
|
||||
if (canAccessAllInputs) {
|
||||
for (const inp of neededInputs) {
|
||||
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
|
||||
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) {
|
||||
for (const inp of neededInputs) {
|
||||
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)
|
||||
// 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;
|
||||
if (logisticsUnits > 0 && pState.stock > 0) {
|
||||
// Check if any active unlocked facility needs this product as an input
|
||||
const isLogisticsActive = pState.logisticsEnabled !== false;
|
||||
|
||||
if (isLogisticsActive && logisticsUnits > 0 && pState.stock > 0) {
|
||||
// If supply downstream is enabled, reserve stock for active downstream lines
|
||||
let reservedStock = 0;
|
||||
for (const otherP of PRODUCTS) {
|
||||
if (otherP.inputs && otherP.inputs.some(inp => inp.id === p.id)) {
|
||||
const otherState = state.products[otherP.id];
|
||||
if (otherState && otherState.unlocked && otherState.lines > 0) {
|
||||
const inpDef = otherP.inputs.find(inp => inp.id === p.id);
|
||||
reservedStock = Math.max(reservedStock, inpDef.amount * otherState.lines);
|
||||
if (pState.supplyDownstreamEnabled !== false) {
|
||||
for (const otherP of PRODUCTS) {
|
||||
if (otherP.inputs && otherP.inputs.some(inp => inp.id === p.id)) {
|
||||
const otherState = state.products[otherP.id];
|
||||
if (otherState && otherState.unlocked && otherState.lines > 0) {
|
||||
const inpDef = otherP.inputs.find(inp => inp.id === p.id);
|
||||
reservedStock = Math.max(reservedStock, inpDef.amount * otherState.lines);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -42,12 +42,15 @@ export function getInitialState() {
|
|||
lines: p.unlockedDefault ? 1 : 0,
|
||||
manager: false,
|
||||
logistics: p.unlockedDefault ? 1 : 0,
|
||||
logisticsEnabled: true,
|
||||
supplyDownstreamEnabled: true,
|
||||
cycleProgress: 0.0,
|
||||
isBottlenecked: false,
|
||||
totalProduced: 0.0
|
||||
};
|
||||
});
|
||||
|
||||
|
||||
// Initialize boardroom perks
|
||||
BOARDROOM_PERKS.forEach(perk => {
|
||||
state.boardroomPerks[perk.id] = 0;
|
||||
|
|
|
|||
|
|
@ -53,6 +53,31 @@ export function mountInspector(container, state, onAction) {
|
|||
</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 -->
|
||||
<div class="inspector-section">
|
||||
<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 });
|
||||
}
|
||||
|
||||
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 => {
|
||||
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}`;
|
||||
}
|
||||
|
||||
// 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
|
||||
const totalLiquidationValue = (pState.stock || 0) * product.basePrice;
|
||||
const halfLiquidationValue = totalLiquidationValue * 0.5;
|
||||
|
|
|
|||
|
|
@ -127,12 +127,29 @@ export function renderFacilityCardHtml(p, pState, state, buyMode) {
|
|||
</div>
|
||||
<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-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>
|
||||
|
||||
${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 -->
|
||||
<div class="facility-actions-row">
|
||||
<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}`);
|
||||
if (elLogistics) {
|
||||
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) {
|
||||
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}`);
|
||||
if (elProgress) {
|
||||
const progressPct = Math.min(100, Math.floor((pState.cycleProgress || 0) * 100));
|
||||
|
|
|
|||
|
|
@ -39,12 +39,17 @@ export function getProductOutputRates(p, pState, state) {
|
|||
|
||||
// Logistics revenue rate: items sold passively per second * price
|
||||
const logisticsUnits = pState.logistics || 0;
|
||||
const logisticsPriceMult = state.upgrades?.global_supply_chain ? 1.5 : 1.0;
|
||||
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);
|
||||
const revRate = effectiveSellRate * salePrice;
|
||||
const isLogisticsActive = pState.logisticsEnabled !== false;
|
||||
let revRate = 0;
|
||||
|
||||
if (isLogisticsActive && logisticsUnits > 0) {
|
||||
const logisticsPriceMult = state.upgrades?.global_supply_chain ? 1.5 : 1.0;
|
||||
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 };
|
||||
}
|
||||
|
|
@ -63,23 +68,32 @@ export function computeProductNetFlow(product, state) {
|
|||
|
||||
// Calculate how much active downstream factories consume per second
|
||||
let consumedRate = 0;
|
||||
for (const otherP of PRODUCTS) {
|
||||
if (!otherP.inputs) continue;
|
||||
const inpMatch = otherP.inputs.find(inp => inp.id === product.id);
|
||||
if (inpMatch) {
|
||||
const otherState = state.products[otherP.id];
|
||||
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 isSupplyDownstreamActive = pState.supplyDownstreamEnabled !== false;
|
||||
|
||||
if (isSupplyDownstreamActive) {
|
||||
for (const otherP of PRODUCTS) {
|
||||
if (!otherP.inputs) continue;
|
||||
const inpMatch = otherP.inputs.find(inp => inp.id === product.id);
|
||||
if (inpMatch) {
|
||||
const otherState = state.products[otherP.id];
|
||||
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 maxSellCapacity = logisticsUnits * 0.75;
|
||||
const availableForSale = Math.max(0, effectiveProd - consumedRate);
|
||||
const soldRate = Math.min(maxSellCapacity, pState.stock > 0 ? maxSellCapacity : availableForSale);
|
||||
const isLogisticsActive = pState.logisticsEnabled !== false;
|
||||
let soldRate = 0;
|
||||
|
||||
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;
|
||||
|
||||
|
|
|
|||
|
|
@ -64,6 +64,14 @@ export function mountProductionFloor(container, state, onAction, engine) {
|
|||
container.querySelectorAll('.btn-expand-logistics').forEach(btn => {
|
||||
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 });
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue