Animate items emerging from building output ports

This commit is contained in:
2026-07-14 20:18:47 +02:00
parent af8a2224c0
commit 6a8c456aa1
13 changed files with 282 additions and 122 deletions

View File

@@ -20,6 +20,21 @@ bool isAutoRecipeBuildingType(BuildingType type)
return type == BuildingType::Smelter
|| type == BuildingType::ReprocessingPlant;
}
// The building body tile that owns an output port, given the port's outside tile
// (port.tile) and its facing direction. The virtual output belt occupies this tile
// and flows toward port.tile (REQ-MAT-OUTPUT-EMERGE).
QPoint outputBodyTile(QPoint portTile, Rotation direction)
{
switch (direction)
{
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;
}
} // namespace
BuildingSystem::BuildingSystem(const GameConfig& config,
@@ -523,6 +538,9 @@ void BuildingSystem::setRecipe(BuildingId id, const std::string& recipeId)
building.inputBuffer.caps.clear();
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).
for (std::vector<BeltItemSlot>& lane : building.emergingItems) { lane.clear(); }
building.production = std::nullopt;
if (!recipeId.empty())
@@ -569,6 +587,7 @@ void BuildingSystem::setShipLayout(BuildingId id, const ShipLayoutConfig& layout
building.inputBuffer.caps.clear();
building.outputBuffer.items.clear();
building.outputBuffer.capacity = 0;
for (std::vector<BeltItemSlot>& lane : building.emergingItems) { lane.clear(); }
if (!building.recipeId.empty() && building.type == BuildingType::Shipyard)
{
initShipyardBuffers(building);
@@ -672,6 +691,7 @@ void BuildingSystem::tickConstruction(Tick currentTick)
absPort.direction = port.direction;
building.outputPorts.push_back(absPort);
}
building.emergingItems.resize(building.outputPorts.size());
building.inputPorts = computeInputPorts(building);
if (building.type == BuildingType::SalvageBay)
@@ -929,8 +949,9 @@ void BuildingSystem::tickProduction(Tick currentTick)
}
}
// 3. Output buffer has space for chosen outputs?
const int newSize = static_cast<int>(building.outputBuffer.items.size())
// 3. Output buffer has space for chosen outputs? Emerging items still
// count against the buffer (REQ-MAT-OUTPUT-EMERGE).
const int newSize = building.outputItemCount()
+ static_cast<int>(chosen.size());
if (newSize > building.outputBuffer.capacity)
{
@@ -1064,31 +1085,69 @@ void BuildingSystem::tickShipyardProduction(Tick currentTick)
}
}
void BuildingSystem::tickBeltPush()
void BuildingSystem::tickOutputBelts()
{
TRACE();
// Use BeltSystem's own per-tick step so emerging items travel at exactly the
// same speed as real belts (REQ-GW-BELT-SPEED, REQ-MAT-OUTPUT-EMERGE).
const double progressPerTick = m_belts.getProgressPerTick_tpt();
for (Building& building : m_buildings)
{
if (building.outputBuffer.items.empty())
for (std::size_t p = 0; p < building.outputPorts.size(); ++p)
{
continue;
}
const Port& port = building.outputPorts[p];
std::vector<BeltItemSlot>& lane = building.emergingItems[p];
for (const Port& outputPort : building.outputPorts)
{
if (building.outputBuffer.items.empty())
// 1. Advance emerging items using the shared belt packing (progress
// caps to 0.5 / 0.75 / 1.0 for up to three items).
advanceBeltSlots(lane, progressPerTick);
// 2. Hand the front item off onto the adjacent real belt once it reaches
// the output edge (progress 1.0). On refusal — no belt, output-edge
// (REQ-MAT-ACCEPT-DIR), or a full belt — it stays stuck at 1.0.
if (!lane.empty() && lane.front().progress >= 1.0
&& m_belts.tryPutItem(port.tile, lane.front().item, port.direction))
{
break;
lane.erase(lane.begin());
}
const Item item = building.outputBuffer.items.front();
if (m_belts.tryPutItem(outputPort.tile, item, outputPort.direction))
// 3. Feed the next buffered item onto the lane at progress 0.5 when the
// entry slot is free — the lane holds at most three items and a new
// one needs a quarter-tile clearance ahead of 0.5.
if (!building.outputBuffer.items.empty()
&& lane.size() < 3
&& (lane.empty() || lane.back().progress >= 0.75))
{
lane.push_back(BeltItemSlot{building.outputBuffer.items.front(), 0.5});
building.outputBuffer.items.erase(building.outputBuffer.items.begin());
}
}
}
}
void BuildingSystem::forEachEmergingItem(
const std::function<void(const ItemType&, QPointF)>& visit) const
{
for (const Building& building : m_buildings)
{
for (std::size_t p = 0; p < building.outputPorts.size(); ++p)
{
const Port& port = building.outputPorts[p];
const QPoint bodyTile = outputBodyTile(port.tile, port.direction);
const std::vector<BeltItemSlot>& lane = building.emergingItems[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
// ---------------------------------------------------------------------------
@@ -1278,6 +1337,10 @@ void BuildingSystem::rotateInPlace(BuildingId id, Rotation newRotation)
absPort.direction = port.direction;
b.outputPorts.push_back(absPort);
}
// The output ports moved; discard any in-flight emerging items and re-size
// the lanes to the new port set (REQ-MAT-OUTPUT-EMERGE).
b.emergingItems.clear();
b.emergingItems.resize(b.outputPorts.size());
b.inputPorts = computeInputPorts(b);
// Re-register with BeltSystem (items on tile are discarded).
@@ -1347,7 +1410,9 @@ bool BuildingSystem::deliverScrapToSalvageBay(BuildingId bayId)
{
return false;
}
if (static_cast<int>(bay->outputBuffer.items.size()) >= bay->outputBuffer.capacity)
// Emerging scrap still counts against the bay's holding capacity
// (REQ-MAT-OUTPUT-EMERGE).
if (bay->outputItemCount() >= bay->outputBuffer.capacity)
{
return false;
}
@@ -1382,6 +1447,7 @@ BuildingId BuildingSystem::placeImmediate(BuildingType type,
absPort.direction = port.direction;
building.outputPorts.push_back(absPort);
}
building.emergingItems.resize(building.outputPorts.size());
building.inputPorts = computeInputPorts(building);
if (type == BuildingType::SalvageBay)
@@ -1488,6 +1554,16 @@ void BuildingSystem::appendChecksum(Hasher& hasher) const
appendInputBuffer(hasher, b.inputBuffer);
appendItems(hasher, b.outputBuffer.items);
hasher.append(b.outputBuffer.capacity);
hasher.append(b.emergingItems.size());
for (const std::vector<BeltItemSlot>& lane : b.emergingItems)
{
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())
{