start the next production cycle in the tick the last one completed
tickProduction and tickShipyardProduction cleared the finished cycle and then refused to start another in the same tick, so every cycle really took its duration plus one tick. The cost scales inversely with duration: a 1s recipe ran at 31 ticks instead of 30 and lost 3.2% of its throughput, a 12s recipe 0.3%, which skewed the relative rates the recipe tree is tuned around. Fall through from completion into the start attempt instead. The start code is straight-line, so at most one cycle still begins per building per tick, even for a duration that rounds to zero ticks, and the outputs just deposited count against the space check, so a cycle whose output no longer fits waits exactly as it did before. The balancing model already computed rates as amount / duration_seconds (tools/threat_report.py), so this brings the simulation up to the documented numbers rather than moving them. The combat arena never ticks production, so its tuning is untouched. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Ne3mejABZoLWKLh8fgpM3x
This commit is contained in:
@@ -495,13 +495,13 @@ TEST_CASE("BuildingSystem: miner output buffer stalls when full", "[building]")
|
||||
|
||||
Tick tick = 0;
|
||||
// Construction (10s) then cycle 1 starts at tick 300 (completesAt=330).
|
||||
// Cycle 1 completes at tick 330: deposit item, continue (no same-tick restart).
|
||||
// Cycle 2 starts at tick 331 (completesAt=361).
|
||||
// Cycle 2 completes at tick 361: deposit item → buffer=2, cycle 3 stalls.
|
||||
// Need to process through tick 361: 362 ticks total.
|
||||
// Cycle 1 completes at tick 330 and cycle 2 starts in that same tick
|
||||
// (completesAt=360). Cycle 2 completes at tick 360: deposit item -> 2 items held,
|
||||
// which fills the buffer (capacity 2), so cycle 3 cannot start.
|
||||
// Need to process through tick 360: 361 ticks total.
|
||||
runTicks(f.bs, f.cfg, f.state, f.belts, f.stock,
|
||||
static_cast<int>(secondsToTicks(10.0))
|
||||
+ 2 * static_cast<int>(secondsToTicks(1.0)) + 2,
|
||||
+ 2 * static_cast<int>(secondsToTicks(1.0)) + 1,
|
||||
tick);
|
||||
|
||||
const Building* b = findBuilding(f.state, id);
|
||||
@@ -512,6 +512,49 @@ TEST_CASE("BuildingSystem: miner output buffer stalls when full", "[building]")
|
||||
REQUIRE_FALSE(b->production.has_value());
|
||||
}
|
||||
|
||||
TEST_CASE("BuildingSystem: the next cycle starts on the tick the last one completed",
|
||||
"[building]")
|
||||
{
|
||||
// A cycle takes exactly its recipe duration, so a building whose output keeps
|
||||
// draining produces at the configured rate (REQ-MAT-CYCLE). An idle tick between
|
||||
// cycles would cost a one-second recipe about 3% of its throughput.
|
||||
PlacementFixture f;
|
||||
|
||||
const BuildingId id =
|
||||
f.bs.place(f.state, BuildingType::Miner, QPoint(0, 0), Rotation::East, 0).value();
|
||||
f.bs.setRecipe(f.state, id, "mine_iron_ore");
|
||||
|
||||
const Tick cycleTicks = secondsToTicks(1.0); // mine_iron_ore duration
|
||||
|
||||
Tick tick = 0;
|
||||
// Construction completes at tick 300 and cycle 1 starts in that same tick.
|
||||
runTicks(f.bs, f.cfg, f.state, f.belts, f.stock,
|
||||
static_cast<int>(secondsToTicks(10.0)) + 1, tick);
|
||||
const Building* b = findBuilding(f.state, id);
|
||||
REQUIRE(b != nullptr);
|
||||
REQUIRE(b->production.has_value());
|
||||
const Tick firstCompletesAt = b->production->completesAt;
|
||||
|
||||
// Process up to and including that completion tick: the next cycle is already
|
||||
// running, due exactly one duration later rather than one duration plus a tick.
|
||||
runTicks(f.bs, f.cfg, f.state, f.belts, f.stock, static_cast<int>(cycleTicks), tick);
|
||||
b = findBuilding(f.state, id);
|
||||
REQUIRE(b->getOutputItemCount() == 1);
|
||||
REQUIRE(b->production.has_value());
|
||||
REQUIRE(b->production->completesAt == firstCompletesAt + cycleTicks);
|
||||
|
||||
// Nothing hauls the ore away here, so the buffer (capacity 2) would stall the third
|
||||
// cycle. Drain it and confirm the cadence holds across the next boundary too.
|
||||
f.bs.forEachBuilding(f.state, [](Building& building) {
|
||||
building.outputBuffer.items.clear();
|
||||
for (std::vector<BeltItemSlot>& lane : building.emergingItems) { lane.clear(); }
|
||||
});
|
||||
runTicks(f.bs, f.cfg, f.state, f.belts, f.stock, static_cast<int>(cycleTicks), tick);
|
||||
b = findBuilding(f.state, id);
|
||||
REQUIRE(b->production.has_value());
|
||||
REQUIRE(b->production->completesAt == firstCompletesAt + 2 * cycleTicks);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// REQ-UI-DEBUG-OVERLAY production counts
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
Reference in New Issue
Block a user