Compare commits

..

2 Commits

Author SHA1 Message Date
b30addab3d implement 4 items on belt tile 2026-05-01 22:37:03 +02:00
0ce7cd7ae8 requirements for up to 4 items on belt tile 2026-05-01 20:56:05 +02:00
4 changed files with 206 additions and 244 deletions

View File

@@ -30,7 +30,8 @@ Output port indicators are not building tiles themselves. A building may have mo
## Game World
- REQ-GW-COORDS: Tile coordinates are integer `(x, y)`. The origin `(0, 0)` is the first column of space — the tile immediately to the right of the asteroid's right edge at game start, at the top of the world. X grows right; Y grows down. All asteroid tiles have `x < 0`; asteroid left-expansions add tiles at increasingly negative X. The origin never shifts.
- REQ-GW-TILE-SIZE: Tiles are square. The tile size in pixels is derived automatically so that the world height (in tiles) exactly fills the game world view's height in pixels. Items on belts are rendered at half-tile size, so each belt tile holds at most 2 items.
- REQ-GW-TILE-SIZE: Tiles are square. The tile size in pixels is derived automatically so that the world height (in tiles) exactly fills the game world view's height in pixels. Items on belts are rendered at half-tile size; when multiple items occupy the same tile they are spaced quarter-tile apart along the direction of travel and overlap, rendered in ascending order of progress — the least-progressed item is drawn first (bottom) and the furthest-progressed item is drawn last (on top).
- REQ-GW-BELT-CAPACITY: Belt tiles and tunnel entry/exit tiles each hold up to four items simultaneously, queued one behind the other in the direction of travel. Splitter tiles hold up to four items: two unassigned items (progress < 0.5, not yet routed to an output) and one item per output slot (progress ≥ 0.5, committed to a specific output direction). Output-slot items are rendered on top of unassigned items; when both output slots are occupied, their rendering order follows the clockwise port order starting from East.
- REQ-GW-BELT-SPEED: Items on belts move at `world.toml [world].belt_speed_tiles_per_second` tiles per second (default 2).
- REQ-GW-HEIGHT: The world height (in tiles) is read from `world.toml [world].height_tiles`.
- REQ-GW-REGIONS: The world is divided into horizontal regions whose widths (in tiles) are read from `world.toml [regions]`:

View File

