#pragma once #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #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 "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 ItemIconCache; class ReplayPlayer; class Simulation; class QPainter; 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; // 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; // 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; 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 buildingAtTile(QPoint tile) const; std::optional siteAtTile(QPoint tile) const; void placeBlueprintAtTile(QPoint center); // 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); // 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 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); 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 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 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 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; 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; };