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
|
# 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:
|
||||||
|
|
|
||||||
|
|
@ -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!")
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
|
|
||||||
|
|
@ -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];
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
|
|
||||||
|
|
@ -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));
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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 });
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue