Files
dota_factory/src/ui/GameWorldView.h

441 lines
21 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 "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<QPoint, TunnelTileInfo, QPointCompare>;
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;
// 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<QRectF> 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<BuildingId> buildingAtTile(QPoint tile) const;
std::optional<BuildingId> 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<BuildingId> 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
// <configDir>/../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<QVector2D> 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<BuildingId> 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<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);
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<QSvgRenderer> white;
std::unique_ptr<QSvgRenderer> dark;
};
std::map<BuildingType, BuildingIconRenderers> 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<ReplayPlayer> 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<ActiveBeam> m_activeBeams;
std::optional<BuildingType> 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<QPoint> 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<BeltPathTile> 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<Blueprint> 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<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).
struct CopyConfigFlash
{
BuildingId id;
qint64 remainingMs;
};
std::vector<CopyConfigFlash> m_copyConfigFlashes;
static constexpr qint64 kCopyFlashDurationMs = 300;
bool m_deconstructMode;
std::optional<BuildingId> 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;
};