Animate items entering building input ports

This commit is contained in:
2026-07-14 20:21:25 +02:00
parent 6a8c456aa1
commit c9f14970a1
7 changed files with 264 additions and 37 deletions

View File

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

View File

@@ -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())
{

View File

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