implement tunnels

This commit is contained in:
2026-04-26 17:15:50 +02:00
parent 63c7df5b7f
commit 89005d6bb7
13 changed files with 819 additions and 18 deletions

View File

@@ -1,5 +1,6 @@
#pragma once
#include <optional>
#include <string>
#include <vector>
@@ -18,6 +19,7 @@ struct BuildingDef
// Stored as raw strings here; parsing into per-cell tiles + output ports
// happens when buildings are placed, not at load time.
std::vector<std::string> surfaceMask;
std::optional<int> tunnelMaxDistance;
};
struct BuildingsConfig

View File

@@ -275,6 +275,11 @@ BuildingsConfig ConfigLoader::loadBuildings(const std::string& path)
def.constructionTimeSeconds = requireDouble(mt["construction_time_seconds"], file, elemPath + ".construction_time_seconds");
def.surfaceMask = requireStringArray(mt["surface_mask"], file, elemPath + ".surface_mask");
if (const std::optional<int64_t> tmd = mt["tunnel_max_distance"].value<int64_t>())
{
def.tunnelMaxDistance = static_cast<int>(*tmd);
}
const std::optional<BuildingType> parsedType = parseBuildingType(def.id);
if (!parsedType)
{

View File

@@ -10,6 +10,8 @@ std::optional<BuildingType> parseBuildingType(const std::string& id)
if (id == "salvage_bay") { return BuildingType::SalvageBay; }
if (id == "belt") { return BuildingType::Belt; }
if (id == "splitter") { return BuildingType::Splitter; }
if (id == "tunnel_entry") { return BuildingType::TunnelEntry; }
if (id == "tunnel_exit") { return BuildingType::TunnelExit; }
if (id == "hq") { return BuildingType::Hq; }
if (id == "player_defence_station") { return BuildingType::PlayerDefenceStation; }
if (id == "enemy_defence_station") { return BuildingType::EnemyDefenceStation; }
@@ -28,6 +30,8 @@ std::string buildingTypeId(BuildingType type)
case BuildingType::SalvageBay: return "salvage_bay";
case BuildingType::Belt: return "belt";
case BuildingType::Splitter: return "splitter";
case BuildingType::TunnelEntry: return "tunnel_entry";
case BuildingType::TunnelExit: return "tunnel_exit";
case BuildingType::Hq: return "hq";
case BuildingType::PlayerDefenceStation: return "player_defence_station";
case BuildingType::EnemyDefenceStation: return "enemy_defence_station";

View File

@@ -16,6 +16,8 @@ enum class BuildingType
SalvageBay,
Belt,
Splitter,
TunnelEntry,
TunnelExit,
Hq,
PlayerDefenceStation,
EnemyDefenceStation,

View File

@@ -70,10 +70,39 @@ void BeltSystem::placeSplitter(QPoint tile, Rotation outputA, Rotation outputB)
m_splitters[key(tile)] = st;
}
void BeltSystem::removeTile(QPoint tile)
void BeltSystem::placeTunnelEntry(QPoint tile, Rotation direction, int maxDistance)
{
m_belts.erase(key(tile));
m_splitters.erase(key(tile));
m_tunnelExits.erase(key(tile));
TunnelEntryTile te;
te.direction = direction;
te.maxDistance = maxDistance;
m_tunnelEntries[key(tile)] = te;
reevaluateTunnelPairing();
}
void BeltSystem::placeTunnelExit(QPoint tile, Rotation direction)
{
m_belts.erase(key(tile));
m_splitters.erase(key(tile));
m_tunnelEntries.erase(key(tile));
TunnelExitTile tx;
tx.direction = direction;
m_tunnelExits[key(tile)] = tx;
reevaluateTunnelPairing();
}
void BeltSystem::removeTile(QPoint tile)
{
const bool wasTunnel = (m_tunnelEntries.erase(key(tile)) > 0)
| (m_tunnelExits.erase(key(tile)) > 0);
m_belts.erase(key(tile));
m_splitters.erase(key(tile));
if (wasTunnel)
{
reevaluateTunnelPairing();
}
}
void BeltSystem::setSplitterFilters(QPoint tile,
@@ -105,18 +134,98 @@ std::optional<BeltSystem::SplitterInfo> BeltSystem::getSplitterInfo(QPoint tile)
};
}
// ---------------------------------------------------------------------------
// Tunnel pairing
// ---------------------------------------------------------------------------
void BeltSystem::reevaluateTunnelPairing()
{
std::vector<TunnelLink> oldLinks;
std::swap(oldLinks, m_tunnelLinks);
for (const std::pair<const std::pair<int, int>, TunnelEntryTile>& entry : m_tunnelEntries)
{
const QPoint entryPos(entry.first.first, entry.first.second);
const Rotation dir = entry.second.direction;
const int maxDist = entry.second.maxDistance;
for (int d = 1; d <= maxDist; ++d)
{
QPoint probe = entryPos;
for (int step = 0; step < d; ++step)
{
probe = adjacentTile(probe, dir);
}
// Check if a same-direction tunnel entry is here (blocks pairing)
const std::map<std::pair<int, int>, TunnelEntryTile>::const_iterator teIt =
m_tunnelEntries.find(key(probe));
if (teIt != m_tunnelEntries.end() && teIt->second.direction == dir)
{
break;
}
// Check if a same-direction tunnel exit is here (forms pair)
const std::map<std::pair<int, int>, TunnelExitTile>::const_iterator txIt =
m_tunnelExits.find(key(probe));
if (txIt != m_tunnelExits.end())
{
if (txIt->second.direction == dir)
{
TunnelLink link;
link.entryTile = entryPos;
link.exitTile = probe;
link.length = static_cast<double>(d);
for (const TunnelLink& old : oldLinks)
{
if (old.entryTile == entryPos && old.exitTile == probe)
{
link.items = old.items;
break;
}
}
m_tunnelLinks.push_back(std::move(link));
break;
}
// Different direction exit — skip, keep searching
}
}
}
}
// ---------------------------------------------------------------------------
// Port interface
// ---------------------------------------------------------------------------
bool BeltSystem::tryPutItem(QPoint tile, Item item)
{
const std::map<std::pair<int, int>, BeltTile>::iterator it = m_belts.find(key(tile));
if (it == m_belts.end())
const std::map<std::pair<int, int>, BeltTile>::iterator bIt = m_belts.find(key(tile));
if (bIt != m_belts.end())
{
return tryPlaceOnBelt(tile, item);
}
const std::map<std::pair<int, int>, TunnelEntryTile>::iterator teIt =
m_tunnelEntries.find(key(tile));
if (teIt != m_tunnelEntries.end())
{
TunnelEntryTile& te = teIt->second;
if (!te.front)
{
te.front = BeltItemSlot{item, 0.0};
return true;
}
if (!te.back)
{
te.back = BeltItemSlot{item, 0.0};
return true;
}
return false;
}
return tryPlaceOnBelt(tile, item);
return false;
}
std::optional<Item> BeltSystem::tryTakeItem(Port port)
@@ -158,6 +267,20 @@ std::optional<Item> BeltSystem::tryTakeItem(Port port)
}
}
const std::map<std::pair<int, int>, TunnelExitTile>::iterator txIt =
m_tunnelExits.find(key(port.tile));
if (txIt != m_tunnelExits.end())
{
TunnelExitTile& tx = txIt->second;
if (tx.direction == port.direction && tx.front && tx.front->progress >= 1.0)
{
const Item taken = tx.front->item;
tx.front = tx.back;
tx.back = std::nullopt;
return taken;
}
}
return std::nullopt;
}
@@ -194,6 +317,17 @@ std::optional<ItemType> BeltSystem::peekItem(Port port) const
}
}
const std::map<std::pair<int, int>, TunnelExitTile>::const_iterator txIt =
m_tunnelExits.find(key(port.tile));
if (txIt != m_tunnelExits.end())
{
const TunnelExitTile& tx = txIt->second;
if (tx.direction == port.direction && tx.front && tx.front->progress >= 1.0)
{
return tx.front->item.type;
}
}
return std::nullopt;
}
@@ -219,6 +353,36 @@ void BeltSystem::clearTiles(const std::vector<QPoint>& tiles)
sIt->second.frontA = std::nullopt;
sIt->second.frontB = std::nullopt;
}
const std::map<std::pair<int, int>, TunnelEntryTile>::iterator teIt =
m_tunnelEntries.find(key(tile));
if (teIt != m_tunnelEntries.end())
{
teIt->second.front = std::nullopt;
teIt->second.back = std::nullopt;
for (TunnelLink& link : m_tunnelLinks)
{
if (link.entryTile == tile)
{
link.items.clear();
}
}
}
const std::map<std::pair<int, int>, TunnelExitTile>::iterator txIt =
m_tunnelExits.find(key(tile));
if (txIt != m_tunnelExits.end())
{
txIt->second.front = std::nullopt;
txIt->second.back = std::nullopt;
for (TunnelLink& link : m_tunnelLinks)
{
if (link.exitTile == tile)
{
link.items.clear();
}
}
}
}
}
@@ -229,7 +393,9 @@ void BeltSystem::clearTiles(const std::vector<QPoint>& tiles)
void BeltSystem::tick()
{
advanceProgress();
advanceTunnelProgress();
moveItemsToNextTile();
moveTunnelItems();
routeSplitterItems();
}
@@ -304,6 +470,98 @@ void BeltSystem::advanceProgress()
}
}
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;
if (te.front)
{
te.front->progress += m_progressPerTick;
if (te.front->progress > 1.0)
{
te.front->progress = 1.0;
}
}
if (te.back)
{
te.back->progress += m_progressPerTick;
if (te.front && te.back->progress >= te.front->progress)
{
te.back->progress = te.front->progress - m_progressPerTick;
if (te.back->progress < 0.0)
{
te.back->progress = 0.0;
}
}
if (te.back->progress > 0.5)
{
te.back->progress = 0.5;
}
}
}
for (std::map<std::pair<int, int>, TunnelExitTile>::iterator it = m_tunnelExits.begin();
it != m_tunnelExits.end(); ++it)
{
TunnelExitTile& tx = it->second;
if (tx.front)
{
tx.front->progress += m_progressPerTick;
if (tx.front->progress > 1.0)
{
tx.front->progress = 1.0;
}
}
if (tx.back)
{
tx.back->progress += m_progressPerTick;
if (tx.front && tx.back->progress >= tx.front->progress)
{
tx.back->progress = tx.front->progress - m_progressPerTick;
if (tx.back->progress < 0.0)
{
tx.back->progress = 0.0;
}
}
if (tx.back->progress > 0.5)
{
tx.back->progress = 0.5;
}
}
}
for (TunnelLink& link : m_tunnelLinks)
{
for (std::size_t i = 0; i < link.items.size(); ++i)
{
TunnelTransitItem& ti = link.items[i];
ti.progress += m_progressPerTick;
if (ti.progress > link.length)
{
ti.progress = link.length;
}
if (i > 0)
{
const double maxProgress = link.items[i - 1].progress - 0.5;
if (ti.progress > maxProgress)
{
ti.progress = maxProgress;
if (ti.progress < 0.0)
{
ti.progress = 0.0;
}
}
}
}
}
}
void BeltSystem::moveItemsToNextTile()
{
// Belt items advancing into the next tile.
@@ -340,11 +598,25 @@ void BeltSystem::moveItemsToNextTile()
bt.front = bt.back;
bt.back = std::nullopt;
}
// else: splitter back occupied — item stays blocked at progress 1.0.
}
// else: no tile registered (e.g. open space, or building input port).
// Items leaving into unregistered tiles are not consumed here — the
// building pull step uses tryTakeItem for that.
else
{
const std::map<std::pair<int, int>, TunnelEntryTile>::iterator nextEntry =
m_tunnelEntries.find(key(next));
if (nextEntry != m_tunnelEntries.end() && !nextEntry->second.back)
{
if (!nextEntry->second.front)
{
nextEntry->second.front = BeltItemSlot{bt.front->item, 0.0};
}
else
{
nextEntry->second.back = BeltItemSlot{bt.front->item, 0.0};
}
bt.front = bt.back;
bt.back = std::nullopt;
}
}
}
// Splitter front slots advancing into downstream belt tiles.
@@ -357,23 +629,89 @@ void BeltSystem::moveItemsToNextTile()
if (st.frontA && st.frontA->progress >= 1.0)
{
const QPoint dest = adjacentTile(here, st.outputA);
if (tryPlaceOnBelt(dest, st.frontA->item))
if (tryPushToTile(dest, st.frontA->item, st.outputA))
{
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))
if (tryPushToTile(dest, st.frontB->item, st.outputB))
{
st.frontB = std::nullopt;
}
}
}
// Tunnel exit items advancing into downstream tiles.
for (std::map<std::pair<int, int>, TunnelExitTile>::iterator it = m_tunnelExits.begin();
it != m_tunnelExits.end(); ++it)
{
TunnelExitTile& tx = it->second;
if (!tx.front || tx.front->progress < 1.0)
{
continue;
}
const QPoint here = QPoint(it->first.first, it->first.second);
const QPoint next = adjacentTile(here, tx.direction);
if (tryPushToTile(next, tx.front->item, tx.direction))
{
tx.front = tx.back;
tx.back = std::nullopt;
}
}
}
void BeltSystem::moveTunnelItems()
{
for (TunnelLink& link : m_tunnelLinks)
{
// Entry front → transit
const std::map<std::pair<int, int>, TunnelEntryTile>::iterator teIt =
m_tunnelEntries.find(key(link.entryTile));
if (teIt != m_tunnelEntries.end())
{
TunnelEntryTile& te = teIt->second;
if (te.front && te.front->progress >= 1.0)
{
const bool canEnter = link.items.empty()
|| link.items.back().progress >= 0.5;
if (canEnter)
{
TunnelTransitItem ti;
ti.item = te.front->item;
ti.progress = 0.0;
link.items.push_back(ti);
te.front = te.back;
te.back = std::nullopt;
}
}
}
// Transit front → exit back
if (!link.items.empty() && link.items.front().progress >= link.length)
{
const std::map<std::pair<int, int>, TunnelExitTile>::iterator txIt =
m_tunnelExits.find(key(link.exitTile));
if (txIt != m_tunnelExits.end())
{
TunnelExitTile& tx = txIt->second;
if (!tx.back && !tx.front)
{
tx.front = BeltItemSlot{link.items.front().item, 0.0};
link.items.erase(link.items.begin());
}
else if (!tx.back && tx.front)
{
tx.back = BeltItemSlot{link.items.front().item, 0.0};
link.items.erase(link.items.begin());
}
}
}
}
}
void BeltSystem::routeSplitterItems()
@@ -474,6 +812,65 @@ bool BeltSystem::tryPlaceOnBelt(QPoint tile, Item item)
return false; // both slots occupied
}
bool BeltSystem::tryPushToTile(QPoint dest, Item item, Rotation fromDir)
{
if (tryPlaceOnBelt(dest, item))
{
return true;
}
const std::map<std::pair<int, int>, SplitterTile>::iterator splIt =
m_splitters.find(key(dest));
if (splIt != m_splitters.end())
{
if (!splIt->second.back)
{
splIt->second.back = BeltItemSlot{item, 0.0};
splIt->second.backDir = fromDir;
return true;
}
return false;
}
const std::map<std::pair<int, int>, TunnelEntryTile>::iterator teIt =
m_tunnelEntries.find(key(dest));
if (teIt != m_tunnelEntries.end())
{
TunnelEntryTile& te = teIt->second;
if (!te.front)
{
te.front = BeltItemSlot{item, 0.0};
return true;
}
if (!te.back)
{
te.back = BeltItemSlot{item, 0.0};
return true;
}
return false;
}
const std::map<std::pair<int, int>, TunnelExitTile>::iterator txIt =
m_tunnelExits.find(key(dest));
if (txIt != m_tunnelExits.end())
{
TunnelExitTile& tx = txIt->second;
if (!tx.front)
{
tx.front = BeltItemSlot{item, 0.0};
return true;
}
if (!tx.back)
{
tx.back = BeltItemSlot{item, 0.0};
return true;
}
return false;
}
return false;
}
// ---------------------------------------------------------------------------
// Rendering
// ---------------------------------------------------------------------------
@@ -542,4 +939,54 @@ void BeltSystem::forEachVisualItem(QRect viewportTiles,
visit(vi);
}
}
for (const std::pair<const std::pair<int, int>, TunnelEntryTile>& entry : m_tunnelEntries)
{
const QPoint tile(entry.first.first, entry.first.second);
if (!viewportTiles.contains(tile))
{
continue;
}
const TunnelEntryTile& te = entry.second;
if (te.front)
{
VisualItem vi;
vi.type = te.front->item.type;
vi.worldPos = slotWorldPos(tile, te.direction, te.front->progress);
visit(vi);
}
if (te.back)
{
VisualItem vi;
vi.type = te.back->item.type;
vi.worldPos = slotWorldPos(tile, te.direction, te.back->progress);
visit(vi);
}
}
for (const std::pair<const std::pair<int, int>, TunnelExitTile>& entry : m_tunnelExits)
{
const QPoint tile(entry.first.first, entry.first.second);
if (!viewportTiles.contains(tile))
{
continue;
}
const TunnelExitTile& tx = entry.second;
if (tx.front)
{
VisualItem vi;
vi.type = tx.front->item.type;
vi.worldPos = slotWorldPos(tile, tx.direction, tx.front->progress);
visit(vi);
}
if (tx.back)
{
VisualItem vi;
vi.type = tx.back->item.type;
vi.worldPos = slotWorldPos(tile, tx.direction, tx.back->progress);
visit(vi);
}
}
}

View File

@@ -37,6 +37,12 @@ public:
// Register a new belt tile. Any items already on this tile are cleared.
void placeBelt(QPoint tile, Rotation direction);
// Register a new tunnel entry tile.
void placeTunnelEntry(QPoint tile, Rotation direction, int maxDistance);
// Register a new tunnel exit tile.
void placeTunnelExit(QPoint tile, Rotation direction);
// Register a new splitter tile. outputA and outputB are the two exit
// directions (e.g. West and East for a default-rotation splitter).
// Items entering from any adjacent belt whose direction points into this
@@ -87,13 +93,20 @@ public:
private:
void advanceProgress();
void advanceTunnelProgress();
void moveItemsToNextTile();
void moveTunnelItems();
void routeSplitterItems();
// Place item into back slot of an existing belt tile at progress 0.
// Returns false if tile is not a belt or is full.
bool tryPlaceOnBelt(QPoint tile, Item item);
// Push an item to any tile type (belt, splitter, tunnel entry, tunnel exit).
bool tryPushToTile(QPoint dest, Item item, Rotation fromDir);
void reevaluateTunnelPairing();
static std::pair<int, int> key(QPoint tile);
static QPoint adjacentTile(QPoint tile, Rotation dir);
@@ -126,9 +139,41 @@ private:
std::optional<BeltItemSlot> frontB; // progress [0, 1]; routed to outputB
};
struct TunnelEntryTile
{
Rotation direction;
int maxDistance;
std::optional<BeltItemSlot> front;
std::optional<BeltItemSlot> back;
};
struct TunnelExitTile
{
Rotation direction;
std::optional<BeltItemSlot> front;
std::optional<BeltItemSlot> back;
};
struct TunnelTransitItem
{
Item item;
double progress;
};
struct TunnelLink
{
QPoint entryTile;
QPoint exitTile;
double length;
std::vector<TunnelTransitItem> items; // front (highest progress) to back
};
double m_progressPerTick; // beltSpeedTilesPerSecond / kTickRateHz
std::map<std::pair<int, int>, BeltTile> m_belts;
std::map<std::pair<int, int>, SplitterTile> m_splitters;
std::map<std::pair<int, int>, BeltTile> m_belts;
std::map<std::pair<int, int>, SplitterTile> m_splitters;
std::map<std::pair<int, int>, TunnelEntryTile> m_tunnelEntries;
std::map<std::pair<int, int>, TunnelExitTile> m_tunnelExits;
std::vector<TunnelLink> m_tunnelLinks;
};

