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)