@@ -66,7 +66,6 @@ void BeltSystem::placeSplitter(QPoint tile, Rotation outputA, Rotation outputB)
st.outputA = outputA;
st.outputB = outputB;
st.nextOutputIsA = true;
st.backDir = Rotation::North; // irrelevant until back is set
m_splitters[key(tile)] = st;
}
@@ -211,10 +210,10 @@ bool BeltSystem::tryPutItem(QPoint tile, Item item, Rotation fromDir)
m_splitters.find(key(tile));
if (splIt != m_splitters.end())
{
if (!splIt->second.back)
if (splIt->second.back.size() < 2)
{
splIt->second.back = BeltItemSlot{item, 0.0};
splIt->second.backDir = fromDir;
splIt->second.back.push_back(BeltItemSlot{item, 0.0});
splIt->second.backDir.push_back(fromDir);
return true;
}
return false;
@@ -224,15 +223,9 @@ bool BeltSystem::tryPutItem(QPoint tile, Item item, Rotation fromDir)
m_tunnelEntries.find(key(tile));
if (teIt != m_tunnelEntries.end())
{
TunnelEntryTile& te = teIt->second;
if (!te.front)
if (teIt->second.itemSlots.size() < 4)
{
te.front = BeltItemSlot{item, 0.0};
return true;
}
if (!te.back)
{
te.back = BeltItemSlot{item, 0.0};
teIt->second.itemSlots.push_back(BeltItemSlot{item, 0.0});
return true;
}
return false;
@@ -251,11 +244,10 @@ std::optional<Item> BeltSystem::tryTakeItem(Port port)
return std::nullopt;
}
BeltTile& bt = beltIt->second;
if (bt.front && bt.front->progress >= 1.0)
if (!bt.itemSlots.empty() && bt.itemSlots.front().progress >= 1.0)
{
const Item taken = bt.front->item;
bt.front = bt.back;
bt.back = std::nullopt;
const Item taken = bt.itemSlots.front().item;
bt.itemSlots.erase(bt.itemSlots.begin());
return taken;
}
return std::nullopt;
@@ -285,11 +277,11 @@ std::optional<Item> BeltSystem::tryTakeItem(Port port)
if (txIt != m_tunnelExits.end())
{
TunnelExitTile& tx = txIt->second;
if (tx.direction == port.direction && tx.front && tx.front->progress >= 1.0)
if (tx.direction == port.direction && !tx.itemSlots.empty()
&& tx.itemSlots.front().progress >= 1.0)
{
const Item taken = tx.front->item;
tx.front = tx.back;
tx.back = std::nullopt;
const Item taken = tx.itemSlots.front().item;
tx.itemSlots.erase(tx.itemSlots.begin());
return taken;
}
}
@@ -308,9 +300,9 @@ std::optional<ItemType> BeltSystem::peekItem(Port port) const
return std::nullopt;
}
const BeltTile& bt = beltIt->second;
if (bt.front && bt.front->progress >= 1.0)
if (!bt.itemSlots.empty() && bt.itemSlots.front().progress >= 1.0)
{
return bt.front->item.type;
return bt.itemSlots.front().item.type;
}
return std::nullopt;
}
@@ -335,9 +327,10 @@ std::optional<ItemType> BeltSystem::peekItem(Port port) const
if (txIt != m_tunnelExits.end())
{
const TunnelExitTile& tx = txIt->second;
if (tx.direction == port.direction && tx.front && tx.front->progress >= 1.0)
if (tx.direction == port.direction && !tx.itemSlots.empty()
&& tx.itemSlots.front().progress >= 1.0)
{
return tx.front->item.type;
return tx.itemSlots.front().item.type;
}
}
@@ -355,14 +348,14 @@ void BeltSystem::clearTiles(const std::vector<QPoint>& tiles)
const std::map<std::pair<int, int>, BeltTile>::iterator bIt = m_belts.find(key(tile));
if (bIt != m_belts.end())
{
bIt->second.front = std::nullopt;
bIt->second.back = std::nullopt;
bIt->second.itemSlots.clear();
}
const std::map<std::pair<int, int>, SplitterTile>::iterator sIt = m_splitters.find(key(tile));
if (sIt != m_splitters.end())
{
sIt->second.back = std::nullopt;
sIt->second.back.clear();
sIt->second.backDir.clear();
sIt->second.frontA = std::nullopt;
sIt->second.frontB = std::nullopt;
}
@@ -371,8 +364,7 @@ void BeltSystem::clearTiles(const std::vector<QPoint>& tiles)
m_tunnelEntries.find(key(tile));
if (teIt != m_tunnelEntries.end())
{
teIt->second.front = std::nullopt;
teIt->second.back = std::nullopt;
teIt->second.itemSlots.clear();
for (TunnelLink& link : m_tunnelLinks)
{
if (link.entryTile == tile)
@@ -386,8 +378,7 @@ void BeltSystem::clearTiles(const std::vector<QPoint>& tiles)
m_tunnelExits.find(key(tile));
if (txIt != m_tunnelExits.end())
{
txIt->second.front = std::nullopt;
txIt->second.back = std::nullopt;
txIt->second.itemSlots.clear();
for (TunnelLink& link : m_tunnelLinks)
{
if (link.exitTile == tile)
@@ -419,33 +410,26 @@ void BeltSystem::advanceProgress()
{
BeltTile& bt = it->second;
if (bt.front)
for (std::size_t i = 0; i < bt.itemSlots.size(); ++i)
{
bt.front->progress += m_progressPerTick;
if (bt.front->progress > 1.0)
bt.itemSlots[i].progress += m_progressPerTick;
// 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.front->progress = 1.0;
bt.itemSlots[i].progress = absoluteCap;
}
}
if (bt.back)
{
bt.back->progress += m_progressPerTick;
// Back must not overtake front.
if (bt.front && bt.back->progress >= bt.front->progress)
// Gap constraint: must stay 0.25 behind the slot ahead.
if (i > 0)
{
bt.back->progress = bt.front->progress - m_progressPerTick;
if (bt.back->progress < 0.0)
const double gapCap = bt.itemSlots[i - 1].progress - 0.25;
if (bt.itemSlots[i].progress > gapCap)
{
bt.back->progress = 0.0;
bt.itemSlots[i].progress = (gapCap < 0.0 ? 0.0 : gapCap);
}
}
if (bt.back->progress > 0.5)
{
bt.back->progress = 0.5;
}
}
}
@@ -454,12 +438,26 @@ void BeltSystem::advanceProgress()
{
SplitterTile& st = it->second;
if (st.back)
for (std::size_t i = 0; i < st.back.size(); ++i)
{
st.back->progress += m_progressPerTick;
if (st.back->progress > 0.5)
st.back[i].progress += m_progressPerTick;
const double absoluteCap = 0.5 - i * 0.25;
if (st.back[i].progress > absoluteCap)
{
st.back->progress = 0.5;
st.back[i].progress = absoluteCap;
}
if (i > 0)
{
const double gapCap = st.back[i - 1].progress - 0.25;
if (gapCap < 0.0)
{
st.back[i].progress = 0.0;
}
else if (st.back[i].progress > gapCap)
{
st.back[i].progress = gapCap;
}
}
}
@@ -490,29 +488,23 @@ void BeltSystem::advanceTunnelProgress()
{
TunnelEntryTile& te = it->second;
if (te.front)
for (std::size_t i = 0; i < te.itemSlots.size(); ++i)
{
te.front->progress += m_progressPerTick;
if (te.front->progress > 1.0)
{
te.front->progress = 1.0;
}
}
te.itemSlots[i].progress += m_progressPerTick;
if (te.back)
{
te.back->progress += m_progressPerTick;
if (te.front && te.back->progress >= te.front->progress)
const double absoluteCap = 1.0 - i * 0.25;
if (te.itemSlots[i].progress > absoluteCap)
{
te.back->progress = te.front->progress - m_progressPerTick;
if (te.back->progress < 0.0)
{
te.back->progress = 0.0;
}
te.itemSlots[i].progress = absoluteCap;
}
if (te.back->progress > 0.5)
if (i > 0)
{
te.back->progress = 0.5;
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);
}
}
}
}
@@ -522,29 +514,23 @@ void BeltSystem::advanceTunnelProgress()
{
TunnelExitTile& tx = it->second;
if (tx.front)
for (std::size_t i = 0; i < tx.itemSlots.size(); ++i)
{
tx.front->progress += m_progressPerTick;
if (tx.front->progress > 1.0)
{
tx.front->progress = 1.0;
}
}
tx.itemSlots[i].progress += m_progressPerTick;
if (tx.back)
{
tx.back->progress += m_progressPerTick;
if (tx.front && tx.back->progress >= tx.front->progress)
const double absoluteCap = 1.0 - i * 0.25;
if (tx.itemSlots[i].progress > absoluteCap)
{
tx.back->progress = tx.front->progress - m_progressPerTick;
if (tx.back->progress < 0.0)
{
tx.back->progress = 0.0;
}
tx.itemSlots[i].progress = absoluteCap;
}
if (tx.back->progress > 0.5)
if (i > 0)
{
tx.back->progress = 0.5;
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);
}
}
}
}
@@ -561,7 +547,7 @@ void BeltSystem::advanceTunnelProgress()
}
if (i > 0)
{
const double maxProgress = link.items[i - 1].progress - 0.5;
const double maxProgress = link.items[i - 1].progress - 0.25;
if (ti.progress > maxProgress)
{
ti.progress = maxProgress;
@@ -582,7 +568,7 @@ void BeltSystem::moveItemsToNextTile()
it != m_belts.end(); ++it)
{
BeltTile& bt = it->second;
if (!bt.front || bt.front->progress < 1.0)
if (bt.itemSlots.empty() || bt.itemSlots.front().progress < 1.0)
{
continue;
}
@@ -595,39 +581,31 @@ void BeltSystem::moveItemsToNextTile()
if (nextBelt != m_belts.end())
{
if (tryPlaceOnBelt(next, bt.front->item))
if (tryPlaceOnBelt(next, bt.itemSlots.front().item))
{
bt.front = bt.back;
bt.back = std::nullopt;
bt.itemSlots.erase(bt.itemSlots.begin());
}
// else: next belt is full — item stays blocked at progress 1.0.
}
else if (nextSplitter != m_splitters.end())
{
if (!nextSplitter->second.back)
if (nextSplitter->second.back.size() < 2)
{
nextSplitter->second.back = BeltItemSlot{bt.front->item, 0.0};
nextSplitter->second.backDir = bt.direction;
bt.front = bt.back;
bt.back = std::nullopt;
nextSplitter->second.back.push_back(BeltItemSlot{bt.itemSlots.front().item, 0.0});
nextSplitter->second.backDir.push_back(bt.direction);
bt.itemSlots.erase(bt.itemSlots.begin());
}
}
else
{
const std::map<std::pair<int, int>, TunnelEntryTile>::iterator nextEntry =
m_tunnelEntries.find(key(next));
if (nextEntry != m_tunnelEntries.end() && !nextEntry->second.back)
if (nextEntry != m_tunnelEntries.end()
&& nextEntry->second.itemSlots.size() < 4)
{
if (!nextEntry->second.front)
{
nextEntry->second.front = BeltItemSlot{bt.front->item, 0.0};
}
else
{
nextEntry->second.back = BeltItemSlot{bt.front->item, 0.0};
}
bt.front = bt.back;
bt.back = std::nullopt;
nextEntry->second.itemSlots.push_back(
BeltItemSlot{bt.itemSlots.front().item, 0.0});
bt.itemSlots.erase(bt.itemSlots.begin());
}
}
}
@@ -663,17 +641,16 @@ void BeltSystem::moveItemsToNextTile()
it != m_tunnelExits.end(); ++it)
{
TunnelExitTile& tx = it->second;
if (!tx.front || tx.front->progress < 1.0)
if (tx.itemSlots.empty() || tx.itemSlots.front().progress < 1.0)
{
continue;
}
const QPoint here = QPoint(it->first.first, it->first.second);
const QPoint next = adjacentTile(here, tx.direction);
if (tryPushToTile(next, tx.front->item, tx.direction))
if (tryPushToTile(next, tx.itemSlots.front().item, tx.direction))
{
tx.front = tx.back;
tx.back = std::nullopt;
tx.itemSlots.erase(tx.itemSlots.begin());
}
}
}
@@ -688,23 +665,22 @@ void BeltSystem::moveTunnelItems()
if (teIt != m_tunnelEntries.end())
{
TunnelEntryTile& te = teIt->second;
if (te.front && te.front->progress >= 1.0)
if (!te.itemSlots.empty() && te.itemSlots.front().progress >= 1.0)
{
const bool canEnter = link.items.empty()
|| link.items.back().progress >= 0.5;
|| link.items.back().progress >= 0.25;
if (canEnter)
{
TunnelTransitItem ti;
ti.item = te.front->item;
ti.item = te.itemSlots.front().item;
ti.progress = 0.0;
link.items.push_back(ti);
te.front = te.back;
te.back = std::nullopt;
te.itemSlots.erase(te.itemSlots.begin());
}
}
}
// Transit front → exit back
// Transit front → exit
if (!link.items.empty() && link.items.front().progress >= link.length)
{
const std::map<std::pair<int, int>, TunnelExitTile>::iterator txIt =
@@ -712,14 +688,9 @@ void BeltSystem::moveTunnelItems()
if (txIt != m_tunnelExits.end())
{
TunnelExitTile& tx = txIt->second;
if (!tx.back && !tx.front)
if (tx.itemSlots.size() < 4)
{
tx.front = BeltItemSlot{link.items.front().item, 0.0};
link.items.erase(link.items.begin());
}
else if (!tx.back && tx.front)
{
tx.back = BeltItemSlot{link.items.front().item, 0.0};
tx.itemSlots.push_back(BeltItemSlot{link.items.front().item, 0.0});
link.items.erase(link.items.begin());
}
}
@@ -733,24 +704,26 @@ void BeltSystem::routeSplitterItems()
it != m_splitters.end(); ++it)
{
SplitterTile& st = it->second;
if (!st.back || st.back->progress < 0.5)
if (st.back.empty() || st.back.front().progress < 0.5)
{
continue;
}
const Item& item = st.back->item;
const Item& item = st.back.front().item;
const bool matchesA = st.filterA.empty() ||
std::find(st.filterA.begin(), st.filterA.end(), item.type) != st.filterA.end();
const bool matchesB = st.filterB.empty() ||
std::find(st.filterB.begin(), st.filterB.end(), item.type) != st.filterB.end();
bool routed = false;
if (matchesA && !matchesB)
{
if (!st.frontA)
{
st.frontA = BeltItemSlot{item, 0.0};
st.back = std::nullopt;
routed = true;
}
}
else if (matchesB && !matchesA)
@@ -758,7 +731,7 @@ void BeltSystem::routeSplitterItems()
if (!st.frontB)
{
st.frontB = BeltItemSlot{item, 0.0};
st.back = std::nullopt;
routed = true;
}
}
else if (matchesA && matchesB)
@@ -769,30 +742,36 @@ void BeltSystem::routeSplitterItems()
if (preferA && !st.frontA)
{
st.frontA = BeltItemSlot{item, 0.0};
st.back = std::nullopt;
st.nextOutputIsA = false;
routed = true;
}
else if (!preferA && !st.frontB)
{
st.frontB = BeltItemSlot{item, 0.0};
st.back = std::nullopt;
st.nextOutputIsA = true;
routed = true;
}
else if (preferA && !st.frontB)
{
// Preferred (A) is full — fall back to B; nextOutputIsA stays.
st.frontB = BeltItemSlot{item, 0.0};
st.back = std::nullopt;
routed = true;
}
else if (!preferA && !st.frontA)
{
// Preferred (B) is full — fall back to A; nextOutputIsA stays.
st.frontA = BeltItemSlot{item, 0.0};
st.back = std::nullopt;
routed = true;
}
// else both fronts occupied — back stays.
}
// else (!matchesA && !matchesB): stall — back stays.
if (routed)
{
st.back.erase(st.back.begin());
st.backDir.erase(st.backDir.begin());
}
}
}
@@ -805,24 +784,12 @@ bool BeltSystem::tryPlaceOnBelt(QPoint tile, Item item)
}
BeltTile& bt = it->second;
if (!bt.front)
if (bt.itemSlots.size() < 4)
{
bt.front = BeltItemSlot{item, 0.0};
bt.itemSlots.push_back(BeltItemSlot{item, 0.0});
return true;
}
if (!bt.back)
{
bt.back = BeltItemSlot{item, 0.0};
// Ensure ordering invariant: front has higher progress.
if (bt.back->progress > bt.front->progress)
{
std::swap(bt.front, bt.back);
}
return true;
}
return false; // both slots occupied
return false; // all slots occupied
}
bool BeltSystem::tryPushToTile(QPoint dest, Item item, Rotation fromDir)
@@ -836,10 +803,10 @@ bool BeltSystem::tryPushToTile(QPoint dest, Item item, Rotation fromDir)
m_splitters.find(key(dest));
if (splIt != m_splitters.end())
{
if (!splIt->second.back)
if (splIt->second.back.size() < 2)
{
splIt->second.back = BeltItemSlot{item, 0.0};
splIt->second.backDir = fromDir;
splIt->second.back.push_back(BeltItemSlot{item, 0.0});
splIt->second.backDir.push_back(fromDir);
return true;
}
return false;
@@ -849,15 +816,9 @@ bool BeltSystem::tryPushToTile(QPoint dest, Item item, Rotation fromDir)
m_tunnelEntries.find(key(dest));
if (teIt != m_tunnelEntries.end())
{
TunnelEntryTile& te = teIt->second;
if (!te.front)
if (teIt->second.itemSlots.size() < 4)
{
te.front = BeltItemSlot{item, 0.0};
return true;
}
if (!te.back)
{
te.back = BeltItemSlot{item, 0.0};
teIt->second.itemSlots.push_back(BeltItemSlot{item, 0.0});
return true;
}
return false;
@@ -867,15 +828,9 @@ bool BeltSystem::tryPushToTile(QPoint dest, Item item, Rotation fromDir)
m_tunnelExits.find(key(dest));
if (txIt != m_tunnelExits.end())
{
TunnelExitTile& tx = txIt->second;
if (!tx.front)
if (txIt->second.itemSlots.size() < 4)
{
tx.front = BeltItemSlot{item, 0.0};
return true;
}
if (!tx.back)
{
tx.back = BeltItemSlot{item, 0.0};
txIt->second.itemSlots.push_back(BeltItemSlot{item, 0.0});
return true;
}
return false;
@@ -901,19 +856,12 @@ void BeltSystem::forEachVisualItem(QRect viewportTiles,
const BeltTile& bt = entry.second;
if (bt.front)
// Render least-progressed first (bottom) → most-progressed last (top).
for (int i = static_cast<int>(bt.itemSlots.size()) - 1; i >= 0; --i)
{
VisualItem vi;
vi.type = bt.front->item.type;
vi.worldPos = slotWorldPos(tile, bt.direction, bt.front->progress);
visit(vi);
}
if (bt.back)
{
VisualItem vi;
vi.type = bt.back->item.type;
vi.worldPos = slotWorldPos(tile, bt.direction, bt.back->progress);
vi.type = bt.itemSlots[i].item.type;
vi.worldPos = slotWorldPos(tile, bt.direction, bt.itemSlots[i].progress);
visit(vi);
}
}
@@ -928,28 +876,51 @@ void BeltSystem::forEachVisualItem(QRect viewportTiles,
const SplitterTile& st = entry.second;
if (st.back)
// Unassigned items: least-progressed first (bottom), then higher-progressed (top).
for (int i = static_cast<int>(st.back.size()) - 1; i >= 0; --i)
{
VisualItem vi;
vi.type = st.back->item.type;
vi.worldPos = slotWorldPos(tile, st.backDir, st.back->progress);
vi.type = st.back[i].item.type;
vi.worldPos = slotWorldPos(tile, st.backDir[i], st.back[i].progress);
visit(vi);
}
if (st.frontA)
// Output-slot items rendered on top of unassigned, in clockwise order from East.
// East=0, South=1, West=2, North=3 — lower rank rendered first (bottom).
auto clockwiseRank = [](Rotation r) -> int
{
VisualItem vi;
vi.type = st.frontA->item.type;
vi.worldPos = slotWorldPos(tile, st.outputA, st.frontA->progress);
visit(vi);
}
switch (r)
{
case Rotation::East: return 0;
case Rotation::South: return 1;
case Rotation::West: return 2;
case Rotation::North: return 3;
}
return 0;
};
if (st.frontB)
const bool aBeforeB = clockwiseRank(st.outputA) <= clockwiseRank(st.outputB);
auto renderFront = [&](const std::optional<BeltItemSlot>& slot, Rotation dir)
{
VisualItem vi;
vi.type = st.frontB->item.type;
vi.worldPos = slotWorldPos(tile, st.outputB, st.frontB->progress);
visit(vi);
if (slot)
{
VisualItem vi;
vi.type = slot->item.type;
vi.worldPos = slotWorldPos(tile, dir, slot->progress);
visit(vi);
}
};
if (aBeforeB)
{
renderFront(st.frontA, st.outputA);
renderFront(st.frontB, st.outputB);
}
else
{
renderFront(st.frontB, st.outputB);
renderFront(st.frontA, st.outputA);
}
}
@@ -962,18 +933,11 @@ void BeltSystem::forEachVisualItem(QRect viewportTiles,
}
const TunnelEntryTile& te = entry.second;
if (te.front)
for (int i = static_cast<int>(te.itemSlots.size()) - 1; i >= 0; --i)
{
VisualItem vi;
vi.type = te.front->item.type;
vi.worldPos = slotWorldPos(tile, te.direction, te.front->progress);
visit(vi);
}
if (te.back)
{
VisualItem vi;
vi.type = te.back->item.type;
vi.worldPos = slotWorldPos(tile, te.direction, te.back->progress);
vi.type = te.itemSlots[i].item.type;
vi.worldPos = slotWorldPos(tile, te.direction, te.itemSlots[i].progress);
visit(vi);
}
}
@@ -987,18 +951,11 @@ void BeltSystem::forEachVisualItem(QRect viewportTiles,
}
const TunnelExitTile& tx = entry.second;
if (tx.front)
for (int i = static_cast<int>(tx.itemSlots.size()) - 1; i >= 0; --i)
{
VisualItem vi;
vi.type = tx.front->item.type;
vi.worldPos = slotWorldPos(tile, tx.direction, tx.front->progress);
visit(vi);
}
if (tx.back)
{
VisualItem vi;
vi.type = tx.back->item.type;
vi.worldPos = slotWorldPos(tile, tx.direction, tx.back->progress);
vi.type = tx.itemSlots[i].item.type;
vi.worldPos = slotWorldPos(tile, tx.direction, tx.itemSlots[i].progress);
visit(vi);
}
}

