move two placement queries out of the view into PlacementRules
This commit is contained in:
@@ -119,3 +119,82 @@ std::optional<BuildingId> findRotateInPlaceTarget(const FactoryState& state, con
|
|||||||
return std::nullopt;
|
return std::nullopt;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|||||||
@@ -5,6 +5,7 @@
|
|||||||
|
|
||||||
#include <QPoint>
|
#include <QPoint>
|
||||||
|
|
||||||
|
#include "BeltDragPath.h"
|
||||||
#include "BuildingId.h"
|
#include "BuildingId.h"
|
||||||
#include "BuildingType.h"
|
#include "BuildingType.h"
|
||||||
#include "FactoryState.h"
|
#include "FactoryState.h"
|
||||||
@@ -36,3 +37,35 @@ std::optional<BuildingId> findRotateInPlaceTarget(const FactoryState& state,
|
|||||||
const GameConfig& config,
|
const GameConfig& config,
|
||||||
BuildingType type, QPoint anchor,
|
BuildingType type, QPoint anchor,
|
||||||
Rotation rot);
|
Rotation rot);
|
||||||
|
|
||||||
|
// True if placing here would actually do something: the terrain and bounds rules of
|
||||||
|
// isPlacementValid hold, and the body cells are either all free or occupied only by
|
||||||
|
// a building this placement would rotate in place. This is the question the ghost
|
||||||
|
// asks to colour itself and the click path asks before enqueuing a command
|
||||||
|
// (REQ-BLD-GHOST, REQ-BLD-PLACE-VALID, REQ-BLD-ROTATE-IN-PLACE).
|
||||||
|
bool canPlaceBuilding(const FactoryState& state, const GameConfig& config,
|
||||||
|
BuildingType type, QPoint anchor, Rotation rotation);
|
||||||
|
|
||||||
|
// What a belt drag would do to one tile of its path (REQ-BLD-BELT-DRAG).
|
||||||
|
enum class BeltTileAction
|
||||||
|
{
|
||||||
|
PlaceNew, // empty, valid cell: a new belt, subject to affordability
|
||||||
|
RotateInPlace, // already a belt (or belt site): re-oriented, free
|
||||||
|
Invalid // occupied by something else, or invalid terrain
|
||||||
|
};
|
||||||
|
|
||||||
|
struct BeltDragResolved
|
||||||
|
{
|
||||||
|
BeltTileAction action;
|
||||||
|
bool affordable; // meaningful only for PlaceNew
|
||||||
|
std::optional<BuildingId> rotateId; // set only for RotateInPlace
|
||||||
|
};
|
||||||
|
|
||||||
|
// Classifies every tile of a belt drag path against the current factory state,
|
||||||
|
// spending `buildingBlocksStock` cumulatively across the PlaceNew tiles so a path
|
||||||
|
// longer than the player can afford is only partly buildable (REQ-BLD-BELT-DRAG).
|
||||||
|
// Shared so the previewed ghosts and the placement on release cannot disagree.
|
||||||
|
std::vector<BeltDragResolved> resolveBeltDragPath(const std::vector<BeltPathTile>& path,
|
||||||
|
const FactoryState& state,
|
||||||
|
const GameConfig& config,
|
||||||
|
int buildingBlocksStock);
|
||||||
|
|||||||
@@ -563,36 +563,11 @@ ScrollBounds GameWorldView::getScrollBounds() const
|
|||||||
// Placement helpers
|
// Placement helpers
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
bool GameWorldView::isValidPlacement(BuildingType type, QPoint anchor,
|
bool GameWorldView::canPlaceBuildingHere(BuildingType type, QPoint anchor,
|
||||||
Rotation rot) const
|
Rotation rot) const
|
||||||
{
|
{
|
||||||
// Terrain and world-bounds validity are owned by the simulation
|
return canPlaceBuilding(m_sim->getFactoryState(), m_sim->getConfig(),
|
||||||
// (REQ-BLD-PLACE-VALID); the presentation layer only adds the occupancy /
|
type, anchor, rot);
|
||||||
// rotate-in-place check.
|
|
||||||
if (!isPlacementValid(m_sim->getFactoryState(), m_sim->getConfig(), type, anchor, rot))
|
|
||||||
{
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
const BuildingDef* def = m_config->buildings.findBuildingDef(type);
|
|
||||||
if (!def) { return false; }
|
|
||||||
const ParsedSurfaceMask parsed = parseSurfaceMask(def->surfaceMask, rot);
|
|
||||||
|
|
||||||
bool anyOccupied = false;
|
|
||||||
for (const QPoint& relCell : parsed.bodyCells)
|
|
||||||
{
|
|
||||||
if (isTileOccupied(m_sim->getFactoryState(), anchor + relCell))
|
|
||||||
{
|
|
||||||
anyOccupied = true;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (anyOccupied)
|
|
||||||
{
|
|
||||||
return findRotateInPlaceTarget(m_sim->getFactoryState(), m_sim->getConfig(), type, anchor, rot).has_value();
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
std::optional<BuildingId> GameWorldView::buildingAtTile(QPoint tile) const
|
std::optional<BuildingId> GameWorldView::buildingAtTile(QPoint tile) const
|
||||||
@@ -736,7 +711,7 @@ void GameWorldView::placeBlueprintAtTile(QPoint center)
|
|||||||
// Locked building types are excluded from this placement entirely
|
// Locked building types are excluded from this placement entirely
|
||||||
// (REQ-LOCK-BUILDING, REQ-LOCK-UI-BLUEPRINT): not validity-checked here.
|
// (REQ-LOCK-BUILDING, REQ-LOCK-UI-BLUEPRINT): not validity-checked here.
|
||||||
if (!m_sim->isBuildingUnlocked(bb.type)) { continue; }
|
if (!m_sim->isBuildingUnlocked(bb.type)) { continue; }
|
||||||
if (!isValidPlacement(bb.type, center + bb.offset, bb.rotation)) { return; }
|
if (!canPlaceBuildingHere(bb.type, center + bb.offset, bb.rotation)) { return; }
|
||||||
}
|
}
|
||||||
|
|
||||||
// Cost only applies to buildings that are genuinely new (not rotate-in-place),
|
// Cost only applies to buildings that are genuinely new (not rotate-in-place),
|
||||||
@@ -878,7 +853,7 @@ void GameWorldView::placeAtTile(QPoint tile)
|
|||||||
const BuildingType type = m_buildMode.getEffectiveBuilderType();
|
const BuildingType type = m_buildMode.getEffectiveBuilderType();
|
||||||
const Rotation rotation = m_buildMode.getGhostRotation();
|
const Rotation rotation = m_buildMode.getGhostRotation();
|
||||||
|
|
||||||
if (!isValidPlacement(type, tile, rotation))
|
if (!canPlaceBuildingHere(type, tile, rotation))
|
||||||
{
|
{
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -980,46 +955,11 @@ void GameWorldView::recomputeBeltDragPath(QPoint cursorTile)
|
|||||||
m_buildMode.setBeltDragPath(std::move(path));
|
m_buildMode.setBeltDragPath(std::move(path));
|
||||||
}
|
}
|
||||||
|
|
||||||
std::vector<GameWorldView::BeltDragResolved> GameWorldView::resolveBeltDragPath() const
|
std::vector<BeltDragResolved> GameWorldView::resolveBeltDragPath() const
|
||||||
{
|
{
|
||||||
std::vector<BeltDragResolved> resolved;
|
return ::resolveBeltDragPath(m_buildMode.getBeltDragPath(),
|
||||||
resolved.reserve(m_buildMode.getBeltDragPath().size());
|
m_sim->getFactoryState(), m_sim->getConfig(),
|
||||||
|
m_sim->getBuildingBlocksStock());
|
||||||
const BuildingDef* def = m_config->buildings.findBuildingDef(BuildingType::Belt);
|
|
||||||
const int beltCost = (def != nullptr) ? def->cost : 0;
|
|
||||||
const int stock = m_sim->getBuildingBlocksStock();
|
|
||||||
int spent = 0;
|
|
||||||
|
|
||||||
for (const BeltPathTile& entry : m_buildMode.getBeltDragPath())
|
|
||||||
{
|
|
||||||
BeltDragResolved item;
|
|
||||||
const std::optional<BuildingId> rotateTarget =
|
|
||||||
findRotateInPlaceTarget(m_sim->getFactoryState(), m_sim->getConfig(), 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 (isValidPlacement(BuildingType::Belt, entry.tile, entry.rotation))
|
|
||||||
{
|
|
||||||
// Empty, valid cell: a new belt, subject to cumulative affordability.
|
|
||||||
item.action = BeltTileAction::PlaceNew;
|
|
||||||
item.affordable = (spent + beltCost <= stock);
|
|
||||||
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;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void GameWorldView::applyBeltDragPath()
|
void GameWorldView::applyBeltDragPath()
|
||||||
@@ -1947,7 +1887,7 @@ void GameWorldView::drawOverlays(QPainter& painter, const WorldCoordinates& coor
|
|||||||
// REQ-LOCK-UI-BLUEPRINT), so they are not ghosted either.
|
// REQ-LOCK-UI-BLUEPRINT), so they are not ghosted either.
|
||||||
if (!m_sim->isBuildingUnlocked(bb.type)) { continue; }
|
if (!m_sim->isBuildingUnlocked(bb.type)) { continue; }
|
||||||
const QPoint anchor = m_buildMode.getBlueprintGhostTile() + bb.offset;
|
const QPoint anchor = m_buildMode.getBlueprintGhostTile() + bb.offset;
|
||||||
const bool valid = isValidPlacement(bb.type, anchor, bb.rotation);
|
const bool valid = canPlaceBuildingHere(bb.type, anchor, bb.rotation);
|
||||||
drawBuildingGhost(painter, coordinates, bb.type, anchor, bb.rotation,
|
drawBuildingGhost(painter, coordinates, bb.type, anchor, bb.rotation,
|
||||||
valid, /*showPortTargetGlyphs*/ false);
|
valid, /*showPortTargetGlyphs*/ false);
|
||||||
}
|
}
|
||||||
@@ -2442,7 +2382,7 @@ void GameWorldView::mouseMoveEvent(QMouseEvent* event)
|
|||||||
{
|
{
|
||||||
m_buildMode.setGhostTile(tile);
|
m_buildMode.setGhostTile(tile);
|
||||||
m_buildMode.setGhostValidity(
|
m_buildMode.setGhostValidity(
|
||||||
isValidPlacement(m_buildMode.getBuilderType(), tile,
|
canPlaceBuildingHere(m_buildMode.getBuilderType(), tile,
|
||||||
m_buildMode.getGhostRotation()));
|
m_buildMode.getGhostRotation()));
|
||||||
|
|
||||||
if (m_buildMode.isTunnelMode())
|
if (m_buildMode.isTunnelMode())
|
||||||
@@ -2569,7 +2509,7 @@ void GameWorldView::rotateGhost(bool clockwise)
|
|||||||
{
|
{
|
||||||
m_buildMode.rotateGhost(clockwise);
|
m_buildMode.rotateGhost(clockwise);
|
||||||
m_buildMode.setGhostValidity(
|
m_buildMode.setGhostValidity(
|
||||||
isValidPlacement(m_buildMode.getBuilderType(), m_buildMode.getGhostTile(),
|
canPlaceBuildingHere(m_buildMode.getBuilderType(), m_buildMode.getGhostTile(),
|
||||||
m_buildMode.getGhostRotation()));
|
m_buildMode.getGhostRotation()));
|
||||||
// A new facing changes which tunnels the ghost could complete (REQ-BLD-TUNNEL-MODE).
|
// A new facing changes which tunnels the ghost could complete (REQ-BLD-TUNNEL-MODE).
|
||||||
if (m_buildMode.isTunnelMode()) { updateTunnelGhost(); }
|
if (m_buildMode.isTunnelMode()) { updateTunnelGhost(); }
|
||||||
|
|||||||
@@ -52,6 +52,7 @@
|
|||||||
#include "CommandManager.h"
|
#include "CommandManager.h"
|
||||||
#include "EntitySelectionChangedEvent.h"
|
#include "EntitySelectionChangedEvent.h"
|
||||||
#include "GameConfig.h"
|
#include "GameConfig.h"
|
||||||
|
#include "PlacementRules.h"
|
||||||
#include "Rotation.h"
|
#include "Rotation.h"
|
||||||
#include "SelectionController.h"
|
#include "SelectionController.h"
|
||||||
#include "Tick.h"
|
#include "Tick.h"
|
||||||
@@ -213,7 +214,8 @@ private:
|
|||||||
// both edges move with asteroid expansion and with pushes (REQ-GW-SCROLL-LIMIT).
|
// both edges move with asteroid expansion and with pushes (REQ-GW-SCROLL-LIMIT).
|
||||||
ScrollBounds getScrollBounds() const;
|
ScrollBounds getScrollBounds() const;
|
||||||
|
|
||||||
bool isValidPlacement(BuildingType type, QPoint anchor, Rotation rot) const;
|
// canPlaceBuilding (PlacementRules) against this view's simulation.
|
||||||
|
bool canPlaceBuildingHere(BuildingType type, QPoint anchor, Rotation rot) const;
|
||||||
std::optional<BuildingId> buildingAtTile(QPoint tile) const;
|
std::optional<BuildingId> buildingAtTile(QPoint tile) const;
|
||||||
std::optional<BuildingId> siteAtTile(QPoint tile) const;
|
std::optional<BuildingId> siteAtTile(QPoint tile) const;
|
||||||
// Ids of all buildings and construction sites whose footprint intersects
|
// Ids of all buildings and construction sites whose footprint intersects
|
||||||
@@ -277,19 +279,10 @@ private:
|
|||||||
const WorldCoordinates& coordinates);
|
const WorldCoordinates& coordinates);
|
||||||
|
|
||||||
// Belt drag placement (REQ-BLD-BELT-DRAG).
|
// Belt drag placement (REQ-BLD-BELT-DRAG).
|
||||||
// Per-path-tile decision, shared by ghost drawing and release-time placement.
|
|
||||||
enum class BeltTileAction { PlaceNew, RotateInPlace, Invalid };
|
|
||||||
struct BeltDragResolved
|
|
||||||
{
|
|
||||||
BeltTileAction action;
|
|
||||||
bool affordable; // meaningful only for PlaceNew
|
|
||||||
std::optional<BuildingId> rotateId; // set only for RotateInPlace
|
|
||||||
};
|
|
||||||
// Recomputes the drag path from its anchor to cursorTile using the current ghost
|
// Recomputes the drag path from its anchor to cursorTile using the current ghost
|
||||||
// orientation, and stores it on the build mode controller.
|
// orientation, and stores it on the build mode controller.
|
||||||
void recomputeBeltDragPath(QPoint cursorTile);
|
void recomputeBeltDragPath(QPoint cursorTile);
|
||||||
// Classifies each path tile against the current sim state, applying cumulative
|
// resolveBeltDragPath (PlacementRules) against this view's simulation.
|
||||||
// affordability to the PlaceNew tiles.
|
|
||||||
std::vector<BeltDragResolved> resolveBeltDragPath() const;
|
std::vector<BeltDragResolved> resolveBeltDragPath() const;
|
||||||
// Enqueues placements and rotate-in-place commands for the resolved path.
|
// Enqueues placements and rotate-in-place commands for the resolved path.
|
||||||
void applyBeltDragPath();
|
void applyBeltDragPath();
|
||||||
|
|||||||
Reference in New Issue
Block a user