#pragma once #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include "Blueprint.h" #include "BuildingConfig.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 "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 "Rotation.h" #include "SelectionController.h" #include "Tick.h" #include "TickDriver.h" #include "TunnelCompletion.h" #include "VisualsConfig.h" #include "WorldCamera.h" #include "WorldCoordinates.h" struct Command; struct ParsedReplay; class ItemIconCache; class ReplayPlayer; class Simulation; class QPainter; class QSvgRenderer; struct QPointCompare { bool operator()(const QPoint& a, const QPoint& b) const { if (a.x() != b.x()) { return a.x() < b.x(); } return a.y() < b.y(); } }; // Tunnel entries/exits indexed by their single-cell tile (REQ-BLD-TUNNEL-MODE). using TunnelTileMap = std::map; class GameWorldView : public QOpenGLWidget, public CombinedEventHandler { 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(); 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 event) override; void handleEvent(std::shared_ptr event) override; void handleEvent(std::shared_ptr event) override; void handleEvent(std::shared_ptr event) override; void handleEvent(std::shared_ptr event) override; void handleEvent(std::shared_ptr event) override; void handleEvent(std::shared_ptr event) override; void handleEvent(std::shared_ptr event) override; void handleEvent(std::shared_ptr event) override; void handleEvent(std::shared_ptr event) override; void handleEvent(std::shared_ptr event) override; void handleEvent(std::shared_ptr event) override; void handleEvent(std::shared_ptr event) override; void handleEvent(std::shared_ptr event) override; // Enqueue a sim command onto the CommandManager (the single mutation path). void enqueueCommand(std::shared_ptr 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; // World-space drawing takes the frame's transform (see getCoordinates()) rather // than reaching for the scroll position and viewport size itself; the // screen-space draws below need neither. void drawTiles(QPainter& painter, const WorldCoordinates& coordinates); void drawPortItems(QPainter& painter, const WorldCoordinates& coordinates); void drawBuildings(QPainter& painter, const WorldCoordinates& coordinates); void drawSelectionHighlights(QPainter& painter, const WorldCoordinates& coordinates); void drawCopyConfigFeedback(QPainter& painter, const WorldCoordinates& coordinates); void drawStations(QPainter& painter, const WorldCoordinates& coordinates); void drawBeltItems(QPainter& painter, const WorldCoordinates& coordinates); // Draws a single item centered at widget-space `center`, spanning `halfPx` in // each direction (a half-tile). Uses the item's icon when one exists // (REQ-UI-ITEM-ICON), otherwise falls back to the colored square from // visuals.toml. Shared by drawBeltItems and drawPortItems. void drawWorldItem(QPainter& painter, const std::string& itemId, QPointF center, float halfPx); void drawDebris(QPainter& painter, const WorldCoordinates& coordinates); void drawShips(QPainter& painter, const WorldCoordinates& coordinates); void drawHpBar(QPainter& painter, const WorldCoordinates& coordinates, qreal left, qreal top, qreal width, float fraction, bool isEnemy); void drawDebugSensorRanges(QPainter& painter, const WorldCoordinates& coordinates); void drawDebugTargetLines(QPainter& painter, const WorldCoordinates& coordinates); void drawDebugOverlay(QPainter& painter); void drawBeams(QPainter& painter, const WorldCoordinates& coordinates); void drawOverlays(QPainter& painter, const WorldCoordinates& coordinates); void drawScreenSpace(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); // 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. WorldCoordinates getCoordinates() const; // Widget-space rectangle covering a building or construction site's footprint, // or nullopt if the id resolves to neither. Shared by the selection highlight // and the copy-settings feedback (REQ-BLD-COPY-CONFIG-FEEDBACK). std::optional footprintWidgetRect(const WorldCoordinates& coordinates, BuildingId id) 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; bool isValidPlacement(BuildingType type, QPoint anchor, Rotation rot) const; std::optional buildingAtTile(QPoint tile) const; std::optional siteAtTile(QPoint tile) const; // Ids of all buildings and construction sites whose footprint intersects // the tile box spanned by the two (unordered) corner tiles. std::vector buildingsInBox(QPoint cornerA, QPoint cornerB) const; void drawPortGlyph(QPainter& painter, const WorldCoordinates& coordinates, QPoint tile, Rotation direction, const QColor& color, bool centered); void drawBuildingGhost(QPainter& painter, const WorldCoordinates& coordinates, BuildingType type, QPoint anchorTile, Rotation rotation, bool valid, bool showPortTargetGlyphs); // Loads the per-building world icons (REQ-UI-WORLD-ICON) from // /../icons/buildings once at construction. Only the building // types with a world icon are loaded (production buildings, HQ, stations); // belts, splitters, and tunnels are deliberately excluded so their // orientation stays readable. The SVG's chip background is stripped; the // glyph is pre-rendered in both white and dark ink for auto-contrast. void loadBuildingIcons(const std::string& configDir); // Draws a building's world icon glyph centered in box, choosing the white or // dark pre-rendered variant by fill luminance so it stays legible. Returns // false if the type has no world icon (caller falls back to the text glyph). bool drawBuildingIcon(QPainter& painter, const WorldCoordinates& coordinates, BuildingType type, const QRectF& box, const QColor& fill) const; void placeBlueprintAtTile(QPoint center); std::optional entityPosition(entt::entity entity) const; // 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); void stepSpeed(int delta); void placeAtTile(QPoint tile); // Unified tunnel build mode (REQ-BLD-TUNNEL-MODE). Active while the builder type // is TunnelEntry; the ghost then resolves to an entry or exit by hovered position. bool inTunnelMode() const; // The building type the ghost currently represents: the position-resolved tunnel // type in tunnel mode, otherwise the plain builder type. BuildingType effectiveBuilderType() const; // Recomputes m_tunnelGhostType and m_tunnelPartnerTile from the current ghost // tile, rotation, and sub-tile cursor position. Only meaningful in tunnel mode. void updateTunnelGhost(); // Indexes every tunnel entry/exit — built or still a construction site — by its // single-cell tile. Shared by the placement preview and the selection highlight. TunnelTileMap collectTunnelTiles() const; // Wraps a tunnel tile index in the lookup functor the TunnelCompletion helpers // take. The returned functor references `tunnels`, which must outlive it. static TunnelLookup makeTunnelLookup(const TunnelTileMap& tunnels); // Draws the green connection highlight for every selected tunnel end that has a // matching end (REQ-BLD-TUNNEL-SELECT-HIGHLIGHT). void drawSelectedTunnelConnections(QPainter& painter, const WorldCoordinates& coordinates); // Belt drag placement (REQ-BLD-BELT-DRAG). // Per-path-tile decision, shared by ghost drawing and release-time placement. enum class BeltTileAction { PlaceNew, RotateInPlace, Invalid }; struct BeltDragResolved { BeltTileAction action; bool affordable; // meaningful only for PlaceNew std::optional rotateId; // set only for RotateInPlace }; // Recomputes m_beltDragPath from m_beltDragAnchor to cursorTile using the // current ghost orientation. void recomputeBeltDragPath(QPoint cursorTile); // Classifies each path tile against the current sim state, applying cumulative // affordability to the PlaceNew tiles. std::vector resolveBeltDragPath() const; // Enqueues placements and rotate-in-place commands for the resolved path. void applyBeltDragPath(); // Copy-settings gesture (REQ-BLD-COPY-CONFIG): Shift+right-click copies a // building's configuration into m_copiedConfig; Shift+left-click applies it to // another building of the same type via the existing configuration commands. void copyConfigFrom(BuildingId id); void pasteConfigTo(BuildingId id); void enterBuilderMode(BuildingType type); void exitBuilderMode(); void enterBlueprintMode(Blueprint blueprint); void exitBlueprintMode(); void toggleDeconstructMode(); void rotateGhost(bool clockwise); struct ActiveBeam { BeamFiredEvent event; QVector2D targetOffset; }; // 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); Simulation* m_sim; const GameConfig* m_config; const VisualsConfig* m_visuals; // World icon glyph renderers per building type (REQ-UI-WORLD-ICON), in a // white and a dark variant so drawBuildingIcon can auto-contrast against the // building's fill. Rendered as vector at the view scale each draw so they // stay crisp. Populated once by loadBuildingIcons(). struct BuildingIconRenderers { std::unique_ptr white; std::unique_ptr dark; }; std::map m_buildingIcons; // Per-item icon cache (REQ-UI-ITEM-ICON), shared window-wide and owned by // MainWindow. Shared draw path for belt and port items; pixmaps are cached // per target size. ItemIconCache* m_itemIcons; // 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 m_replayPlayer; 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 m_activeBeams; std::optional m_builderType; Rotation m_ghostRotation; QPoint m_ghostTile; bool m_ghostValid; // Unified tunnel build mode (REQ-BLD-TUNNEL-MODE): while m_builderType is // TunnelEntry the ghost resolves to an entry or an exit based on the hovered // position; m_tunnelPartnerTile is the existing tunnel it would complete (drawn // as the green connection preview), or unset when there is no completion match. BuildingType m_tunnelGhostType = BuildingType::TunnelEntry; std::optional m_tunnelPartnerTile; // Deferred belt drag placement (REQ-BLD-BELT-DRAG): while dragging, the // rectilinear anchor->cursor path is recomputed on each move and only applied // on release. Empty unless a belt drag is in progress. std::vector m_beltDragPath; QPoint m_beltDragAnchor; // 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). QVector2D m_cursorWorldPos; bool m_dragging; std::optional m_blueprintMode; QPoint m_blueprintGhostTile; // Temporary cache for the copy-settings gesture (REQ-BLD-COPY-CONFIG); held // only while Shift is down and cleared on Shift release. std::optional m_copiedConfig; // Brief outline flash shown on a building when settings are copied from it or // pasted onto it (REQ-BLD-COPY-CONFIG-FEEDBACK). remainingMs counts down in // wall-clock time so the flash plays at a fixed length regardless of game speed // (and while paused). struct CopyConfigFlash { BuildingId id; qint64 remainingMs; }; std::vector m_copyConfigFlashes; static constexpr qint64 kCopyFlashDurationMs = 300; bool m_deconstructMode; std::optional m_deconstructHoverBuildingId; 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; bool m_boxSelecting; QPoint m_boxStartTile; QPoint m_boxCurrentTile; // 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; };