View File

@@ -269,7 +269,8 @@ int BuildingSystem::demolish(EntityId id)
{
if (it->id == id)
{
if (it->type == BuildingType::Belt || it->type == BuildingType::Splitter)
if (it->type == BuildingType::Belt || it->type == BuildingType::Splitter
|| it->type == BuildingType::TunnelEntry || it->type == BuildingType::TunnelExit)
{
m_belts.removeTile(it->anchor);
}
@@ -424,6 +425,16 @@ void BuildingSystem::tickConstruction(Tick currentTick)
mask.outputPorts[0].direction,
mask.outputPorts[1].direction);
}
else if (front.type == BuildingType::TunnelEntry)
{
const BuildingDef* bdef = findBuildingDef(front.type);
const int maxDist = (bdef && bdef->tunnelMaxDistance) ? *bdef->tunnelMaxDistance : 0;
m_belts.placeTunnelEntry(front.anchor, front.rotation, maxDist);
}
else if (front.type == BuildingType::TunnelExit)
{
m_belts.placeTunnelExit(front.anchor, front.rotation);
}
m_buildings.push_back(std::move(building));
@@ -902,7 +913,8 @@ bool BuildingSystem::removeBuilding(EntityId id)
{
if (it->id == id)
{
if (it->type == BuildingType::Belt || it->type == BuildingType::Splitter)
if (it->type == BuildingType::Belt || it->type == BuildingType::Splitter
|| it->type == BuildingType::TunnelEntry || it->type == BuildingType::TunnelExit)
{
m_belts.removeTile(it->anchor);
}