Animate items emerging from building output ports
This commit is contained in:
@@ -68,7 +68,7 @@ Modules in `modules.toml` define a `surface_mask` — a list of strings that des
|
||||
## 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; 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-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). Items emerging from a building's output port are rendered by these same rules on that port's output belt (REQ-MAT-OUTPUT-EMERGE).
|
||||
- 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`.
|
||||
@@ -148,7 +148,13 @@ Modules in `modules.toml` define a `surface_mask` — a list of strings that des
|
||||
|
||||
- REQ-MAT-BELT-ONLY: Materials are transported exclusively via belts, splitters, and tunnels.
|
||||
- REQ-MAT-INPUT-PORTS: A building accepts items from any adjacent belt tile on any edge of its footprint (excluding cells occupied by output port(s)) whose direction points toward the building, provided the item is an input required by the currently selected recipe and the matching per-material input buffer has free space.
|
||||
- REQ-MAT-OUTPUT-PORT: Each building has one or more fixed output port(s) defined by its surface_mask (direction determined by rotation). Produced items are placed onto the belt at the output port tile. The belt's direction is otherwise unconstrained (it may flow away from the building or perpendicular to it), except that a belt oriented with its own output edge facing back into the building refuses the item and the deposit fails (REQ-MAT-ACCEPT-DIR).
|
||||
- REQ-MAT-OUTPUT-PORT: Each building has one or more fixed output port(s) defined by its surface_mask (direction determined by rotation). Produced items do not appear on the outgoing belt instantly; each item leaves the building by first emerging across the output port tile on that port's own output belt and then transferring onto the adjacent real belt tile (REQ-MAT-OUTPUT-EMERGE). The adjacent belt's direction is otherwise unconstrained (it may flow away from the building or perpendicular to it), except that a belt oriented with its own output edge facing back into the building refuses the transfer and the item stays stuck at the port (REQ-MAT-ACCEPT-DIR, REQ-MAT-OUTPUT-EMERGE).
|
||||
- REQ-MAT-OUTPUT-EMERGE: Items emerge from a building output port as an animation rather than popping directly onto the outgoing belt. Each output port has its own **output belt** — a virtual belt tile occupying the output port tile, oriented in the port's facing direction, with progress 0.0 at the tile's inner edge and 1.0 at the outer (port) edge adjacent to the next real belt tile. It reuses the belt subsystem: movement at belt speed (REQ-GW-BELT-SPEED), item rendering and spacing (REQ-GW-TILE-SIZE), and capacity/packing (REQ-GW-BELT-CAPACITY), but restricted to the 0.5→1.0 half of the tile. This applies to every building that outputs items onto belts (Miner, Smelter, Assembler, Reprocessing Plant, Salvage Bay); it does not apply to the Shipyard, which spawns a ship rather than a belt item (REQ-SHP-SPAWN-PLAYER).
|
||||
- **Feeding.** While the output buffer (REQ-MAT-OUTPUT-BUFFER) holds an item that has not yet begun emerging and the output belt's entry slot at progress 0.5 is free (per REQ-GW-BELT-CAPACITY spacing — no emerging item within a quarter tile of progress 0.5), the next buffered item is placed on the output belt at progress 0.5. Because only the 0.5→1.0 span is used, the output belt holds at most three emerging items (progress 0.5, 0.75, 1.0); once that span is full the building places no further items on it even if the output buffer still holds more.
|
||||
- **Cosmetic hold.** An emerging item still counts as residing in the output buffer (REQ-MAT-GLOBAL-STOCK) for the whole animation; it only leaves the building when it transfers onto a real belt tile at progress 1.0. The output belt therefore adds no inventory capacity beyond the output buffer, and clearing the output buffer on a recipe or schematic change (REQ-MAT-OUTPUT-BUFFER) also removes any items currently emerging.
|
||||
- **Travel & handoff.** An emerging item advances from progress 0.5 to 1.0 at belt speed. At progress 1.0 it attempts to transfer onto the adjacent real belt tile using the normal belt hand-off and accept-direction rules (REQ-MAT-OUTPUT-PORT, REQ-MAT-ACCEPT-DIR): the transfer succeeds only if a transport tile exists there, is not oriented with its output edge facing back into the building, and has free space. On success the item leaves the output buffer and becomes an ordinary item on that belt tile.
|
||||
- **Stuck items.** If there is no next real belt tile, or the transfer is refused or blocked, the emerging item stops at progress 1.0 and is rendered there (still counted in the output buffer). Following items pile up behind it at progress 0.75 and 0.5 per the packing above, and once the 0.5→1.0 span is full no further items emerge until the front item transfers.
|
||||
- **Emergence rendering (no pop-in).** An emerging item must not simply appear at progress 0.5. The output port tile's building is rendered over the output belt, so an emerging item is occluded while inside the footprint and is revealed progressively as it slides past the port edge — appearing to physically emerge from the building. The portion of the item still within the output port tile is hidden; the portion past the outer edge is drawn.
|
||||
- REQ-MAT-ACCEPT-DIR: A transport tile (belt, splitter, tunnel entry, or tunnel exit) accepts an incoming item only through a non-output edge; an item that would enter through one of the tile's output edges is refused. For a belt or a tunnel entry/exit the sole output edge is the one in its facing direction; for a splitter either of its two output directions is an output edge. This applies both to items pushed from an adjacent transport tile and to items deposited by a building's output port (REQ-MAT-OUTPUT-PORT).
|
||||
- REQ-MAT-INPUT-BUFFER: Each building has one input buffer per required input material. Each per-material buffer holds up to twice that material's per-cycle requirement. When the player selects a new recipe or schematic, all items in all input buffers are cleared.
|
||||
- REQ-MAT-OUTPUT-BUFFER: Each building has an output buffer that holds up to twice the quantity produced by one production cycle. If the output buffer is full, production stops until space is available. When the player selects a new recipe or schematic, all items in the output buffer are cleared (relevant when the adjacent belt is jammed and items have accumulated).
|
||||
|
||||
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];
|
||||
|
||||
// 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))
|
||||
{
|
||||
lane.erase(lane.begin());
|
||||
}
|
||||
|
||||
for (const Port& outputPort : building.outputPorts)
|
||||
{
|
||||
if (building.outputBuffer.items.empty())
|
||||
{
|
||||
break;
|
||||
}
|
||||
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