move two placement queries out of the view into PlacementRules
Prerequisite for the WorldRenderer extraction: both the renderer and the click
path need these, so neither can own them.
GameWorldView::isValidPlacement was isPlacementValid plus the occupancy and
rotate-in-place rule - the gap PlacementRules.h already names in its own comment
("Tile occupancy is NOT checked here"). It becomes canPlaceBuilding there, named
for what it adds rather than colliding with isPlacementValid.
resolveBeltDragPath was a pure function of the path, the factory state and the
config, living in the view only because the view happened to draw the ghosts.
It moves next to the other placement rules, taking its BeltTileAction and
BeltDragResolved types with it out of the GameWorldView class body. It stays
shared for the reason it always was: the previewed ghosts and the placement on
release must not disagree about which tiles are affordable.
The view keeps two one-line wrappers that bind its own simulation, so call sites
still read canPlaceBuildingHere(type, anchor, rotation).
Behaviour is unchanged; both are now testable without a widget.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JcReq7hVk4KUPhTDKWAG7K
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);
|
||||||
}
|
}
|
||||||
@@ -2437,7 +2377,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())
|
||||||
@@ -2564,7 +2504,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
|
||||||
@@ -276,19 +278,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