Animate items emerging from building output ports
Produced items now emerge across a per-port virtual output belt (progress 0.5 -> 1.0) before handing off to the adjacent real belt, instead of appearing instantly (REQ-MAT-OUTPUT-EMERGE). Emerging items still count against the output buffer and are drawn occluded by the building so they slide out of the port. The belt movement/geometry is extracted into shared BeltSlot helpers (advanceBeltSlots, beltSlotWorldPos) reused by belts, tunnel entries/exits, and the new building output belts. The virtual belt state lives on the Building; BuildingSystem::tickOutputBelts advances it at the BeltSystem's own speed and feeds it from the output buffer. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DZR44tA8sn4dPqDzAVXyps
This commit is contained in:
45
src/lib/sim/BeltSlot.cpp
Normal file
45
src/lib/sim/BeltSlot.cpp
Normal file
@@ -0,0 +1,45 @@
|
||||
#include "BeltSlot.h"
|
||||
|
||||
#include <cstddef>
|
||||
|
||||
void advanceBeltSlots(std::vector<BeltItemSlot>& slots, double progressPerTick)
|
||||
{
|
||||
for (std::size_t i = 0; i < slots.size(); ++i)
|
||||
{
|
||||
slots[i].progress += progressPerTick;
|
||||
|
||||
// Absolute cap: slot i cannot exceed 1.0 - i * 0.25.
|
||||
const double absoluteCap = 1.0 - static_cast<double>(i) * 0.25;
|
||||
if (slots[i].progress > absoluteCap)
|
||||
{
|
||||
slots[i].progress = absoluteCap;
|
||||
}
|
||||
|
||||
// Gap constraint: must stay 0.25 behind the slot ahead.
|
||||
if (i > 0)
|
||||
{
|
||||
const double gapCap = slots[i - 1].progress - 0.25;
|
||||
if (slots[i].progress > gapCap)
|
||||
{
|
||||
slots[i].progress = (gapCap < 0.0 ? 0.0 : gapCap);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
QPointF beltSlotWorldPos(QPoint tile, Rotation dir, double progress)
|
||||
{
|
||||
// Map progress [0, 1] along the belt direction to a fractional tile-unit position.
|
||||
// Progress 0 = entered from opposite side; 1 = at output edge.
|
||||
const double baseX = tile.x() + 0.5;
|
||||
const double baseY = tile.y() + 0.5;
|
||||
|
||||
switch (dir)
|
||||
{
|
||||
case Rotation::North: return {baseX, baseY - (progress - 0.5)};
|
||||
case Rotation::East: return {baseX + (progress - 0.5), baseY};
|
||||
case Rotation::South: return {baseX, baseY + (progress - 0.5)};
|
||||
case Rotation::West: return {baseX - (progress - 0.5), baseY};
|
||||
}
|
||||
return {baseX, baseY};
|
||||
}
|
||||
30
src/lib/sim/BeltSlot.h
Normal file
30
src/lib/sim/BeltSlot.h
Normal file
@@ -0,0 +1,30 @@
|
||||
#pragma once
|
||||
|
||||
#include <vector>
|
||||
|
||||
#include <QPoint>
|
||||
#include <QPointF>
|
||||
|
||||
#include "Item.h"
|
||||
#include "Rotation.h"
|
||||
|
||||
// A single item on a belt-like lane: an item plus its fractional progress along
|
||||
// the lane's travel direction. Shared by BeltSystem's belt/tunnel tiles and by a
|
||||
// building's virtual output belt (REQ-MAT-OUTPUT-EMERGE) so the packing and
|
||||
// geometry live in exactly one place.
|
||||
struct BeltItemSlot
|
||||
{
|
||||
Item item;
|
||||
double progress; // [0.0, 1.0]: 0 = just entered, 1 = at output edge
|
||||
};
|
||||
|
||||
// Advances every slot in `slots` by `progressPerTick`, applying the standard belt
|
||||
// packing: the front (index 0) carries the highest progress; each following slot
|
||||
// stays at least 0.25 behind the slot ahead and is capped at 1.0 - i * 0.25.
|
||||
// `slots` must be ordered front (highest progress) first. This is the per-tile
|
||||
// advance shared by belts, tunnel entries, and tunnel exits.
|
||||
void advanceBeltSlots(std::vector<BeltItemSlot>& slots, double progressPerTick);
|
||||
|
||||
// World-space centre (in tile units) of a slot at `progress` on a lane occupying
|
||||
// `tile` and flowing in `dir`. Progress 0 = entry edge, 1 = output edge.
|
||||
QPointF beltSlotWorldPos(QPoint tile, Rotation dir, double progress);
|
||||
@@ -77,23 +77,6 @@ bool BeltSystem::entersThroughOutputEdge(QPoint tile, Rotation travelDir) const
|
||||
return false;
|
||||
}
|
||||
|
||||
QPointF BeltSystem::slotWorldPos(QPoint tile, Rotation dir, double progress)
|
||||
{
|
||||
// Map progress [0, 1] along the belt direction to a fractional tile-unit position.
|
||||
// Progress 0 = entered from opposite side; 1 = at output edge.
|
||||
double baseX = tile.x() + 0.5;
|
||||
double baseY = tile.y() + 0.5;
|
||||
|
||||
switch (dir)
|
||||
{
|
||||
case Rotation::North: return {baseX, baseY - (progress - 0.5)};
|
||||
case Rotation::East: return {baseX + (progress - 0.5), baseY};
|
||||
case Rotation::South: return {baseX, baseY + (progress - 0.5)};
|
||||
case Rotation::West: return {baseX - (progress - 0.5), baseY};
|
||||
}
|
||||
return {baseX, baseY};
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Construction / placement
|
||||
// ---------------------------------------------------------------------------
|
||||
@@ -467,29 +450,7 @@ void BeltSystem::advanceProgress()
|
||||
for (std::map<std::pair<int, int>, BeltTile>::iterator it = m_belts.begin();
|
||||
it != m_belts.end(); ++it)
|
||||
{
|
||||
BeltTile& bt = it->second;
|
||||
|
||||
for (std::size_t i = 0; i < bt.itemSlots.size(); ++i)
|
||||
{
|
||||
bt.itemSlots[i].progress += m_progressPerTick_tpt;
|
||||
|
||||
// Absolute cap: slot i cannot exceed 1.0 - i * 0.25.
|
||||
const double absoluteCap = 1.0 - i * 0.25;
|
||||
if (bt.itemSlots[i].progress > absoluteCap)
|
||||
{
|
||||
bt.itemSlots[i].progress = absoluteCap;
|
||||
}
|
||||
|
||||
// Gap constraint: must stay 0.25 behind the slot ahead.
|
||||
if (i > 0)
|
||||
{
|
||||
const double gapCap = bt.itemSlots[i - 1].progress - 0.25;
|
||||
if (bt.itemSlots[i].progress > gapCap)
|
||||
{
|
||||
bt.itemSlots[i].progress = (gapCap < 0.0 ? 0.0 : gapCap);
|
||||
}
|
||||
}
|
||||
}
|
||||
advanceBeltSlots(it->second.itemSlots, m_progressPerTick_tpt);
|
||||
}
|
||||
|
||||
for (std::map<std::pair<int, int>, SplitterTile>::iterator it = m_splitters.begin();
|
||||
@@ -545,53 +506,13 @@ void BeltSystem::advanceTunnelProgress()
|
||||
for (std::map<std::pair<int, int>, TunnelEntryTile>::iterator it = m_tunnelEntries.begin();
|
||||
it != m_tunnelEntries.end(); ++it)
|
||||
{
|
||||
TunnelEntryTile& te = it->second;
|
||||
|
||||
for (std::size_t i = 0; i < te.itemSlots.size(); ++i)
|
||||
{
|
||||
te.itemSlots[i].progress += m_progressPerTick_tpt;
|
||||
|
||||
const double absoluteCap = 1.0 - i * 0.25;
|
||||
if (te.itemSlots[i].progress > absoluteCap)
|
||||
{
|
||||
te.itemSlots[i].progress = absoluteCap;
|
||||
}
|
||||
|
||||
if (i > 0)
|
||||
{
|
||||
const double gapCap = te.itemSlots[i - 1].progress - 0.25;
|
||||
if (te.itemSlots[i].progress > gapCap)
|
||||
{
|
||||
te.itemSlots[i].progress = (gapCap < 0.0 ? 0.0 : gapCap);
|
||||
}
|
||||
}
|
||||
}
|
||||
advanceBeltSlots(it->second.itemSlots, m_progressPerTick_tpt);
|
||||
}
|
||||
|
||||
for (std::map<std::pair<int, int>, TunnelExitTile>::iterator it = m_tunnelExits.begin();
|
||||
it != m_tunnelExits.end(); ++it)
|
||||
{
|
||||
TunnelExitTile& tx = it->second;
|
||||
|
||||
for (std::size_t i = 0; i < tx.itemSlots.size(); ++i)
|
||||
{
|
||||
tx.itemSlots[i].progress += m_progressPerTick_tpt;
|
||||
|
||||
const double absoluteCap = 1.0 - i * 0.25;
|
||||
if (tx.itemSlots[i].progress > absoluteCap)
|
||||
{
|
||||
tx.itemSlots[i].progress = absoluteCap;
|
||||
}
|
||||
|
||||
if (i > 0)
|
||||
{
|
||||
const double gapCap = tx.itemSlots[i - 1].progress - 0.25;
|
||||
if (tx.itemSlots[i].progress > gapCap)
|
||||
{
|
||||
tx.itemSlots[i].progress = (gapCap < 0.0 ? 0.0 : gapCap);
|
||||
}
|
||||
}
|
||||
}
|
||||
advanceBeltSlots(it->second.itemSlots, m_progressPerTick_tpt);
|
||||
}
|
||||
|
||||
for (TunnelLink& link : m_tunnelLinks)
|
||||
@@ -940,7 +861,7 @@ void BeltSystem::forEachVisualItem(QRect viewportTiles,
|
||||
{
|
||||
VisualItem vi;
|
||||
vi.type = bt.itemSlots[i].item.type;
|
||||
vi.worldPos = slotWorldPos(tile, bt.direction, bt.itemSlots[i].progress);
|
||||
vi.worldPos = beltSlotWorldPos(tile, bt.direction, bt.itemSlots[i].progress);
|
||||
visit(vi);
|
||||
}
|
||||
}
|
||||
@@ -960,7 +881,7 @@ void BeltSystem::forEachVisualItem(QRect viewportTiles,
|
||||
{
|
||||
VisualItem vi;
|
||||
vi.type = st.back[i].item.type;
|
||||
vi.worldPos = slotWorldPos(tile, st.backDir[i], st.back[i].progress);
|
||||
vi.worldPos = beltSlotWorldPos(tile, st.backDir[i], st.back[i].progress);
|
||||
visit(vi);
|
||||
}
|
||||
|
||||
@@ -986,7 +907,7 @@ void BeltSystem::forEachVisualItem(QRect viewportTiles,
|
||||
{
|
||||
VisualItem vi;
|
||||
vi.type = slot->item.type;
|
||||
vi.worldPos = slotWorldPos(tile, dir, slot->progress);
|
||||
vi.worldPos = beltSlotWorldPos(tile, dir, slot->progress);
|
||||
visit(vi);
|
||||
}
|
||||
};
|
||||
@@ -1016,7 +937,7 @@ void BeltSystem::forEachVisualItem(QRect viewportTiles,
|
||||
{
|
||||
VisualItem vi;
|
||||
vi.type = te.itemSlots[i].item.type;
|
||||
vi.worldPos = slotWorldPos(tile, te.direction, te.itemSlots[i].progress);
|
||||
vi.worldPos = beltSlotWorldPos(tile, te.direction, te.itemSlots[i].progress);
|
||||
visit(vi);
|
||||
}
|
||||
}
|
||||
@@ -1034,7 +955,7 @@ void BeltSystem::forEachVisualItem(QRect viewportTiles,
|
||||
{
|
||||
VisualItem vi;
|
||||
vi.type = tx.itemSlots[i].item.type;
|
||||
vi.worldPos = slotWorldPos(tile, tx.direction, tx.itemSlots[i].progress);
|
||||
vi.worldPos = beltSlotWorldPos(tile, tx.direction, tx.itemSlots[i].progress);
|
||||
visit(vi);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,6 +10,7 @@
|
||||
#include <QPointF>
|
||||
#include <QRect>
|
||||
|
||||
#include "BeltSlot.h"
|
||||
#include "Item.h"
|
||||
#include "ItemType.h"
|
||||
#include "Port.h"
|
||||
@@ -88,6 +89,11 @@ public:
|
||||
// Returns nullopt if tile is not a belt, direction mismatches, or tile empty.
|
||||
std::optional<ItemType> peekItem(Port port) const;
|
||||
|
||||
// Progress advanced per tick at the configured belt speed (tile fraction per
|
||||
// tick). Shared with building output belts so emerging items travel at exactly
|
||||
// the same speed as real belts (REQ-MAT-OUTPUT-EMERGE).
|
||||
double getProgressPerTick_tpt() const { return m_progressPerTick_tpt; }
|
||||
|
||||
// -- Maintenance ---------------------------------------------------------
|
||||
void clearTiles(const std::vector<QPoint>& tiles); // REQ-UI-BELT-CLEAR
|
||||
void tick();
|
||||
@@ -126,15 +132,6 @@ private:
|
||||
// refused). Returns false if no transport tile occupies `tile`.
|
||||
bool entersThroughOutputEdge(QPoint tile, Rotation travelDir) const;
|
||||
|
||||
// Returns the world-space centre of a slot given tile origin and progress.
|
||||
static QPointF slotWorldPos(QPoint tile, Rotation dir, double progress);
|
||||
|
||||
struct BeltItemSlot
|
||||
{
|
||||
Item item;
|
||||
double progress; // [0.0, 1.0]: 0 = just entered, 1 = at output edge
|
||||
};
|
||||
|
||||
struct BeltTile
|
||||
{
|
||||
Rotation direction;
|
||||
|
||||
@@ -12,6 +12,7 @@
|
||||
#include "BuildingId.h"
|
||||
|
||||
#include "entt/entity/entity.hpp"
|
||||
#include "BeltSlot.h"
|
||||
#include "Item.h"
|
||||
#include "ItemType.h"
|
||||
#include "Port.h"
|
||||
@@ -75,6 +76,26 @@ struct Building
|
||||
OutputBuffer outputBuffer;
|
||||
std::optional<Production> production;
|
||||
|
||||
// Items currently emerging from each output port on its virtual output belt
|
||||
// (REQ-MAT-OUTPUT-EMERGE); one lane per output port, parallel to outputPorts.
|
||||
// Each lane holds slots at progress [0.5, 1.0], front (highest progress) first.
|
||||
// An emerging item still counts as residing in the output buffer until it hands
|
||||
// off onto a real belt at progress 1.0.
|
||||
std::vector<std::vector<BeltItemSlot>> emergingItems;
|
||||
|
||||
// Total items held on the output side: buffered plus still-emerging. The
|
||||
// output-buffer capacity rule (REQ-MAT-OUTPUT-BUFFER) counts emerging items,
|
||||
// since they have not yet left the building.
|
||||
int outputItemCount() const
|
||||
{
|
||||
int count = static_cast<int>(outputBuffer.items.size());
|
||||
for (const std::vector<BeltItemSlot>& lane : emergingItems)
|
||||
{
|
||||
count += static_cast<int>(lane.size());
|
||||
}
|
||||
return count;
|
||||
}
|
||||
|
||||
// Pre-computed from surface mask at placement; in absolute world coordinates.
|
||||
std::vector<QPoint> bodyCells;
|
||||
std::vector<Port> outputPorts;
|
||||
|
||||
@@ -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())
|
||||
{
|
||||
|
||||
@@ -10,6 +10,7 @@
|
||||
#include <vector>
|
||||
|
||||
#include <QPoint>
|
||||
#include <QPointF>
|
||||
#include <QVector2D>
|
||||
|
||||
#include "BeltSystem.h"
|
||||
@@ -94,7 +95,10 @@ public:
|
||||
void tickBeltPull();
|
||||
void tickProduction(Tick currentTick);
|
||||
void tickShipyardProduction(Tick currentTick);
|
||||
void tickBeltPush();
|
||||
// Advances each building's virtual output belts, hands finished items off onto
|
||||
// the adjacent real belt, and feeds new buffered items into them
|
||||
// (REQ-MAT-OUTPUT-EMERGE).
|
||||
void tickOutputBelts();
|
||||
|
||||
// -- Queries -------------------------------------------------------------
|
||||
struct BeltTileInfo
|
||||
@@ -121,6 +125,13 @@ public:
|
||||
std::vector<BeltTileInfo> allBeltTiles() const;
|
||||
bool isTileOccupied(QPoint tile) const;
|
||||
|
||||
// Visits every item currently emerging from a building output port on its
|
||||
// virtual output belt (REQ-MAT-OUTPUT-EMERGE), passing the item type and its
|
||||
// world-space centre (in tile units). Least-progressed first (drawn bottom) so
|
||||
// callers can paint in visit order (REQ-GW-TILE-SIZE ordering).
|
||||
void forEachEmergingItem(
|
||||
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.
|
||||
|
||||
@@ -8,6 +8,7 @@ SET(HDRS
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/ReplayReader.h
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/ReplayPlayer.h
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/TickDriver.h
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/BeltSlot.h
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/BeltSystem.h
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/Building.h
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/BuildingConfig.h
|
||||
@@ -31,6 +32,7 @@ SET(SRCS
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/ReplayReader.cpp
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/ReplayPlayer.cpp
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/TickDriver.cpp
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/BeltSlot.cpp
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/BeltSystem.cpp
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/BuildingConfig.cpp
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/BuildingSystem.cpp
|
||||
|
||||
@@ -315,7 +315,7 @@ void Simulation::tick()
|
||||
m_buildingSystem->tickBeltPull(); // step 3
|
||||
m_buildingSystem->tickProduction(m_currentTick); // step 4
|
||||
m_buildingSystem->tickShipyardProduction(m_currentTick); // step 4b
|
||||
m_buildingSystem->tickBeltPush(); // step 5
|
||||
m_buildingSystem->tickOutputBelts(); // step 5
|
||||
m_beltSystem.tick(); // step 6
|
||||
|
||||
// Step 7: ship behavior systems (movement arbitration via intent priority)
|
||||
|
||||
@@ -58,12 +58,28 @@ static void runTicks(BuildingSystem& bs, BeltSystem& belts, int n, Tick& tick)
|
||||
bs.tickConstruction(tick);
|
||||
bs.tickBeltPull();
|
||||
bs.tickProduction(tick);
|
||||
bs.tickBeltPush();
|
||||
bs.tickOutputBelts();
|
||||
belts.tick();
|
||||
++tick;
|
||||
}
|
||||
}
|
||||
|
||||
// All items currently on a building's output side: buffered plus still-emerging on
|
||||
// the virtual output belts (REQ-MAT-OUTPUT-EMERGE). A produced item leaves the
|
||||
// output buffer the moment it starts emerging, so tests count both.
|
||||
static std::vector<Item> outputSideItems(const Building& b)
|
||||
{
|
||||
std::vector<Item> items = b.outputBuffer.items;
|
||||
for (const std::vector<BeltItemSlot>& lane : b.emergingItems)
|
||||
{
|
||||
for (const BeltItemSlot& slot : lane)
|
||||
{
|
||||
items.push_back(slot.item);
|
||||
}
|
||||
}
|
||||
return items;
|
||||
}
|
||||
|
||||
// Owns a BuildingSystem and its dependencies for placement-bounds tests.
|
||||
struct PlacementFixture
|
||||
{
|
||||
@@ -402,8 +418,11 @@ TEST_CASE("BuildingSystem: miner produces iron_ore after recipe duration", "[bui
|
||||
|
||||
const Building* b = bs.findBuilding(id);
|
||||
REQUIRE(b != nullptr);
|
||||
REQUIRE_FALSE(b->outputBuffer.items.empty());
|
||||
REQUIRE(b->outputBuffer.items.front().type.id == "iron_ore");
|
||||
// No belt at the output port, so the produced item emerges and stays on the
|
||||
// building's virtual output belt (REQ-MAT-OUTPUT-EMERGE).
|
||||
const std::vector<Item> out = outputSideItems(*b);
|
||||
REQUIRE(out.size() == 1);
|
||||
REQUIRE(out.front().type.id == "iron_ore");
|
||||
}
|
||||
|
||||
TEST_CASE("BuildingSystem: miner output buffer stalls when full", "[building]")
|
||||
@@ -436,7 +455,9 @@ TEST_CASE("BuildingSystem: miner output buffer stalls when full", "[building]")
|
||||
|
||||
const Building* b = bs.findBuilding(id);
|
||||
REQUIRE(b != nullptr);
|
||||
REQUIRE(static_cast<int>(b->outputBuffer.items.size()) == 2);
|
||||
// Both produced items are held on the output side (buffer + emerging lane),
|
||||
// which is what the capacity rule counts (REQ-MAT-OUTPUT-EMERGE).
|
||||
REQUIRE(b->outputItemCount() == 2);
|
||||
REQUIRE_FALSE(b->production.has_value());
|
||||
}
|
||||
|
||||
@@ -515,7 +536,7 @@ TEST_CASE("BuildingSystem: activeProductionBuildingCount tracks production cycle
|
||||
|
||||
const Building* b = bs.findBuilding(id);
|
||||
REQUIRE(b != nullptr);
|
||||
REQUIRE(static_cast<int>(b->outputBuffer.items.size()) == 2);
|
||||
REQUIRE(b->outputItemCount() == 2);
|
||||
REQUIRE_FALSE(b->production.has_value());
|
||||
REQUIRE(bs.activeProductionBuildingCount() == 0);
|
||||
}
|
||||
@@ -603,7 +624,7 @@ TEST_CASE("BuildingSystem: smelter auto-smelts ore without a recipe selection",
|
||||
const Building* b = bs.findBuilding(sid);
|
||||
REQUIRE(b != nullptr);
|
||||
bool hasIronIngot = false;
|
||||
for (const Item& item : b->outputBuffer.items)
|
||||
for (const Item& item : outputSideItems(*b))
|
||||
{
|
||||
if (item.type.id == "iron_ingot") { hasIronIngot = true; }
|
||||
}
|
||||
@@ -652,7 +673,7 @@ TEST_CASE("BuildingSystem: smelter runs a satisfiable recipe while an incomplete
|
||||
|
||||
// Copper was smelted; the lone iron_ore still waits for a second unit.
|
||||
bool hasCopperIngot = false;
|
||||
for (const Item& item : b->outputBuffer.items)
|
||||
for (const Item& item : outputSideItems(*b))
|
||||
{
|
||||
if (item.type.id == "copper_ingot") { hasCopperIngot = true; }
|
||||
}
|
||||
@@ -734,13 +755,15 @@ TEST_CASE("BuildingSystem: setRecipe clears output buffer and active production"
|
||||
{
|
||||
const Building* b = bs.findBuilding(id);
|
||||
REQUIRE(b != nullptr);
|
||||
REQUIRE_FALSE(b->outputBuffer.items.empty());
|
||||
REQUIRE(b->outputItemCount() > 0);
|
||||
}
|
||||
|
||||
bs.setRecipe(id, "mine_copper_ore");
|
||||
|
||||
const Building* b = bs.findBuilding(id);
|
||||
REQUIRE(b->outputBuffer.items.empty());
|
||||
// Clearing the output buffer on a recipe change also discards emerging items
|
||||
// (REQ-MAT-OUTPUT-EMERGE).
|
||||
REQUIRE(b->outputItemCount() == 0);
|
||||
REQUIRE_FALSE(b->production.has_value());
|
||||
}
|
||||
|
||||
|
||||
@@ -402,6 +402,10 @@ 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);
|
||||
drawBuildings(painter);
|
||||
drawCopyConfigFeedback(painter);
|
||||
drawStations(painter);
|
||||
@@ -1153,6 +1157,29 @@ void GameWorldView::drawCopyConfigFeedback(QPainter& painter)
|
||||
}
|
||||
}
|
||||
|
||||
void GameWorldView::drawEmergingItems(QPainter& painter)
|
||||
{
|
||||
const float halfPx = tilePx() * 0.5f * 0.5f;
|
||||
|
||||
m_sim->buildings().forEachEmergingItem(
|
||||
[&](const ItemType& type, QPointF worldPos)
|
||||
{
|
||||
const std::map<std::string, ItemVisuals>::const_iterator it =
|
||||
m_visuals->items.find(type.id);
|
||||
if (it == m_visuals->items.end()) { return; }
|
||||
|
||||
const QPointF center = worldToWidget(
|
||||
QVector2D(static_cast<float>(worldPos.x()),
|
||||
static_cast<float>(worldPos.y())));
|
||||
const QRectF rect(center.x() - halfPx, center.y() - halfPx,
|
||||
halfPx * 2, halfPx * 2);
|
||||
painter.fillRect(rect, it->second.fill);
|
||||
painter.setPen(QPen(it->second.outline, 1));
|
||||
painter.setBrush(Qt::NoBrush);
|
||||
painter.drawRect(rect);
|
||||
});
|
||||
}
|
||||
|
||||
void GameWorldView::drawBeltItems(QPainter& painter)
|
||||
{
|
||||
const float halfPx = tilePx() * 0.5f * 0.5f;
|
||||
|
||||
@@ -115,6 +115,7 @@ private:
|
||||
bool canAfford(BuildingType type) const;
|
||||
|
||||
void drawTiles(QPainter& painter);
|
||||
void drawEmergingItems(QPainter& painter);
|
||||
void drawBuildings(QPainter& painter);
|
||||
void drawSelectionHighlights(QPainter& painter);
|
||||
void drawCopyConfigFeedback(QPainter& painter);
|
||||
|
||||
Reference in New Issue
Block a user