312 lines
11 KiB
C++
312 lines
11 KiB
C++
#include "PlacementRules.h"
|
|
|
|
#include "BuildingType.h"
|
|
#include "FactoryQueries.h"
|
|
#include "SurfaceMask.h"
|
|
|
|
bool bodyCellsWithinWorldBounds(const FactoryState& state, const GameConfig& config,const std::vector<QPoint>& bodyCells,
|
|
QPoint anchor)
|
|
{
|
|
const int heightTiles = config.world.heightTiles;
|
|
const int leftEdgeX = -state.asteroidWidth_tiles;
|
|
for (const QPoint& cell : bodyCells)
|
|
{
|
|
const QPoint worldCell = anchor + cell;
|
|
if (worldCell.y() < 0 || worldCell.y() >= heightTiles)
|
|
{
|
|
return false;
|
|
}
|
|
if (worldCell.x() < leftEdgeX)
|
|
{
|
|
return false;
|
|
}
|
|
}
|
|
return true;
|
|
}
|
|
|
|
bool isPlacementValid(const FactoryState& state, const GameConfig& config,BuildingType type, QPoint anchor,
|
|
Rotation rotation)
|
|
{
|
|
const BuildingDef* def = config.buildings.findBuildingDef(type);
|
|
if (def == nullptr)
|
|
{
|
|
return false;
|
|
}
|
|
const ParsedSurfaceMask mask = parseSurfaceMask(def->surfaceMask, rotation);
|
|
|
|
if (!bodyCellsWithinWorldBounds(state, config, mask.bodyCells, anchor))
|
|
{
|
|
return false;
|
|
}
|
|
|
|
// Terrain: ship-dock (S) cells must sit in space (x >= 0); all other body
|
|
// (A) cells must sit on the asteroid (x < 0). (REQ-BLD-PLACE-VALID)
|
|
for (const QPoint& cell : mask.bodyCells)
|
|
{
|
|
const QPoint worldCell = anchor + cell;
|
|
bool isShipDock = false;
|
|
for (const QPoint& dock : mask.shipDockCells)
|
|
{
|
|
if (dock == cell)
|
|
{
|
|
isShipDock = true;
|
|
break;
|
|
}
|
|
}
|
|
if (isShipDock)
|
|
{
|
|
if (worldCell.x() < 0)
|
|
{
|
|
return false;
|
|
}
|
|
}
|
|
else if (worldCell.x() >= 0)
|
|
{
|
|
return false;
|
|
}
|
|
}
|
|
return true;
|
|
}
|
|
|
|
|
|
std::optional<CoincidingBuilding> findCoincidingSameTypeBuilding(const FactoryState& state,
|
|
const GameConfig& config, BuildingType type, QPoint anchor, Rotation rot)
|
|
{
|
|
const BuildingDef* def = config.buildings.findBuildingDef(type);
|
|
if (!def) { return std::nullopt; }
|
|
|
|
const ParsedSurfaceMask mask = parseSurfaceMask(def->surfaceMask, rot);
|
|
if (mask.bodyCells.empty()) { return std::nullopt; }
|
|
|
|
// All body cells must be occupied by the same entity.
|
|
const QPoint firstAbs = anchor + mask.bodyCells[0];
|
|
const std::optional<BuildingId> firstOwner = state.grid.findOwner(firstAbs);
|
|
if (!firstOwner.has_value()) { return std::nullopt; }
|
|
const BuildingId candidateId = *firstOwner;
|
|
|
|
for (const QPoint& rel : mask.bodyCells)
|
|
{
|
|
const std::optional<BuildingId> owner = state.grid.findOwner(anchor + rel);
|
|
if (!owner.has_value() || *owner != candidateId)
|
|
{
|
|
return std::nullopt;
|
|
}
|
|
}
|
|
|
|
// Verify the candidate is the same building type with the same cell count.
|
|
for (const ConstructionSite& site : state.constructionQueue)
|
|
{
|
|
if (site.id != candidateId) { continue; }
|
|
if (site.type != type) { return std::nullopt; }
|
|
if (site.bodyCells.size() != mask.bodyCells.size()) { return std::nullopt; }
|
|
return CoincidingBuilding{candidateId, site.rotation};
|
|
}
|
|
for (const Building& b : state.buildings)
|
|
{
|
|
if (b.id != candidateId) { continue; }
|
|
if (b.type != type) { return std::nullopt; }
|
|
if (b.bodyCells.size() != mask.bodyCells.size()) { return std::nullopt; }
|
|
return CoincidingBuilding{candidateId, b.rotation};
|
|
}
|
|
|
|
return std::nullopt;
|
|
}
|
|
|
|
|
|
std::optional<BuildingId> findRotateInPlaceTarget(const FactoryState& state, const GameConfig& config,
|
|
BuildingType type, QPoint anchor, Rotation rot)
|
|
{
|
|
// Tunnel Entries and Tunnel Exits cannot be rotated in place; re-orienting a
|
|
// tunnel requires deconstructing and re-placing it (REQ-BLD-ROTATE-IN-PLACE).
|
|
if (type == BuildingType::TunnelEntry || type == BuildingType::TunnelExit)
|
|
{
|
|
return std::nullopt;
|
|
}
|
|
|
|
const std::optional<CoincidingBuilding> target =
|
|
findCoincidingSameTypeBuilding(state, config, type, anchor, rot);
|
|
if (!target.has_value()) { return std::nullopt; }
|
|
return target->id;
|
|
}
|
|
|
|
|
|
namespace
|
|
{
|
|
|
|
// The building or construction site occupying `tile`, if it is of `type`, together with
|
|
// where it stands. The single-building transfer gesture hit-tests the cursor with this
|
|
// instead of comparing footprints (REQ-UI-BLUEPRINT-TRANSFER).
|
|
std::optional<CoincidingBuilding> findSameTypeBuildingAt(const FactoryState& state,
|
|
BuildingType type, QPoint tile,
|
|
QPoint& anchorOut)
|
|
{
|
|
const std::optional<BuildingId> owner = state.grid.findOwner(tile);
|
|
if (!owner.has_value()) { return std::nullopt; }
|
|
|
|
if (const ConstructionSite* site = findSite(state, *owner))
|
|
{
|
|
if (site->type != type) { return std::nullopt; }
|
|
anchorOut = site->anchor;
|
|
return CoincidingBuilding{site->id, site->rotation};
|
|
}
|
|
if (const Building* building = findBuilding(state, *owner))
|
|
{
|
|
if (building->type != type) { return std::nullopt; }
|
|
anchorOut = building->anchor;
|
|
return CoincidingBuilding{building->id, building->rotation};
|
|
}
|
|
return std::nullopt;
|
|
}
|
|
|
|
} // namespace
|
|
|
|
|
|
BlueprintGhostResolved resolveBlueprintGhost(const FactoryState& state, const GameConfig& config,
|
|
BuildingType type, QPoint anchor, Rotation rotation, std::optional<QPoint> hoverTile)
|
|
{
|
|
// The single-building gesture first, because it answers without looking at the
|
|
// ghost's own position at all: whatever same-type building the cursor is on takes the
|
|
// settings, at any facing, and the ghost snaps onto it (REQ-UI-BLUEPRINT-TRANSFER).
|
|
if (hoverTile.has_value() && isConfigurableBuildingType(type))
|
|
{
|
|
QPoint hoveredAnchor;
|
|
const std::optional<CoincidingBuilding> hovered =
|
|
findSameTypeBuildingAt(state, type, *hoverTile, hoveredAnchor);
|
|
if (hovered.has_value())
|
|
{
|
|
return BlueprintGhostResolved{BlueprintGhostAction::Transfer, hovered->id,
|
|
hoveredAnchor, hovered->rotation};
|
|
}
|
|
}
|
|
|
|
// Terrain and world bounds next: nothing rescues a ghost hanging off the asteroid
|
|
// (REQ-BLD-PLACE-VALID condition (a)).
|
|
if (!isPlacementValid(state, config, type, anchor, rotation))
|
|
{
|
|
return BlueprintGhostResolved{BlueprintGhostAction::Invalid, std::nullopt,
|
|
anchor, rotation};
|
|
}
|
|
|
|
const std::optional<CoincidingBuilding> coinciding =
|
|
findCoincidingSameTypeBuilding(state, config, type, anchor, rotation);
|
|
if (coinciding.has_value())
|
|
{
|
|
// The building the blueprint wants is already there, facing the same way. It
|
|
// takes the blueprint's settings if it has any to take (REQ-UI-BLUEPRINT-TRANSFER)
|
|
// and is otherwise left exactly as it is (REQ-UI-BLUEPRINT-OVERLAP). Tunnels are
|
|
// not excluded from the latter: nothing is rotated, so the reason for their
|
|
// REQ-BLD-ROTATE-IN-PLACE exception does not arise.
|
|
if (coinciding->rotation == rotation)
|
|
{
|
|
return BlueprintGhostResolved{isConfigurableBuildingType(type)
|
|
? BlueprintGhostAction::Transfer
|
|
: BlueprintGhostAction::CompatibleOverlap,
|
|
coinciding->id, anchor, rotation};
|
|
}
|
|
|
|
// Facing the other way, and placement may not re-orient it.
|
|
return BlueprintGhostResolved{BlueprintGhostAction::Invalid, std::nullopt,
|
|
anchor, rotation};
|
|
}
|
|
|
|
// No coincidence: any occupancy at all is an ordinary overlap
|
|
// (REQ-BLD-PLACE-VALID condition (b)).
|
|
const BuildingDef* def = config.buildings.findBuildingDef(type);
|
|
if (!def)
|
|
{
|
|
return BlueprintGhostResolved{BlueprintGhostAction::Invalid, std::nullopt,
|
|
anchor, rotation};
|
|
}
|
|
const ParsedSurfaceMask parsed = parseSurfaceMask(def->surfaceMask, rotation);
|
|
for (const QPoint& relativeCell : parsed.bodyCells)
|
|
{
|
|
if (isTileOccupied(state, anchor + relativeCell))
|
|
{
|
|
return BlueprintGhostResolved{BlueprintGhostAction::Invalid, std::nullopt,
|
|
anchor, rotation};
|
|
}
|
|
}
|
|
return BlueprintGhostResolved{BlueprintGhostAction::PlaceNew, std::nullopt,
|
|
anchor, rotation};
|
|
}
|
|
|
|
|
|
bool canPlaceBuilding(const FactoryState& state, const GameConfig& config,
|
|
BuildingType type, QPoint anchor, Rotation rotation)
|
|
{
|
|
// Terrain and world-bounds validity first (REQ-BLD-PLACE-VALID); occupancy is
|
|
// the extra rule this adds.
|
|
if (!isPlacementValid(state, config, type, anchor, rotation))
|
|
{
|
|
return false;
|
|
}
|
|
|
|
const BuildingDef* def = config.buildings.findBuildingDef(type);
|
|
if (!def) { return false; }
|
|
const ParsedSurfaceMask parsed = parseSurfaceMask(def->surfaceMask, rotation);
|
|
|
|
bool anyOccupied = false;
|
|
for (const QPoint& relativeCell : parsed.bodyCells)
|
|
{
|
|
if (isTileOccupied(state, anchor + relativeCell))
|
|
{
|
|
anyOccupied = true;
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (anyOccupied)
|
|
{
|
|
// Occupied is still placeable when what is there is the same building being
|
|
// re-oriented (REQ-BLD-ROTATE-IN-PLACE).
|
|
return findRotateInPlaceTarget(state, config, type, anchor, rotation).has_value();
|
|
}
|
|
return true;
|
|
}
|
|
|
|
std::vector<BeltDragResolved> resolveBeltDragPath(const std::vector<BeltPathTile>& path,
|
|
const FactoryState& state,
|
|
const GameConfig& config,
|
|
int buildingBlocksStock)
|
|
{
|
|
std::vector<BeltDragResolved> resolved;
|
|
resolved.reserve(path.size());
|
|
|
|
const BuildingDef* def = config.buildings.findBuildingDef(BuildingType::Belt);
|
|
const int beltCost = (def != nullptr) ? def->cost : 0;
|
|
int spent = 0;
|
|
|
|
for (const BeltPathTile& entry : path)
|
|
{
|
|
BeltDragResolved item;
|
|
const std::optional<BuildingId> rotateTarget =
|
|
findRotateInPlaceTarget(state, config, BuildingType::Belt,
|
|
entry.tile, entry.rotation);
|
|
if (rotateTarget.has_value())
|
|
{
|
|
// A tile holding only a belt (or belt site) is re-oriented, no cost.
|
|
item.action = BeltTileAction::RotateInPlace;
|
|
item.affordable = true;
|
|
item.rotateId = rotateTarget;
|
|
}
|
|
else if (canPlaceBuilding(state, config, BuildingType::Belt,
|
|
entry.tile, entry.rotation))
|
|
{
|
|
// Empty, valid cell: a new belt, subject to cumulative affordability.
|
|
item.action = BeltTileAction::PlaceNew;
|
|
item.affordable = (spent + beltCost <= buildingBlocksStock);
|
|
item.rotateId = std::nullopt;
|
|
if (item.affordable) { spent += beltCost; }
|
|
}
|
|
else
|
|
{
|
|
// Occupied by a non-belt building/site, or otherwise invalid terrain.
|
|
item.action = BeltTileAction::Invalid;
|
|
item.affordable = false;
|
|
item.rotateId = std::nullopt;
|
|
}
|
|
resolved.push_back(item);
|
|
}
|
|
return resolved;
|
|
}
|