317 lines
14 KiB
C++
317 lines
14 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 "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 "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<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();
|
|
|
|
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);
|
|
|
|
// 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<BeltDragResolved> 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);
|
|
|
|
// 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;
|
|
|
|
// Temporary cache for the copy-settings gesture (REQ-BLD-COPY-CONFIG); held
|
|
// only while Shift is down and cleared on Shift release.
|
|
std::optional<BuildingConfig> 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).
|
|
std::vector<CopyConfigFlash> m_copyConfigFlashes;
|
|
static constexpr qint64 kCopyFlashDurationMs = 300;
|
|
|
|
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;
|
|
};
|