let the hover follow a view that scrolls under a still cursor

Only the selection box was refreshed while the camera panned, so the ghost,
its validity, the resolved tunnel end and the deconstruct hover all kept the
tile of the last mouse move. Give the whole hover update one entry point and
run it from the pan step as well, for a cursor that is over the world.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Ne3mejABZoLWKLh8fgpM3x
This commit is contained in:
2026-08-17 14:54:34 +02:00
parent fd0c246bc0
commit 9490a96e12
3 changed files with 76 additions and 57 deletions

View File

@@ -296,10 +296,18 @@ void GameWorldView::onFrame()
m_camera.advance(m_panDirection, elapsed, getScrollBounds());
// While the view scrolls, the world position under a stationary cursor
// changes, so refresh the box even though no mouse move fires.
if (m_boxSelecting && viewMoved)
// changes, so the hover state is refreshed even though no mouse move fires.
if (viewMoved)
{
updateBoxDrag(mapFromGlobal(QCursor::pos()));
const QPoint cursorWidgetPos = mapFromGlobal(QCursor::pos());
// A running box drag holds the button and follows the cursor wherever it
// is, the floating panels included; hover state only tracks a cursor over
// the world, so scrolling past a cursor resting on a panel leaves the
// ghost where it was (REQ-UI-CONTROLS-PANEL).
if (m_boxSelecting || underMouse())
{
updateHoverAt(cursorWidgetPos);
}
}
}
@@ -1327,6 +1335,63 @@ void GameWorldView::selectInBox(bool additive)
if (!additive) { m_selection.clearAll(); }
}
void GameWorldView::updateHoverAt(QPoint cursorWidgetPos)
{
const WorldCoordinates coordinates = getCoordinates();
const QPoint tile = coordinates.widgetToTile(cursorWidgetPos);
m_cursorWorldPos = coordinates.widgetToWorld(cursorWidgetPos);
if (m_buildMode.isBuilderMode())
{
m_buildMode.setGhostTile(tile);
m_buildMode.setGhostValidity(
canPlaceBuildingHere(m_buildMode.getBuilderType(), tile,
m_buildMode.getGhostRotation()));
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_buildMode.isDraggingBelt())
{
// Belt drag: update the previewed path; placement happens on release
// (REQ-BLD-BELT-DRAG).
recomputeBeltDragPath(tile);
}
}
else if (m_buildMode.isBlueprintMode())
{
m_buildMode.setBlueprintGhostTile(tile);
// Resolved here, once, through the same classifier the click and the ghost's
// colour use, and stored on the mode: the controls panel says "Apply settings"
// exactly when clicking would transfer (REQ-UI-BLUEPRINT-TRANSFER). Only a
// single-building blueprint hit-tests the cursor, so only it can be a hovered
// transfer target.
const std::vector<BlueprintBuilding>& buildings =
m_buildMode.getBlueprint().buildings;
bool transfer = false;
if (buildings.size() == 1)
{
transfer = resolveBlueprintGhostHere(buildings.front(), tile).action
== BlueprintGhostAction::Transfer;
}
m_buildMode.setHoveredGhostTransfer(transfer);
}
else if (m_buildMode.isDeconstructMode())
{
m_buildMode.setDeconstructHoverBuildingId(buildingAtTile(tile));
if (m_boxSelecting) { updateBoxDrag(cursorWidgetPos); }
}
else if (m_boxSelecting)
{
updateBoxDrag(cursorWidgetPos);
}
}
void GameWorldView::updateBoxDrag(QPoint cursorWidgetPos)
{
const WorldCoordinates coordinates = getCoordinates();
@@ -1387,59 +1452,7 @@ void GameWorldView::publishSelectionAnchor(SelectionMode mode,
void GameWorldView::mouseMoveEvent(QMouseEvent* event)
{
const WorldCoordinates coordinates = getCoordinates();
const QPoint tile = coordinates.widgetToTile(event->pos());
m_cursorWorldPos = coordinates.widgetToWorld(event->pos());
if (m_buildMode.isBuilderMode())
{
m_buildMode.setGhostTile(tile);
m_buildMode.setGhostValidity(
canPlaceBuildingHere(m_buildMode.getBuilderType(), tile,
m_buildMode.getGhostRotation()));
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_buildMode.isDraggingBelt())
{
// Belt drag: update the previewed path; placement happens on release
// (REQ-BLD-BELT-DRAG).
recomputeBeltDragPath(tile);
}
}
else if (m_buildMode.isBlueprintMode())
{
m_buildMode.setBlueprintGhostTile(tile);
// Resolved here, once, through the same classifier the click and the ghost's
// colour use, and stored on the mode: the controls panel says "Apply settings"
// exactly when clicking would transfer (REQ-UI-BLUEPRINT-TRANSFER). Only a
// single-building blueprint hit-tests the cursor, so only it can be a hovered
// transfer target.
const std::vector<BlueprintBuilding>& buildings =
m_buildMode.getBlueprint().buildings;
bool transfer = false;
if (buildings.size() == 1)
{
transfer = resolveBlueprintGhostHere(buildings.front(), tile).action
== BlueprintGhostAction::Transfer;
}
m_buildMode.setHoveredGhostTransfer(transfer);
}
else if (m_buildMode.isDeconstructMode())
{
m_buildMode.setDeconstructHoverBuildingId(buildingAtTile(tile));
if (m_boxSelecting) { updateBoxDrag(event->pos()); }
}
else if (m_boxSelecting)
{
updateBoxDrag(event->pos());
}
updateHoverAt(event->pos());
}
void GameWorldView::mouseReleaseEvent(QMouseEvent* event)

View File

@@ -226,6 +226,12 @@ private:
// which is the only case that goes on to start a box drag.
bool selectAtPoint(QPoint tile, QVector2D worldPos, bool additive);
void selectInBox(bool additive);
// Re-resolves everything that follows from where the cursor points into the world:
// the ghost tile and its validity, the tunnel ends, a running belt or box drag, the
// deconstruct hover. Called for every mouse move, and once per frame while the view
// scrolls under a cursor that has not moved, since that changes the world position
// the cursor points at just as moving the mouse does (REQ-BLD-GHOST).
void updateHoverAt(QPoint cursorWidgetPos);
// Moves the running box drag's far corner to the world position under
// `cursorWidgetPos` and, once the cursor sits far enough from where the anchor is
// drawn, promotes the gesture from a click to a drag. Every corner update goes