View File

@@ -123,8 +123,8 @@ private:
struct BeltTile
{
Rotation direction;
std::optional<BeltItemSlot> front; // higher progress; closer to output
std::optional<BeltItemSlot> back; // lower progress; closer to input
// front (highest progress) at index 0; back (just entered) at end. Max 4.
std::vector<BeltItemSlot> itemSlots;
};
struct SplitterTile
@@ -134,8 +134,9 @@ private:
std::vector<ItemType> filterA; // empty = accept all
std::vector<ItemType> filterB;
bool nextOutputIsA; // alternation state
std::optional<BeltItemSlot> back; // progress [0, 0.5]; entering from input belt
Rotation backDir; // direction of the feeding belt (for animation)
// Unassigned items: [0] = routing candidate (higher progress, caps at 0.5). Max 2.
std::vector<BeltItemSlot> back;
std::vector<Rotation> backDir; // feeding belt direction, parallel to back
std::optional<BeltItemSlot> frontA; // progress [0, 1]; routed to outputA
std::optional<BeltItemSlot> frontB; // progress [0, 1]; routed to outputB
};
@@ -144,15 +145,15 @@ private:
{
Rotation direction;
int maxDistance;
std::optional<BeltItemSlot> front;
std::optional<BeltItemSlot> back;
// front (highest progress) at index 0; back at end. Max 4.
std::vector<BeltItemSlot> itemSlots;
};
struct TunnelExitTile
{
Rotation direction;
std::optional<BeltItemSlot> front;
std::optional<BeltItemSlot> back;
// front (highest progress) at index 0; back at end. Max 4.
std::vector<BeltItemSlot> itemSlots;
};
struct TunnelTransitItem

