Compare commits
6 Commits
78f746d352
...
409ec93d7d
| Author | SHA1 | Date | |
|---|---|---|---|
| 409ec93d7d | |||
| 604ad7f13f | |||
| 55997ef851 | |||
| fff5d43352 | |||
| eba8caac31 | |||
| ea30d2ab7b |
@@ -66,6 +66,7 @@ void BeltSystem::placeSplitter(QPoint tile, Rotation outputA, Rotation outputB)
|
|||||||
st.outputA = outputA;
|
st.outputA = outputA;
|
||||||
st.outputB = outputB;
|
st.outputB = outputB;
|
||||||
st.nextOutputIsA = true;
|
st.nextOutputIsA = true;
|
||||||
|
st.backDir = Rotation::North; // irrelevant until back is set
|
||||||
m_splitters[key(tile)] = st;
|
m_splitters[key(tile)] = st;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -104,17 +105,14 @@ bool BeltSystem::tryPutItem(QPoint tile, Item item)
|
|||||||
|
|
||||||
std::optional<Item> BeltSystem::tryTakeItem(Port port)
|
std::optional<Item> BeltSystem::tryTakeItem(Port port)
|
||||||
{
|
{
|
||||||
const std::map<std::pair<int, int>, BeltTile>::iterator it = m_belts.find(key(port.tile));
|
const std::map<std::pair<int, int>, BeltTile>::iterator beltIt = m_belts.find(key(port.tile));
|
||||||
if (it == m_belts.end())
|
if (beltIt != m_belts.end())
|
||||||
|
{
|
||||||
|
if (beltIt->second.direction != port.direction)
|
||||||
{
|
{
|
||||||
return std::nullopt;
|
return std::nullopt;
|
||||||
}
|
}
|
||||||
if (it->second.direction != port.direction)
|
BeltTile& bt = beltIt->second;
|
||||||
{
|
|
||||||
return std::nullopt;
|
|
||||||
}
|
|
||||||
|
|
||||||
BeltTile& bt = it->second;
|
|
||||||
if (bt.front && bt.front->progress >= 1.0)
|
if (bt.front && bt.front->progress >= 1.0)
|
||||||
{
|
{
|
||||||
const Item taken = bt.front->item;
|
const Item taken = bt.front->item;
|
||||||
@@ -125,20 +123,39 @@ std::optional<Item> BeltSystem::tryTakeItem(Port port)
|
|||||||
return std::nullopt;
|
return std::nullopt;
|
||||||
}
|
}
|
||||||
|
|
||||||
std::optional<ItemType> BeltSystem::peekItem(Port port) const
|
const std::map<std::pair<int, int>, SplitterTile>::iterator splIt =
|
||||||
|
m_splitters.find(key(port.tile));
|
||||||
|
if (splIt != m_splitters.end())
|
||||||
{
|
{
|
||||||
const std::map<std::pair<int, int>, BeltTile>::const_iterator it =
|
SplitterTile& st = splIt->second;
|
||||||
m_belts.find(key(port.tile));
|
if (port.direction == st.outputA && st.frontA && st.frontA->progress >= 1.0)
|
||||||
if (it == m_belts.end())
|
|
||||||
{
|
{
|
||||||
return std::nullopt;
|
const Item taken = st.frontA->item;
|
||||||
|
st.frontA = std::nullopt;
|
||||||
|
return taken;
|
||||||
}
|
}
|
||||||
if (it->second.direction != port.direction)
|
if (port.direction == st.outputB && st.frontB && st.frontB->progress >= 1.0)
|
||||||
{
|
{
|
||||||
|
const Item taken = st.frontB->item;
|
||||||
|
st.frontB = std::nullopt;
|
||||||
|
return taken;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return std::nullopt;
|
return std::nullopt;
|
||||||
}
|
}
|
||||||
|
|
||||||
const BeltTile& bt = it->second;
|
std::optional<ItemType> BeltSystem::peekItem(Port port) const
|
||||||
|
{
|
||||||
|
const std::map<std::pair<int, int>, BeltTile>::const_iterator beltIt =
|
||||||
|
m_belts.find(key(port.tile));
|
||||||
|
if (beltIt != m_belts.end())
|
||||||
|
{
|
||||||
|
if (beltIt->second.direction != port.direction)
|
||||||
|
{
|
||||||
|
return std::nullopt;
|
||||||
|
}
|
||||||
|
const BeltTile& bt = beltIt->second;
|
||||||
if (bt.front && bt.front->progress >= 1.0)
|
if (bt.front && bt.front->progress >= 1.0)
|
||||||
{
|
{
|
||||||
return bt.front->item.type;
|
return bt.front->item.type;
|
||||||
@@ -146,6 +163,24 @@ std::optional<ItemType> BeltSystem::peekItem(Port port) const
|
|||||||
return std::nullopt;
|
return std::nullopt;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const std::map<std::pair<int, int>, SplitterTile>::const_iterator splIt =
|
||||||
|
m_splitters.find(key(port.tile));
|
||||||
|
if (splIt != m_splitters.end())
|
||||||
|
{
|
||||||
|
const SplitterTile& st = splIt->second;
|
||||||
|
if (port.direction == st.outputA && st.frontA && st.frontA->progress >= 1.0)
|
||||||
|
{
|
||||||
|
return st.frontA->item.type;
|
||||||
|
}
|
||||||
|
if (port.direction == st.outputB && st.frontB && st.frontB->progress >= 1.0)
|
||||||
|
{
|
||||||
|
return st.frontB->item.type;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return std::nullopt;
|
||||||
|
}
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
// Maintenance
|
// Maintenance
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
@@ -164,7 +199,9 @@ void BeltSystem::clearTiles(const std::vector<QPoint>& tiles)
|
|||||||
const std::map<std::pair<int, int>, SplitterTile>::iterator sIt = m_splitters.find(key(tile));
|
const std::map<std::pair<int, int>, SplitterTile>::iterator sIt = m_splitters.find(key(tile));
|
||||||
if (sIt != m_splitters.end())
|
if (sIt != m_splitters.end())
|
||||||
{
|
{
|
||||||
sIt->second.heldItem = std::nullopt;
|
sIt->second.back = std::nullopt;
|
||||||
|
sIt->second.frontA = std::nullopt;
|
||||||
|
sIt->second.frontB = std::nullopt;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -210,9 +247,42 @@ void BeltSystem::advanceProgress()
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (bt.back->progress > 1.0)
|
if (bt.back->progress > 0.5)
|
||||||
{
|
{
|
||||||
bt.back->progress = 1.0;
|
bt.back->progress = 0.5;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (std::map<std::pair<int, int>, SplitterTile>::iterator it = m_splitters.begin();
|
||||||
|
it != m_splitters.end(); ++it)
|
||||||
|
{
|
||||||
|
SplitterTile& st = it->second;
|
||||||
|
|
||||||
|
if (st.back)
|
||||||
|
{
|
||||||
|
st.back->progress += m_progressPerTick;
|
||||||
|
if (st.back->progress > 0.5)
|
||||||
|
{
|
||||||
|
st.back->progress = 0.5;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (st.frontA)
|
||||||
|
{
|
||||||
|
st.frontA->progress += m_progressPerTick;
|
||||||
|
if (st.frontA->progress > 1.0)
|
||||||
|
{
|
||||||
|
st.frontA->progress = 1.0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (st.frontB)
|
||||||
|
{
|
||||||
|
st.frontB->progress += m_progressPerTick;
|
||||||
|
if (st.frontB->progress > 1.0)
|
||||||
|
{
|
||||||
|
st.frontB->progress = 1.0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -220,6 +290,7 @@ void BeltSystem::advanceProgress()
|
|||||||
|
|
||||||
void BeltSystem::moveItemsToNextTile()
|
void BeltSystem::moveItemsToNextTile()
|
||||||
{
|
{
|
||||||
|
// Belt items advancing into the next tile.
|
||||||
for (std::map<std::pair<int, int>, BeltTile>::iterator it = m_belts.begin();
|
for (std::map<std::pair<int, int>, BeltTile>::iterator it = m_belts.begin();
|
||||||
it != m_belts.end(); ++it)
|
it != m_belts.end(); ++it)
|
||||||
{
|
{
|
||||||
@@ -246,18 +317,47 @@ void BeltSystem::moveItemsToNextTile()
|
|||||||
}
|
}
|
||||||
else if (nextSplitter != m_splitters.end())
|
else if (nextSplitter != m_splitters.end())
|
||||||
{
|
{
|
||||||
if (!nextSplitter->second.heldItem)
|
if (!nextSplitter->second.back)
|
||||||
{
|
{
|
||||||
nextSplitter->second.heldItem = bt.front->item;
|
nextSplitter->second.back = BeltItemSlot{bt.front->item, 0.0};
|
||||||
|
nextSplitter->second.backDir = bt.direction;
|
||||||
bt.front = bt.back;
|
bt.front = bt.back;
|
||||||
bt.back = std::nullopt;
|
bt.back = std::nullopt;
|
||||||
}
|
}
|
||||||
// else: splitter busy — item stays blocked at progress 1.0.
|
// else: splitter back occupied — item stays blocked at progress 1.0.
|
||||||
}
|
}
|
||||||
// else: no tile registered (e.g. open space, or building input port).
|
// else: no tile registered (e.g. open space, or building input port).
|
||||||
// Items leaving into unregistered tiles are not consumed here — the
|
// Items leaving into unregistered tiles are not consumed here — the
|
||||||
// building pull step uses tryTakeItem for that.
|
// building pull step uses tryTakeItem for that.
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Splitter front slots advancing into downstream belt tiles.
|
||||||
|
for (std::map<std::pair<int, int>, SplitterTile>::iterator it = m_splitters.begin();
|
||||||
|
it != m_splitters.end(); ++it)
|
||||||
|
{
|
||||||
|
SplitterTile& st = it->second;
|
||||||
|
const QPoint here = QPoint(it->first.first, it->first.second);
|
||||||
|
|
||||||
|
if (st.frontA && st.frontA->progress >= 1.0)
|
||||||
|
{
|
||||||
|
const QPoint dest = adjacentTile(here, st.outputA);
|
||||||
|
if (tryPlaceOnBelt(dest, st.frontA->item))
|
||||||
|
{
|
||||||
|
st.frontA = std::nullopt;
|
||||||
|
}
|
||||||
|
// else: downstream belt full or absent — item stays at progress 1.0
|
||||||
|
// for building pickup via tryTakeItem.
|
||||||
|
}
|
||||||
|
|
||||||
|
if (st.frontB && st.frontB->progress >= 1.0)
|
||||||
|
{
|
||||||
|
const QPoint dest = adjacentTile(here, st.outputB);
|
||||||
|
if (tryPlaceOnBelt(dest, st.frontB->item))
|
||||||
|
{
|
||||||
|
st.frontB = std::nullopt;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void BeltSystem::routeSplitterItems()
|
void BeltSystem::routeSplitterItems()
|
||||||
@@ -266,12 +366,12 @@ void BeltSystem::routeSplitterItems()
|
|||||||
it != m_splitters.end(); ++it)
|
it != m_splitters.end(); ++it)
|
||||||
{
|
{
|
||||||
SplitterTile& st = it->second;
|
SplitterTile& st = it->second;
|
||||||
if (!st.heldItem)
|
if (!st.back || st.back->progress < 0.5)
|
||||||
{
|
{
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
const Item& item = *st.heldItem;
|
const Item& item = st.back->item;
|
||||||
|
|
||||||
const bool matchesA = st.filterA.empty() ||
|
const bool matchesA = st.filterA.empty() ||
|
||||||
std::find(st.filterA.begin(), st.filterA.end(), item.type) != st.filterA.end();
|
std::find(st.filterA.begin(), st.filterA.end(), item.type) != st.filterA.end();
|
||||||
@@ -280,42 +380,52 @@ void BeltSystem::routeSplitterItems()
|
|||||||
|
|
||||||
if (matchesA && !matchesB)
|
if (matchesA && !matchesB)
|
||||||
{
|
{
|
||||||
const QPoint dest = adjacentTile(QPoint(it->first.first, it->first.second), st.outputA);
|
if (!st.frontA)
|
||||||
if (tryPlaceOnBelt(dest, item))
|
|
||||||
{
|
{
|
||||||
st.heldItem = std::nullopt;
|
st.frontA = BeltItemSlot{item, 0.0};
|
||||||
|
st.back = std::nullopt;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else if (matchesB && !matchesA)
|
else if (matchesB && !matchesA)
|
||||||
{
|
{
|
||||||
const QPoint dest = adjacentTile(QPoint(it->first.first, it->first.second), st.outputB);
|
if (!st.frontB)
|
||||||
if (tryPlaceOnBelt(dest, item))
|
|
||||||
{
|
{
|
||||||
st.heldItem = std::nullopt;
|
st.frontB = BeltItemSlot{item, 0.0};
|
||||||
|
st.back = std::nullopt;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else if (matchesA && matchesB)
|
else if (matchesA && matchesB)
|
||||||
{
|
{
|
||||||
// Alternation: try preferred output first, fall back to other.
|
// Alternation: try preferred output first, fall back to other if preferred full.
|
||||||
const Rotation preferred = st.nextOutputIsA ? st.outputA : st.outputB;
|
const bool preferA = st.nextOutputIsA;
|
||||||
const Rotation fallback = st.nextOutputIsA ? st.outputB : st.outputA;
|
|
||||||
|
|
||||||
const QPoint prefDest = adjacentTile(QPoint(it->first.first, it->first.second), preferred);
|
if (preferA && !st.frontA)
|
||||||
const QPoint fbDest = adjacentTile(QPoint(it->first.first, it->first.second), fallback);
|
|
||||||
|
|
||||||
if (tryPlaceOnBelt(prefDest, item))
|
|
||||||
{
|
{
|
||||||
st.heldItem = std::nullopt;
|
st.frontA = BeltItemSlot{item, 0.0};
|
||||||
st.nextOutputIsA = !st.nextOutputIsA;
|
st.back = std::nullopt;
|
||||||
|
st.nextOutputIsA = false;
|
||||||
}
|
}
|
||||||
else if (tryPlaceOnBelt(fbDest, item))
|
else if (!preferA && !st.frontB)
|
||||||
{
|
{
|
||||||
st.heldItem = std::nullopt;
|
st.frontB = BeltItemSlot{item, 0.0};
|
||||||
// nextOutputIsA stays: preferred was blocked, so we still owe it next.
|
st.back = std::nullopt;
|
||||||
|
st.nextOutputIsA = true;
|
||||||
}
|
}
|
||||||
// else both blocked — item stays.
|
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;
|
||||||
}
|
}
|
||||||
// else (!matchesA && !matchesB): stall — item stays in splitter.
|
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;
|
||||||
|
}
|
||||||
|
// else both fronts occupied — back stays.
|
||||||
|
}
|
||||||
|
// else (!matchesA && !matchesB): stall — back stays.
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -381,4 +491,39 @@ void BeltSystem::forEachVisualItem(QRect viewportTiles,
|
|||||||
visit(vi);
|
visit(vi);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
for (const std::pair<const std::pair<int, int>, SplitterTile>& entry : m_splitters)
|
||||||
|
{
|
||||||
|
const QPoint tile(entry.first.first, entry.first.second);
|
||||||
|
if (!viewportTiles.contains(tile))
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const SplitterTile& st = entry.second;
|
||||||
|
|
||||||
|
if (st.back)
|
||||||
|
{
|
||||||
|
VisualItem vi;
|
||||||
|
vi.type = st.back->item.type;
|
||||||
|
vi.worldPos = slotWorldPos(tile, st.backDir, st.back->progress);
|
||||||
|
visit(vi);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (st.frontA)
|
||||||
|
{
|
||||||
|
VisualItem vi;
|
||||||
|
vi.type = st.frontA->item.type;
|
||||||
|
vi.worldPos = slotWorldPos(tile, st.outputA, st.frontA->progress);
|
||||||
|
visit(vi);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (st.frontB)
|
||||||
|
{
|
||||||
|
VisualItem vi;
|
||||||
|
vi.type = st.frontB->item.type;
|
||||||
|
vi.worldPos = slotWorldPos(tile, st.outputB, st.frontB->progress);
|
||||||
|
visit(vi);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -111,7 +111,10 @@ private:
|
|||||||
std::vector<ItemType> filterA; // empty = accept all
|
std::vector<ItemType> filterA; // empty = accept all
|
||||||
std::vector<ItemType> filterB;
|
std::vector<ItemType> filterB;
|
||||||
bool nextOutputIsA; // alternation state
|
bool nextOutputIsA; // alternation state
|
||||||
std::optional<Item> heldItem; // item buffered waiting to exit
|
std::optional<BeltItemSlot> back; // progress [0, 0.5]; entering from input belt
|
||||||
|
Rotation backDir; // direction of the feeding belt (for animation)
|
||||||
|
std::optional<BeltItemSlot> frontA; // progress [0, 1]; routed to outputA
|
||||||
|
std::optional<BeltItemSlot> frontB; // progress [0, 1]; routed to outputB
|
||||||
};
|
};
|
||||||
|
|
||||||
double m_progressPerTick; // beltSpeedTilesPerSecond / kTickRateHz
|
double m_progressPerTick; // beltSpeedTilesPerSecond / kTickRateHz
|
||||||
|
|||||||
@@ -241,24 +241,6 @@ EntityId BuildingSystem::place(BuildingType type, QPoint anchor,
|
|||||||
|
|
||||||
int BuildingSystem::demolish(EntityId id)
|
int BuildingSystem::demolish(EntityId id)
|
||||||
{
|
{
|
||||||
// Belt / splitter?
|
|
||||||
const std::map<EntityId, BeltEntry>::iterator beltIt = m_beltEntities.find(id);
|
|
||||||
if (beltIt != m_beltEntities.end())
|
|
||||||
{
|
|
||||||
const QPoint tile = beltIt->second.tile;
|
|
||||||
const BuildingType btype = beltIt->second.type;
|
|
||||||
m_belts.removeTile(tile);
|
|
||||||
m_tileOccupancy.erase({tile.x(), tile.y()});
|
|
||||||
m_beltEntities.erase(beltIt);
|
|
||||||
|
|
||||||
const BuildingDef* def = findBuildingDef(btype);
|
|
||||||
if (def)
|
|
||||||
{
|
|
||||||
return def->cost * m_config.world.refundPercentage / 100;
|
|
||||||
}
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Construction queue?
|
// Construction queue?
|
||||||
for (std::deque<ConstructionSite>::iterator it = m_constructionQueue.begin();
|
for (std::deque<ConstructionSite>::iterator it = m_constructionQueue.begin();
|
||||||
it != m_constructionQueue.end();
|
it != m_constructionQueue.end();
|
||||||
@@ -287,6 +269,10 @@ int BuildingSystem::demolish(EntityId id)
|
|||||||
{
|
{
|
||||||
if (it->id == id)
|
if (it->id == id)
|
||||||
{
|
{
|
||||||
|
if (it->type == BuildingType::Belt || it->type == BuildingType::Splitter)
|
||||||
|
{
|
||||||
|
m_belts.removeTile(it->anchor);
|
||||||
|
}
|
||||||
const BuildingDef* def = findBuildingDef(it->type);
|
const BuildingDef* def = findBuildingDef(it->type);
|
||||||
for (const QPoint& cell : it->bodyCells)
|
for (const QPoint& cell : it->bodyCells)
|
||||||
{
|
{
|
||||||
@@ -381,25 +367,7 @@ void BuildingSystem::tickConstruction(Tick currentTick)
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Promote construction site — belts/splitters go into BeltSystem, others become Buildings.
|
// Promote construction site to an operational Building.
|
||||||
if (front.type == BuildingType::Belt)
|
|
||||||
{
|
|
||||||
m_belts.placeBelt(front.anchor, front.rotation);
|
|
||||||
m_beltEntities[front.id] = BeltEntry{front.anchor, BuildingType::Belt, front.rotation, front.rotation};
|
|
||||||
}
|
|
||||||
else if (front.type == BuildingType::Splitter)
|
|
||||||
{
|
|
||||||
const BuildingDef* def = findBuildingDef(front.type);
|
|
||||||
assert(def != nullptr);
|
|
||||||
const ParsedSurfaceMask mask = parseSurfaceMask(def->surfaceMask, front.rotation);
|
|
||||||
assert(mask.outputPorts.size() >= 2);
|
|
||||||
const Rotation outA = mask.outputPorts[0].direction;
|
|
||||||
const Rotation outB = mask.outputPorts[1].direction;
|
|
||||||
m_belts.placeSplitter(front.anchor, outA, outB);
|
|
||||||
m_beltEntities[front.id] = BeltEntry{front.anchor, BuildingType::Splitter, outA, outB};
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
const BuildingDef* def = findBuildingDef(front.type);
|
const BuildingDef* def = findBuildingDef(front.type);
|
||||||
const ParsedSurfaceMask mask = parseSurfaceMask(
|
const ParsedSurfaceMask mask = parseSurfaceMask(
|
||||||
def ? def->surfaceMask : std::vector<std::string>{},
|
def ? def->surfaceMask : std::vector<std::string>{},
|
||||||
@@ -444,8 +412,20 @@ void BuildingSystem::tickConstruction(Tick currentTick)
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
m_buildings.push_back(std::move(building));
|
// Register with BeltSystem before the move (mask stays valid).
|
||||||
|
if (front.type == BuildingType::Belt)
|
||||||
|
{
|
||||||
|
m_belts.placeBelt(front.anchor, front.rotation);
|
||||||
}
|
}
|
||||||
|
else if (front.type == BuildingType::Splitter)
|
||||||
|
{
|
||||||
|
assert(mask.outputPorts.size() >= 2);
|
||||||
|
m_belts.placeSplitter(front.anchor,
|
||||||
|
mask.outputPorts[0].direction,
|
||||||
|
mask.outputPorts[1].direction);
|
||||||
|
}
|
||||||
|
|
||||||
|
m_buildings.push_back(std::move(building));
|
||||||
|
|
||||||
m_constructionQueue.pop_front();
|
m_constructionQueue.pop_front();
|
||||||
|
|
||||||
@@ -774,15 +754,30 @@ std::vector<ConstructionSite> BuildingSystem::allSites() const
|
|||||||
std::vector<BuildingSystem::BeltTileInfo> BuildingSystem::allBeltTiles() const
|
std::vector<BuildingSystem::BeltTileInfo> BuildingSystem::allBeltTiles() const
|
||||||
{
|
{
|
||||||
std::vector<BeltTileInfo> result;
|
std::vector<BeltTileInfo> result;
|
||||||
result.reserve(m_beltEntities.size());
|
for (const Building& b : m_buildings)
|
||||||
for (const std::map<EntityId, BeltEntry>::value_type& kv : m_beltEntities)
|
|
||||||
{
|
{
|
||||||
|
if (b.type != BuildingType::Belt && b.type != BuildingType::Splitter)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
BeltTileInfo info;
|
BeltTileInfo info;
|
||||||
info.id = kv.first;
|
info.id = b.id;
|
||||||
info.tile = kv.second.tile;
|
info.tile = b.bodyCells.empty() ? b.anchor : b.bodyCells[0];
|
||||||
info.type = kv.second.type;
|
info.type = b.type;
|
||||||
info.directionA = kv.second.directionA;
|
if (!b.outputPorts.empty())
|
||||||
info.directionB = kv.second.directionB;
|
{
|
||||||
|
info.directionA = b.outputPorts[0].direction;
|
||||||
|
info.directionB = b.outputPorts[0].direction;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
info.directionA = b.rotation;
|
||||||
|
info.directionB = b.rotation;
|
||||||
|
}
|
||||||
|
if (b.type == BuildingType::Splitter && b.outputPorts.size() >= 2)
|
||||||
|
{
|
||||||
|
info.directionB = b.outputPorts[1].direction;
|
||||||
|
}
|
||||||
result.push_back(info);
|
result.push_back(info);
|
||||||
}
|
}
|
||||||
return result;
|
return result;
|
||||||
@@ -907,6 +902,10 @@ bool BuildingSystem::removeBuilding(EntityId id)
|
|||||||
{
|
{
|
||||||
if (it->id == id)
|
if (it->id == id)
|
||||||
{
|
{
|
||||||
|
if (it->type == BuildingType::Belt || it->type == BuildingType::Splitter)
|
||||||
|
{
|
||||||
|
m_belts.removeTile(it->anchor);
|
||||||
|
}
|
||||||
for (const QPoint& cell : it->bodyCells)
|
for (const QPoint& cell : it->bodyCells)
|
||||||
{
|
{
|
||||||
m_tileOccupancy.erase({cell.x(), cell.y()});
|
m_tileOccupancy.erase({cell.x(), cell.y()});
|
||||||
|
|||||||
@@ -23,8 +23,8 @@
|
|||||||
|
|
||||||
// Manages building placement, construction queuing, and the per-tick
|
// Manages building placement, construction queuing, and the per-tick
|
||||||
// production loop (belt→building pull, production, building→belt push).
|
// production loop (belt→building pull, production, building→belt push).
|
||||||
// Belt and Splitter types are forwarded to BeltSystem rather than stored
|
// All types including Belt and Splitter are stored as Building instances;
|
||||||
// as Building instances.
|
// BeltSystem owns the per-tile simulation data (item slots, flow).
|
||||||
class BuildingSystem
|
class BuildingSystem
|
||||||
{
|
{
|
||||||
public:
|
public:
|
||||||
@@ -106,14 +106,6 @@ public:
|
|||||||
void forEachBuilding(std::function<void(Building&)> fn);
|
void forEachBuilding(std::function<void(Building&)> fn);
|
||||||
|
|
||||||
private:
|
private:
|
||||||
struct BeltEntry
|
|
||||||
{
|
|
||||||
QPoint tile;
|
|
||||||
BuildingType type; // Belt or Splitter
|
|
||||||
Rotation directionA; // Belt: its direction; Splitter: first output
|
|
||||||
Rotation directionB; // Splitter: second output; Belt: same as directionA
|
|
||||||
};
|
|
||||||
|
|
||||||
const BuildingDef* findBuildingDef(BuildingType type) const;
|
const BuildingDef* findBuildingDef(BuildingType type) const;
|
||||||
const RecipeDef* findRecipe(const std::string& id, BuildingType type) const;
|
const RecipeDef* findRecipe(const std::string& id, BuildingType type) const;
|
||||||
const ShipDef* findShipDef(const std::string& id) const;
|
const ShipDef* findShipDef(const std::string& id) const;
|
||||||
@@ -131,7 +123,6 @@ private:
|
|||||||
|
|
||||||
std::vector<Building> m_buildings;
|
std::vector<Building> m_buildings;
|
||||||
std::deque<ConstructionSite> m_constructionQueue;
|
std::deque<ConstructionSite> m_constructionQueue;
|
||||||
std::map<EntityId, BeltEntry> m_beltEntities;
|
|
||||||
|
|
||||||
// Maps every occupied body-cell coordinate to the entity that owns it.
|
// Maps every occupied body-cell coordinate to the entity that owns it.
|
||||||
std::map<std::pair<int, int>, EntityId> m_tileOccupancy;
|
std::map<std::pair<int, int>, EntityId> m_tileOccupancy;
|
||||||
|
|||||||
@@ -226,6 +226,38 @@ TEST_CASE("BeltSystem: item stays blocked when next tile is full", "[belt]")
|
|||||||
REQUIRE(bs.tryTakeItem(eastPort(tileA)).has_value());
|
REQUIRE(bs.tryTakeItem(eastPort(tileA)).has_value());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
TEST_CASE("BeltSystem: belt back slot is capped at progress 0.5", "[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.
|
||||||
|
const double medBeltSpeed = 0.4 * static_cast<double>(kTickRateHz);
|
||||||
|
BeltSystem bs(medBeltSpeed);
|
||||||
|
|
||||||
|
const QPoint tile(0, 0);
|
||||||
|
bs.placeBelt(tile, Rotation::East);
|
||||||
|
|
||||||
|
// 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)
|
||||||
|
|
||||||
|
// Place back item; front is at 1.0 and not blocking (back < 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
|
||||||
|
|
||||||
|
// Remove front; back (now promoted to front) must be at 0.5, not 0.8.
|
||||||
|
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.
|
||||||
|
bs.tick();
|
||||||
|
REQUIRE(bs.tryTakeItem(eastPort(tile)).has_value());
|
||||||
|
}
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
// clearTiles
|
// clearTiles
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
@@ -296,8 +328,9 @@ TEST_CASE("BeltSystem: forEachVisualItem reports correct ItemType", "[belt]")
|
|||||||
|
|
||||||
TEST_CASE("BeltSystem: splitter alternates between outputA and outputB", "[belt]")
|
TEST_CASE("BeltSystem: splitter alternates between outputA and outputB", "[belt]")
|
||||||
{
|
{
|
||||||
// Layout: tileIn -> splitter -> tileA (West output)
|
// Layout: tileIn -> splitter -> tileA (North output)
|
||||||
// -> tileB (East output)
|
// -> tileB (South output)
|
||||||
|
// Pipeline per item: tileIn(1) -> back(2) -> front(3) -> output belt(4)
|
||||||
BeltSystem bs(kFastBeltSpeed);
|
BeltSystem bs(kFastBeltSpeed);
|
||||||
|
|
||||||
const QPoint tileIn(0, 0);
|
const QPoint tileIn(0, 0);
|
||||||
@@ -311,12 +344,14 @@ TEST_CASE("BeltSystem: splitter alternates between outputA and outputB", "[belt]
|
|||||||
bs.placeBelt(tileB, Rotation::South);
|
bs.placeBelt(tileB, Rotation::South);
|
||||||
|
|
||||||
bs.tryPutItem(tileIn, makeItem("item1"));
|
bs.tryPutItem(tileIn, makeItem("item1"));
|
||||||
bs.tick(); // item moves: tileIn -> splitter held
|
bs.tick(); // item1: tileIn -> splitter back (progress 0)
|
||||||
|
|
||||||
bs.tryPutItem(tileIn, makeItem("item2"));
|
bs.tryPutItem(tileIn, makeItem("item2"));
|
||||||
bs.tick(); // item1 routes to outputA (North=tileA); item2 moves to splitter
|
bs.tick(); // item1 back -> 0.5 -> frontA; item2 advances but back is occupied
|
||||||
|
bs.tick(); // item1 frontA -> 1.0 -> tileA; item2 enters splitter back
|
||||||
bs.tick(); // item2 routes to outputB (South=tileB)
|
bs.tick(); // item2 back -> 0.5 -> frontB; item1 at tileA output edge
|
||||||
|
bs.tick(); // item2 frontB -> 1.0 -> tileB
|
||||||
|
bs.tick(); // item2 at tileB output edge
|
||||||
|
|
||||||
const bool inA = bs.tryTakeItem(Port{tileA, Rotation::North}).has_value();
|
const bool inA = bs.tryTakeItem(Port{tileA, Rotation::North}).has_value();
|
||||||
const bool inB = bs.tryTakeItem(Port{tileB, Rotation::South}).has_value();
|
const bool inB = bs.tryTakeItem(Port{tileB, Rotation::South}).has_value();
|
||||||
@@ -345,12 +380,126 @@ TEST_CASE("BeltSystem: splitter routes filtered item to matching output", "[belt
|
|||||||
bs.placeBelt(tileB, Rotation::South);
|
bs.placeBelt(tileB, Rotation::South);
|
||||||
|
|
||||||
// Filter: outputA = iron_ore only; outputB = accept all.
|
// Filter: outputA = iron_ore only; outputB = accept all.
|
||||||
|
// iron_ore matches both filters → alternation; preferred = outputA (nextOutputIsA=true).
|
||||||
bs.setSplitterFilters(tileSpl, {ItemType{"iron_ore"}}, {});
|
bs.setSplitterFilters(tileSpl, {ItemType{"iron_ore"}}, {});
|
||||||
|
|
||||||
bs.tryPutItem(tileIn, makeItem("iron_ore"));
|
bs.tryPutItem(tileIn, makeItem("iron_ore"));
|
||||||
bs.tick(); // tileIn -> splitter held
|
bs.tick(); // tileIn -> splitter back
|
||||||
bs.tick(); // routed to outputA (filter match)
|
bs.tick(); // back -> frontA (filter + alternation → preferred outputA)
|
||||||
|
bs.tick(); // frontA -> tileA
|
||||||
|
bs.tick(); // item at tileA output edge
|
||||||
|
|
||||||
REQUIRE(bs.tryTakeItem(Port{tileA, Rotation::North}).has_value());
|
REQUIRE(bs.tryTakeItem(Port{tileA, Rotation::North}).has_value());
|
||||||
REQUIRE_FALSE(bs.tryTakeItem(Port{tileB, Rotation::South}).has_value());
|
REQUIRE_FALSE(bs.tryTakeItem(Port{tileB, Rotation::South}).has_value());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Splitter — direct building input (no output belts)
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
TEST_CASE("BeltSystem: splitter back slot is capped at 0.5 and waits before routing", "[belt]")
|
||||||
|
{
|
||||||
|
BeltSystem bs(kFastBeltSpeed);
|
||||||
|
|
||||||
|
const QPoint tileIn(0, 0);
|
||||||
|
const QPoint tileSpl(1, 0);
|
||||||
|
|
||||||
|
bs.placeBelt(tileIn, Rotation::East);
|
||||||
|
bs.placeSplitter(tileSpl, Rotation::North, Rotation::South);
|
||||||
|
|
||||||
|
bs.tryPutItem(tileIn, makeItem("iron_ore"));
|
||||||
|
bs.tick(); // item enters splitter back at progress 0; routing not yet triggered
|
||||||
|
|
||||||
|
// Back has not yet reached 0.5 — front slots empty, nothing available.
|
||||||
|
REQUIRE_FALSE(bs.peekItem(Port{tileSpl, Rotation::North}).has_value());
|
||||||
|
REQUIRE_FALSE(bs.peekItem(Port{tileSpl, Rotation::South}).has_value());
|
||||||
|
|
||||||
|
bs.tick(); // back advances to 0.5, routes to frontA at progress 0
|
||||||
|
bs.tick(); // frontA advances to 1.0, available for building pickup
|
||||||
|
|
||||||
|
REQUIRE(bs.peekItem(Port{tileSpl, Rotation::North}).has_value());
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_CASE("BeltSystem: splitter delivers item directly to building input via tryTakeItem", "[belt]")
|
||||||
|
{
|
||||||
|
// Bug 1: splitter could not insert into a building input with no belt in between.
|
||||||
|
BeltSystem bs(kFastBeltSpeed);
|
||||||
|
|
||||||
|
const QPoint tileIn(0, 0);
|
||||||
|
const QPoint tileSpl(1, 0);
|
||||||
|
|
||||||
|
bs.placeBelt(tileIn, Rotation::East);
|
||||||
|
bs.placeSplitter(tileSpl, Rotation::North, Rotation::South);
|
||||||
|
// No output belts — both outputs lead directly to building inputs.
|
||||||
|
|
||||||
|
bs.tryPutItem(tileIn, makeItem("iron_ore"));
|
||||||
|
bs.tick(); // tileIn -> splitter back
|
||||||
|
bs.tick(); // back -> frontA at progress 0
|
||||||
|
bs.tick(); // frontA reaches 1.0; no downstream belt, item waits for building pickup
|
||||||
|
|
||||||
|
REQUIRE(bs.peekItem(Port{tileSpl, Rotation::North}).has_value());
|
||||||
|
const std::optional<Item> taken = bs.tryTakeItem(Port{tileSpl, Rotation::North});
|
||||||
|
REQUIRE(taken.has_value());
|
||||||
|
REQUIRE(taken->type.id == "iron_ore");
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_CASE("BeltSystem: splitter accepts new items after building pulls from front slot", "[belt]")
|
||||||
|
{
|
||||||
|
// Bug 2: when outputs had no belts, splitter never cleared its held state
|
||||||
|
// so no new items could enter.
|
||||||
|
BeltSystem bs(kFastBeltSpeed);
|
||||||
|
|
||||||
|
const QPoint tileIn(0, 0);
|
||||||
|
const QPoint tileSpl(1, 0);
|
||||||
|
|
||||||
|
bs.placeBelt(tileIn, Rotation::East);
|
||||||
|
bs.placeSplitter(tileSpl, Rotation::North, Rotation::South);
|
||||||
|
|
||||||
|
bs.tryPutItem(tileIn, makeItem("item1"));
|
||||||
|
bs.tick();
|
||||||
|
bs.tick();
|
||||||
|
bs.tick(); // item1 now in frontA at 1.0
|
||||||
|
|
||||||
|
// Building pulls item1 — clears frontA; nextOutputIsA toggled to false.
|
||||||
|
REQUIRE(bs.tryTakeItem(Port{tileSpl, Rotation::North}).has_value());
|
||||||
|
|
||||||
|
// Feed item2; preferred is now South.
|
||||||
|
bs.tryPutItem(tileIn, makeItem("item2"));
|
||||||
|
bs.tick();
|
||||||
|
bs.tick();
|
||||||
|
bs.tick(); // item2 now in frontB at 1.0
|
||||||
|
|
||||||
|
REQUIRE(bs.peekItem(Port{tileSpl, Rotation::South}).has_value());
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_CASE("BeltSystem: splitter alternates between two unregistered outputs (building inputs)", "[belt]")
|
||||||
|
{
|
||||||
|
BeltSystem bs(kFastBeltSpeed);
|
||||||
|
|
||||||
|
const QPoint tileIn(0, 0);
|
||||||
|
const QPoint tileSpl(1, 0);
|
||||||
|
|
||||||
|
bs.placeBelt(tileIn, Rotation::East);
|
||||||
|
bs.placeSplitter(tileSpl, Rotation::North, Rotation::South);
|
||||||
|
|
||||||
|
// item1 → frontA (preferred, nextOutputIsA=true)
|
||||||
|
bs.tryPutItem(tileIn, makeItem("item1"));
|
||||||
|
bs.tick();
|
||||||
|
bs.tick();
|
||||||
|
bs.tick();
|
||||||
|
REQUIRE(bs.tryTakeItem(Port{tileSpl, Rotation::North}).has_value());
|
||||||
|
|
||||||
|
// item2 → frontB (preferred, nextOutputIsA now false)
|
||||||
|
bs.tryPutItem(tileIn, makeItem("item2"));
|
||||||
|
bs.tick();
|
||||||
|
bs.tick();
|
||||||
|
bs.tick();
|
||||||
|
REQUIRE(bs.tryTakeItem(Port{tileSpl, Rotation::South}).has_value());
|
||||||
|
|
||||||
|
// item3 → frontA again (nextOutputIsA toggled back to true)
|
||||||
|
bs.tryPutItem(tileIn, makeItem("item3"));
|
||||||
|
bs.tick();
|
||||||
|
bs.tick();
|
||||||
|
bs.tick();
|
||||||
|
REQUIRE(bs.tryTakeItem(Port{tileSpl, Rotation::North}).has_value());
|
||||||
|
}
|
||||||
|
|||||||
@@ -116,7 +116,9 @@ TEST_CASE("BuildingSystem: placing a belt registers it with BeltSystem after con
|
|||||||
runTicks(bs, belts, static_cast<int>(secondsToTicks(1.0)) + 1, tick);
|
runTicks(bs, belts, static_cast<int>(secondsToTicks(1.0)) + 1, tick);
|
||||||
|
|
||||||
REQUIRE(belts.tryPutItem(QPoint(5, 5), makeItem("iron_ore")));
|
REQUIRE(belts.tryPutItem(QPoint(5, 5), makeItem("iron_ore")));
|
||||||
REQUIRE(bs.allBuildings().empty()); // belts do not create Building instances
|
REQUIRE(bs.allBuildings().size() == 1);
|
||||||
|
REQUIRE(bs.allBuildings()[0].type == BuildingType::Belt);
|
||||||
|
REQUIRE(bs.allBuildings()[0].anchor == QPoint(5, 5));
|
||||||
}
|
}
|
||||||
|
|
||||||
TEST_CASE("BuildingSystem: placed building enters construction queue", "[building]")
|
TEST_CASE("BuildingSystem: placed building enters construction queue", "[building]")
|
||||||
|
|||||||
@@ -194,8 +194,7 @@ TEST_CASE("CombatSystem: no fire when target is out of range", "[combat]")
|
|||||||
|
|
||||||
TEST_CASE("CombatSystem: player station fires at enemy ship in range", "[combat]")
|
TEST_CASE("CombatSystem: player station fires at enemy ship in range", "[combat]")
|
||||||
{
|
{
|
||||||
const GameConfig cfg = loadConfig();
|
Simulation sim(loadConfig(), 42);
|
||||||
Simulation sim(cfg, 42);
|
|
||||||
|
|
||||||
// Find the player defence station.
|
// Find the player defence station.
|
||||||
EntityId stationId = kInvalidEntityId;
|
EntityId stationId = kInvalidEntityId;
|
||||||
@@ -223,7 +222,7 @@ TEST_CASE("CombatSystem: player station fires at enemy ship in range", "[combat]
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Find a combat ship blueprint for the enemy.
|
// Find a combat ship blueprint for the enemy.
|
||||||
const ShipDef* combatDef = findCombatShip(cfg);
|
const ShipDef* combatDef = findCombatShip(sim.config());
|
||||||
REQUIRE(combatDef != nullptr);
|
REQUIRE(combatDef != nullptr);
|
||||||
|
|
||||||
const EntityId enemyId = sim.ships().spawn(
|
const EntityId enemyId = sim.ships().spawn(
|
||||||
@@ -246,8 +245,7 @@ TEST_CASE("CombatSystem: player station fires at enemy ship in range", "[combat]
|
|||||||
|
|
||||||
TEST_CASE("CombatSystem: enemy station fires at player ship in range", "[combat]")
|
TEST_CASE("CombatSystem: enemy station fires at player ship in range", "[combat]")
|
||||||
{
|
{
|
||||||
const GameConfig cfg = loadConfig();
|
Simulation sim(loadConfig(), 42);
|
||||||
Simulation sim(cfg, 42);
|
|
||||||
|
|
||||||
// Find the enemy defence station.
|
// Find the enemy defence station.
|
||||||
EntityId stationId = kInvalidEntityId;
|
EntityId stationId = kInvalidEntityId;
|
||||||
@@ -265,7 +263,7 @@ TEST_CASE("CombatSystem: enemy station fires at player ship in range", "[combat]
|
|||||||
}
|
}
|
||||||
REQUIRE(stationId != kInvalidEntityId);
|
REQUIRE(stationId != kInvalidEntityId);
|
||||||
|
|
||||||
const ShipDef* combatDef = findCombatShip(cfg);
|
const ShipDef* combatDef = findCombatShip(sim.config());
|
||||||
REQUIRE(combatDef != nullptr);
|
REQUIRE(combatDef != nullptr);
|
||||||
|
|
||||||
// Spawn a player ship right next to the enemy station.
|
// Spawn a player ship right next to the enemy station.
|
||||||
@@ -291,10 +289,9 @@ TEST_CASE("CombatSystem: enemy station fires at player ship in range", "[combat]
|
|||||||
|
|
||||||
TEST_CASE("CombatSystem: dead ship is removed after tick step 9", "[combat]")
|
TEST_CASE("CombatSystem: dead ship is removed after tick step 9", "[combat]")
|
||||||
{
|
{
|
||||||
const GameConfig cfg = loadConfig();
|
Simulation sim(loadConfig(), 42);
|
||||||
Simulation sim(cfg, 42);
|
|
||||||
|
|
||||||
const ShipDef* combatDef = findCombatShip(cfg);
|
const ShipDef* combatDef = findCombatShip(sim.config());
|
||||||
REQUIRE(combatDef != nullptr);
|
REQUIRE(combatDef != nullptr);
|
||||||
|
|
||||||
const EntityId shipId = sim.ships().spawn(combatDef->id, 1,
|
const EntityId shipId = sim.ships().spawn(combatDef->id, 1,
|
||||||
@@ -310,12 +307,11 @@ TEST_CASE("CombatSystem: dead ship is removed after tick step 9", "[combat]")
|
|||||||
|
|
||||||
TEST_CASE("CombatSystem: scrap is spawned on ship death", "[combat]")
|
TEST_CASE("CombatSystem: scrap is spawned on ship death", "[combat]")
|
||||||
{
|
{
|
||||||
const GameConfig cfg = loadConfig();
|
Simulation sim(loadConfig(), 42);
|
||||||
Simulation sim(cfg, 42);
|
|
||||||
|
|
||||||
// Find a ship def that drops scrap.
|
// Find a ship def that drops scrap.
|
||||||
const ShipDef* droppingDef = nullptr;
|
const ShipDef* droppingDef = nullptr;
|
||||||
for (const ShipDef& def : cfg.ships.ships)
|
for (const ShipDef& def : sim.config().ships.ships)
|
||||||
{
|
{
|
||||||
if (def.loot.scrapDrop > 0)
|
if (def.loot.scrapDrop > 0)
|
||||||
{
|
{
|
||||||
@@ -337,8 +333,7 @@ TEST_CASE("CombatSystem: scrap is spawned on ship death", "[combat]")
|
|||||||
|
|
||||||
TEST_CASE("CombatSystem: HQ death sets game over", "[combat]")
|
TEST_CASE("CombatSystem: HQ death sets game over", "[combat]")
|
||||||
{
|
{
|
||||||
const GameConfig cfg = loadConfig();
|
Simulation sim(loadConfig(), 42);
|
||||||
Simulation sim(cfg, 42);
|
|
||||||
|
|
||||||
sim.buildings().forEachBuilding([](Building& b)
|
sim.buildings().forEachBuilding([](Building& b)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -73,12 +73,11 @@ static void fillMaterials(Simulation& sim, EntityId yardId, const ShipDef& def)
|
|||||||
TEST_CASE("Shipyard: spawns a player ship after production cycle completes",
|
TEST_CASE("Shipyard: spawns a player ship after production cycle completes",
|
||||||
"[shipyard]")
|
"[shipyard]")
|
||||||
{
|
{
|
||||||
const GameConfig cfg = loadConfig();
|
Simulation sim(loadConfig(), 42);
|
||||||
Simulation sim(cfg, 42);
|
|
||||||
|
|
||||||
const ShipDef* def = findAvailableBlueprint(cfg);
|
const ShipDef* def = findAvailableBlueprint(sim.config());
|
||||||
REQUIRE(def != nullptr);
|
REQUIRE(def != nullptr);
|
||||||
const BuildingDef* yardDef = findShipyardDef(cfg);
|
const BuildingDef* yardDef = findShipyardDef(sim.config());
|
||||||
REQUIRE(yardDef != nullptr);
|
REQUIRE(yardDef != nullptr);
|
||||||
|
|
||||||
const int shipsBefore = static_cast<int>(sim.ships().allShips().size());
|
const int shipsBefore = static_cast<int>(sim.ships().allShips().size());
|
||||||
@@ -119,10 +118,9 @@ TEST_CASE("Shipyard: spawns a player ship after production cycle completes",
|
|||||||
|
|
||||||
TEST_CASE("Shipyard: does not spawn without a blueprint set", "[shipyard]")
|
TEST_CASE("Shipyard: does not spawn without a blueprint set", "[shipyard]")
|
||||||
{
|
{
|
||||||
const GameConfig cfg = loadConfig();
|
Simulation sim(loadConfig(), 42);
|
||||||
Simulation sim(cfg, 42);
|
|
||||||
|
|
||||||
const BuildingDef* yardDef = findShipyardDef(cfg);
|
const BuildingDef* yardDef = findShipyardDef(sim.config());
|
||||||
REQUIRE(yardDef != nullptr);
|
REQUIRE(yardDef != nullptr);
|
||||||
|
|
||||||
const int shipsBefore = static_cast<int>(sim.ships().allShips().size());
|
const int shipsBefore = static_cast<int>(sim.ships().allShips().size());
|
||||||
@@ -136,12 +134,11 @@ TEST_CASE("Shipyard: does not spawn without a blueprint set", "[shipyard]")
|
|||||||
|
|
||||||
TEST_CASE("Shipyard: does not spawn with insufficient materials", "[shipyard]")
|
TEST_CASE("Shipyard: does not spawn with insufficient materials", "[shipyard]")
|
||||||
{
|
{
|
||||||
const GameConfig cfg = loadConfig();
|
Simulation sim(loadConfig(), 42);
|
||||||
Simulation sim(cfg, 42);
|
|
||||||
|
|
||||||
const ShipDef* def = findAvailableBlueprint(cfg);
|
const ShipDef* def = findAvailableBlueprint(sim.config());
|
||||||
REQUIRE(def != nullptr);
|
REQUIRE(def != nullptr);
|
||||||
const BuildingDef* yardDef = findShipyardDef(cfg);
|
const BuildingDef* yardDef = findShipyardDef(sim.config());
|
||||||
REQUIRE(yardDef != nullptr);
|
REQUIRE(yardDef != nullptr);
|
||||||
|
|
||||||
const int shipsBefore = static_cast<int>(sim.ships().allShips().size());
|
const int shipsBefore = static_cast<int>(sim.ships().allShips().size());
|
||||||
@@ -161,12 +158,11 @@ TEST_CASE("Shipyard: does not spawn with insufficient materials", "[shipyard]")
|
|||||||
|
|
||||||
TEST_CASE("Shipyard: spawns a second ship after materials replenished", "[shipyard]")
|
TEST_CASE("Shipyard: spawns a second ship after materials replenished", "[shipyard]")
|
||||||
{
|
{
|
||||||
const GameConfig cfg = loadConfig();
|
Simulation sim(loadConfig(), 42);
|
||||||
Simulation sim(cfg, 42);
|
|
||||||
|
|
||||||
const ShipDef* def = findAvailableBlueprint(cfg);
|
const ShipDef* def = findAvailableBlueprint(sim.config());
|
||||||
REQUIRE(def != nullptr);
|
REQUIRE(def != nullptr);
|
||||||
const BuildingDef* yardDef = findShipyardDef(cfg);
|
const BuildingDef* yardDef = findShipyardDef(sim.config());
|
||||||
REQUIRE(yardDef != nullptr);
|
REQUIRE(yardDef != nullptr);
|
||||||
|
|
||||||
const EntityId yardId = placeShipyard(sim, *yardDef);
|
const EntityId yardId = placeShipyard(sim, *yardDef);
|
||||||
|
|||||||
@@ -17,16 +17,14 @@ static GameConfig loadConfig()
|
|||||||
|
|
||||||
TEST_CASE("Simulation::currentTick starts at 0", "[simulation]")
|
TEST_CASE("Simulation::currentTick starts at 0", "[simulation]")
|
||||||
{
|
{
|
||||||
const GameConfig config = loadConfig();
|
const Simulation sim(loadConfig());
|
||||||
const Simulation sim(config);
|
|
||||||
|
|
||||||
REQUIRE(sim.currentTick() == 0);
|
REQUIRE(sim.currentTick() == 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
TEST_CASE("Simulation::tick increments currentTick by 1", "[simulation]")
|
TEST_CASE("Simulation::tick increments currentTick by 1", "[simulation]")
|
||||||
{
|
{
|
||||||
const GameConfig config = loadConfig();
|
Simulation sim(loadConfig());
|
||||||
Simulation sim(config);
|
|
||||||
|
|
||||||
sim.tick();
|
sim.tick();
|
||||||
|
|
||||||
@@ -35,8 +33,7 @@ TEST_CASE("Simulation::tick increments currentTick by 1", "[simulation]")
|
|||||||
|
|
||||||
TEST_CASE("Simulation::tick 10 times yields currentTick == 10", "[simulation]")
|
TEST_CASE("Simulation::tick 10 times yields currentTick == 10", "[simulation]")
|
||||||
{
|
{
|
||||||
const GameConfig config = loadConfig();
|
Simulation sim(loadConfig());
|
||||||
Simulation sim(config);
|
|
||||||
|
|
||||||
for (int i = 0; i < 10; ++i)
|
for (int i = 0; i < 10; ++i)
|
||||||
{
|
{
|
||||||
@@ -48,16 +45,14 @@ TEST_CASE("Simulation::tick 10 times yields currentTick == 10", "[simulation]")
|
|||||||
|
|
||||||
TEST_CASE("Simulation::drainFireEvents returns empty initially", "[simulation]")
|
TEST_CASE("Simulation::drainFireEvents returns empty initially", "[simulation]")
|
||||||
{
|
{
|
||||||
const GameConfig config = loadConfig();
|
Simulation sim(loadConfig());
|
||||||
Simulation sim(config);
|
|
||||||
|
|
||||||
REQUIRE(sim.drainFireEvents().empty());
|
REQUIRE(sim.drainFireEvents().empty());
|
||||||
}
|
}
|
||||||
|
|
||||||
TEST_CASE("Simulation::drainFireEvents clears queue on drain", "[simulation]")
|
TEST_CASE("Simulation::drainFireEvents clears queue on drain", "[simulation]")
|
||||||
{
|
{
|
||||||
const GameConfig config = loadConfig();
|
Simulation sim(loadConfig());
|
||||||
Simulation sim(config);
|
|
||||||
|
|
||||||
// First drain: empty.
|
// First drain: empty.
|
||||||
sim.drainFireEvents();
|
sim.drainFireEvents();
|
||||||
@@ -68,8 +63,7 @@ TEST_CASE("Simulation::drainFireEvents clears queue on drain", "[simulation]")
|
|||||||
|
|
||||||
TEST_CASE("Simulation::drainBlueprintDropEvents returns empty initially", "[simulation]")
|
TEST_CASE("Simulation::drainBlueprintDropEvents returns empty initially", "[simulation]")
|
||||||
{
|
{
|
||||||
const GameConfig config = loadConfig();
|
Simulation sim(loadConfig());
|
||||||
Simulation sim(config);
|
|
||||||
|
|
||||||
REQUIRE(sim.drainBlueprintDropEvents().empty());
|
REQUIRE(sim.drainBlueprintDropEvents().empty());
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -98,8 +98,7 @@ TEST_CASE("WaveSystem: generation starts at 0 and increments on push", "[wave]")
|
|||||||
|
|
||||||
TEST_CASE("WaveSystem: Simulation pre-places HQ + 2 player + 2 enemy stations", "[wave]")
|
TEST_CASE("WaveSystem: Simulation pre-places HQ + 2 player + 2 enemy stations", "[wave]")
|
||||||
{
|
{
|
||||||
const GameConfig cfg = loadConfig();
|
const Simulation sim(loadConfig(), 42);
|
||||||
const Simulation sim(cfg, 42);
|
|
||||||
|
|
||||||
int hqCount = 0;
|
int hqCount = 0;
|
||||||
int playerCount = 0;
|
int playerCount = 0;
|
||||||
@@ -118,11 +117,10 @@ TEST_CASE("WaveSystem: Simulation pre-places HQ + 2 player + 2 enemy stations",
|
|||||||
|
|
||||||
TEST_CASE("WaveSystem: HQ has correct initial HP from config", "[wave]")
|
TEST_CASE("WaveSystem: HQ has correct initial HP from config", "[wave]")
|
||||||
{
|
{
|
||||||
const GameConfig cfg = loadConfig();
|
const Simulation sim(loadConfig(), 42);
|
||||||
const Simulation sim(cfg, 42);
|
|
||||||
|
|
||||||
const float expectedHp =
|
const float expectedHp =
|
||||||
static_cast<float>(cfg.stations.hq.hpFormula.evaluate(0.0));
|
static_cast<float>(sim.config().stations.hq.hpFormula.evaluate(0.0));
|
||||||
bool found = false;
|
bool found = false;
|
||||||
float actualHp = 0.0f;
|
float actualHp = 0.0f;
|
||||||
for (const Building& b : sim.buildings().allBuildings())
|
for (const Building& b : sim.buildings().allBuildings())
|
||||||
@@ -141,8 +139,7 @@ TEST_CASE("WaveSystem: HQ has correct initial HP from config", "[wave]")
|
|||||||
|
|
||||||
TEST_CASE("WaveSystem: HQ anchor is at asteroid right edge", "[wave]")
|
TEST_CASE("WaveSystem: HQ anchor is at asteroid right edge", "[wave]")
|
||||||
{
|
{
|
||||||
const GameConfig cfg = loadConfig();
|
const Simulation sim(loadConfig(), 42);
|
||||||
const Simulation sim(cfg, 42);
|
|
||||||
|
|
||||||
for (const Building& b : sim.buildings().allBuildings())
|
for (const Building& b : sim.buildings().allBuildings())
|
||||||
{
|
{
|
||||||
@@ -159,8 +156,7 @@ TEST_CASE("WaveSystem: HQ anchor is at asteroid right edge", "[wave]")
|
|||||||
|
|
||||||
TEST_CASE("WaveSystem: player stations have weapon set", "[wave]")
|
TEST_CASE("WaveSystem: player stations have weapon set", "[wave]")
|
||||||
{
|
{
|
||||||
const GameConfig cfg = loadConfig();
|
const Simulation sim(loadConfig(), 42);
|
||||||
const Simulation sim(cfg, 42);
|
|
||||||
|
|
||||||
int armedPlayerStations = 0;
|
int armedPlayerStations = 0;
|
||||||
for (const Building& b : sim.buildings().allBuildings())
|
for (const Building& b : sim.buildings().allBuildings())
|
||||||
@@ -178,8 +174,7 @@ TEST_CASE("WaveSystem: player stations have weapon set", "[wave]")
|
|||||||
|
|
||||||
TEST_CASE("WaveSystem: enemy stations have weapon set", "[wave]")
|
TEST_CASE("WaveSystem: enemy stations have weapon set", "[wave]")
|
||||||
{
|
{
|
||||||
const GameConfig cfg = loadConfig();
|
const Simulation sim(loadConfig(), 42);
|
||||||
const Simulation sim(cfg, 42);
|
|
||||||
|
|
||||||
int armedEnemyStations = 0;
|
int armedEnemyStations = 0;
|
||||||
for (const Building& b : sim.buildings().allBuildings())
|
for (const Building& b : sim.buildings().allBuildings())
|
||||||
@@ -201,8 +196,7 @@ TEST_CASE("WaveSystem: enemy stations have weapon set", "[wave]")
|
|||||||
|
|
||||||
TEST_CASE("WaveSystem: enemy ships spawn after the initial gap elapses", "[wave]")
|
TEST_CASE("WaveSystem: enemy ships spawn after the initial gap elapses", "[wave]")
|
||||||
{
|
{
|
||||||
const GameConfig cfg = loadConfig();
|
Simulation sim(loadConfig(), 42);
|
||||||
Simulation sim(cfg, 42);
|
|
||||||
|
|
||||||
// The maximum gap is gapMaxSeconds = 45s → 1350 ticks.
|
// The maximum gap is gapMaxSeconds = 45s → 1350 ticks.
|
||||||
// Run 1500 ticks to guarantee at least one wave has triggered.
|
// Run 1500 ticks to guarantee at least one wave has triggered.
|
||||||
@@ -226,8 +220,7 @@ TEST_CASE("WaveSystem: enemy ships spawn after the initial gap elapses", "[wave]
|
|||||||
|
|
||||||
TEST_CASE("WaveSystem: only eligible ships (cost > 0) appear in waves", "[wave]")
|
TEST_CASE("WaveSystem: only eligible ships (cost > 0) appear in waves", "[wave]")
|
||||||
{
|
{
|
||||||
const GameConfig cfg = loadConfig();
|
Simulation sim(loadConfig(), 42);
|
||||||
Simulation sim(cfg, 42);
|
|
||||||
|
|
||||||
// Run long enough for several waves.
|
// Run long enough for several waves.
|
||||||
const int limit = static_cast<int>(secondsToTicks(120.0));
|
const int limit = static_cast<int>(secondsToTicks(120.0));
|
||||||
@@ -251,8 +244,7 @@ TEST_CASE("WaveSystem: only eligible ships (cost > 0) appear in waves", "[wave]"
|
|||||||
|
|
||||||
TEST_CASE("WaveSystem: destroying both enemy stations triggers a push", "[wave]")
|
TEST_CASE("WaveSystem: destroying both enemy stations triggers a push", "[wave]")
|
||||||
{
|
{
|
||||||
const GameConfig cfg = loadConfig();
|
Simulation sim(loadConfig(), 42);
|
||||||
Simulation sim(cfg, 42);
|
|
||||||
|
|
||||||
// Damage both enemy stations to 0.
|
// Damage both enemy stations to 0.
|
||||||
sim.buildings().forEachBuilding([](Building& b)
|
sim.buildings().forEachBuilding([](Building& b)
|
||||||
@@ -276,8 +268,7 @@ TEST_CASE("WaveSystem: destroying both enemy stations triggers a push", "[wave]"
|
|||||||
|
|
||||||
TEST_CASE("WaveSystem: push emits exactly one BlueprintDropEvent", "[wave]")
|
TEST_CASE("WaveSystem: push emits exactly one BlueprintDropEvent", "[wave]")
|
||||||
{
|
{
|
||||||
const GameConfig cfg = loadConfig();
|
Simulation sim(loadConfig(), 42);
|
||||||
Simulation sim(cfg, 42);
|
|
||||||
|
|
||||||
sim.buildings().forEachBuilding([](Building& b)
|
sim.buildings().forEachBuilding([](Building& b)
|
||||||
{
|
{
|
||||||
@@ -295,8 +286,7 @@ TEST_CASE("WaveSystem: push emits exactly one BlueprintDropEvent", "[wave]")
|
|||||||
|
|
||||||
TEST_CASE("WaveSystem: push blueprint drop awards a known ship id", "[wave]")
|
TEST_CASE("WaveSystem: push blueprint drop awards a known ship id", "[wave]")
|
||||||
{
|
{
|
||||||
const GameConfig cfg = loadConfig();
|
Simulation sim(loadConfig(), 42);
|
||||||
Simulation sim(cfg, 42);
|
|
||||||
|
|
||||||
sim.buildings().forEachBuilding([](Building& b)
|
sim.buildings().forEachBuilding([](Building& b)
|
||||||
{
|
{
|
||||||
@@ -311,7 +301,7 @@ TEST_CASE("WaveSystem: push blueprint drop awards a known ship id", "[wave]")
|
|||||||
REQUIRE(events.size() == 1);
|
REQUIRE(events.size() == 1);
|
||||||
|
|
||||||
bool validId = false;
|
bool validId = false;
|
||||||
for (const ShipDef& def : cfg.ships.ships)
|
for (const ShipDef& def : sim.config().ships.ships)
|
||||||
{
|
{
|
||||||
if (def.id == events[0].blueprintId)
|
if (def.id == events[0].blueprintId)
|
||||||
{
|
{
|
||||||
@@ -325,8 +315,7 @@ TEST_CASE("WaveSystem: push blueprint drop awards a known ship id", "[wave]")
|
|||||||
|
|
||||||
TEST_CASE("WaveSystem: push places new enemy stations further right", "[wave]")
|
TEST_CASE("WaveSystem: push places new enemy stations further right", "[wave]")
|
||||||
{
|
{
|
||||||
const GameConfig cfg = loadConfig();
|
Simulation sim(loadConfig(), 42);
|
||||||
Simulation sim(cfg, 42);
|
|
||||||
|
|
||||||
// Record the X position of the initial enemy stations.
|
// Record the X position of the initial enemy stations.
|
||||||
int initialX = std::numeric_limits<int>::min();
|
int initialX = std::numeric_limits<int>::min();
|
||||||
|
|||||||
@@ -393,16 +393,31 @@ EntityId GameWorldView::buildingAtTile(QPoint tile) const
|
|||||||
{
|
{
|
||||||
for (const QPoint& cell : b.bodyCells)
|
for (const QPoint& cell : b.bodyCells)
|
||||||
{
|
{
|
||||||
if (cell == tile) { return b.id; }
|
if (cell == tile)
|
||||||
}
|
|
||||||
}
|
|
||||||
for (const BuildingSystem::BeltTileInfo& info : m_sim->buildings().allBeltTiles())
|
|
||||||
{
|
{
|
||||||
if (info.tile == tile) { return info.id; }
|
return b.id;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return kInvalidEntityId;
|
return kInvalidEntityId;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
EntityId GameWorldView::siteAtTile(QPoint tile) const
|
||||||
|
{
|
||||||
|
for (const ConstructionSite& s : m_sim->buildings().allSites())
|
||||||
|
{
|
||||||
|
for (const QPoint& cell : s.bodyCells)
|
||||||
|
{
|
||||||
|
if (cell == tile)
|
||||||
|
{
|
||||||
|
return s.id;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return kInvalidEntityId;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
std::optional<QVector2D> GameWorldView::entityPosition(EntityId id) const
|
std::optional<QVector2D> GameWorldView::entityPosition(EntityId id) const
|
||||||
{
|
{
|
||||||
const Ship* ship = m_sim->ships().findShip(id);
|
const Ship* ship = m_sim->ships().findShip(id);
|
||||||
@@ -439,14 +454,23 @@ void GameWorldView::stepSpeed(int delta)
|
|||||||
|
|
||||||
void GameWorldView::placeAtTile(QPoint tile)
|
void GameWorldView::placeAtTile(QPoint tile)
|
||||||
{
|
{
|
||||||
if (!m_builderType.has_value()) { return; }
|
if (!m_builderType.has_value())
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
const BuildingType type = *m_builderType;
|
const BuildingType type = *m_builderType;
|
||||||
|
|
||||||
if (!isValidPlacement(type, tile, m_ghostRotation)) { return; }
|
if (!isValidPlacement(type, tile, m_ghostRotation))
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (type == BuildingType::Belt)
|
if (type == BuildingType::Belt)
|
||||||
{
|
{
|
||||||
if (m_beltDragTiles.count(tile) > 0) { return; }
|
if (m_beltDragTiles.count(tile) > 0)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
if (!m_sim->buildings().isTileOccupied(tile))
|
if (!m_sim->buildings().isTileOccupied(tile))
|
||||||
{
|
{
|
||||||
const EntityId id = m_sim->tryPlaceBuilding(
|
const EntityId id = m_sim->tryPlaceBuilding(
|
||||||
@@ -576,27 +600,6 @@ void GameWorldView::drawBuildings(QPainter& painter)
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Belt and splitter tiles (stored separately from regular buildings)
|
|
||||||
for (const BuildingSystem::BeltTileInfo& info : m_sim->buildings().allBeltTiles())
|
|
||||||
{
|
|
||||||
const std::map<BuildingType, BuildingVisuals>::const_iterator it =
|
|
||||||
m_visuals->buildings.find(info.type);
|
|
||||||
if (it == m_visuals->buildings.end()) { continue; }
|
|
||||||
const BuildingVisuals& bv = it->second;
|
|
||||||
|
|
||||||
painter.setPen(Qt::NoPen);
|
|
||||||
painter.fillRect(tileRect(info.tile), bv.fill);
|
|
||||||
painter.setPen(QPen(bv.outline, 1));
|
|
||||||
painter.setBrush(Qt::NoBrush);
|
|
||||||
painter.drawRect(tileRect(info.tile));
|
|
||||||
|
|
||||||
drawPortGlyph(painter, info.tile, info.directionA, bv.outline);
|
|
||||||
if (info.type == BuildingType::Splitter)
|
|
||||||
{
|
|
||||||
drawPortGlyph(painter, info.tile, info.directionB, bv.outline);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
painter.setOpacity(0.5);
|
painter.setOpacity(0.5);
|
||||||
for (const ConstructionSite& s : m_sim->buildings().allSites())
|
for (const ConstructionSite& s : m_sim->buildings().allSites())
|
||||||
{
|
{
|
||||||
@@ -782,17 +785,6 @@ void GameWorldView::drawOverlays(QPainter& painter)
|
|||||||
painter.fillRect(tileRect(cell), m_visuals->overlays.demolishTint);
|
painter.fillRect(tileRect(cell), m_visuals->overlays.demolishTint);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else
|
|
||||||
{
|
|
||||||
for (const BuildingSystem::BeltTileInfo& info : m_sim->buildings().allBeltTiles())
|
|
||||||
{
|
|
||||||
if (info.id == m_demolishHoverId)
|
|
||||||
{
|
|
||||||
painter.fillRect(tileRect(info.tile), m_visuals->overlays.demolishTint);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Box-select rectangle
|
// Box-select rectangle
|
||||||
@@ -950,13 +942,17 @@ void GameWorldView::mousePressEvent(QMouseEvent* event)
|
|||||||
}
|
}
|
||||||
else if (m_demolishMode)
|
else if (m_demolishMode)
|
||||||
{
|
{
|
||||||
const EntityId hovered = buildingAtTile(tile);
|
EntityId hovered = buildingAtTile(tile);
|
||||||
|
if (hovered == kInvalidEntityId)
|
||||||
|
{
|
||||||
|
hovered = siteAtTile(tile);
|
||||||
|
}
|
||||||
if (hovered != kInvalidEntityId)
|
if (hovered != kInvalidEntityId)
|
||||||
{
|
{
|
||||||
const Building* b = m_sim->buildings().findBuilding(hovered);
|
const Building* b = m_sim->buildings().findBuilding(hovered);
|
||||||
const bool protected_ = b && (b->type == BuildingType::Hq
|
const bool isProtected = b && (b->type == BuildingType::Hq
|
||||||
|| b->type == BuildingType::PlayerDefenceStation);
|
|| b->type == BuildingType::PlayerDefenceStation);
|
||||||
if (!protected_)
|
if (!isProtected)
|
||||||
{
|
{
|
||||||
m_sim->demolish(hovered);
|
m_sim->demolish(hovered);
|
||||||
m_demolishHoverId = kInvalidEntityId;
|
m_demolishHoverId = kInvalidEntityId;
|
||||||
|
|||||||
@@ -96,6 +96,7 @@ private:
|
|||||||
bool isValidPlacement(BuildingType type, QPoint anchor, Rotation rot) const;
|
bool isValidPlacement(BuildingType type, QPoint anchor, Rotation rot) const;
|
||||||
const BuildingDef* findBuildingDef(BuildingType type) const;
|
const BuildingDef* findBuildingDef(BuildingType type) const;
|
||||||
EntityId buildingAtTile(QPoint tile) const;
|
EntityId buildingAtTile(QPoint tile) const;
|
||||||
|
EntityId siteAtTile(QPoint tile) const;
|
||||||
|
|
||||||
void drawPortGlyph(QPainter& painter, QPoint bodyTile,
|
void drawPortGlyph(QPainter& painter, QPoint bodyTile,
|
||||||
Rotation direction, const QColor& color);
|
Rotation direction, const QColor& color);
|
||||||
|
|||||||
Reference in New Issue
Block a user