The panel was anchored to the right edge of the view, which is nowhere near whatever the player just clicked. It now stands beside the selection: right of it where it fits, otherwise left, otherwise the roomier side pushed inside the view -- the one case where it covers part of what it describes. Nothing told the panel where the selection was. The selection events carry ids only, and the mode that separates a fresh selection from an expanded one is consumed inside SelectionController before they are built, so the view now publishes the selection's screen bounds itself, immediately before selecting and only when the selection is starting. Freezing that rectangle is what holds the panel still: it does not chase a scrolling view, a ship flying off, or a selection being added to. Only the panel's own size still moves it, and even then it keeps its side and the edge facing the selection. The rectangles come from what the renderer was already computing for the selection outlines, now shared rather than duplicated. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JcReq7hVk4KUPhTDKWAG7K
336 lines
16 KiB
C++
336 lines
16 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 "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,
|
|
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;
|
|
|
|
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 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;
|
|
|
|
// 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<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);
|
|
// 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);
|
|
|
|
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;
|
|
|
|
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;
|
|
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;
|
|
};
|