Coincidence required the ghost's anchor to land exactly on the target's anchor, so on a 2x2 assembler three of its four tiles read as an ordinary overlap and went red: you had to find the top-left tile before the copy gesture would fire. A single-building blueprint now finds its target by hit-testing the cursor instead -- any body tile, any facing -- and its ghost snaps onto the target so it shows what the click will act on. A constellation is unchanged: it is placed as a layout, so its ghosts stay where the blueprint puts them and still need coincidence plus a matching rotation. The old "single-building blueprints ignore target rotation" carve-out is gone, subsumed by hovering. blueprintHoldsOneBuilding becomes std::optional<QPoint> hoverTile, which carries both facts in one parameter: set means "single-building blueprint, cursor here". BlueprintGhostResolved gains ghostAnchor/ghostRotation so the snap comes out of the classifier both callers already share, rather than the renderer working it out separately. Note the shipyard, which I had cited as the motivating case, is not one: its mask needs A cells on asteroid and S cells in space, and the asteroid edge is vertical, so a rotated shipyard can never be placed at all. Rotation was never the real problem here; alignment was. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JcReq7hVk4KUPhTDKWAG7K
132 lines
6.8 KiB
C++
132 lines
6.8 KiB
C++
#pragma once
|
|
|
|
#include <optional>
|
|
#include <vector>
|
|
|
|
#include <QPoint>
|
|
|
|
#include "BeltDragPath.h"
|
|
#include "BuildingId.h"
|
|
#include "BuildingType.h"
|
|
#include "FactoryState.h"
|
|
#include "GameConfig.h"
|
|
#include "Rotation.h"
|
|
|
|
// Where a building may be placed, and what is already sitting on those tiles.
|
|
// Free functions over the factory state and the config — they read no other
|
|
// system state, so they do not belong to BuildingSystem.
|
|
|
|
// True if every body cell lies inside the world: 0 <= y < world.height_tiles and
|
|
// x >= the current asteroid left edge (REQ-BLD-PLACE-VALID). Terrain type is not
|
|
// checked — see isPlacementValid for the full rule.
|
|
bool bodyCellsWithinWorldBounds(const FactoryState& state, const GameConfig& config,
|
|
const std::vector<QPoint>& bodyCells, QPoint anchor);
|
|
|
|
// True if the placement satisfies REQ-BLD-PLACE-VALID terrain and world-bounds
|
|
// rules: every ship-dock (S) cell sits in space (x >= 0), every other body (A)
|
|
// cell sits on the asteroid (x < 0 and x >= the left edge), and every cell has
|
|
// 0 <= y < world.height_tiles. There is no right-side bound — space extends
|
|
// rightward. Tile occupancy is NOT checked here.
|
|
bool isPlacementValid(const FactoryState& state, const GameConfig& config,
|
|
BuildingType type, QPoint anchor, Rotation rotation);
|
|
|
|
// An existing building or site whose footprint a ghost exactly covers.
|
|
struct CoincidingBuilding
|
|
{
|
|
BuildingId id;
|
|
Rotation rotation;
|
|
};
|
|
|
|
// The building or site whose footprint a ghost of the given type/anchor/rotation
|
|
// exactly covers: same type, same body cells, a single owner. Operational buildings
|
|
// and construction sites alike. Rotation is not part of the test -- the target's own
|
|
// facing is reported so each caller can apply its own rule to it.
|
|
std::optional<CoincidingBuilding> findCoincidingSameTypeBuilding(const FactoryState& state,
|
|
const GameConfig& config,
|
|
BuildingType type,
|
|
QPoint anchor,
|
|
Rotation rot);
|
|
|
|
// The building or site that a ghost of the given type/anchor/rotation would
|
|
// rotate in place rather than replace: same type, same body cells, one owner
|
|
// (REQ-BLD-ROTATE-IN-PLACE). Tunnels never qualify. Builder mode only; blueprint
|
|
// placement mode never rotates anything -- see resolveBlueprintGhost.
|
|
std::optional<BuildingId> findRotateInPlaceTarget(const FactoryState& state,
|
|
const GameConfig& config,
|
|
BuildingType type, QPoint anchor,
|
|
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 one ghost of a blueprint would do at its resolved tile.
|
|
enum class BlueprintGhostAction
|
|
{
|
|
PlaceNew, // free, valid cells: a new construction site, charged for
|
|
CompatibleOverlap, // the same building is already there: left untouched, free
|
|
// (REQ-UI-BLUEPRINT-OVERLAP)
|
|
Transfer, // hand this blueprint's settings to the building already there,
|
|
// free (REQ-UI-BLUEPRINT-TRANSFER)
|
|
Invalid // terrain, bounds, or an overlap that is neither of the above
|
|
};
|
|
|
|
struct BlueprintGhostResolved
|
|
{
|
|
BlueprintGhostAction action;
|
|
std::optional<BuildingId> targetId; // set for CompatibleOverlap and Transfer
|
|
// Where the ghost belongs on screen. The queried anchor and rotation, except at a
|
|
// hovered transfer target, where the ghost snaps onto the target so it shows what the
|
|
// click will act on (REQ-UI-BLUEPRINT-TRANSFER).
|
|
QPoint ghostAnchor;
|
|
Rotation ghostRotation;
|
|
};
|
|
|
|
// Classifies one ghost of a blueprint against the current factory state
|
|
// (REQ-UI-BLUEPRINT-OVERLAP, REQ-UI-BLUEPRINT-TRANSFER).
|
|
//
|
|
// `hoverTile` is set only for a blueprint holding exactly one building, and is then the
|
|
// tile under the cursor. That gesture is a copying tool rather than a layout, so it finds
|
|
// its transfer target by hit-testing the cursor instead of by footprint coincidence --
|
|
// without which a Shipyard could never be targeted at a different facing, its 4x2
|
|
// footprint covering entirely different tiles once rotated. Pass nullopt for a
|
|
// constellation, whose ghosts are judged purely by where the blueprint puts them. The
|
|
// size is read from the blueprint as stored, before locked types are dropped, so the
|
|
// gesture does not change behavior as the player unlocks things.
|
|
//
|
|
// Shared by the ghost coloring and the click path so a preview cannot disagree with what
|
|
// the click then does -- the same reason resolveBeltDragPath is shared.
|
|
BlueprintGhostResolved resolveBlueprintGhost(const FactoryState& state,
|
|
const GameConfig& config,
|
|
BuildingType type, QPoint anchor,
|
|
Rotation rotation,
|
|
std::optional<QPoint> hoverTile);
|
|
|
|
// 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);
|