fix(engine): implement input batch reservation to prevent finished goods disappearance on input shortage

This commit is contained in:
Christian 2026-10-16 00:01:18 +02:00
parent da5435afbc
commit 007fee47d4
2 changed files with 60 additions and 48 deletions

View file

@ -269,30 +269,33 @@ def process_offline_and_ticks(state: dict) -> dict:
p_state['stock'] = p_state.get('stock', 0.0) + produced p_state['stock'] = p_state.get('stock', 0.0) + produced
p_state['totalProduced'] = p_state.get('totalProduced', 0.0) + produced p_state['totalProduced'] = p_state.get('totalProduced', 0.0) + produced
else: else:
# Manufacturing requiring inputs # Manufacturing logic with input batch reservation
max_cycles = int(min( needed = [(inp['id'], inp['amount'] * lines) for inp in inputs]
state['products'].get(inp['id'], {}).get('stock', 0.0) // (inp['amount'] * lines) can_start = all(state['products'].get(item_id, {}).get('stock', 0.0) >= req for item_id, req in needed)
for inp in inputs
)) if inputs else 0
if max_cycles <= 0: if p_state.get('cycleProgress', 0.0) <= 0.0:
p_state['isBottlenecked'] = True if not can_start:
p_state['isBottlenecked'] = True
else:
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)
p_state['cycleProgress'] = p_state.get('cycleProgress', 0.0) + (cycle_rate * dt)
else: else:
p_state['isBottlenecked'] = False p_state['isBottlenecked'] = False
p_state['cycleProgress'] = p_state.get('cycleProgress', 0.0) + (cycle_rate * dt) p_state['cycleProgress'] = p_state.get('cycleProgress', 0.0) + (cycle_rate * dt)
if p_state['cycleProgress'] >= 1.0: if p_state['cycleProgress'] >= 1.0:
cycles_to_run = min(int(p_state['cycleProgress']), max_cycles) p_state['cycleProgress'] = 0.0
if cycles_to_run > 0: produced = p_cfg['outputAmount'] * lines
p_state['cycleProgress'] -= cycles_to_run p_state['stock'] = p_state.get('stock', 0.0) + produced
for inp in inputs: p_state['totalProduced'] = p_state.get('totalProduced', 0.0) + produced
if inp['id'] in state['products']:
state['products'][inp['id']]['stock'] = max( # Check next batch
0.0, if can_start:
state['products'][inp['id']]['stock'] - (inp['amount'] * lines * cycles_to_run) for item_id, req in needed:
) state['products'][item_id]['stock'] = max(0.0, state['products'][item_id].get('stock', 0.0) - req)
produced = cycles_to_run * p_cfg['outputAmount'] * lines else:
p_state['stock'] = p_state.get('stock', 0.0) + produced p_state['isBottlenecked'] = True
p_state['totalProduced'] = p_state.get('totalProduced', 0.0) + produced
# Passive Logistics Sales # Passive Logistics Sales
logistics = p_state.get('logistics', 0) logistics = p_state.get('logistics', 0)

View file

@ -67,45 +67,54 @@ export class SimulationEngine {
} }
} }
} else { } else {
// Check if we have required input stock // Manufacturing logic with input batch reservation:
const maxCyclesPossible = Math.min( // Check if factory is currently executing an in-progress cycle or needs to start one
...p.inputs.map(inp => { const neededInputs = p.inputs.map(inp => ({
const inputStock = state.products[inp.id]?.stock || 0; id: inp.id,
const reqPerCycle = inp.amount * lines; amount: inp.amount * lines
return reqPerCycle > 0 ? Math.floor(inputStock / reqPerCycle) : 0; }));
})
);
if (maxCyclesPossible <= 0) { const canStartBatch = neededInputs.every(inp => (state.products[inp.id]?.stock || 0) >= inp.amount);
pState.isBottlenecked = true;
// Retain existing cycle progress until inputs arrive if (pState.cycleProgress <= 0.0) {
// Not currently running a cycle
if (!canStartBatch) {
pState.isBottlenecked = true;
} else {
// Start batch by reserving required inputs
pState.isBottlenecked = false;
for (const inp of neededInputs) {
if (state.products[inp.id]) {
state.products[inp.id].stock = Math.max(0, state.products[inp.id].stock - inp.amount);
}
}
pState.cycleProgress += cycleRate * dt;
}
} else { } else {
// Cycle already in progress (inputs already secured)
pState.isBottlenecked = false; pState.isBottlenecked = false;
pState.cycleProgress += cycleRate * dt; pState.cycleProgress += cycleRate * dt;
if (pState.cycleProgress >= 1.0) { if (pState.cycleProgress >= 1.0) {
const cyclesToRun = Math.min(Math.floor(pState.cycleProgress), maxCyclesPossible); // Finished cycle, deliver produced goods
if (cyclesToRun > 0) { pState.cycleProgress = 0.0;
pState.cycleProgress -= cyclesToRun; const produced = p.outputAmount * lines;
pState.stock += produced;
pState.totalProduced = (pState.totalProduced || 0) + produced;
state.stats.totalProduced = (state.stats.totalProduced || 0) + produced;
if (this._sectorAccumulators[p.sector]) {
this._sectorAccumulators[p.sector].units += produced;
}
// Deduct inputs // Immediately check if next batch can start
for (const inp of p.inputs) { if (canStartBatch) {
for (const inp of neededInputs) {
if (state.products[inp.id]) { if (state.products[inp.id]) {
state.products[inp.id].stock = Math.max( state.products[inp.id].stock = Math.max(0, state.products[inp.id].stock - inp.amount);
0,
state.products[inp.id].stock - (inp.amount * lines * cyclesToRun)
);
} }
} }
} else {
// Credit finished goods pState.isBottlenecked = true;
const produced = cyclesToRun * p.outputAmount * lines;
pState.stock += produced;
pState.totalProduced = (pState.totalProduced || 0) + produced;
state.stats.totalProduced = (state.stats.totalProduced || 0) + produced;
if (this._sectorAccumulators[p.sector]) {
this._sectorAccumulators[p.sector].units += produced;
}
} }
} }
} }