start the next production cycle in the tick the last one completed

This commit is contained in:
2026-08-12 21:27:25 +02:00
parent dd061082fb
commit 0b859bd1a4
2 changed files with 83 additions and 32 deletions

View File

@@ -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
// ---------------------------------------------------------------------------