Animate items entering building input ports
Accepted input items now travel inward across the input port's footprint cell on a virtual input belt (progress 0.0 -> 0.5) before entering the buffer, mirroring the output emergence (REQ-MAT-INPUT-INTAKE). Uses reserve-on-entry accounting: pendingInputCount (buffered + in-transit) gates acceptance so a material never exceeds its 2x cap, and the item becomes consumable only on arrival at the tile centre. Covers the HQ global-stock intake, clears in-transit items on recipe/schematic change, folds the lanes into the checksum, and renders them occluded by the building (shared draw pass with the output emergence). Reuses the shared BeltSlot helpers and the belt-speed getter; no new belt logic. Adds intake travel/arrival and reservation-cap tests, and updates input-buffer assertions to count buffered + in-transit. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DZR44tA8sn4dPqDzAVXyps
This commit is contained in:
@@ -96,6 +96,31 @@ struct Building
|
|||||||
return count;
|
return count;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Items currently travelling inward on each input port's virtual input belt
|
||||||
|
// (REQ-MAT-INPUT-INTAKE); one lane per input port, parallel to inputPorts. Each
|
||||||
|
// lane holds slots at progress [0.0, 0.5], front (highest progress) first. An
|
||||||
|
// in-transit item has reserved a slot in its per-material input buffer but is
|
||||||
|
// not yet consumable — it enters the buffer only on reaching progress 0.5.
|
||||||
|
std::vector<std::vector<BeltItemSlot>> incomingItems;
|
||||||
|
|
||||||
|
// Buffered plus in-transit count of one input material. The acceptance/space
|
||||||
|
// test (REQ-MAT-INPUT-PORTS, REQ-MAT-INPUT-INTAKE) counts in-transit items, so
|
||||||
|
// buffered + reserved never exceeds the material's cap (REQ-MAT-INPUT-BUFFER).
|
||||||
|
int pendingInputCount(const ItemType& type) const
|
||||||
|
{
|
||||||
|
int count = 0;
|
||||||
|
const std::map<ItemType, int>::const_iterator it = inputBuffer.counts.find(type);
|
||||||
|
if (it != inputBuffer.counts.end()) { count = it->second; }
|
||||||
|
for (const std::vector<BeltItemSlot>& lane : incomingItems)
|
||||||
|
{
|
||||||
|
for (const BeltItemSlot& slot : lane)
|
||||||
|
{
|
||||||
|
if (slot.item.type == type) { ++count; }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return count;
|
||||||
|
}
|
||||||
|
|
||||||
// Pre-computed from surface mask at placement; in absolute world coordinates.
|
// Pre-computed from surface mask at placement; in absolute world coordinates.
|
||||||
std::vector<QPoint> bodyCells;
|
std::vector<QPoint> bodyCells;
|
||||||
std::vector<Port> outputPorts;
|
std::vector<Port> outputPorts;
|
||||||
|
|||||||
@@ -35,6 +35,30 @@ QPoint outputBodyTile(QPoint portTile, Rotation direction)
|
|||||||
}
|
}
|
||||||
return portTile;
|
return portTile;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// The building body tile an input port feeds into, given the port's outside belt
|
||||||
|
// tile (port.tile) and its inward flow direction. The virtual input belt occupies
|
||||||
|
// this tile and flows from the outer edge (progress 0.0) to the centre (0.5)
|
||||||
|
// (REQ-MAT-INPUT-INTAKE).
|
||||||
|
QPoint inputBodyTile(QPoint portTile, Rotation inwardDirection)
|
||||||
|
{
|
||||||
|
switch (inwardDirection)
|
||||||
|
{
|
||||||
|
case Rotation::East: return portTile + QPoint( 1, 0);
|
||||||
|
case Rotation::West: return portTile + QPoint(-1, 0);
|
||||||
|
case Rotation::North: return portTile + QPoint( 0, -1);
|
||||||
|
case Rotation::South: return portTile + QPoint( 0, 1);
|
||||||
|
}
|
||||||
|
return portTile;
|
||||||
|
}
|
||||||
|
|
||||||
|
// An input belt accepts a new item at progress 0.0 only when it holds fewer than
|
||||||
|
// three items and the entry slot is clear (nothing within a quarter tile of 0.0),
|
||||||
|
// matching the belt packing used elsewhere (REQ-GW-BELT-CAPACITY).
|
||||||
|
bool inputLaneEntryFree(const std::vector<BeltItemSlot>& lane)
|
||||||
|
{
|
||||||
|
return lane.size() < 3 && (lane.empty() || lane.back().progress >= 0.25);
|
||||||
|
}
|
||||||
} // namespace
|
} // namespace
|
||||||
|
|
||||||
BuildingSystem::BuildingSystem(const GameConfig& config,
|
BuildingSystem::BuildingSystem(const GameConfig& config,
|
||||||
@@ -539,8 +563,11 @@ void BuildingSystem::setRecipe(BuildingId id, const std::string& recipeId)
|
|||||||
building.outputBuffer.items.clear();
|
building.outputBuffer.items.clear();
|
||||||
building.outputBuffer.capacity = 0;
|
building.outputBuffer.capacity = 0;
|
||||||
// Emerging items are part of the output buffer, so clearing it on a
|
// Emerging items are part of the output buffer, so clearing it on a
|
||||||
// recipe change discards them too (REQ-MAT-OUTPUT-EMERGE).
|
// recipe change discards them too (REQ-MAT-OUTPUT-EMERGE); in-transit
|
||||||
|
// input items are discarded and their reservations released
|
||||||
|
// (REQ-MAT-INPUT-INTAKE).
|
||||||
for (std::vector<BeltItemSlot>& lane : building.emergingItems) { lane.clear(); }
|
for (std::vector<BeltItemSlot>& lane : building.emergingItems) { lane.clear(); }
|
||||||
|
for (std::vector<BeltItemSlot>& lane : building.incomingItems) { lane.clear(); }
|
||||||
building.production = std::nullopt;
|
building.production = std::nullopt;
|
||||||
|
|
||||||
if (!recipeId.empty())
|
if (!recipeId.empty())
|
||||||
@@ -588,6 +615,7 @@ void BuildingSystem::setShipLayout(BuildingId id, const ShipLayoutConfig& layout
|
|||||||
building.outputBuffer.items.clear();
|
building.outputBuffer.items.clear();
|
||||||
building.outputBuffer.capacity = 0;
|
building.outputBuffer.capacity = 0;
|
||||||
for (std::vector<BeltItemSlot>& lane : building.emergingItems) { lane.clear(); }
|
for (std::vector<BeltItemSlot>& lane : building.emergingItems) { lane.clear(); }
|
||||||
|
for (std::vector<BeltItemSlot>& lane : building.incomingItems) { lane.clear(); }
|
||||||
if (!building.recipeId.empty() && building.type == BuildingType::Shipyard)
|
if (!building.recipeId.empty() && building.type == BuildingType::Shipyard)
|
||||||
{
|
{
|
||||||
initShipyardBuffers(building);
|
initShipyardBuffers(building);
|
||||||
@@ -693,6 +721,7 @@ void BuildingSystem::tickConstruction(Tick currentTick)
|
|||||||
}
|
}
|
||||||
building.emergingItems.resize(building.outputPorts.size());
|
building.emergingItems.resize(building.outputPorts.size());
|
||||||
building.inputPorts = computeInputPorts(building);
|
building.inputPorts = computeInputPorts(building);
|
||||||
|
building.incomingItems.assign(building.inputPorts.size(), {});
|
||||||
|
|
||||||
if (building.type == BuildingType::SalvageBay)
|
if (building.type == BuildingType::SalvageBay)
|
||||||
{
|
{
|
||||||
@@ -765,21 +794,53 @@ void BuildingSystem::tickConstruction(Tick currentTick)
|
|||||||
void BuildingSystem::tickBeltPull()
|
void BuildingSystem::tickBeltPull()
|
||||||
{
|
{
|
||||||
TRACE();
|
TRACE();
|
||||||
|
// Same per-tick step as the belts, so items travel inward at belt speed
|
||||||
|
// (REQ-GW-BELT-SPEED, REQ-MAT-INPUT-INTAKE).
|
||||||
|
const double progressPerTick = m_belts.getProgressPerTick_tpt();
|
||||||
|
|
||||||
for (Building& building : m_buildings)
|
for (Building& building : m_buildings)
|
||||||
{
|
{
|
||||||
// HQ: pull building_block items and add to global stock.
|
const bool isHq = (building.type == BuildingType::Hq);
|
||||||
if (building.type == BuildingType::Hq)
|
|
||||||
|
// 1. Advance every input belt and deliver arrivals (progress >= 0.5) into
|
||||||
|
// the input buffer — or the global stock for the HQ. Runs for all
|
||||||
|
// buildings so in-transit items keep moving even when feeding is gated
|
||||||
|
// off, and arrivals become consumable before tickProduction (step 4).
|
||||||
|
for (std::size_t i = 0; i < building.incomingItems.size(); ++i)
|
||||||
{
|
{
|
||||||
for (const Port& port : building.inputPorts)
|
std::vector<BeltItemSlot>& lane = building.incomingItems[i];
|
||||||
|
advanceBeltSlots(lane, progressPerTick);
|
||||||
|
while (!lane.empty() && lane.front().progress >= 0.5)
|
||||||
{
|
{
|
||||||
const std::optional<ItemType> peeked = m_belts.peekItem(port);
|
const Item arrived = lane.front().item;
|
||||||
if (peeked && peeked->id == "building_block")
|
lane.erase(lane.begin());
|
||||||
|
if (isHq)
|
||||||
{
|
{
|
||||||
const std::optional<Item> taken = m_belts.tryTakeItem(port);
|
m_addBuildingBlocks(1);
|
||||||
if (taken)
|
}
|
||||||
{
|
else
|
||||||
m_addBuildingBlocks(1);
|
{
|
||||||
}
|
building.inputBuffer.counts[arrived.type]++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. Feed newly accepted items from adjacent belts onto the input belts at
|
||||||
|
// progress 0.0. HQ accepts building blocks into the global stock with no
|
||||||
|
// reservation; other buildings reserve a per-material buffer slot.
|
||||||
|
if (isHq)
|
||||||
|
{
|
||||||
|
for (std::size_t i = 0; i < building.inputPorts.size(); ++i)
|
||||||
|
{
|
||||||
|
const Port& port = building.inputPorts[i];
|
||||||
|
std::vector<BeltItemSlot>& lane = building.incomingItems[i];
|
||||||
|
const std::optional<ItemType> peeked = m_belts.peekItem(port);
|
||||||
|
if (!peeked || peeked->id != "building_block") { continue; }
|
||||||
|
if (!inputLaneEntryFree(lane)) { continue; }
|
||||||
|
const std::optional<Item> taken = m_belts.tryTakeItem(port);
|
||||||
|
if (taken)
|
||||||
|
{
|
||||||
|
lane.push_back(BeltItemSlot{*taken, 0.0});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
continue;
|
continue;
|
||||||
@@ -805,8 +866,11 @@ void BuildingSystem::tickBeltPull()
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
for (const Port& port : building.inputPorts)
|
for (std::size_t i = 0; i < building.inputPorts.size(); ++i)
|
||||||
{
|
{
|
||||||
|
const Port& port = building.inputPorts[i];
|
||||||
|
std::vector<BeltItemSlot>& lane = building.incomingItems[i];
|
||||||
|
|
||||||
const std::optional<ItemType> peeked = m_belts.peekItem(port);
|
const std::optional<ItemType> peeked = m_belts.peekItem(port);
|
||||||
if (!peeked)
|
if (!peeked)
|
||||||
{
|
{
|
||||||
@@ -815,7 +879,7 @@ void BuildingSystem::tickBeltPull()
|
|||||||
|
|
||||||
const ItemType& type = *peeked;
|
const ItemType& type = *peeked;
|
||||||
|
|
||||||
// Accept only if this type is a required input and buffer has space.
|
// Accept only if this type is a required input and the buffer has space.
|
||||||
const std::map<ItemType, int>::const_iterator capIt =
|
const std::map<ItemType, int>::const_iterator capIt =
|
||||||
building.inputBuffer.caps.find(type);
|
building.inputBuffer.caps.find(type);
|
||||||
if (capIt == building.inputBuffer.caps.end() || capIt->second == 0)
|
if (capIt == building.inputBuffer.caps.end() || capIt->second == 0)
|
||||||
@@ -823,14 +887,14 @@ void BuildingSystem::tickBeltPull()
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
const int current = [&]() -> int
|
// Reservation-aware space test: buffered + in-transit must stay under
|
||||||
|
// the cap (REQ-MAT-INPUT-INTAKE).
|
||||||
|
if (building.pendingInputCount(type) >= capIt->second)
|
||||||
{
|
{
|
||||||
const std::map<ItemType, int>::const_iterator it =
|
continue;
|
||||||
building.inputBuffer.counts.find(type);
|
}
|
||||||
return (it != building.inputBuffer.counts.end()) ? it->second : 0;
|
|
||||||
}();
|
|
||||||
|
|
||||||
if (current >= capIt->second)
|
if (!inputLaneEntryFree(lane))
|
||||||
{
|
{
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
@@ -838,7 +902,7 @@ void BuildingSystem::tickBeltPull()
|
|||||||
const std::optional<Item> taken = m_belts.tryTakeItem(port);
|
const std::optional<Item> taken = m_belts.tryTakeItem(port);
|
||||||
if (taken)
|
if (taken)
|
||||||
{
|
{
|
||||||
building.inputBuffer.counts[taken->type]++;
|
lane.push_back(BeltItemSlot{*taken, 0.0});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1148,6 +1212,28 @@ void BuildingSystem::forEachEmergingItem(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void BuildingSystem::forEachIncomingItem(
|
||||||
|
const std::function<void(const ItemType&, QPointF)>& visit) const
|
||||||
|
{
|
||||||
|
for (const Building& building : m_buildings)
|
||||||
|
{
|
||||||
|
for (std::size_t p = 0; p < building.inputPorts.size(); ++p)
|
||||||
|
{
|
||||||
|
const Port& port = building.inputPorts[p];
|
||||||
|
const QPoint bodyTile = inputBodyTile(port.tile, port.direction);
|
||||||
|
const std::vector<BeltItemSlot>& lane = building.incomingItems[p];
|
||||||
|
|
||||||
|
// Render least-progressed first (bottom) → most-progressed last (top),
|
||||||
|
// matching belt item ordering (REQ-GW-TILE-SIZE).
|
||||||
|
for (int i = static_cast<int>(lane.size()) - 1; i >= 0; --i)
|
||||||
|
{
|
||||||
|
visit(lane[i].item.type,
|
||||||
|
beltSlotWorldPos(bodyTile, port.direction, lane[i].progress));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
// Queries
|
// Queries
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
@@ -1342,6 +1428,9 @@ void BuildingSystem::rotateInPlace(BuildingId id, Rotation newRotation)
|
|||||||
b.emergingItems.clear();
|
b.emergingItems.clear();
|
||||||
b.emergingItems.resize(b.outputPorts.size());
|
b.emergingItems.resize(b.outputPorts.size());
|
||||||
b.inputPorts = computeInputPorts(b);
|
b.inputPorts = computeInputPorts(b);
|
||||||
|
// Likewise discard in-transit input items and re-size the input belts to
|
||||||
|
// the new port set (REQ-MAT-INPUT-INTAKE).
|
||||||
|
b.incomingItems.assign(b.inputPorts.size(), {});
|
||||||
|
|
||||||
// Re-register with BeltSystem (items on tile are discarded).
|
// Re-register with BeltSystem (items on tile are discarded).
|
||||||
if (b.type == BuildingType::Belt)
|
if (b.type == BuildingType::Belt)
|
||||||
@@ -1449,6 +1538,7 @@ BuildingId BuildingSystem::placeImmediate(BuildingType type,
|
|||||||
}
|
}
|
||||||
building.emergingItems.resize(building.outputPorts.size());
|
building.emergingItems.resize(building.outputPorts.size());
|
||||||
building.inputPorts = computeInputPorts(building);
|
building.inputPorts = computeInputPorts(building);
|
||||||
|
building.incomingItems.assign(building.inputPorts.size(), {});
|
||||||
|
|
||||||
if (type == BuildingType::SalvageBay)
|
if (type == BuildingType::SalvageBay)
|
||||||
{
|
{
|
||||||
@@ -1564,6 +1654,16 @@ void BuildingSystem::appendChecksum(Hasher& hasher) const
|
|||||||
hasher.append(slot.progress);
|
hasher.append(slot.progress);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
hasher.append(b.incomingItems.size());
|
||||||
|
for (const std::vector<BeltItemSlot>& lane : b.incomingItems)
|
||||||
|
{
|
||||||
|
hasher.append(lane.size());
|
||||||
|
for (const BeltItemSlot& slot : lane)
|
||||||
|
{
|
||||||
|
hasher.append(slot.item.type.id);
|
||||||
|
hasher.append(slot.progress);
|
||||||
|
}
|
||||||
|
}
|
||||||
hasher.append(b.production.has_value());
|
hasher.append(b.production.has_value());
|
||||||
if (b.production.has_value())
|
if (b.production.has_value())
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -132,6 +132,12 @@ public:
|
|||||||
void forEachEmergingItem(
|
void forEachEmergingItem(
|
||||||
const std::function<void(const ItemType&, QPointF)>& visit) const;
|
const std::function<void(const ItemType&, QPointF)>& visit) const;
|
||||||
|
|
||||||
|
// Visits every item currently travelling inward on a building input port's
|
||||||
|
// virtual input belt (REQ-MAT-INPUT-INTAKE), passing the item type and its
|
||||||
|
// world-space centre (in tile units). Least-progressed first (drawn bottom).
|
||||||
|
void forEachIncomingItem(
|
||||||
|
const std::function<void(const ItemType&, QPointF)>& visit) const;
|
||||||
|
|
||||||
// Returns the entity id of the building or construction site whose footprint
|
// Returns the entity id of the building or construction site whose footprint
|
||||||
// exactly coincides with the ghost (type, anchor, rot) and is of the same
|
// exactly coincides with the ghost (type, anchor, rot) and is of the same
|
||||||
// building type. Returns nullopt otherwise.
|
// building type. Returns nullopt otherwise.
|
||||||
|
|||||||
@@ -580,10 +580,93 @@ TEST_CASE("BuildingSystem: smelter input buffer fills from adjacent west-flowing
|
|||||||
|
|
||||||
const Building* b = bs.findBuilding(sid);
|
const Building* b = bs.findBuilding(sid);
|
||||||
REQUIRE(b != nullptr);
|
REQUIRE(b != nullptr);
|
||||||
const std::map<ItemType, int>::const_iterator it =
|
// The item was accepted; it may still be travelling inward on the input belt,
|
||||||
|
// so count buffered + in-transit (REQ-MAT-INPUT-INTAKE).
|
||||||
|
REQUIRE(b->pendingInputCount(ItemType{"iron_ore"}) >= 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
// An accepted input item travels inward on its input belt before it becomes usable
|
||||||
|
// stock: it is reserved (counts against the cap) on entry and only enters the
|
||||||
|
// buffer on reaching the tile centre (REQ-MAT-INPUT-INTAKE).
|
||||||
|
TEST_CASE("BuildingSystem: accepted input travels inward before entering the buffer",
|
||||||
|
"[building]")
|
||||||
|
{
|
||||||
|
const GameConfig cfg = loadConfig();
|
||||||
|
BeltSystem belts(static_cast<double>(kTickRateHz)); // fast belt: 1 tile/tick
|
||||||
|
int stock = 0;
|
||||||
|
std::mt19937 rng(0);
|
||||||
|
BuildingId nextBuildingId = 1;
|
||||||
|
BuildingSystem bs(cfg, belts,
|
||||||
|
[&nextBuildingId]() { return nextBuildingId++; },
|
||||||
|
[&stock](int n) { stock += n; },
|
||||||
|
[](const std::string&, QVector2D, const std::optional<ShipLayoutConfig>&) {},
|
||||||
|
[](const std::string&) -> bool { return true; },
|
||||||
|
rng);
|
||||||
|
|
||||||
|
const BuildingId sid = bs.place(BuildingType::Smelter, QPoint(0, 0), Rotation::East, 0);
|
||||||
|
Tick tick = 0;
|
||||||
|
runTicks(bs, belts, static_cast<int>(secondsToTicks(15.0)) + 1, tick);
|
||||||
|
|
||||||
|
belts.placeBelt(QPoint(2, 0), Rotation::West);
|
||||||
|
belts.tryPutItem(QPoint(2, 0), makeItem("iron_ore"));
|
||||||
|
belts.tick();
|
||||||
|
bs.tickBeltPull(); // accepts the item onto the input belt at progress 0.0
|
||||||
|
|
||||||
|
const Building* b = bs.findBuilding(sid);
|
||||||
|
REQUIRE(b != nullptr);
|
||||||
|
// Reserved but not yet consumable: nothing in the buffer, but it counts against
|
||||||
|
// the cap via pendingInputCount.
|
||||||
|
const std::map<ItemType, int>::const_iterator it0 =
|
||||||
b->inputBuffer.counts.find(ItemType{"iron_ore"});
|
b->inputBuffer.counts.find(ItemType{"iron_ore"});
|
||||||
REQUIRE(it != b->inputBuffer.counts.end());
|
REQUIRE((it0 == b->inputBuffer.counts.end() || it0->second == 0));
|
||||||
REQUIRE(it->second >= 1);
|
REQUIRE(b->pendingInputCount(ItemType{"iron_ore"}) == 1);
|
||||||
|
|
||||||
|
// One more pull tick advances the input belt to the centre; the item arrives.
|
||||||
|
bs.tickBeltPull();
|
||||||
|
REQUIRE(b->inputBuffer.counts.at(ItemType{"iron_ore"}) == 1);
|
||||||
|
REQUIRE(b->pendingInputCount(ItemType{"iron_ore"}) == 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
// The acceptance test counts in-transit items, so buffered + reserved never exceeds
|
||||||
|
// the per-material cap; excess items stay on the belt (REQ-MAT-INPUT-INTAKE).
|
||||||
|
TEST_CASE("BuildingSystem: input reservation caps buffered plus in-transit at the cap",
|
||||||
|
"[building]")
|
||||||
|
{
|
||||||
|
const GameConfig cfg = loadConfig();
|
||||||
|
BeltSystem belts(static_cast<double>(kTickRateHz)); // fast belt
|
||||||
|
int stock = 0;
|
||||||
|
std::mt19937 rng(0);
|
||||||
|
BuildingId nextBuildingId = 1;
|
||||||
|
BuildingSystem bs(cfg, belts,
|
||||||
|
[&nextBuildingId]() { return nextBuildingId++; },
|
||||||
|
[&stock](int n) { stock += n; },
|
||||||
|
[](const std::string&, QVector2D, const std::optional<ShipLayoutConfig>&) {},
|
||||||
|
[](const std::string&) -> bool { return true; },
|
||||||
|
rng);
|
||||||
|
|
||||||
|
const BuildingId id = bs.place(BuildingType::ReprocessingPlant,
|
||||||
|
QPoint(0, 0), Rotation::East, 0);
|
||||||
|
Tick tick = 0;
|
||||||
|
runTicks(bs, belts, static_cast<int>(secondsToTicks(25.0)) + 1, tick);
|
||||||
|
|
||||||
|
// Feed scrap via an input belt without ever running production (only pull), so
|
||||||
|
// the buffer fills and stays full. Try to over-fill it well past the cap.
|
||||||
|
belts.placeBelt(QPoint(-1, 0), Rotation::East);
|
||||||
|
for (int i = 0; i < 20; ++i)
|
||||||
|
{
|
||||||
|
belts.tryPutItem(QPoint(-1, 0), makeItem("scrap"), Rotation::East);
|
||||||
|
belts.tick();
|
||||||
|
bs.tickBeltPull();
|
||||||
|
}
|
||||||
|
|
||||||
|
const Building* b = bs.findBuilding(id);
|
||||||
|
REQUIRE(b != nullptr);
|
||||||
|
const int cap = b->inputBuffer.caps.at(ItemType{"scrap"});
|
||||||
|
REQUIRE(cap > 0);
|
||||||
|
// buffered + in-transit is capped; the plant never over-pulls.
|
||||||
|
REQUIRE(b->pendingInputCount(ItemType{"scrap"}) == cap);
|
||||||
|
// Excess scrap is left stuck on the feeding belt rather than silently dropped.
|
||||||
|
REQUIRE(belts.peekItem(eastPort(QPoint(-1, 0))).has_value());
|
||||||
}
|
}
|
||||||
|
|
||||||
// A smelter auto-selects the matching recipe for whatever it is fed, with no
|
// A smelter auto-selects the matching recipe for whatever it is fed, with no
|
||||||
@@ -838,14 +921,12 @@ TEST_CASE("BuildingSystem: reprocessing plant produces one cycle output then sta
|
|||||||
bs.tickBeltPull();
|
bs.tickBeltPull();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Verify scrap is in input buffer.
|
// Verify all five scrap were accepted; some may still be travelling inward on
|
||||||
|
// the input belt (REQ-MAT-INPUT-INTAKE), so count buffered + in-transit.
|
||||||
{
|
{
|
||||||
const Building* b = bs.findBuilding(id);
|
const Building* b = bs.findBuilding(id);
|
||||||
REQUIRE(b != nullptr);
|
REQUIRE(b != nullptr);
|
||||||
const std::map<ItemType, int>::const_iterator it =
|
REQUIRE(b->pendingInputCount(ItemType{"scrap"}) == 5);
|
||||||
b->inputBuffer.counts.find(ItemType{"scrap"});
|
|
||||||
REQUIRE(it != b->inputBuffer.counts.end());
|
|
||||||
REQUIRE(it->second == 5);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Run production cycle (3s = 90 ticks + 1 for the completion tick).
|
// Run production cycle (3s = 90 ticks + 1 for the completion tick).
|
||||||
|
|||||||
@@ -402,10 +402,11 @@ void GameWorldView::paintGL()
|
|||||||
painter.setRenderHint(QPainter::Antialiasing, false);
|
painter.setRenderHint(QPainter::Antialiasing, false);
|
||||||
|
|
||||||
drawTiles(painter);
|
drawTiles(painter);
|
||||||
// Emerging items are drawn before the buildings so the building body occludes
|
// Port items are drawn before the buildings so the building body occludes the
|
||||||
// the portion still inside the footprint, making items appear to slide out of
|
// portion still inside the footprint: items appear to slide out of the output
|
||||||
// the output port rather than pop into existence (REQ-MAT-OUTPUT-EMERGE).
|
// port (REQ-MAT-OUTPUT-EMERGE) and into the input port (REQ-MAT-INPUT-INTAKE)
|
||||||
drawEmergingItems(painter);
|
// rather than popping in or out of existence.
|
||||||
|
drawPortItems(painter);
|
||||||
drawBuildings(painter);
|
drawBuildings(painter);
|
||||||
drawCopyConfigFeedback(painter);
|
drawCopyConfigFeedback(painter);
|
||||||
drawStations(painter);
|
drawStations(painter);
|
||||||
@@ -1157,11 +1158,14 @@ void GameWorldView::drawCopyConfigFeedback(QPainter& painter)
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void GameWorldView::drawEmergingItems(QPainter& painter)
|
void GameWorldView::drawPortItems(QPainter& painter)
|
||||||
{
|
{
|
||||||
const float halfPx = tilePx() * 0.5f * 0.5f;
|
const float halfPx = tilePx() * 0.5f * 0.5f;
|
||||||
|
|
||||||
m_sim->buildings().forEachEmergingItem(
|
// Shared with belt items (REQ-GW-TILE-SIZE): a half-tile filled square with an
|
||||||
|
// outline, occluded by the building drawn afterwards so the item slides out of
|
||||||
|
// (REQ-MAT-OUTPUT-EMERGE) or into (REQ-MAT-INPUT-INTAKE) the port.
|
||||||
|
const std::function<void(const ItemType&, QPointF)> drawItem =
|
||||||
[&](const ItemType& type, QPointF worldPos)
|
[&](const ItemType& type, QPointF worldPos)
|
||||||
{
|
{
|
||||||
const std::map<std::string, ItemVisuals>::const_iterator it =
|
const std::map<std::string, ItemVisuals>::const_iterator it =
|
||||||
@@ -1177,7 +1181,10 @@ void GameWorldView::drawEmergingItems(QPainter& painter)
|
|||||||
painter.setPen(QPen(it->second.outline, 1));
|
painter.setPen(QPen(it->second.outline, 1));
|
||||||
painter.setBrush(Qt::NoBrush);
|
painter.setBrush(Qt::NoBrush);
|
||||||
painter.drawRect(rect);
|
painter.drawRect(rect);
|
||||||
});
|
};
|
||||||
|
|
||||||
|
m_sim->buildings().forEachEmergingItem(drawItem);
|
||||||
|
m_sim->buildings().forEachIncomingItem(drawItem);
|
||||||
}
|
}
|
||||||
|
|
||||||
void GameWorldView::drawBeltItems(QPainter& painter)
|
void GameWorldView::drawBeltItems(QPainter& painter)
|
||||||
|
|||||||
@@ -115,7 +115,7 @@ private:
|
|||||||
bool canAfford(BuildingType type) const;
|
bool canAfford(BuildingType type) const;
|
||||||
|
|
||||||
void drawTiles(QPainter& painter);
|
void drawTiles(QPainter& painter);
|
||||||
void drawEmergingItems(QPainter& painter);
|
void drawPortItems(QPainter& painter);
|
||||||
void drawBuildings(QPainter& painter);
|
void drawBuildings(QPainter& painter);
|
||||||
void drawSelectionHighlights(QPainter& painter);
|
void drawSelectionHighlights(QPainter& painter);
|
||||||
void drawCopyConfigFeedback(QPainter& painter);
|
void drawCopyConfigFeedback(QPainter& painter);
|
||||||
|
|||||||
Reference in New Issue
Block a user