View File

@@ -66,17 +66,19 @@ TEST_CASE("BeltSystem: tryPutItem fails after removeTile", "[belt]")
// Capacity
// ---------------------------------------------------------------------------
TEST_CASE("BeltSystem: two items fit in one tile", "[belt]")
TEST_CASE("BeltSystem: four items fit in one tile", "[belt]")
{
BeltSystem bs(kFastBeltSpeed);
const QPoint tile(0, 0);
bs.placeBelt(tile, Rotation::East);
REQUIRE(bs.tryPutItem(tile, makeItem("iron_ore")));
REQUIRE(bs.tryPutItem(tile, makeItem("copper_ore")));
REQUIRE(bs.tryPutItem(tile, makeItem("a")));
REQUIRE(bs.tryPutItem(tile, makeItem("b")));
REQUIRE(bs.tryPutItem(tile, makeItem("c")));
REQUIRE(bs.tryPutItem(tile, makeItem("d")));
}
TEST_CASE("BeltSystem: third tryPutItem on full tile returns false", "[belt]")
TEST_CASE("BeltSystem: fifth tryPutItem on full tile returns false", "[belt]")
{
BeltSystem bs(kFastBeltSpeed);
const QPoint tile(0, 0);
@@ -84,8 +86,10 @@ TEST_CASE("BeltSystem: third tryPutItem on full tile returns false", "[belt]")
bs.tryPutItem(tile, makeItem("a"));
bs.tryPutItem(tile, makeItem("b"));
bs.tryPutItem(tile, makeItem("c"));
bs.tryPutItem(tile, makeItem("d"));
REQUIRE_FALSE(bs.tryPutItem(tile, makeItem("c")));
REQUIRE_FALSE(bs.tryPutItem(tile, makeItem("e")));
}
// ---------------------------------------------------------------------------
@@ -217,6 +221,8 @@ TEST_CASE("BeltSystem: item stays blocked when next tile is full", "[belt]")
// Fill tileB to capacity.
bs.tryPutItem(tileB, makeItem("b1"));
bs.tryPutItem(tileB, makeItem("b2"));
bs.tryPutItem(tileB, makeItem("b3"));
bs.tryPutItem(tileB, makeItem("b4"));
// Place item in tileA — should be blocked.
bs.tryPutItem(tileA, makeItem("a1"));
@@ -226,11 +232,11 @@ TEST_CASE("BeltSystem: item stays blocked when next tile is full", "[belt]")
REQUIRE(bs.tryTakeItem(eastPort(tileA)).has_value());
}
TEST_CASE("BeltSystem: belt back slot is capped at progress 0.5", "[belt]")
TEST_CASE("BeltSystem: belt second slot is capped at progress 0.75", "[belt]")
{
// Use progress/tick = 0.4 so the cap is observable: without it, back would
// advance to 0.8 while front is stuck at 1.0, and then need only 1 more tick
// after being promoted. With the cap it stays at 0.5 and needs 2 more ticks.
// Use progress/tick = 0.4 so the cap is observable: without it, slot[1] would
// advance to 0.8 while slot[0] is stuck at 1.0. With the 0.75 cap it stays
// at 0.75 and needs exactly 1 more tick after promotion.
const double medBeltSpeed = 0.4 * static_cast<double>(kTickRateHz);
BeltSystem bs(medBeltSpeed);
@@ -239,21 +245,18 @@ TEST_CASE("BeltSystem: belt back slot is capped at progress 0.5", "[belt]")
// Advance front item to the output edge; it stays there (no next tile).
bs.tryPutItem(tile, makeItem("front_item"));
bs.tick(); // front: 0.4
bs.tick(); // front: 0.8
bs.tick(); // front: 1.0 (capped, stuck)
bs.tick(); // slot[0]: 0.4
bs.tick(); // slot[0]: 0.8
bs.tick(); // slot[0]: 1.0 (capped, stuck)
// Place back item; front is at 1.0 and not blocking (back < 1.0).
// Place second item; slot[0] is at 1.0.
bs.tryPutItem(tile, makeItem("back_item"));
bs.tick(); // back: 0.4
bs.tick(); // back would reach 0.8 — must be capped at 0.5
bs.tick(); // slot[1]: 0.4
bs.tick(); // slot[1] would reach 0.8 — capped at 0.75
// Remove front; back (now promoted to front) must be at 0.5, not 0.8.
// Remove front; slot[1] (now promoted to slot[0]) must be at 0.75.
REQUIRE(bs.tryTakeItem(eastPort(tile)).has_value());
// At 0.4/tick, 0.5 → 0.9 after one tick — not at 1.0 yet.
bs.tick();
REQUIRE_FALSE(bs.tryTakeItem(eastPort(tile)).has_value());
// 0.9 → 1.0 after a second tick — now available.
// At 0.4/tick, 0.75 → 1.0 (capped) after one tick — available.
bs.tick();
REQUIRE(bs.tryTakeItem(eastPort(tile)).has_value());
}