Lift unlocking into first-class unlock groups defined in unlocks.toml, so one defence-station drop can grant a bundle of ships/modules/buildings/ recipes at once (e.g. the salvager module + salvage bay), and buildings can now be locked at game start and unlocked via a drop. Config: - New UnlocksConfig + ConfigLoader::loadUnlocks + validateUnlocks (unknown/ duplicate/non-assembler grants, unknown requires, empty group all fail load). - Drop unlock_at_station_level/unlock_requires from ship/module/recipe defs; add unlocked_at_start bool to recipes for graph-unreachable base recipes. Simulation: - Track awarded groups + per-building unlock state; an item starts unlocked iff no group grants it. Rework generateSchematicChoices/applySchematicChoice to operate on groups (grant all members at once). Extend state checksum. - isBuildingUnlocked query; tryPlaceBuilding rejects locked types. UI: - Build-menu buttons for locked buildings start hidden, revealed via a new UnlockedBuildingsChangedEvent (emitted from GameWorldView's poll loop). - Schematic choice dialog lists a group's granted items. - Blueprint placement excludes locked building types (REQ-LOCK-UI-BLUEPRINT). Data: unlocks.toml (app + test) reproduces the prior per-item gates as singleton groups and adds salvage_operations (salvager + salvage_bay, lvl 1) and reprocessing (reprocessing_plant, lvl 2). Tests: rewrote unlock/config/recipe-schematic/artifact/wave/shipyard tests for the group model; added UnlockGroupTest. All 443 cases pass. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Y7N59FsLA5e2kuVdqe4Uhc
356 lines
16 KiB
C++
356 lines
16 KiB
C++
#pragma once
|
|
|
|
#include <map>
|
|
#include <optional>
|
|
#include <random>
|
|
#include <set>
|
|
#include <string>
|
|
#include <utility>
|
|
#include <vector>
|
|
|
|
#include <QElapsedTimer>
|
|
#include <QOpenGLWidget>
|
|
#include <QPoint>
|
|
#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 "DemolishModeChangedEvent.h"
|
|
#include "DemolishModeToggleRequestedEvent.h"
|
|
#include "EventHandler.h"
|
|
#include "ExitBlueprintModeRequestedEvent.h"
|
|
#include "ExitBuilderModeRequestedEvent.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 "Tick.h"
|
|
#include "TickDriver.h"
|
|
#include "TunnelCompletion.h"
|
|
#include "VisualsConfig.h"
|
|
|
|
struct Command;
|
|
struct ParsedReplay;
|
|
class ReplayPlayer;
|
|
class Simulation;
|
|
class QPainter;
|
|
|
|
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();
|
|
}
|
|
};
|
|
|
|
class GameWorldView : public QOpenGLWidget,
|
|
public CombinedEventHandler<BeamFiredEvent,
|
|
BuildingTypeSelectedEvent,
|
|
ExitBuilderModeRequestedEvent,
|
|
DemolishModeToggleRequestedEvent,
|
|
BlueprintPlacementRequestedEvent,
|
|
ExitBlueprintModeRequestedEvent,
|
|
SpeedChangeRequestedEvent,
|
|
CommandRequestedEvent>
|
|
{
|
|
Q_OBJECT
|
|
|
|
public:
|
|
GameWorldView(Simulation* sim, const GameConfig* config,
|
|
const VisualsConfig* visuals, const std::string& configDir,
|
|
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;
|
|
void keyPressEvent(QKeyEvent* event) override;
|
|
void keyReleaseEvent(QKeyEvent* 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 DemolishModeToggleRequestedEvent> 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 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;
|
|
|
|
void drawTiles(QPainter& painter);
|
|
void drawPortItems(QPainter& painter);
|
|
void drawBuildings(QPainter& painter);
|
|
void drawSelectionHighlights(QPainter& painter);
|
|
void drawCopyConfigFeedback(QPainter& painter);
|
|
void drawStations(QPainter& painter);
|
|
void drawBeltItems(QPainter& painter);
|
|
void drawScrap(QPainter& painter);
|
|
void drawShips(QPainter& painter);
|
|
void drawHpBar(QPainter& painter, qreal left, qreal top, qreal width,
|
|
float fraction, bool isEnemy);
|
|
void drawDebugSensorRanges(QPainter& painter);
|
|
void drawDebugTargetLines(QPainter& painter);
|
|
void drawDebugOverlay(QPainter& painter);
|
|
void drawBeams(QPainter& painter);
|
|
void drawOverlays(QPainter& painter);
|
|
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 demolish mode is active (REQ-UI-DEMOLISH-BORDER),
|
|
// tinted with the demolish overlay color (including its configured alpha) from
|
|
// visuals.toml instead of black.
|
|
void drawDemolishBorder(QPainter& painter);
|
|
// Shared drawing for the pause/demolish 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);
|
|
|
|
float getTilePx() const;
|
|
float getViewportWidthTiles() const;
|
|
// World-X (tiles) at the left edge of the viewport. m_scrollXTiles stores the
|
|
// view center; this derives the left edge the world<->widget conversions need.
|
|
float getViewLeftTiles() const;
|
|
QPointF worldToWidget(QVector2D worldPos) const;
|
|
QPointF tileToWidget(QPoint tile) const;
|
|
QPoint widgetToTile(QPoint widgetPt) const;
|
|
QRectF tileRect(QPoint tile) const;
|
|
QRect getViewportRect() 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(BuildingId id) const;
|
|
|
|
float getAsteroidLeftEdge() const;
|
|
float getEnemyStationRightEdge() const;
|
|
// Horizontal pan speed at a given view-center X, in tiles/s (REQ-UI-SCROLL-SPEED).
|
|
float panSpeedTilesPerSecondAt(float viewCenterXTiles) const;
|
|
void clampScroll();
|
|
|
|
bool isValidPlacement(BuildingType type, QPoint anchor, Rotation rot) const;
|
|
const BuildingDef* findBuildingDef(BuildingType type) 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;
|
|
QVector2D widgetToWorld(QPoint widgetPt) const;
|
|
|
|
void drawPortGlyph(QPainter& painter, QPoint tile,
|
|
Rotation direction, const QColor& color,
|
|
bool centered);
|
|
|
|
void drawBuildingGhost(QPainter& painter, BuildingType type,
|
|
QPoint anchorTile, Rotation rotation, bool valid,
|
|
bool showPortTargetGlyphs);
|
|
|
|
void placeBlueprintAtTile(QPoint center);
|
|
|
|
std::optional<QVector2D> entityPosition(entt::entity entity) const;
|
|
// Clears the scrap selection, emitting an empty ScrapSelectionChangedEvent when
|
|
// it was non-empty (REQ-UI-SCRAP-CLICK-SELECT). Used when another selection
|
|
// category takes over.
|
|
void clearScrapSelection();
|
|
// Drops despawned or fully-collected piles from the scrap selection and re-emits
|
|
// when it changed (REQ-UI-SCRAP-CLICK-SELECT). Called each frame from onFrame().
|
|
void pruneDespawnedScrap();
|
|
// Clears the actor selection, emitting an empty EntitySelectionChangedEvent when it was
|
|
// non-empty (REQ-UI-ENTITY-CLICK-SELECT). Used when buildings take over.
|
|
void clearEntitySelection();
|
|
// Drops despawned or dead actors from the selection and re-emits when it changed
|
|
// (REQ-UI-ENTITY-CLICK-SELECT). Called each frame from onFrame().
|
|
void pruneDespawnedActors();
|
|
// True if the given actor is part of the current actor selection.
|
|
bool isEntitySelected(entt::entity entity) const;
|
|
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.
|
|
std::map<std::pair<int, int>, TunnelTileInfo> collectTunnelTiles() const;
|
|
// Draws the green connection highlight for every selected tunnel end that has a
|
|
// matching end (REQ-BLD-TUNNEL-SELECT-HIGHLIGHT).
|
|
void drawSelectedTunnelConnections(QPainter& painter);
|
|
|
|
// 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 toggleDemolishMode();
|
|
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;
|
|
|
|
// 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;
|
|
// World-X (tiles) at the center of the viewport (see getViewLeftTiles()).
|
|
float m_scrollXTiles;
|
|
|
|
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_demolishMode;
|
|
std::optional<BuildingId> m_demolishHoverBuildingId;
|
|
bool m_debugDraw;
|
|
|
|
std::vector<BuildingId> m_selectedBuildingIds;
|
|
std::vector<entt::entity> m_selectedEntities;
|
|
std::vector<entt::entity> m_selectedScrap;
|
|
bool m_boxSelecting;
|
|
QPoint m_boxStartTile;
|
|
QPoint m_boxCurrentTile;
|
|
|
|
bool m_scrollLeft;
|
|
bool m_scrollRight;
|
|
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;
|
|
};
|