Animate items entering building input ports
This commit is contained in:
@@ -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.
|
||||
|
||||
Reference in New Issue
Block a user