extract BuildModeController, fixing a silent blueprint exit

This commit is contained in:
2026-08-05 20:41:57 +02:00
parent 202f583067
commit 26d7448492
7 changed files with 803 additions and 204 deletions

View File

@@ -199,10 +199,6 @@ GameWorldView::GameWorldView(Simulation* sim, const GameConfig* config,
, m_gameSpeedMultiplier(1.0)
, m_prevNonZeroSpeed(1.0)
, m_camera(config->world.scroll, config->world.regions)
, m_ghostRotation(Rotation::East)
, m_ghostValid(false)
, m_dragging(false)
, m_deconstructMode(false)
, m_debugDraw(false)
, m_rng(std::random_device{}())
, m_boxSelecting(false)
@@ -732,7 +728,7 @@ void GameWorldView::stepSpeed(int delta)
void GameWorldView::placeBlueprintAtTile(QPoint center)
{
const Blueprint& bp = *m_blueprintMode;
const Blueprint& bp = m_buildMode.getBlueprint();
for (const BlueprintBuilding& bb : bp.buildings)
{
@@ -819,16 +815,6 @@ void GameWorldView::placeBlueprintAtTile(QPoint center)
}
}
bool GameWorldView::inTunnelMode() const
{
return m_builderType.has_value() && *m_builderType == BuildingType::TunnelEntry;
}
BuildingType GameWorldView::effectiveBuilderType() const
{
return inTunnelMode() ? m_tunnelGhostType : *m_builderType;
}
TunnelTileMap GameWorldView::collectTunnelTiles() const
{
// Index every tunnel entry/exit — built or still a construction site — by its
@@ -864,13 +850,11 @@ TunnelLookup GameWorldView::makeTunnelLookup(const TunnelTileMap& tunnels)
void GameWorldView::updateTunnelGhost()
{
m_tunnelGhostType = BuildingType::TunnelEntry;
m_tunnelPartnerTile.reset();
// The connection preview and entry/exit switch only apply at a valid placement
// (REQ-BLD-TUNNEL-MODE); at an invalid position the ghost stays a plain entry.
if (!m_ghostValid)
if (!m_buildMode.isGhostValid())
{
m_buildMode.setTunnelGhost(BuildingType::TunnelEntry, std::nullopt);
return;
}
@@ -878,33 +862,34 @@ void GameWorldView::updateTunnelGhost()
const TunnelLookup lookup = makeTunnelLookup(tunnels);
const TunnelCompletion completion =
resolveTunnelCompletion(lookup, m_ghostTile, m_ghostRotation,
resolveTunnelCompletion(lookup, m_buildMode.getGhostTile(),
m_buildMode.getGhostRotation(),
m_config->world.tunnelMaxDistance_tiles, m_cursorWorldPos);
m_tunnelGhostType = completion.resolvedType;
m_tunnelPartnerTile = completion.partnerTile;
m_buildMode.setTunnelGhost(completion.resolvedType, completion.partnerTile);
}
void GameWorldView::placeAtTile(QPoint tile)
{
if (!m_builderType.has_value())
if (!m_buildMode.isBuilderMode())
{
return;
}
const BuildingType type = effectiveBuilderType();
const BuildingType type = m_buildMode.getEffectiveBuilderType();
const Rotation rotation = m_buildMode.getGhostRotation();
if (!isValidPlacement(type, tile, m_ghostRotation))
if (!isValidPlacement(type, tile, rotation))
{
return;
}
const std::optional<BuildingId> rotateTarget =
findRotateInPlaceTarget(m_sim->getFactoryState(), m_sim->getConfig(), type, tile, m_ghostRotation);
findRotateInPlaceTarget(m_sim->getFactoryState(), m_sim->getConfig(), type, tile, rotation);
if (rotateTarget.has_value())
{
std::shared_ptr<RotateInPlaceCommand> command =
std::make_shared<RotateInPlaceCommand>();
command->id = *rotateTarget;
command->newRotation = m_ghostRotation;
command->newRotation = rotation;
enqueueCommand(command);
return;
}
@@ -920,12 +905,12 @@ void GameWorldView::placeAtTile(QPoint tile)
{
if (!isTileOccupied(m_sim->getFactoryState(), tile) && canAfford(type))
{
enqueuePlaceBuilding(type, tile, m_ghostRotation);
enqueuePlaceBuilding(type, tile, rotation);
}
}
else
{
enqueuePlaceBuilding(type, tile, m_ghostRotation);
enqueuePlaceBuilding(type, tile, rotation);
}
}
@@ -983,26 +968,28 @@ void GameWorldView::recomputeBeltDragPath(QPoint cursorTile)
}
}
m_beltDragPath = computeBeltDragPath(m_beltDragAnchor, endTile, m_ghostRotation);
if (forcedEndRotation.has_value() && !m_beltDragPath.empty())
std::vector<BeltPathTile> path = computeBeltDragPath(
m_buildMode.getBeltDragAnchor(), endTile, m_buildMode.getGhostRotation());
if (forcedEndRotation.has_value() && !path.empty())
{
// The end tile points into the target, overriding its incoming-step
// orientation (REQ-BLD-BELT-DRAG "Snapping to a building").
m_beltDragPath.back().rotation = *forcedEndRotation;
path.back().rotation = *forcedEndRotation;
}
m_buildMode.setBeltDragPath(std::move(path));
}
std::vector<GameWorldView::BeltDragResolved> GameWorldView::resolveBeltDragPath() const
{
std::vector<BeltDragResolved> resolved;
resolved.reserve(m_beltDragPath.size());
resolved.reserve(m_buildMode.getBeltDragPath().size());
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_beltDragPath)
for (const BeltPathTile& entry : m_buildMode.getBeltDragPath())
{
BeltDragResolved item;
const std::optional<BuildingId> rotateTarget =
@@ -1040,7 +1027,7 @@ void GameWorldView::applyBeltDragPath()
for (std::size_t index = 0; index < resolved.size(); ++index)
{
const BeltDragResolved& item = resolved[index];
const BeltPathTile& entry = m_beltDragPath[index];
const BeltPathTile& entry = m_buildMode.getBeltDragPath()[index];
if (item.action == BeltTileAction::PlaceNew && item.affordable)
{
enqueuePlaceBuilding(BuildingType::Belt, entry.tile, entry.rotation);
@@ -1927,9 +1914,10 @@ void GameWorldView::drawOverlays(QPainter& painter, const WorldCoordinates& coor
drawSelectedTunnelConnections(painter, coordinates);
// Builder-mode ghost
if (m_builderType.has_value())
if (m_buildMode.isBuilderMode())
{
if (*m_builderType == BuildingType::Belt && m_dragging)
if (m_buildMode.getBuilderType() == BuildingType::Belt
&& m_buildMode.isDraggingBelt())
{
// Belt drag: a ghost per path tile (REQ-BLD-BELT-DRAG). Rotate-in-place
// and affordable new tiles use the belt colors; occupied/invalid tiles
@@ -1942,7 +1930,7 @@ void GameWorldView::drawOverlays(QPainter& painter, const WorldCoordinates& coor
{
continue;
}
const BeltPathTile& entry = m_beltDragPath[index];
const BeltPathTile& entry = m_buildMode.getBeltDragPath()[index];
drawBuildingGhost(painter, coordinates, BuildingType::Belt,
entry.tile, entry.rotation,
/*valid*/ item.action != BeltTileAction::Invalid,
@@ -1955,37 +1943,42 @@ void GameWorldView::drawOverlays(QPainter& painter, const WorldCoordinates& coor
// exit) and, when it would complete an existing tunnel, the matched end
// and the tiles between it and the ghost are tinted green
// (REQ-BLD-TUNNEL-MODE).
if (inTunnelMode() && m_ghostValid && m_tunnelPartnerTile.has_value())
const QPoint ghostTile = m_buildMode.getGhostTile();
const std::optional<QPoint>& partnerTile = m_buildMode.getTunnelPartnerTile();
if (m_buildMode.isTunnelMode() && m_buildMode.isGhostValid()
&& partnerTile.has_value())
{
const QColor green = m_visuals->overlays.tunnelPreview;
painter.fillRect(coordinates.tileRect(*m_tunnelPartnerTile), green);
painter.fillRect(coordinates.tileRect(*partnerTile), green);
// Partner and ghost tile are colinear along the tunnel run; tint the
// tiles strictly between them.
const QPoint delta = *m_tunnelPartnerTile - m_ghostTile;
const QPoint delta = *partnerTile - ghostTile;
const QPoint step((delta.x() > 0) - (delta.x() < 0),
(delta.y() > 0) - (delta.y() < 0));
for (QPoint t = m_ghostTile + step; t != *m_tunnelPartnerTile; t += step)
for (QPoint t = ghostTile + step; t != *partnerTile; t += step)
{
painter.fillRect(coordinates.tileRect(t), green);
}
}
drawBuildingGhost(painter, coordinates, effectiveBuilderType(),
m_ghostTile, m_ghostRotation, m_ghostValid,
drawBuildingGhost(painter, coordinates,
m_buildMode.getEffectiveBuilderType(),
ghostTile, m_buildMode.getGhostRotation(),
m_buildMode.isGhostValid(),
/*showPortTargetGlyphs*/ true);
}
}
// Blueprint placement ghost
if (m_blueprintMode.has_value())
if (m_buildMode.isBlueprintMode())
{
for (const BlueprintBuilding& bb : m_blueprintMode->buildings)
for (const BlueprintBuilding& bb : m_buildMode.getBlueprint().buildings)
{
// Locked building types are omitted from the blueprint (REQ-LOCK-BUILDING,
// REQ-LOCK-UI-BLUEPRINT), so they are not ghosted either.
if (!m_sim->isBuildingUnlocked(bb.type)) { continue; }
const QPoint anchor = m_blueprintGhostTile + bb.offset;
const QPoint anchor = m_buildMode.getBlueprintGhostTile() + bb.offset;
const bool valid = isValidPlacement(bb.type, anchor, bb.rotation);
drawBuildingGhost(painter, coordinates, bb.type, anchor, bb.rotation,
valid, /*showPortTargetGlyphs*/ false);
@@ -2006,7 +1999,7 @@ void GameWorldView::drawOverlays(QPainter& painter, const WorldCoordinates& coor
// Deconstruct tint: while dragging a deconstruct box, tint every covered
// building/site (REQ-BLD-DECONSTRUCT-BOX); otherwise tint the hovered one.
if (m_deconstructMode && m_boxSelecting)
if (m_buildMode.isDeconstructMode() && m_boxSelecting)
{
for (BuildingId id : buildingsInBox(m_boxStartTile, m_boxCurrentTile))
{
@@ -2026,9 +2019,11 @@ void GameWorldView::drawOverlays(QPainter& painter, const WorldCoordinates& coor
}
}
}
else if (m_deconstructMode && m_deconstructHoverBuildingId.has_value())
else if (m_buildMode.isDeconstructMode()
&& m_buildMode.getDeconstructHoverBuildingId().has_value())
{
const Building* b = findBuilding(m_sim->getFactoryState(), *m_deconstructHoverBuildingId);
const Building* b = findBuilding(m_sim->getFactoryState(),
*m_buildMode.getDeconstructHoverBuildingId());
if (b)
{
for (const QPoint& cell : b->bodyCells)
@@ -2151,7 +2146,7 @@ void GameWorldView::drawPauseBorder(QPainter& painter)
void GameWorldView::drawDeconstructBorder(QPainter& painter)
{
if (!m_deconstructMode) { return; }
if (!m_buildMode.isDeconstructMode()) { return; }
// Reuse the deconstruct overlay color (with its configured alpha) as the edge color.
drawVignetteBorder(painter, m_visuals->overlays.deconstructTint);
}
@@ -2322,22 +2317,16 @@ void GameWorldView::mousePressEvent(QMouseEvent* event)
{
if (event->button() == Qt::RightButton)
{
if (m_builderType.has_value())
if (m_buildMode.isBuilderMode() && m_buildMode.isDraggingBelt())
{
if (m_dragging)
{
// Cancel the in-progress belt drag without placing anything;
// stay in belt builder mode (REQ-BLD-BELT-DRAG).
m_dragging = false;
m_beltDragPath.clear();
}
else
{
exitBuilderMode();
}
// Cancel the in-progress belt drag without placing anything;
// stay in belt builder mode (REQ-BLD-BELT-DRAG).
m_buildMode.cancelBeltDrag();
}
else if (m_buildMode.getMode() != BuildMode::None)
{
m_buildMode.exitCurrentMode();
}
else if (m_blueprintMode.has_value()) { exitBlueprintMode(); }
else if (m_deconstructMode) { toggleDeconstructMode(); }
else if (event->modifiers() & Qt::ShiftModifier)
{
// Shift + right-click copies a building's settings, but only in the
@@ -2353,15 +2342,13 @@ void GameWorldView::mousePressEvent(QMouseEvent* event)
const QPoint tile = coordinates.widgetToTile(event->pos());
if (m_builderType.has_value())
if (m_buildMode.isBuilderMode())
{
const BuildingType type = *m_builderType;
if (type == BuildingType::Belt)
if (m_buildMode.getBuilderType() == BuildingType::Belt)
{
// Deferred placement: start the drag and show the path ghost; nothing
// is placed until release (REQ-BLD-BELT-DRAG).
m_dragging = true;
m_beltDragAnchor = tile;
m_buildMode.beginBeltDrag(tile);
m_cursorWorldPos = coordinates.widgetToWorld(event->pos());
recomputeBeltDragPath(tile);
}
@@ -2370,11 +2357,11 @@ void GameWorldView::mousePressEvent(QMouseEvent* event)
placeAtTile(tile);
}
}
else if (m_blueprintMode.has_value())
else if (m_buildMode.isBlueprintMode())
{
placeBlueprintAtTile(tile);
}
else if (m_deconstructMode)
else if (m_buildMode.isDeconstructMode())
{
// Start a deconstruct box drag; a plain click resolves as a 1x1 box on
// release (REQ-BLD-DECONSTRUCT-CLICK, REQ-BLD-DECONSTRUCT-BOX).
@@ -2483,32 +2470,34 @@ void GameWorldView::mouseMoveEvent(QMouseEvent* event)
const QPoint tile = coordinates.widgetToTile(event->pos());
m_cursorWorldPos = coordinates.widgetToWorld(event->pos());
if (m_builderType.has_value())
if (m_buildMode.isBuilderMode())
{
m_ghostTile = tile;
m_ghostValid = isValidPlacement(*m_builderType, tile, m_ghostRotation);
m_buildMode.setGhostTile(tile);
m_buildMode.setGhostValidity(
isValidPlacement(m_buildMode.getBuilderType(), tile,
m_buildMode.getGhostRotation()));
if (inTunnelMode())
if (m_buildMode.isTunnelMode())
{
// Resolve entry vs exit and the completion partner for the new hover
// position and sub-tile cursor (REQ-BLD-TUNNEL-MODE).
updateTunnelGhost();
}
if (m_dragging)
if (m_buildMode.isDraggingBelt())
{
// Belt drag: update the previewed path; placement happens on release
// (REQ-BLD-BELT-DRAG).
recomputeBeltDragPath(tile);
}
}
else if (m_blueprintMode.has_value())
else if (m_buildMode.isBlueprintMode())
{
m_blueprintGhostTile = tile;
m_buildMode.setBlueprintGhostTile(tile);
}
else if (m_deconstructMode)
else if (m_buildMode.isDeconstructMode())
{
m_deconstructHoverBuildingId = buildingAtTile(tile);
m_buildMode.setDeconstructHoverBuildingId(buildingAtTile(tile));
if (m_boxSelecting) { m_boxCurrentTile = tile; }
}
else if (m_boxSelecting)
@@ -2521,13 +2510,12 @@ void GameWorldView::mouseReleaseEvent(QMouseEvent* event)
{
if (event->button() != Qt::LeftButton) { return; }
if (m_dragging)
if (m_buildMode.isDraggingBelt())
{
// Apply the previewed belt path now that the button is released
// (REQ-BLD-BELT-DRAG).
applyBeltDragPath();
m_dragging = false;
m_beltDragPath.clear();
m_buildMode.cancelBeltDrag();
}
if (m_boxSelecting)
@@ -2537,7 +2525,7 @@ void GameWorldView::mouseReleaseEvent(QMouseEvent* event)
const std::vector<BuildingId> boxIds =
buildingsInBox(m_boxStartTile, m_boxCurrentTile);
if (m_deconstructMode)
if (m_buildMode.isDeconstructMode())
{
const FactoryState& factory = m_sim->getFactoryState();
@@ -2595,7 +2583,7 @@ void GameWorldView::mouseReleaseEvent(QMouseEvent* event)
}
}
m_deconstructHoverBuildingId = std::nullopt;
m_buildMode.setDeconstructHoverBuildingId(std::nullopt);
return;
}
@@ -2607,39 +2595,26 @@ void GameWorldView::mouseReleaseEvent(QMouseEvent* event)
// Methods (formerly slots)
// ---------------------------------------------------------------------------
void GameWorldView::toggleDeconstructMode()
{
if (m_deconstructMode)
{
m_deconstructMode = false;
m_deconstructHoverBuildingId = std::nullopt;
}
else
{
if (m_builderType.has_value()) { exitBuilderMode(); }
if (m_blueprintMode.has_value()) { exitBlueprintMode(); }
m_deconstructMode = true;
}
EventManager::getInstance()->sendEventImmediately(
std::make_shared<DeconstructModeChangedEvent>(m_deconstructMode));
}
void GameWorldView::rotateGhost(bool clockwise)
{
if (m_builderType.has_value())
if (m_buildMode.isBuilderMode())
{
m_ghostRotation = clockwise ? rotateClockwise(m_ghostRotation)
: rotateCounterClockwise(m_ghostRotation);
m_ghostValid = isValidPlacement(*m_builderType, m_ghostTile, m_ghostRotation);
m_buildMode.rotateGhost(clockwise);
m_buildMode.setGhostValidity(
isValidPlacement(m_buildMode.getBuilderType(), m_buildMode.getGhostTile(),
m_buildMode.getGhostRotation()));
// A new facing changes which tunnels the ghost could complete (REQ-BLD-TUNNEL-MODE).
if (inTunnelMode()) { updateTunnelGhost(); }
if (m_buildMode.isTunnelMode()) { updateTunnelGhost(); }
// Rotating during a belt drag re-picks the path's primary axis immediately,
// without waiting for the next mouse move (REQ-BLD-BELT-DRAG).
if (m_dragging) { recomputeBeltDragPath(m_ghostTile); }
if (m_buildMode.isDraggingBelt())
{
recomputeBeltDragPath(m_buildMode.getGhostTile());
}
}
else if (m_blueprintMode.has_value())
else if (m_buildMode.isBlueprintMode())
{
for (BlueprintBuilding& bb : m_blueprintMode->buildings)
for (BlueprintBuilding& bb : m_buildMode.getMutableBlueprint().buildings)
{
const BuildingDef* def = m_config->buildings.findBuildingDef(bb.type);
if (!def) { continue; }
@@ -2745,45 +2720,6 @@ void GameWorldView::pasteConfigTo(BuildingId id)
}
}
void GameWorldView::enterBuilderMode(BuildingType type)
{
m_builderType = type;
m_ghostRotation = Rotation::East;
m_ghostValid = false;
m_tunnelGhostType = BuildingType::TunnelEntry;
m_tunnelPartnerTile.reset();
m_deconstructMode = false;
m_blueprintMode.reset();
EventManager::getInstance()->sendEventImmediately(
std::make_shared<DeconstructModeChangedEvent>(false));
}
void GameWorldView::enterBlueprintMode(Blueprint blueprint)
{
if (m_builderType.has_value()) { exitBuilderMode(); }
m_deconstructMode = false;
EventManager::getInstance()->sendEventImmediately(
std::make_shared<DeconstructModeChangedEvent>(false));
m_blueprintGhostTile = m_ghostTile;
m_blueprintMode = std::move(blueprint);
}
void GameWorldView::exitBlueprintMode()
{
m_blueprintMode.reset();
EventManager::getInstance()->sendEventImmediately(
std::make_shared<BlueprintModeExitedEvent>());
}
void GameWorldView::exitBuilderMode()
{
m_builderType.reset();
m_beltDragPath.clear();
m_dragging = false;
EventManager::getInstance()->sendEventImmediately(
std::make_shared<BuilderModeExitedEvent>());
}
double GameWorldView::getGameSpeed() const
{
return m_gameSpeedMultiplier;
@@ -2808,16 +2744,11 @@ void GameWorldView::setGameSpeed(double multiplier)
void GameWorldView::resetForNewGame()
{
exitBuilderMode();
exitBlueprintMode();
// Leaves whichever mode was active, announcing that one exit rather than all
// three; a mode that was not active has nothing to announce.
m_buildMode.exitCurrentMode();
m_activeBeams.clear();
m_schematicChoiceShown = false;
m_ghostRotation = Rotation::East;
m_ghostValid = false;
m_deconstructMode = false;
m_deconstructHoverBuildingId = std::nullopt;
EventManager::getInstance()->sendEventImmediately(
std::make_shared<DeconstructModeChangedEvent>(false));
m_selection.clearAll();
m_copiedConfig = std::nullopt;
m_copyConfigFlashes.clear();
@@ -2884,27 +2815,27 @@ void GameWorldView::handleEvent(std::shared_ptr<const BeamFiredEvent> event)
void GameWorldView::handleEvent(std::shared_ptr<const BuildingTypeSelectedEvent> event)
{
enterBuilderMode(event->type);
m_buildMode.enterBuilderMode(event->type);
}
void GameWorldView::handleEvent(std::shared_ptr<const ExitBuilderModeRequestedEvent> /*event*/)
{
exitBuilderMode();
m_buildMode.exitBuilderMode();
}
void GameWorldView::handleEvent(std::shared_ptr<const DeconstructModeToggleRequestedEvent> /*event*/)
{
toggleDeconstructMode();
m_buildMode.toggleDeconstructMode();
}
void GameWorldView::handleEvent(std::shared_ptr<const BlueprintPlacementRequestedEvent> event)
{
enterBlueprintMode(event->blueprint);
m_buildMode.enterBlueprintMode(event->blueprint);
}
void GameWorldView::handleEvent(std::shared_ptr<const ExitBlueprintModeRequestedEvent> /*event*/)
{
exitBlueprintMode();
m_buildMode.exitBlueprintMode();
}
void GameWorldView::handleEvent(std::shared_ptr<const SpeedChangeRequestedEvent> event)
@@ -2944,9 +2875,10 @@ void GameWorldView::handleEvent(std::shared_ptr<const ModeCancelRequestedEvent>
{
// One key backs out of whichever mode is active, and enters deconstruct mode
// when none is (REQ-UI-HOTKEYS).
if (m_builderType.has_value()) { exitBuilderMode(); }
else if (m_blueprintMode.has_value()) { exitBlueprintMode(); }
else { toggleDeconstructMode(); }
// One key backs out of whichever mode is active, and enters deconstruct mode
// when none is (REQ-UI-HOTKEYS).
if (m_buildMode.getMode() == BuildMode::None) { m_buildMode.toggleDeconstructMode(); }
else { m_buildMode.exitCurrentMode(); }
}
void GameWorldView::handleEvent(std::shared_ptr<const DebugDrawToggleRequestedEvent> /*event*/)