Files
dota_factory/src/ui/GameWorldView.h
Malte Langkabel abe8c8f448 stand the expansion button on the ground it buys
The button leaves the header bar for the world, centered on the columns
the next purchase unlocks and panning with them until it leaves the view.
Those columns now take a lighter tint than the locked ground behind them,
so the boundary between the two says how far one purchase reaches.

Nothing in the code reported a scroll: the camera knew it had moved and
told only the hover. A ViewScrolledEvent says it now, which is what a
widget keeping a place in the world rather than on the screen needs. The
panels want none of it -- they are placed against the screen and stay put
as the world moves under them -- so the button is placed after their pass
and never joins the rectangles they step around.

Its face is two lines, "Expand" over the cost. That makes the cost a
display of its own, so it carries the building-block tooltip the way every
other item value does, on hover alone since the click buys the expansion.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Ne3mejABZoLWKLh8fgpM3x
2026-08-18 22:55:06 +02:00

392 lines
20 KiB
C++

#pragma once
#include <map>
#include <memory>
#include <optional>
#include <random>
#include <set>
#include <string>
#include <utility>
#include <vector>
#include <QColor>
#include <QElapsedTimer>
#include <QOpenGLWidget>
#include <QPoint>
#include <QPointF>
#include <QRectF>
#include <QTimer>
#include <QVector2D>
#include "Blueprint.h"
#include "BuildModeController.h"
#include "BlueprintModeExitedEvent.h"
#include "BlueprintPlacementRequestedEvent.h"
#include "BuilderModeExitedEvent.h"
#include "BuildingType.h"
#include "BuildingTypeSelectedEvent.h"
#include "BuildingId.h"
#include "DeconstructModeChangedEvent.h"
#include "DeconstructModeToggleRequestedEvent.h"
#include "EventHandler.h"
#include "ExitBlueprintModeRequestedEvent.h"
#include "ExitBuilderModeRequestedEvent.h"
#include "DebugDrawToggleRequestedEvent.h"
#include "GhostRotationRequestedEvent.h"
#include "InputMapper.h"
#include "ModeCancelRequestedEvent.h"
#include "PanDirectionChangedEvent.h"
#include "PauseToggleRequestedEvent.h"
#include "SelectionClearRequestedEvent.h"
#include "SpeedStepRequestedEvent.h"
#include "DebugDrawToggledEvent.h"
#include "ArtifactCountChangedEvent.h"
#include "BeamFiredEvent.h"
#include "CommandRequestedEvent.h"
#include "SchematicChoiceOption.h"
#include "WinEvent.h"
#include "SpeedChangeRequestedEvent.h"
#include "entt/entity/entity.hpp"
#include "BeltDragPath.h"
#include "CommandManager.h"
#include "EntitySelectionChangedEvent.h"
#include "GameConfig.h"
#include "PlacementRules.h"
#include "Rotation.h"
#include "SelectionController.h"
#include "Tick.h"
#include "TickDriver.h"
#include "TunnelCompletion.h"
#include "VisualsConfig.h"
#include "WorldCamera.h"
#include "WorldCoordinates.h"
#include "WorldRenderer.h"
struct Command;
struct ParsedReplay;
class BlueprintLibrary;
class ItemIconCache;
class ReplayPlayer;
class Simulation;
class QPainter;
class GameWorldView : public QOpenGLWidget,
public CombinedEventHandler<BeamFiredEvent,
BuildingTypeSelectedEvent,
ExitBuilderModeRequestedEvent,
DeconstructModeToggleRequestedEvent,
BlueprintPlacementRequestedEvent,
ExitBlueprintModeRequestedEvent,
SpeedChangeRequestedEvent,
PanDirectionChangedEvent,
PauseToggleRequestedEvent,
SpeedStepRequestedEvent,
GhostRotationRequestedEvent,
ModeCancelRequestedEvent,
SelectionClearRequestedEvent,
DebugDrawToggleRequestedEvent,
CommandRequestedEvent>
{
Q_OBJECT
public:
// itemIcons is the window-wide per-item icon cache (REQ-UI-ITEM-ICON); not
// owned, must outlive this widget.
GameWorldView(Simulation* sim, const GameConfig* config,
const VisualsConfig* visuals, const std::string& configDir,
ItemIconCache* itemIcons, const ParsedReplay* replay,
QWidget* parent = nullptr);
~GameWorldView() override;
double getGameSpeed() const;
bool isDebugDrawEnabled() const;
void resetFrameTimer();
void setGameSpeed(double multiplier);
void resetForNewGame();
// The blueprint library is constructed after this widget, so it arrives by setter.
// Not owned; supplies the two facts about blueprints that the control context needs
// (REQ-UI-CONTROLS-CONTENT).
void setBlueprintLibrary(const BlueprintLibrary* library);
// The player's current situation, as the one snapshot every reader of the control
// table works from: this widget's own mouse dispatch, the input mapper, and the
// controls panel (REQ-UI-CONTROLS-CONTENT). Built here because this widget owns the
// build mode and the selection.
ControlContext getControlContext() const;
// Name of the blueprint being placed, for the controls panel's heading. Empty while
// no blueprint is active and for the unnamed temporary one (REQ-UI-BLUEPRINT-TEMP).
QString getActiveBlueprintName() const;
// The world <-> widget transform for the current viewport size and scroll
// position. Cheap to build and deliberately not cached: it is a snapshot that
// a resize or a scroll invalidates, so every user takes a fresh one. Public because
// a widget standing in the world needs it to find its place (REQ-UI-EXPAND-BUTTON);
// a ViewScrolledEvent says when it has gone stale.
WorldCoordinates getCoordinates() const;
protected:
void initializeGL() override;
void paintGL() override;
// Only forwards to the input mapper; every key this widget acts on reaches it
// as a published action instead (REQ-UI-HOTKEYS).
void keyPressEvent(QKeyEvent* event) override;
void keyReleaseEvent(QKeyEvent* event) override;
// Key-up never arrives for a key that was still held when focus moved away —
// to a modal dialog, another widget, or another window. Any held action would
// otherwise stay held forever, so focus loss drops all of them.
void focusOutEvent(QFocusEvent* event) override;
void mousePressEvent(QMouseEvent* event) override;
void mouseMoveEvent(QMouseEvent* event) override;
void mouseReleaseEvent(QMouseEvent* event) override;
private slots:
void onFrame();
private:
void handleEvent(std::shared_ptr<const BeamFiredEvent> event) override;
void handleEvent(std::shared_ptr<const BuildingTypeSelectedEvent> event) override;
void handleEvent(std::shared_ptr<const ExitBuilderModeRequestedEvent> event) override;
void handleEvent(std::shared_ptr<const DeconstructModeToggleRequestedEvent> event) override;
void handleEvent(std::shared_ptr<const BlueprintPlacementRequestedEvent> event) override;
void handleEvent(std::shared_ptr<const ExitBlueprintModeRequestedEvent> event) override;
void handleEvent(std::shared_ptr<const SpeedChangeRequestedEvent> event) override;
void handleEvent(std::shared_ptr<const PanDirectionChangedEvent> event) override;
void handleEvent(std::shared_ptr<const PauseToggleRequestedEvent> event) override;
void handleEvent(std::shared_ptr<const SpeedStepRequestedEvent> event) override;
void handleEvent(std::shared_ptr<const GhostRotationRequestedEvent> event) override;
void handleEvent(std::shared_ptr<const ModeCancelRequestedEvent> event) override;
void handleEvent(std::shared_ptr<const SelectionClearRequestedEvent> event) override;
void handleEvent(std::shared_ptr<const DebugDrawToggleRequestedEvent> event) override;
void handleEvent(std::shared_ptr<const CommandRequestedEvent> event) override;
// Enqueue a sim command onto the CommandManager (the single mutation path).
void enqueueCommand(std::shared_ptr<const Command> command);
// Enqueue a plain (unconfigured) building placement.
void enqueuePlaceBuilding(BuildingType type, QPoint anchor, Rotation rotation);
// True if the player can currently afford to place one building of `type`.
// Used to pre-validate placements whose UI follow-up depends on success.
bool canAfford(BuildingType type) const;
// Screen-anchored chrome, drawn after the world (see WorldRenderer): these
// need no world transform, which is exactly why they stayed here.
void drawScreenSpace(QPainter& painter);
// Threat and production readout shown while debug draw is on (F3). Positioned
// in pixels at the top-left corner, so it belongs with the chrome rather than
// with the world.
void drawDebugOverlay(QPainter& painter);
// Vignette-style border shown while the game is paused (speed 0x, REQ-UI-PAUSE-BORDER)
// to make the paused state hard to miss: a black frame whose alpha fades from 50% at
// the viewport edges to 0% toward the center.
void drawPauseBorder(QPainter& painter);
// Vignette-style border shown while deconstruct mode is active (REQ-UI-DECONSTRUCT-BORDER),
// tinted with the deconstruct overlay color (including its configured alpha) from
// visuals.toml instead of black.
void drawDeconstructBorder(QPainter& painter);
// Shared drawing for the pause/deconstruct vignettes: a mitred picture-frame border
// whose alpha fades from `edgeColor` (with its own alpha) at the viewport edge to
// fully transparent toward the center.
void drawVignetteBorder(QPainter& painter, const QColor& edgeColor);
void drawReplayOverlay(QPainter& painter);
// Gathers the interaction state the renderer needs for this frame.
WorldRenderFrame makeRenderFrame() const;
float getAsteroidLeftEdge() const;
float getEnemyStationRightEdge() const;
// The camera's current pan limits, read fresh from the simulation each frame:
// both edges move with asteroid expansion and with pushes (REQ-GW-SCROLL-LIMIT).
ScrollBounds getScrollBounds() 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> siteAtTile(QPoint tile) const;
void placeBlueprintAtTile(QPoint center);
// resolveBlueprintGhost (PlacementRules) for one building of the blueprint currently
// in placement mode, anchored at the cursor tile `center`.
BlueprintGhostResolved resolveBlueprintGhostHere(const BlueprintBuilding& building,
QPoint center) const;
// The blueprint's stored recipe or schematic id, or empty when it stores none or the
// stored one is currently locked (REQ-LOCK-UI-BLUEPRINT).
std::string unlockedRecipeId(const BlueprintBuilding& building) const;
// Applies a single-building blueprint's settings to the building it is hovering,
// instead of placing anything (REQ-UI-BLUEPRINT-TRANSFER).
void transferConfigTo(BuildingId id, const BlueprintBuilding& source);
// Drops despawned or fully-collected debris from the selection
// (REQ-UI-DEBRIS-CLICK-SELECT). Called each frame from onFrame().
void pruneDespawnedDebris();
// Drops despawned or dead actors from the selection (REQ-UI-ENTITY-CLICK-SELECT).
// Called each frame from onFrame().
void pruneDespawnedActors();
// Resolves a click into the selection it should produce, applying the category
// precedence of REQ-UI-SELECTION-CATEGORIES; the controller owns what that then
// does to the existing selection. Returns false when the click hit nothing,
// 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);
// A build mode and a selection are mutually exclusive, so entering any mode drops
// the selection (REQ-UI-SELECTION-EXCLUSIVE). Called from each of the three events
// that enter a mode -- the only ways in, whichever button or key the player used.
// Clearing an empty selection publishes nothing, so the exiting half of the
// deconstruct toggle costs nothing.
void clearSelectionForBuildMode();
// Whether the cursor points at the game world at all: it does while it is over
// this widget, and while a belt or box drag holds the button, which goes on
// following the cursor onto the floating panels and past the window edge.
bool isHoverLive() const;
// Re-derives the hover from wherever the cursor is now, or drops it when the
// cursor points at nothing (REQ-BLD-GHOST). The entry point for everything a
// mouse move does not cover: a scrolling view, a cursor crossing onto a panel or
// out of the window, and a mode just entered under a cursor that has not moved.
void refreshHover();
// 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
// through here, including the ones a scrolling view causes under a cursor that
// has not moved (REQ-UI-MULTI-SELECT).
void updateBoxDrag(QPoint cursorWidgetPos);
// The box the drag currently spans, in world coordinates and normalized: the
// rectangle between its two corners once it reads as a drag, and the whole tile
// the button went down on before that (REQ-UI-MULTI-SELECT). Both what is drawn
// and what is selected come from here, so they can never disagree.
QRectF getBoxWorldRect() const;
// Publishes where on the screen the selection about to be made sits, so the
// selection panel can be placed beside it (REQ-UI-SELECTION-PANEL). Called with
// what is about to be selected, immediately before selecting it, and publishes
// nothing unless that selection is starting rather than growing.
void publishSelectionAnchor(
SelectionMode mode,
const std::vector<BuildingId>& buildings,
const std::vector<entt::entity>& actors,
const std::vector<entt::entity>& debris);
void stepSpeed(int delta);
void placeAtTile(QPoint tile);
// Re-resolves the tunnel ghost type and completion partner from the current
// ghost tile, rotation, and sub-tile cursor position, storing both on the build
// mode controller. Only meaningful in tunnel mode (REQ-BLD-TUNNEL-MODE).
void updateTunnelGhost();
// Belt drag placement (REQ-BLD-BELT-DRAG).
// Recomputes the drag path from its anchor to cursorTile using the current ghost
// orientation, and stores it on the build mode controller.
void recomputeBeltDragPath(QPoint cursorTile);
// resolveBeltDragPath (PlacementRules) against this view's simulation.
std::vector<BeltDragResolved> resolveBeltDragPath() const;
// Enqueues placements and rotate-in-place commands for the resolved path.
void applyBeltDragPath();
// Turns the ghost and refreshes everything that depends on its facing: placement
// validity, the tunnel completion match, and an in-progress belt drag's path.
// The mode transitions themselves live on m_buildMode.
void rotateGhost(bool clockwise);
// Beam lifetime in game ticks so beams freeze with the simulation when
// paused or slowed, instead of fading on wall-clock time (REQ-SHP-FIRING-BEAM).
static constexpr Tick kBeamLifetimeTicks = secondsToTicks(0.3);
// How far the cursor must travel from the press position, in widget pixels
// (Manhattan distance), before a box drag shows its rectangle
// (REQ-UI-MULTI-SELECT).
static constexpr int kBoxDragThresholdPixels = 2;
Simulation* m_sim;
const GameConfig* m_config;
const VisualsConfig* m_visuals;
// Funnels all player input into the single Simulation::apply chokepoint.
CommandManager m_commandManager;
// A Reset command was enqueued; reset the view after the next drain applies it.
bool m_viewResetPending = false;
// Non-null => view-only playback: ticks are driven by the recorded stream and
// live input is ignored (the CommandManager is in replay mode).
std::unique_ptr<ReplayPlayer> m_replayPlayer;
// Draws the world; this widget supplies the interaction state each frame and
// keeps only the screen-anchored chrome for itself.
std::unique_ptr<WorldRenderer> m_renderer;
TickDriver m_tickDriver;
QElapsedTimer m_frameTimer;
std::mt19937 m_rng;
double m_gameSpeedMultiplier;
double m_prevNonZeroSpeed;
// Horizontal view position (REQ-UI-SCROLL). Owns the scroll position itself;
// this widget only supplies the pan intent and the simulation-derived bounds.
WorldCamera m_camera;
QTimer* m_renderTimer;
std::vector<ActiveBeam> m_activeBeams;
// The active build mode (builder / blueprint / deconstruct, or none) and the
// ghost, tunnel and belt-drag state belonging to it. Owns the transitions
// between them; this widget supplies the parts that need the simulation.
BuildModeController m_buildMode;
// Last known cursor position in world (tile) units; used to pick the belt-drag
// end tile closest to the cursor when snapping to a building (REQ-BLD-BELT-DRAG)
// and to resolve the tunnel ghost sub-tile (REQ-BLD-TUNNEL-MODE).
QVector2D m_cursorWorldPos;
// Whether the hover state currently stands for a cursor pointing at the world,
// so that losing it is noticed once rather than every frame. Kept here rather
// than asked of Qt per reader: it has to agree with what was last written to the
// build mode controller, not with where the cursor happens to be mid-frame.
bool m_hoverLive = false;
bool m_debugDraw;
// Owns the selection across all three categories and the rules for moving
// between them (REQ-UI-SELECTION-CATEGORIES), including publishing the change
// events. This widget only resolves what was hit.
SelectionController m_selection;
// Not owned; set after construction, so null until MainWindow has built it.
const BlueprintLibrary* m_blueprintLibrary = nullptr;
bool m_boxSelecting;
// The drag's two corners in world coordinates, unsnapped: where the button went
// down and where the cursor is now (REQ-UI-MULTI-SELECT). World rather than
// widget coordinates so the anchor keeps the spot in the world it was placed on
// when the view scrolls under a held button.
QVector2D m_boxStartWorld;
QVector2D m_boxCurrentWorld;
// Whether the cursor has moved far enough from the anchor for this to read as a
// drag. Until it has, the rectangle is not drawn and the box resolves as the
// whole anchor tile (REQ-UI-MULTI-SELECT). Sticky for the rest of the drag, so
// coming back to the press position does not hide the rectangle again.
bool m_boxDragMoved;
// Interprets this widget's key events into semantic actions and publishes them
// (REQ-UI-HOTKEYS). Owned here for now because this is the widget that holds
// focus; everything it produces travels by event, so it can move to a
// window-wide owner later without touching its consumers.
InputMapper m_inputMapper;
// Latest pan direction published by the input mapper (REQ-UI-SCROLL), fed to
// the camera each frame. Cached from the event payload rather than re-read:
// input has no other source of truth (see PanDirectionChangedEvent).
PanDirection m_panDirection = PanDirection::None;
bool m_gameOverShown;
bool m_winShown;
bool m_schematicChoiceShown;
Tick m_lastTick = Tick(-1);
int m_lastBlocks = -1;
int m_lastExpansionCost = -1;
int m_lastBossCounter = -1;
Tick m_lastBossCountdown = Tick(-1);
int m_lastArtifactCount = -1;
int m_lastUnlockedBuildingCount = -1;
};