Animate items entering building input ports
This commit is contained in:
@@ -147,7 +147,15 @@ Modules in `modules.toml` define a `surface_mask` — a list of strings that des
|
||||
## Material Transport & Buffers
|
||||
|
||||
- REQ-MAT-BELT-ONLY: Materials are transported exclusively via belts, splitters, and tunnels.
|
||||
- REQ-MAT-INPUT-PORTS: A building accepts items from any adjacent belt tile on any edge of its footprint (excluding cells occupied by output port(s)) whose direction points toward the building, provided the item is an input required by the currently selected recipe and the matching per-material input buffer has free space.
|
||||
- REQ-MAT-INPUT-PORTS: A building accepts items from any adjacent belt tile on any edge of its footprint (excluding cells occupied by output port(s)) whose direction points toward the building, provided the item is an input required by the currently selected recipe and the matching per-material input buffer has free space. An accepted item does not enter the building instantly; it is removed from the belt and travels inward across the input port's footprint cell on that port's own input belt before being added to the buffer (REQ-MAT-INPUT-INTAKE).
|
||||
- REQ-MAT-INPUT-INTAKE: Accepted input items travel into a building as an animation rather than vanishing off the belt instantly — the input-side mirror of REQ-MAT-OUTPUT-EMERGE. Each input port has its own **input belt** — a virtual belt tile occupying the input port's footprint cell (the body cell the feeding belt points into), oriented in the port's inward flow direction, with progress 0.0 at the outer edge adjacent to the feeding belt and 0.5 at the tile centre. It reuses the belt subsystem: movement at belt speed (REQ-GW-BELT-SPEED), item rendering and spacing (REQ-GW-TILE-SIZE), and capacity/packing (REQ-GW-BELT-CAPACITY), but restricted to the 0.0→0.5 half of the tile. This applies to every building that pulls items from adjacent belts into an input buffer (Smelter, Assembler, Reprocessing Plant, Shipyard); a building may run several input belts at once when belts feed it from more than one side. The HQ is included with the one difference noted below.
|
||||
- **Acceptance & reservation.** The acceptance test of REQ-MAT-INPUT-PORTS is unchanged — an item is accepted only if it is a required input whose per-material input buffer has space — except that "has space" now counts both the items already buffered **and** the items of that material currently travelling on the building's input belts (reserved but not yet arrived), so the total (buffered + in-transit) never exceeds that material's buffer cap (REQ-MAT-INPUT-BUFFER). An item that fails this test is not placed on an input belt and stays on the feeding belt exactly as before, so items that are not required inputs never enter the building.
|
||||
- **Feeding.** An accepted item is removed from the feeding belt on the same tick it would have been taken without this animation, and placed on the input belt at progress 0.0, reserving a slot in its per-material buffer. A new item is placed only when the input belt's entry slot at progress 0.0 is free (per REQ-GW-BELT-CAPACITY spacing — no in-transit item within a quarter tile of 0.0). The 0.0→0.5 span holds at most three in-transit items (progress 0.0, 0.25, 0.5); the reservation limit above may permit fewer.
|
||||
- **Travel & arrival.** An in-transit item advances from progress 0.0 to 0.5 at belt speed. On reaching progress 0.5 it leaves the input belt and is added to its per-material input buffer, turning its reservation into buffered stock; only then does it count toward starting a production cycle (REQ-MAT-CYCLE). Because the slot was reserved on entry, arrival always succeeds — there is no deadlock.
|
||||
- **Reservation may delay production.** A reserved item occupies buffer capacity for its whole 0.0→0.5 travel without yet being consumable, so an input-starved building may briefly wait for an in-transit item to arrive before it can start a cycle. This is accepted.
|
||||
- **Clearing.** Clearing the input buffers on a recipe or schematic change (REQ-MAT-INPUT-BUFFER) also discards any items currently travelling on the input belts and releases their reservations.
|
||||
- **HQ.** The HQ has no input buffer (REQ-HQ-BELT-INPUT); a building block accepted at an HQ input port travels its input belt the same way but reserves nothing, and is added to the global building blocks stock (REQ-MAT-GLOBAL-STOCK) on reaching progress 0.5.
|
||||
- **Intake rendering (no pop-out).** Mirror of the emergence rendering in REQ-MAT-OUTPUT-EMERGE: the building is rendered over the input belt, so an in-transit item is occluded while inside the footprint and is only visible as it crosses the outer edge — appearing to sink into the port. The portion inside the footprint is hidden, and the item disappears at the tile centre (progress 0.5) as it enters the buffer.
|
||||
- REQ-MAT-OUTPUT-PORT: Each building has one or more fixed output port(s) defined by its surface_mask (direction determined by rotation). Produced items do not appear on the outgoing belt instantly; each item leaves the building by first emerging across the output port tile on that port's own output belt and then transferring onto the adjacent real belt tile (REQ-MAT-OUTPUT-EMERGE). The adjacent belt's direction is otherwise unconstrained (it may flow away from the building or perpendicular to it), except that a belt oriented with its own output edge facing back into the building refuses the transfer and the item stays stuck at the port (REQ-MAT-ACCEPT-DIR, REQ-MAT-OUTPUT-EMERGE).
|
||||
- REQ-MAT-OUTPUT-EMERGE: Items emerge from a building output port as an animation rather than popping directly onto the outgoing belt. Each output port has its own **output belt** — a virtual belt tile occupying the output port tile, oriented in the port's facing direction, with progress 0.0 at the tile's inner edge and 1.0 at the outer (port) edge adjacent to the next real belt tile. It reuses the belt subsystem: movement at belt speed (REQ-GW-BELT-SPEED), item rendering and spacing (REQ-GW-TILE-SIZE), and capacity/packing (REQ-GW-BELT-CAPACITY), but restricted to the 0.5→1.0 half of the tile. This applies to every building that outputs items onto belts (Miner, Smelter, Assembler, Reprocessing Plant, Salvage Bay); it does not apply to the Shipyard, which spawns a ship rather than a belt item (REQ-SHP-SPAWN-PLAYER).
|
||||
- **Feeding.** While the output buffer (REQ-MAT-OUTPUT-BUFFER) holds an item that has not yet begun emerging and the output belt's entry slot at progress 0.5 is free (per REQ-GW-BELT-CAPACITY spacing — no emerging item within a quarter tile of progress 0.5), the next buffered item is placed on the output belt at progress 0.5. Because only the 0.5→1.0 span is used, the output belt holds at most three emerging items (progress 0.5, 0.75, 1.0); once that span is full the building places no further items on it even if the output buffer still holds more.
|
||||
|
||||
@@ -96,6 +96,31 @@ struct Building
|
||||
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.
|
||||
std::vector<QPoint> bodyCells;
|
||||
std::vector<Port> outputPorts;
|
||||
|
||||
@@ -35,6 +35,30 @@ QPoint outputBodyTile(QPoint portTile, Rotation direction)
|
||||
}
|
||||
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
|
||||
|
||||
BuildingSystem::BuildingSystem(const GameConfig& config,
|
||||
@@ -539,8 +563,11 @@ void BuildingSystem::setRecipe(BuildingId id, const std::string& recipeId)
|
||||
building.outputBuffer.items.clear();
|
||||
building.outputBuffer.capacity = 0;
|
||||
// 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.incomingItems) { lane.clear(); }
|
||||
building.production = std::nullopt;
|
||||
|
||||
if (!recipeId.empty())
|
||||
@@ -588,6 +615,7 @@ void BuildingSystem::setShipLayout(BuildingId id, const ShipLayoutConfig& layout
|
||||
building.outputBuffer.items.clear();
|
||||
building.outputBuffer.capacity = 0;
|
||||
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)
|
||||
{
|
||||
initShipyardBuffers(building);
|
||||
@@ -693,6 +721,7 @@ void BuildingSystem::tickConstruction(Tick currentTick)
|
||||
}
|
||||
building.emergingItems.resize(building.outputPorts.size());
|
||||
building.inputPorts = computeInputPorts(building);
|
||||
building.incomingItems.assign(building.inputPorts.size(), {});
|
||||
|
||||
if (building.type == BuildingType::SalvageBay)
|
||||
{
|
||||
@@ -765,21 +794,53 @@ void BuildingSystem::tickConstruction(Tick currentTick)
|
||||
void BuildingSystem::tickBeltPull()
|
||||
{
|
||||
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)
|
||||
{
|
||||
// HQ: pull building_block items and add to global stock.
|
||||
if (building.type == BuildingType::Hq)
|
||||
const bool isHq = (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);
|
||||
if (peeked && peeked->id == "building_block")
|
||||
const Item arrived = lane.front().item;
|
||||
lane.erase(lane.begin());
|
||||
if (isHq)
|
||||
{
|
||||
const std::optional<Item> taken = m_belts.tryTakeItem(port);
|
||||
if (taken)
|
||||
{
|
||||
m_addBuildingBlocks(1);
|
||||
}
|
||||
m_addBuildingBlocks(1);
|
||||
}
|
||||
else
|
||||
{
|
||||
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;
|
||||
@@ -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);
|
||||
if (!peeked)
|
||||
{
|
||||
@@ -815,7 +879,7 @@ void BuildingSystem::tickBeltPull()
|
||||
|
||||
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 =
|
||||
building.inputBuffer.caps.find(type);
|
||||
if (capIt == building.inputBuffer.caps.end() || capIt->second == 0)
|
||||
@@ -823,14 +887,14 @@ void BuildingSystem::tickBeltPull()
|
||||
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 =
|
||||
building.inputBuffer.counts.find(type);
|
||||
return (it != building.inputBuffer.counts.end()) ? it->second : 0;
|
||||
}();
|
||||
continue;
|
||||
}
|
||||
|
||||
if (current >= capIt->second)
|
||||
if (!inputLaneEntryFree(lane))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
@@ -838,7 +902,7 @@ void BuildingSystem::tickBeltPull()
|
||||
const std::optional<Item> taken = m_belts.tryTakeItem(port);
|
||||
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
|
||||
// ---------------------------------------------------------------------------
|
||||
@@ -1342,6 +1428,9 @@ void BuildingSystem::rotateInPlace(BuildingId id, Rotation newRotation)
|
||||
b.emergingItems.clear();
|
||||
b.emergingItems.resize(b.outputPorts.size());
|
||||
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).
|
||||
if (b.type == BuildingType::Belt)
|
||||
@@ -1449,6 +1538,7 @@ BuildingId BuildingSystem::placeImmediate(BuildingType type,
|
||||
}
|
||||
building.emergingItems.resize(building.outputPorts.size());
|
||||
building.inputPorts = computeInputPorts(building);
|
||||
building.incomingItems.assign(building.inputPorts.size(), {});
|
||||
|
||||
if (type == BuildingType::SalvageBay)
|
||||
{
|
||||
@@ -1564,6 +1654,16 @@ void BuildingSystem::appendChecksum(Hasher& hasher) const
|
||||
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());
|
||||
if (b.production.has_value())
|
||||
{
|
||||
|
||||
@@ -132,6 +132,12 @@ public:
|
||||
void forEachEmergingItem(
|
||||
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
|
||||
// exactly coincides with the ghost (type, anchor, rot) and is of the same
|
||||
// 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);
|
||||
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"});
|
||||
REQUIRE(it != b->inputBuffer.counts.end());
|
||||
REQUIRE(it->second >= 1);
|
||||
REQUIRE((it0 == b->inputBuffer.counts.end() || it0->second == 0));
|
||||
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
|
||||
@@ -838,14 +921,12 @@ TEST_CASE("BuildingSystem: reprocessing plant produces one cycle output then sta
|
||||
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);
|
||||
REQUIRE(b != nullptr);
|
||||
const std::map<ItemType, int>::const_iterator it =
|
||||
b->inputBuffer.counts.find(ItemType{"scrap"});
|
||||
REQUIRE(it != b->inputBuffer.counts.end());
|
||||
REQUIRE(it->second == 5);
|
||||
REQUIRE(b->pendingInputCount(ItemType{"scrap"}) == 5);
|
||||
}
|
||||
|
||||
// Run production cycle (3s = 90 ticks + 1 for the completion tick).
|
||||
|
||||
@@ -402,10 +402,11 @@ void GameWorldView::paintGL()
|
||||
painter.setRenderHint(QPainter::Antialiasing, false);
|
||||
|
||||
drawTiles(painter);
|
||||
// Emerging items are drawn before the buildings so the building body occludes
|
||||
// the portion still inside the footprint, making items appear to slide out of
|
||||
// the output port rather than pop into existence (REQ-MAT-OUTPUT-EMERGE).
|
||||
drawEmergingItems(painter);
|
||||
// Port items are drawn before the buildings so the building body occludes the
|
||||
// portion still inside the footprint: items appear to slide out of the output
|
||||
// port (REQ-MAT-OUTPUT-EMERGE) and into the input port (REQ-MAT-INPUT-INTAKE)
|
||||
// rather than popping in or out of existence.
|
||||
drawPortItems(painter);
|
||||
drawBuildings(painter);
|
||||
drawCopyConfigFeedback(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;
|
||||
|
||||
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 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.setBrush(Qt::NoBrush);
|
||||
painter.drawRect(rect);
|
||||
});
|
||||
};
|
||||
|
||||
m_sim->buildings().forEachEmergingItem(drawItem);
|
||||
m_sim->buildings().forEachIncomingItem(drawItem);
|
||||
}
|
||||
|
||||
void GameWorldView::drawBeltItems(QPainter& painter)
|
||||
|
||||
@@ -115,7 +115,7 @@ private:
|
||||
bool canAfford(BuildingType type) const;
|
||||
|
||||
void drawTiles(QPainter& painter);
|
||||
void drawEmergingItems(QPainter& painter);
|
||||
void drawPortItems(QPainter& painter);
|
||||
void drawBuildings(QPainter& painter);
|
||||
void drawSelectionHighlights(QPainter& painter);
|
||||
void drawCopyConfigFeedback(QPainter& painter);
|
||||
|
||||
Reference in New Issue
Block a user