switch to using own event system

This commit is contained in:
2026-06-13 17:42:16 +02:00
parent ed17664ef1
commit 5317f35198
49 changed files with 611 additions and 300 deletions

View File

@@ -59,26 +59,43 @@ Simulation types shared across subsystems:
- `Item``struct Item { ItemType type; }`. Items on belts have no persistent identity across ticks. - `Item``struct Item { ItemType type; }`. Items on belts have no persistent identity across ticks.
- `Port``struct Port { QPoint tile; Rotation direction; }`. Identifies a belt-adjacent cell and the direction of flow across that cell. - `Port``struct Port { QPoint tile; Rotation direction; }`. Identifies a belt-adjacent cell and the direction of flow across that cell.
- `MovementIntent``struct MovementIntent { int priority; QVector2D target; }`. Priority follows the order declared under Movement Arbitration. Cleared at the start of each tick; the highest-priority write wins; `tickMovement` reads the winner. - `MovementIntent``struct MovementIntent { int priority; QVector2D target; }`. Priority follows the order declared under Movement Arbitration. Cleared at the start of each tick; the highest-priority write wins; `tickMovement` reads the winner.
- `FireEvent``struct FireEvent { EntityId shooter; EntityId target; Tick emittedAt; }`. Transient record emitted each time a weapon fires (REQ-SHP-FIRING, REQ-SHP-FIRING-BEAM). Buffered in a sim-owned queue and drained by the renderer; see Sim → UI Events. - `WeaponFiredEvent``struct WeaponFiredEvent : public Event { entt::entity shooter; entt::entity target; Tick emittedAt; }`. Transient record emitted each time a weapon fires (REQ-SHP-FIRING, REQ-SHP-FIRING-BEAM). Buffered in a sim-owned vector during the tick, then drained and re-emitted via EventManager by the UI frame handler; see Sim → UI Events.
- `SchematicChoiceOption``struct SchematicChoiceOption { string schematicId; SchematicType type; string displayName; bool isNewUnlock; int targetLevel; }`. Describes one option in the schematic choice dialog (REQ-DEF-SCHEMATIC-DROP). Up to three are generated when an enemy station set is destroyed. `SchematicType` is `Ship`, `Module`, or `Recipe`. - `SchematicChoiceOption``struct SchematicChoiceOption { string schematicId; SchematicType type; string displayName; bool isNewUnlock; int targetLevel; }`. Describes one option in the schematic choice dialog (REQ-DEF-SCHEMATIC-DROP). Up to three are generated when an enemy station set is destroyed. `SchematicType` is `Ship`, `Module`, or `Recipe`.
- `SchematicChoicesAvailableEvent` — EventManager event carrying a `vector<SchematicChoiceOption>`. Sent by the UI each frame when pending choices are detected; handled by `MainWindow` which opens the schematic choice dialog. - `SchematicChoicesAvailableEvent` — EventManager event carrying a `vector<SchematicChoiceOption>`. Sent by the UI each frame when pending choices are detected; handled by `MainWindow` which opens the schematic choice dialog.
## Sim → UI Events ## Event System
The sim owns a per-frame event queue for weapon fires that the UI drains on each render. `FireEvent` records are one-shot signals not derivable from persistent state. Additional one-shot event types can be added here later (e.g., building-complete, unit-death flashes) without changing the pattern. All inter-component communication — both sim→UI and UI→UI — uses a unified `EventManager`/`EventHandler` system. No custom Qt signals/slots are used for inter-widget communication.
Implementation: a plain `std::vector<FireEvent>` owned by `Simulation`. Combat resolution (tick-order step 8) appends to it. The UI calls `simulation.drainFireEvents()` once per rendered frame, which returns the accumulated vector by move and clears the internal one. Beams are tracked by the renderer for 0.3 s of wall time (9 ticks at 30 Hz) using the events' `emittedAt` tick, then discarded. If either the shooter or target entity is gone when the renderer looks them up, the beam is dropped early. ### EventManager
Schematic drops use a different pattern: when an enemy station set is destroyed, the simulation generates up to 3 `SchematicChoiceOption` entries and stores them as pending state. The UI polls `hasSchematicChoicesPending()` each frame and, when true, sends a `SchematicChoicesAvailableEvent` via the EventManager. `MainWindow` handles this event by pausing the game and opening a modal `SchematicChoiceDialog`. The player's selection is fed back via `applySchematicChoice(index)`, which applies the chosen schematic and clears the pending state. `EventManager` is a singleton (`EventManager::getInstance()`) that routes events to registered handlers.
We deliberately do **not** use `QObject` signals/slots or `QEvent` for the fire-event queue: - `sendEventImmediately(shared_ptr<Event>)` — synchronous dispatch to all handlers of the event's type.
- `addEvent(shared_ptr<Event>)` — queues the event for later batch processing.
- `processEvents()` — drains the queue, dispatching each event to its handlers.
- **Determinism.** A plain ordered vector preserves tick-order exactly; the queue is part of per-tick state, inspectable in tests. The EventManager is thread-safe (mutex-guarded).
- **Sim/UI seam.** The sim exposes pull-style access only; the UI never subscribes into the sim, keeping the simulation/presentation split clean.
- **Headless testability.** Catch2 tests read the queue directly after `tick()`; no event loop, no `QApplication`.
- **Zero overhead.** Sim types remain plain structs — no `QObject`, no moc, no signal dispatch machinery.
If the number of event types grows past a handful, we can wrap them in a small `EventQueue<T>` template, still owned by the sim. ### EventHandler
`EventHandler<T>` is a CRTP-style template that a class inherits to receive events of type `T`. It provides `registerForEvent()` / `unregisterForEvent()` and requires an override of `handleEvent(shared_ptr<const T>)`.
`CombinedEventHandler<Ts...>` is a variadic template for classes that handle multiple event types. It provides `registerForEvents()` / `unregisterForEvents()` and requires one `handleEvent` override per type.
### Sim → UI Events
The simulation layer stays free of EventManager — it uses a plain `std::vector<WeaponFiredEvent>` internally (owned by `CombatSystem`). This preserves determinism, tick-order fidelity, and headless testability (Catch2 tests read the queue directly via `drainWeaponFiredEvents()` after `tick()`).
The UI frame handler (`GameWorldView::onFrame` / `ArenaView::onFrame`) bridges the gap: each frame it calls `simulation.drainWeaponFiredEvents()`, then re-emits each `WeaponFiredEvent` via `EventManager::sendEventImmediately()`. Subscribers (the same view's `handleEvent(WeaponFiredEvent)`) create `ActiveBeam` records tracked for 0.3 s of wall time, then discarded. If either the shooter or target entity is gone when the renderer looks them up, the beam is dropped early.
Schematic drops: when an enemy station set is destroyed, the simulation generates up to 3 `SchematicChoiceOption` entries and stores them as pending state. The UI polls `hasSchematicChoicesPending()` each frame and, when true, sends a `SchematicChoicesAvailableEvent` via EventManager. `MainWindow` handles this event by pausing the game and opening a modal `SchematicChoiceDialog`. The player's selection is fed back via `applySchematicChoice(index)`.
### UI Events
All UI interactions — building selection, builder/blueprint mode transitions, speed changes, demolish mode, escape menu, layout dialog requests — are communicated via EventManager events rather than Qt signals/slots. Each event is a small struct inheriting `Event` (e.g., `SelectionChangedEvent`, `BuildingTypeSelectedEvent`, `SpeedChangeRequestedEvent`). Widgets register as `CombinedEventHandler` for the events they care about and emit events via `EventManager::sendEventImmediately()`.
Bidirectional interactions use separate request/notification event types to avoid infinite recursion (e.g., `ExitBuilderModeRequestedEvent` from `BuildButtonGrid``GameWorldView`, vs. `BuilderModeExitedEvent` from `GameWorldView``BuildButtonGrid`).
## Tick Order ## Tick Order
@@ -91,7 +108,7 @@ Within a single simulation tick, subsystems run in this fixed order. The order i
5. **Building → belt push** — buildings push items from output buffer onto the belt tile at their output port (REQ-MAT-OUTPUT-PORT). 5. **Building → belt push** — buildings push items from output buffer onto the belt tile at their output port (REQ-MAT-OUTPUT-PORT).
6. **Belt tick** — advance items along belt tiles; apply splitter routing (REQ-BLD-SPLITTER). 6. **Belt tick** — advance items along belt tiles; apply splitter routing (REQ-BLD-SPLITTER).
7. **Ship behavior systems** — clear `MovementIntent` on each ship, then run `tickThreatResponse`, `tickScrapCollector`, `tickRepairBehavior`, `tickHomeReturn` in any order (arbitration is via intent priority). 7. **Ship behavior systems** — clear `MovementIntent` on each ship, then run `tickThreatResponse`, `tickScrapCollector`, `tickRepairBehavior`, `tickHomeReturn` in any order (arbitration is via intent priority).
8. **Combat resolution** — ships and defence stations acquire targets, fire, apply damage; queue deaths. Each fire appends a `FireEvent` to the sim's fire-event queue (REQ-SHP-FIRING-BEAM). 8. **Combat resolution** — ships and defence stations acquire targets, fire, apply damage; queue deaths. Each fire appends a `WeaponFiredEvent` to the sim's weapon-fired-event queue (REQ-SHP-FIRING-BEAM).
9. **Deaths & loot** — process queued deaths: drop scrap (REQ-RES-SCRAP-DROP); if a full enemy-defence-station set was destroyed this tick, generate up to 3 schematic choice options (REQ-DEF-SCHEMATIC-DROP) stored as pending state for the UI to present; remove entities. 9. **Deaths & loot** — process queued deaths: drop scrap (REQ-RES-SCRAP-DROP); if a full enemy-defence-station set was destroyed this tick, generate up to 3 schematic choice options (REQ-DEF-SCHEMATIC-DROP) stored as pending state for the UI to present; remove entities.
10. **`tickMovement`** — advance ship positions based on final `MovementIntent`. 10. **`tickMovement`** — advance ship positions based on final `MovementIntent`.
11. **Scrap despawn** — decrement scrap timers; remove expired scrap (REQ-RES-SCRAP-DROP). 11. **Scrap despawn** — decrement scrap timers; remove expired scrap (REQ-RES-SCRAP-DROP).
@@ -283,7 +300,7 @@ The game world is rendered by a single `GameWorldView` widget that inherits `QOp
### Threading ### Threading
Sim and UI run on the same thread for v1. `paintEvent` reads sim state directly without locks. If profiling later justifies moving the sim to a worker thread, the pull-style `drainFireEvents()` / `getPendingSchematicChoices()` / `applySchematicChoice()` / `forEachVisualItem()` APIs already support a clean snapshot-and-render split; a single mutex at the sim boundary would suffice. Sim and UI run on the same thread for v1. `paintEvent` reads sim state directly without locks. If profiling later justifies moving the sim to a worker thread, the pull-style `drainWeaponFiredEvents()` / `getPendingSchematicChoices()` / `applySchematicChoice()` / `forEachVisualItem()` APIs already support a clean snapshot-and-render split; a single mutex at the sim boundary would suffice. The `ArenaSimulation` used by the balancing tool runs headlessly on a worker thread; fire events accumulate in its internal vector and are only drained when `ArenaView` drives `tickOnce()` on the main thread during interactive inspection.
### Layer Order (back to front) ### Layer Order (back to front)
@@ -292,7 +309,7 @@ Sim and UI run on the same thread for v1. `paintEvent` reads sim state directly
3. **Belt items** — 10×10 colored squares emitted by `BeltSystem::forEachVisualItem`. 3. **Belt items** — 10×10 colored squares emitted by `BeltSystem::forEachVisualItem`.
4. **Scrap** — glyphs at world positions. 4. **Scrap** — glyphs at world positions.
5. **Ships** — colored arrows oriented by velocity; color keyed to role (player combat / salvage / repair / enemy). 5. **Ships** — colored arrows oriented by velocity; color keyed to role (player combat / salvage / repair / enemy).
6. **Laser beams** — lines derived from live `FireEvent`s kept by the renderer for 0.3 s (REQ-SHP-FIRING-BEAM). 6. **Laser beams** — lines derived from live `WeaponFiredEvent`s kept by the renderer for 0.3 s (REQ-SHP-FIRING-BEAM).
7. **Build overlays** — ghost in builder mode (REQ-BLD-GHOST), demolish-mode tint, tile highlight under cursor, box-drag selection rectangle. 7. **Build overlays** — ghost in builder mode (REQ-BLD-GHOST), demolish-mode tint, tile highlight under cursor, box-drag selection rectangle.
8. **Screen-space UI** — screen-anchored elements, drawn after resetting the world-space transform. 8. **Screen-space UI** — screen-anchored elements, drawn after resetting the world-space transform.

View File

@@ -259,9 +259,9 @@ void ArenaSimulation::tick()
m_aiSystem->tickSalvageBehavior(m_admin, *m_scrapSystem, *m_buildingSystem); m_aiSystem->tickSalvageBehavior(m_admin, *m_scrapSystem, *m_buildingSystem);
// Combat resolution (tick step 8). // Combat resolution (tick step 8).
std::vector<FireEvent> fireEvents; std::vector<WeaponFiredEvent> weaponFiredEvents;
m_combatSystem->tick(m_currentTick, m_admin, *m_buildingSystem, fireEvents); m_combatSystem->tick(m_currentTick, m_admin, *m_buildingSystem, weaponFiredEvents);
m_fireEvents.insert(m_fireEvents.end(), fireEvents.begin(), fireEvents.end()); m_weaponFiredEvents.insert(m_weaponFiredEvents.end(), weaponFiredEvents.begin(), weaponFiredEvents.end());
m_combatSystem->applyPendingDamage(m_currentTick, m_admin); m_combatSystem->applyPendingDamage(m_currentTick, m_admin);
// Deaths (tick step 9, simplified). // Deaths (tick step 9, simplified).
@@ -393,10 +393,10 @@ void ArenaSimulation::tickOnce()
} }
} }
std::vector<FireEvent> ArenaSimulation::drainFireEvents() std::vector<WeaponFiredEvent> ArenaSimulation::drainWeaponFiredEvents()
{ {
std::vector<FireEvent> result; std::vector<WeaponFiredEvent> result;
result.swap(m_fireEvents); result.swap(m_weaponFiredEvents);
return result; return result;
} }

View File

@@ -13,7 +13,7 @@
#include "BuildingId.h" #include "BuildingId.h"
#include "entt/entity/entity.hpp" #include "entt/entity/entity.hpp"
#include "FireEvent.h" #include "WeaponFiredEvent.h"
#include "GameConfig.h" #include "GameConfig.h"
#include "Tick.h" #include "Tick.h"
@@ -58,7 +58,7 @@ public:
void requestStop(); void requestStop();
void tickOnce(); void tickOnce();
std::vector<FireEvent> drainFireEvents(); std::vector<WeaponFiredEvent> drainWeaponFiredEvents();
ArenaStatus status() const; ArenaStatus status() const;
bool isFinished() const; bool isFinished() const;
@@ -104,7 +104,7 @@ private:
int m_winnerTeam; int m_winnerTeam;
std::atomic<bool> m_stopRequested; std::atomic<bool> m_stopRequested;
std::vector<FireEvent> m_fireEvents; std::vector<WeaponFiredEvent> m_weaponFiredEvents;
mutable std::mutex m_statusMutex; mutable std::mutex m_statusMutex;
ArenaStatus m_status; ArenaStatus m_status;

View File

@@ -16,6 +16,7 @@
#include "EventManager.h" #include "EventManager.h"
#include "FacingComponent.h" #include "FacingComponent.h"
#include "FactionComponent.h" #include "FactionComponent.h"
#include "GameSpeedChangedEvent.h"
#include "HealthComponent.h" #include "HealthComponent.h"
#include "PositionComponent.h" #include "PositionComponent.h"
#include "ScrapSystem.h" #include "ScrapSystem.h"
@@ -45,6 +46,13 @@ ArenaView::ArenaView(ArenaSimulation* sim, const VisualsConfig* visuals,
connect(m_renderTimer, &QTimer::timeout, this, &ArenaView::onFrame); connect(m_renderTimer, &QTimer::timeout, this, &ArenaView::onFrame);
m_renderTimer->start(); m_renderTimer->start();
m_frameTimer.start(); m_frameTimer.start();
registerForEvent();
}
ArenaView::~ArenaView()
{
unregisterForEvent();
} }
void ArenaView::setGameSpeed(double multiplier) void ArenaView::setGameSpeed(double multiplier)
@@ -54,7 +62,8 @@ void ArenaView::setGameSpeed(double multiplier)
m_prevNonZeroSpeed = multiplier; m_prevNonZeroSpeed = multiplier;
} }
m_gameSpeedMultiplier = multiplier; m_gameSpeedMultiplier = multiplier;
emit speedChanged(multiplier); EventManager::getInstance()->sendEventImmediately(
std::make_shared<GameSpeedChangedEvent>(multiplier));
} }
double ArenaView::gameSpeed() const double ArenaView::gameSpeed() const
@@ -93,34 +102,17 @@ void ArenaView::onFrame()
} }
} }
// Emit fire events via EventManager
{ {
const std::vector<FireEvent> fires = m_sim->drainFireEvents(); const std::vector<WeaponFiredEvent> fires = m_sim->drainWeaponFiredEvents();
for (const FireEvent& fe : fires) for (const WeaponFiredEvent& fe : fires)
{ {
float maxRadius = 0.125f; EventManager::getInstance()->sendEventImmediately(
if (m_sim->admin().isValid(fe.target) std::make_shared<WeaponFiredEvent>(fe));
&& m_sim->admin().hasAll<StationBodyComponent>(fe.target))
{
const StationBodyComponent& sb = m_sim->admin().get<StationBodyComponent>(fe.target);
const int shorter = std::min(sb.footprint.width(),
sb.footprint.height());
maxRadius = shorter / 2.0f;
}
std::uniform_real_distribution<float> angleDist(0.0f, 6.28318530f);
std::uniform_real_distribution<float> radiusDist(0.0f, maxRadius);
const float angle = angleDist(m_rng);
const float radius = radiusDist(m_rng);
ActiveBeam beam;
beam.event = fe;
beam.emittedWallMs = m_wallMs;
beam.targetOffset = QVector2D(radius * std::cos(angle),
radius * std::sin(angle));
m_activeBeams.push_back(beam);
} }
} }
// Expire old beams
{ {
std::vector<ActiveBeam> live; std::vector<ActiveBeam> live;
for (const ActiveBeam& b : m_activeBeams) for (const ActiveBeam& b : m_activeBeams)
@@ -136,12 +128,36 @@ void ArenaView::onFrame()
if (m_sim->isFinished() && !m_finishedEmitted) if (m_sim->isFinished() && !m_finishedEmitted)
{ {
m_finishedEmitted = true; m_finishedEmitted = true;
emit finished();
} }
update(); update();
} }
void ArenaView::handleEvent(std::shared_ptr<const WeaponFiredEvent> event)
{
float maxRadius = 0.125f;
if (m_sim->admin().isValid(event->target)
&& m_sim->admin().hasAll<StationBodyComponent>(event->target))
{
const StationBodyComponent& sb = m_sim->admin().get<StationBodyComponent>(event->target);
const int shorter = std::min(sb.footprint.width(),
sb.footprint.height());
maxRadius = shorter / 2.0f;
}
std::uniform_real_distribution<float> angleDist(0.0f, 6.28318530f);
std::uniform_real_distribution<float> radiusDist(0.0f, maxRadius);
const float angle = angleDist(m_rng);
const float radius = radiusDist(m_rng);
ActiveBeam beam;
beam.event = *event;
beam.emittedWallMs = m_wallMs;
beam.targetOffset = QVector2D(radius * std::cos(angle),
radius * std::sin(angle));
m_activeBeams.push_back(beam);
}
void ArenaView::paintGL() void ArenaView::paintGL()
{ {
QPainter painter(this); QPainter painter(this);
@@ -414,4 +430,3 @@ void ArenaView::drawBeams(QPainter& painter)
worldToWidget(*targetPos + beam.targetOffset)); worldToWidget(*targetPos + beam.targetOffset));
} }
} }

View File

@@ -9,7 +9,8 @@
#include <QTimer> #include <QTimer>
#include <QVector2D> #include <QVector2D>
#include "FireEvent.h" #include "EventHandler.h"
#include "WeaponFiredEvent.h"
#include "entt/entity/entity.hpp" #include "entt/entity/entity.hpp"
#include "EntitySelectedEvent.h" #include "EntitySelectedEvent.h"
@@ -20,23 +21,21 @@
class ArenaSimulation; class ArenaSimulation;
class QPainter; class QPainter;
class ArenaView : public QOpenGLWidget class ArenaView : public QOpenGLWidget,
public EventHandler<WeaponFiredEvent>
{ {
Q_OBJECT Q_OBJECT
public: public:
ArenaView(ArenaSimulation* sim, const VisualsConfig* visuals, ArenaView(ArenaSimulation* sim, const VisualsConfig* visuals,
QWidget* parent = nullptr); QWidget* parent = nullptr);
~ArenaView() override;
void setGameSpeed(double multiplier); void setGameSpeed(double multiplier);
double gameSpeed() const; double gameSpeed() const;
void togglePause(); void togglePause();
void stopRendering(); void stopRendering();
signals:
void speedChanged(double multiplier);
void finished();
protected: protected:
void paintGL() override; void paintGL() override;
void mousePressEvent(QMouseEvent* event) override; void mousePressEvent(QMouseEvent* event) override;
@@ -45,6 +44,8 @@ private slots:
void onFrame(); void onFrame();
private: private:
void handleEvent(std::shared_ptr<const WeaponFiredEvent> event) override;
void drawTiles(QPainter& painter); void drawTiles(QPainter& painter);
void drawBuildings(QPainter& painter); void drawBuildings(QPainter& painter);
void drawStations(QPainter& painter); void drawStations(QPainter& painter);
@@ -62,7 +63,7 @@ private:
struct ActiveBeam struct ActiveBeam
{ {
FireEvent event; WeaponFiredEvent event;
qint64 emittedWallMs; qint64 emittedWallMs;
QVector2D targetOffset; QVector2D targetOffset;
}; };

View File

@@ -3,8 +3,13 @@
#include <QHBoxLayout> #include <QHBoxLayout>
#include <QVBoxLayout> #include <QVBoxLayout>
ArenaWidget::ArenaWidget(const std::string& arenaName, QWidget* parent) #include "ArenaInspectRequestedEvent.h"
#include "ArenaStartRequestedEvent.h"
#include "EventManager.h"
ArenaWidget::ArenaWidget(int arenaIndex, const std::string& arenaName, QWidget* parent)
: QFrame(parent) : QFrame(parent)
, m_arenaIndex(arenaIndex)
, m_running(false) , m_running(false)
, m_wasFinished(false) , m_wasFinished(false)
{ {
@@ -31,11 +36,17 @@ void ArenaWidget::buildLayout(const std::string& arenaName)
titleRow->addStretch(); titleRow->addStretch();
m_inspectButton = new QPushButton(tr("Inspect"), this); m_inspectButton = new QPushButton(tr("Inspect"), this);
connect(m_inspectButton, &QPushButton::clicked, this, &ArenaWidget::inspectRequested); connect(m_inspectButton, &QPushButton::clicked, this, [this]() {
EventManager::getInstance()->sendEventImmediately(
std::make_shared<ArenaInspectRequestedEvent>(m_arenaIndex));
});
titleRow->addWidget(m_inspectButton); titleRow->addWidget(m_inspectButton);
m_startButton = new QPushButton(tr("Start"), this); m_startButton = new QPushButton(tr("Start"), this);
connect(m_startButton, &QPushButton::clicked, this, &ArenaWidget::startRequested); connect(m_startButton, &QPushButton::clicked, this, [this]() {
EventManager::getInstance()->sendEventImmediately(
std::make_shared<ArenaStartRequestedEvent>(m_arenaIndex));
});
titleRow->addWidget(m_startButton); titleRow->addWidget(m_startButton);
outerLayout->addLayout(titleRow); outerLayout->addLayout(titleRow);

View File

@@ -14,19 +14,16 @@ class ArenaWidget : public QFrame
Q_OBJECT Q_OBJECT
public: public:
explicit ArenaWidget(const std::string& arenaName, QWidget* parent = nullptr); ArenaWidget(int arenaIndex, const std::string& arenaName, QWidget* parent = nullptr);
void updateStatus(const ArenaStatus& status); void updateStatus(const ArenaStatus& status);
void startSimulation(); void startSimulation();
void resetToGrey(); void resetToGrey();
signals:
void startRequested();
void inspectRequested();
private: private:
void buildLayout(const std::string& arenaName); void buildLayout(const std::string& arenaName);
int m_arenaIndex;
QLabel* m_titleLabel; QLabel* m_titleLabel;
QLabel* m_team1Header; QLabel* m_team1Header;
QLabel* m_team2Header; QLabel* m_team2Header;

View File

@@ -48,14 +48,17 @@ BalancingWindow::BalancingWindow(const BalancingConfig& balancingConfig,
m_pollTimer = new QTimer(this); m_pollTimer = new QTimer(this);
connect(m_pollTimer, &QTimer::timeout, this, &BalancingWindow::pollStatuses); connect(m_pollTimer, &QTimer::timeout, this, &BalancingWindow::pollStatuses);
m_pollTimer->start(100); m_pollTimer->start(100);
registerForEvents();
} }
BalancingWindow::~BalancingWindow() BalancingWindow::~BalancingWindow()
{ {
unregisterForEvents();
m_pollTimer->stop(); m_pollTimer->stop();
if (m_inspectWindow) if (m_inspectWindow)
{ {
m_inspectWindow->disconnect(this);
delete m_inspectWindow; delete m_inspectWindow;
m_inspectWindow = nullptr; m_inspectWindow = nullptr;
} }
@@ -81,16 +84,11 @@ void BalancingWindow::populateArenas(const BalancingConfig& balancingConfig)
entry.config = arenaConfig; entry.config = arenaConfig;
entry.simulation = std::make_unique<ArenaSimulation>( entry.simulation = std::make_unique<ArenaSimulation>(
m_gameConfig, arenaConfig, m_nextSeed++); m_gameConfig, arenaConfig, m_nextSeed++);
entry.widget = new ArenaWidget(arenaConfig.name, scrollContent); entry.widget = new ArenaWidget(index, arenaConfig.name, scrollContent);
contentLayout->addWidget(entry.widget); contentLayout->addWidget(entry.widget);
entry.widget->updateStatus(entry.simulation->status()); entry.widget->updateStatus(entry.simulation->status());
connect(entry.widget, &ArenaWidget::startRequested,
this, [this, index]() { startArena(index); });
connect(entry.widget, &ArenaWidget::inspectRequested,
this, [this, index]() { inspectArena(index); });
m_arenas.push_back(std::move(entry)); m_arenas.push_back(std::move(entry));
} }
@@ -158,6 +156,21 @@ void BalancingWindow::startAll()
} }
} }
void BalancingWindow::handleEvent(std::shared_ptr<const ArenaStartRequestedEvent> event)
{
startArena(event->arenaIndex);
}
void BalancingWindow::handleEvent(std::shared_ptr<const ArenaInspectRequestedEvent> event)
{
inspectArena(event->arenaIndex);
}
void BalancingWindow::handleEvent(std::shared_ptr<const InspectWindowClosedEvent> /*event*/)
{
closeInspectWindow();
}
void BalancingWindow::startArena(int index) void BalancingWindow::startArena(int index)
{ {
ArenaEntry& entry = m_arenas[index]; ArenaEntry& entry = m_arenas[index];
@@ -179,7 +192,6 @@ void BalancingWindow::inspectArena(int index)
{ {
if (m_inspectWindow) if (m_inspectWindow)
{ {
m_inspectWindow->disconnect(this);
delete m_inspectWindow; delete m_inspectWindow;
m_inspectWindow = nullptr; m_inspectWindow = nullptr;
@@ -210,8 +222,6 @@ void BalancingWindow::inspectArena(int index)
m_inspectWindow = new InspectWindow( m_inspectWindow = new InspectWindow(
m_inspectedSim.get(), &m_gameConfig, &m_visuals, entry.config.name, nullptr); m_inspectedSim.get(), &m_gameConfig, &m_visuals, entry.config.name, nullptr);
connect(m_inspectWindow, &InspectWindow::closed,
this, &BalancingWindow::closeInspectWindow);
setMainControlsEnabled(false); setMainControlsEnabled(false);
m_inspectWindow->show(); m_inspectWindow->show();
@@ -224,7 +234,6 @@ void BalancingWindow::closeInspectWindow()
return; return;
} }
m_inspectWindow->disconnect(this);
m_inspectWindow->deleteLater(); m_inspectWindow->deleteLater();
m_inspectWindow = nullptr; m_inspectWindow = nullptr;

View File

@@ -10,15 +10,22 @@
#include <QTimer> #include <QTimer>
#include <QWidget> #include <QWidget>
#include "ArenaInspectRequestedEvent.h"
#include "ArenaStartRequestedEvent.h"
#include "ArenaWidget.h" #include "ArenaWidget.h"
#include "ArenaSimulation.h" #include "ArenaSimulation.h"
#include "BalancingConfig.h" #include "BalancingConfig.h"
#include "EventHandler.h"
#include "GameConfig.h" #include "GameConfig.h"
#include "InspectWindowClosedEvent.h"
#include "VisualsConfig.h" #include "VisualsConfig.h"
class InspectWindow; class InspectWindow;
class BalancingWindow : public QWidget class BalancingWindow : public QWidget,
public CombinedEventHandler<ArenaStartRequestedEvent,
ArenaInspectRequestedEvent,
InspectWindowClosedEvent>
{ {
Q_OBJECT Q_OBJECT
@@ -30,15 +37,20 @@ public:
QWidget* parent = nullptr); QWidget* parent = nullptr);
~BalancingWindow() override; ~BalancingWindow() override;
private:
void handleEvent(std::shared_ptr<const ArenaStartRequestedEvent> event) override;
void handleEvent(std::shared_ptr<const ArenaInspectRequestedEvent> event) override;
void handleEvent(std::shared_ptr<const InspectWindowClosedEvent> event) override;
private slots: private slots:
void pollStatuses(); void pollStatuses();
void reloadConfig(); void reloadConfig();
void startAll(); void startAll();
private:
void startArena(int index); void startArena(int index);
void inspectArena(int index); void inspectArena(int index);
void closeInspectWindow(); void closeInspectWindow();
private:
void populateArenas(const BalancingConfig& balancingConfig); void populateArenas(const BalancingConfig& balancingConfig);
void stopAllArenas(); void stopAllArenas();
void updateButtons(); void updateButtons();

View File

@@ -10,7 +10,9 @@
#include "ArenaView.h" #include "ArenaView.h"
#include "EntityAdmin.h" #include "EntityAdmin.h"
#include "EventManager.h"
#include "HealthComponent.h" #include "HealthComponent.h"
#include "InspectWindowClosedEvent.h"
#include "ModuleOwnerComponent.h" #include "ModuleOwnerComponent.h"
#include "ShipIdentityComponent.h" #include "ShipIdentityComponent.h"
#include "ShipStatsCalculator.h" #include "ShipStatsCalculator.h"
@@ -76,9 +78,6 @@ InspectWindow::InspectWindow(ArenaSimulation* sim, const GameConfig* config,
m_arenaView = new ArenaView(sim, visuals, this); m_arenaView = new ArenaView(sim, visuals, this);
mainLayout->addWidget(m_arenaView, 1); mainLayout->addWidget(m_arenaView, 1);
connect(m_arenaView, &ArenaView::speedChanged,
this, &InspectWindow::onSpeedChanged);
// Info panel (bottom) // Info panel (bottom)
{ {
QWidget* infoPanel = new QWidget(this); QWidget* infoPanel = new QWidget(this);
@@ -140,19 +139,20 @@ InspectWindow::InspectWindow(ArenaSimulation* sim, const GameConfig* config,
setFocusPolicy(Qt::StrongFocus); setFocusPolicy(Qt::StrongFocus);
registerForEvent(); registerForEvents();
} }
InspectWindow::~InspectWindow() InspectWindow::~InspectWindow()
{ {
unregisterForEvent(); unregisterForEvents();
} }
void InspectWindow::closeEvent(QCloseEvent* event) void InspectWindow::closeEvent(QCloseEvent* event)
{ {
m_arenaView->stopRendering(); m_arenaView->stopRendering();
m_pollTimer->stop(); m_pollTimer->stop();
emit closed(); EventManager::getInstance()->sendEventImmediately(
std::make_shared<InspectWindowClosedEvent>());
event->accept(); event->accept();
} }
@@ -176,11 +176,11 @@ void InspectWindow::onSpeedButton(int index)
} }
} }
void InspectWindow::onSpeedChanged(double multiplier) void InspectWindow::handleEvent(std::shared_ptr<const GameSpeedChangedEvent> event)
{ {
for (int i = 0; i < kSpeedCount; ++i) for (int i = 0; i < kSpeedCount; ++i)
{ {
const bool active = (std::abs(kSpeeds[i] - multiplier) < 0.001); const bool active = (std::abs(kSpeeds[i] - event->speed) < 0.001);
m_speedButtons[static_cast<std::size_t>(i)]->setChecked(active); m_speedButtons[static_cast<std::size_t>(i)]->setChecked(active);
} }
} }

View File

@@ -15,13 +15,15 @@
#include "EntitySelectedEvent.h" #include "EntitySelectedEvent.h"
#include "EventHandler.h" #include "EventHandler.h"
#include "GameConfig.h" #include "GameConfig.h"
#include "GameSpeedChangedEvent.h"
#include "VisualsConfig.h" #include "VisualsConfig.h"
class ArenaView; class ArenaView;
class ShipStatsPanel; class ShipStatsPanel;
class InspectWindow : public QWidget, class InspectWindow : public QWidget,
public EventHandler<EntitySelectedEvent> public CombinedEventHandler<EntitySelectedEvent,
GameSpeedChangedEvent>
{ {
Q_OBJECT Q_OBJECT
@@ -31,19 +33,16 @@ public:
const std::string& arenaName, QWidget* parent = nullptr); const std::string& arenaName, QWidget* parent = nullptr);
~InspectWindow() override; ~InspectWindow() override;
signals:
void closed();
protected: protected:
void closeEvent(QCloseEvent* event) override; void closeEvent(QCloseEvent* event) override;
void keyPressEvent(QKeyEvent* event) override; void keyPressEvent(QKeyEvent* event) override;
private: private:
void handleEvent(std::shared_ptr<const EntitySelectedEvent> event) override; void handleEvent(std::shared_ptr<const EntitySelectedEvent> event) override;
void handleEvent(std::shared_ptr<const GameSpeedChangedEvent> event) override;
private slots: private slots:
void onSpeedButton(int index); void onSpeedButton(int index);
void onSpeedChanged(double multiplier);
void pollStatus(); void pollStatus();
private: private:

View File

@@ -6,7 +6,6 @@ SET(HDRS
${CMAKE_CURRENT_SOURCE_DIR}/EntityAdmin.h ${CMAKE_CURRENT_SOURCE_DIR}/EntityAdmin.h
${CMAKE_CURRENT_SOURCE_DIR}/Blueprint.h ${CMAKE_CURRENT_SOURCE_DIR}/Blueprint.h
${CMAKE_CURRENT_SOURCE_DIR}/BuildingId.h ${CMAKE_CURRENT_SOURCE_DIR}/BuildingId.h
${CMAKE_CURRENT_SOURCE_DIR}/FireEvent.h
${CMAKE_CURRENT_SOURCE_DIR}/ItemType.h ${CMAKE_CURRENT_SOURCE_DIR}/ItemType.h
${CMAKE_CURRENT_SOURCE_DIR}/Item.h ${CMAKE_CURRENT_SOURCE_DIR}/Item.h
${CMAKE_CURRENT_SOURCE_DIR}/Port.h ${CMAKE_CURRENT_SOURCE_DIR}/Port.h

View File

@@ -1,15 +0,0 @@
#pragma once
#include "Tick.h"
#include "entt/entity/entity.hpp"
// Transient record emitted each time a weapon fires (REQ-SHP-FIRING,
// REQ-SHP-FIRING-BEAM). Buffered in a sim-owned queue and drained by the
// renderer each frame to draw the 0.3-second laser beam.
struct FireEvent
{
entt::entity shooter;
entt::entity target;
Tick emittedAt;
};

View File

@@ -21,7 +21,7 @@ CombatSystem::CombatSystem(const GameConfig& config)
void CombatSystem::tick(Tick currentTick, void CombatSystem::tick(Tick currentTick,
EntityAdmin& admin, EntityAdmin& admin,
BuildingSystem& /*buildings*/, BuildingSystem& /*buildings*/,
std::vector<FireEvent>& outFireEvents) std::vector<WeaponFiredEvent>& outWeaponFiredEvents)
{ {
TRACE(); TRACE();
// All weapons (ships and stations) are child entities linked via ModuleOwnerComponent. // All weapons (ships and stations) are child entities linked via ModuleOwnerComponent.
@@ -35,7 +35,7 @@ void CombatSystem::tick(Tick currentTick,
} }
const PositionComponent& pos = admin.get<PositionComponent>(owner.owner); const PositionComponent& pos = admin.get<PositionComponent>(owner.owner);
const FactionComponent& faction = admin.get<FactionComponent>(owner.owner); const FactionComponent& faction = admin.get<FactionComponent>(owner.owner);
resolveWeapon(owner.owner, weapon, pos, faction, currentTick, admin, outFireEvents); resolveWeapon(owner.owner, weapon, pos, faction, currentTick, admin, outWeaponFiredEvents);
}); });
} }
@@ -46,7 +46,7 @@ void CombatSystem::resolveWeapon(
const FactionComponent& ownFaction, const FactionComponent& ownFaction,
Tick currentTick, Tick currentTick,
EntityAdmin& admin, EntityAdmin& admin,
std::vector<FireEvent>& out) std::vector<WeaponFiredEvent>& out)
{ {
if (weapon.cooldownTicks > 0.0f) if (weapon.cooldownTicks > 0.0f)
{ {
@@ -115,7 +115,7 @@ void CombatSystem::resolveWeapon(
m_pendingDamage.push_back({targetEntity, weapon.damage, m_pendingDamage.push_back({targetEntity, weapon.damage,
currentTick + kWeaponImpactDelayTicks}); currentTick + kWeaponImpactDelayTicks});
FireEvent evt; WeaponFiredEvent evt;
evt.shooter = shipEntity; evt.shooter = shipEntity;
evt.target = targetEntity; evt.target = targetEntity;
evt.emittedAt = currentTick; evt.emittedAt = currentTick;

View File

@@ -7,7 +7,7 @@
#include "Building.h" #include "Building.h"
#include "FactionComponent.h" #include "FactionComponent.h"
#include "FireEvent.h" #include "WeaponFiredEvent.h"
#include "GameConfig.h" #include "GameConfig.h"
#include "PositionComponent.h" #include "PositionComponent.h"
#include "Tick.h" #include "Tick.h"
@@ -26,7 +26,7 @@ public:
void tick(Tick currentTick, void tick(Tick currentTick,
EntityAdmin& admin, EntityAdmin& admin,
BuildingSystem& buildings, BuildingSystem& buildings,
std::vector<FireEvent>& outFireEvents); std::vector<WeaponFiredEvent>& outWeaponFiredEvents);
void applyPendingDamage(Tick currentTick, EntityAdmin& admin); void applyPendingDamage(Tick currentTick, EntityAdmin& admin);
@@ -47,7 +47,7 @@ private:
const FactionComponent& ownFaction, const FactionComponent& ownFaction,
Tick currentTick, Tick currentTick,
EntityAdmin& admin, EntityAdmin& admin,
std::vector<FireEvent>& out); std::vector<WeaponFiredEvent>& out);
const GameConfig& m_config; const GameConfig& m_config;
}; };

View File

@@ -0,0 +1,10 @@
#pragma once
#include "Event.h"
class ArenaInspectRequestedEvent : public Event
{
public:
explicit ArenaInspectRequestedEvent(int arenaIndex) : arenaIndex(arenaIndex) {}
const int arenaIndex;
};

View File

@@ -0,0 +1,10 @@
#pragma once
#include "Event.h"
class ArenaStartRequestedEvent : public Event
{
public:
explicit ArenaStartRequestedEvent(int arenaIndex) : arenaIndex(arenaIndex) {}
const int arenaIndex;
};

View File

@@ -0,0 +1,7 @@
#pragma once
#include "Event.h"
class BlueprintModeExitedEvent : public Event
{
};

View File

@@ -0,0 +1,12 @@
#pragma once
#include "Blueprint.h"
#include "Event.h"
class BlueprintPlacementRequestedEvent : public Event
{
public:
explicit BlueprintPlacementRequestedEvent(Blueprint blueprint)
: blueprint(std::move(blueprint)) {}
const Blueprint blueprint;
};

View File

@@ -0,0 +1,7 @@
#pragma once
#include "Event.h"
class BuilderModeExitedEvent : public Event
{
};

View File

@@ -0,0 +1,11 @@
#pragma once
#include "BuildingType.h"
#include "Event.h"
class BuildingTypeSelectedEvent : public Event
{
public:
explicit BuildingTypeSelectedEvent(BuildingType type) : type(type) {}
const BuildingType type;
};

View File

@@ -7,6 +7,23 @@ SET(HDRS
${CMAKE_CURRENT_SOURCE_DIR}/GameSpeedChangedEvent.h ${CMAKE_CURRENT_SOURCE_DIR}/GameSpeedChangedEvent.h
${CMAKE_CURRENT_SOURCE_DIR}/BossWaveUpdatedEvent.h ${CMAKE_CURRENT_SOURCE_DIR}/BossWaveUpdatedEvent.h
${CMAKE_CURRENT_SOURCE_DIR}/SchematicChoicesAvailableEvent.h ${CMAKE_CURRENT_SOURCE_DIR}/SchematicChoicesAvailableEvent.h
${CMAKE_CURRENT_SOURCE_DIR}/SelectionChangedEvent.h
${CMAKE_CURRENT_SOURCE_DIR}/GameOverEvent.h
${CMAKE_CURRENT_SOURCE_DIR}/BuilderModeExitedEvent.h
${CMAKE_CURRENT_SOURCE_DIR}/BlueprintModeExitedEvent.h
${CMAKE_CURRENT_SOURCE_DIR}/EscapeMenuRequestedEvent.h
${CMAKE_CURRENT_SOURCE_DIR}/DemolishModeChangedEvent.h
${CMAKE_CURRENT_SOURCE_DIR}/BuildingTypeSelectedEvent.h
${CMAKE_CURRENT_SOURCE_DIR}/ExitBuilderModeRequestedEvent.h
${CMAKE_CURRENT_SOURCE_DIR}/DemolishModeToggleRequestedEvent.h
${CMAKE_CURRENT_SOURCE_DIR}/BlueprintPlacementRequestedEvent.h
${CMAKE_CURRENT_SOURCE_DIR}/ExitBlueprintModeRequestedEvent.h
${CMAKE_CURRENT_SOURCE_DIR}/SpeedChangeRequestedEvent.h
${CMAKE_CURRENT_SOURCE_DIR}/LayoutDialogRequestedEvent.h
${CMAKE_CURRENT_SOURCE_DIR}/InspectWindowClosedEvent.h
${CMAKE_CURRENT_SOURCE_DIR}/ArenaStartRequestedEvent.h
${CMAKE_CURRENT_SOURCE_DIR}/ArenaInspectRequestedEvent.h
${CMAKE_CURRENT_SOURCE_DIR}/WeaponFiredEvent.h
PARENT_SCOPE PARENT_SCOPE
) )

View File

@@ -0,0 +1,10 @@
#pragma once
#include "Event.h"
class DemolishModeChangedEvent : public Event
{
public:
explicit DemolishModeChangedEvent(bool active) : active(active) {}
const bool active;
};

View File

@@ -0,0 +1,7 @@
#pragma once
#include "Event.h"
class DemolishModeToggleRequestedEvent : public Event
{
};

View File

@@ -0,0 +1,7 @@
#pragma once
#include "Event.h"
class EscapeMenuRequestedEvent : public Event
{
};

View File

@@ -0,0 +1,7 @@
#pragma once
#include "Event.h"
class ExitBlueprintModeRequestedEvent : public Event
{
};

View File

@@ -0,0 +1,7 @@
#pragma once
#include "Event.h"
class ExitBuilderModeRequestedEvent : public Event
{
};

View File

@@ -0,0 +1,7 @@
#pragma once
#include "Event.h"
class GameOverEvent : public Event
{
};

View File

@@ -0,0 +1,7 @@
#pragma once
#include "Event.h"
class InspectWindowClosedEvent : public Event
{
};

View File

@@ -0,0 +1,12 @@
#pragma once
#include "BuildingId.h"
#include "Event.h"
class LayoutDialogRequestedEvent : public Event
{
public:
explicit LayoutDialogRequestedEvent(BuildingId shipyardId)
: shipyardId(shipyardId) {}
const BuildingId shipyardId;
};

View File

@@ -0,0 +1,14 @@
#pragma once
#include <vector>
#include "BuildingId.h"
#include "Event.h"
class SelectionChangedEvent : public Event
{
public:
explicit SelectionChangedEvent(std::vector<BuildingId> ids)
: ids(std::move(ids)) {}
const std::vector<BuildingId> ids;
};

View File

@@ -0,0 +1,10 @@
#pragma once
#include "Event.h"
class SpeedChangeRequestedEvent : public Event
{
public:
explicit SpeedChangeRequestedEvent(double multiplier) : multiplier(multiplier) {}
const double multiplier;
};

View File

@@ -0,0 +1,17 @@
#pragma once
#include "Event.h"
#include "Tick.h"
#include "entt/entity/entity.hpp"
struct WeaponFiredEvent : public Event
{
WeaponFiredEvent() = default;
WeaponFiredEvent(entt::entity shooter, entt::entity target, Tick emittedAt)
: shooter(shooter), target(target), emittedAt(emittedAt) {}
entt::entity shooter = entt::null;
entt::entity target = entt::null;
Tick emittedAt = 0;
};

View File

@@ -136,7 +136,7 @@ void Simulation::reset(unsigned int seed)
m_playerStation2Entity = entt::null; m_playerStation2Entity = entt::null;
m_currentEnemyStationEntities[0] = entt::null; m_currentEnemyStationEntities[0] = entt::null;
m_currentEnemyStationEntities[1] = entt::null; m_currentEnemyStationEntities[1] = entt::null;
m_fireEvents.clear(); m_weaponFiredEvents.clear();
m_pendingSchematicChoices.clear(); m_pendingSchematicChoices.clear();
m_admin.clear(); m_admin.clear();
@@ -246,7 +246,7 @@ void Simulation::tick()
// Step 8: combat resolution // Step 8: combat resolution
m_combatSystem->tick(m_currentTick, m_admin, m_combatSystem->tick(m_currentTick, m_admin,
*m_buildingSystem, m_fireEvents); *m_buildingSystem, m_weaponFiredEvents);
// Step 8b: deferred damage whose impact tick has arrived // Step 8b: deferred damage whose impact tick has arrived
m_combatSystem->applyPendingDamage(m_currentTick, m_admin); m_combatSystem->applyPendingDamage(m_currentTick, m_admin);
@@ -745,10 +745,10 @@ bool Simulation::isItemUnlocked(const std::string& itemId) const
// Drains // Drains
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
std::vector<FireEvent> Simulation::drainFireEvents() std::vector<WeaponFiredEvent> Simulation::drainWeaponFiredEvents()
{ {
std::vector<FireEvent> result; std::vector<WeaponFiredEvent> result;
result.swap(m_fireEvents); result.swap(m_weaponFiredEvents);
return result; return result;
} }

View File

@@ -16,7 +16,7 @@
#include "BuildingType.h" #include "BuildingType.h"
#include "BuildingId.h" #include "BuildingId.h"
#include "EventHandler.h" #include "EventHandler.h"
#include "FireEvent.h" #include "WeaponFiredEvent.h"
#include "GameConfig.h" #include "GameConfig.h"
#include "Rotation.h" #include "Rotation.h"
#include "Tick.h" #include "Tick.h"
@@ -50,7 +50,7 @@ public:
// Returns all fire events accumulated since the last drain, clearing the // Returns all fire events accumulated since the last drain, clearing the
// internal queue. Call once per rendered frame (REQ-SHP-FIRING-BEAM). // internal queue. Call once per rendered frame (REQ-SHP-FIRING-BEAM).
std::vector<FireEvent> drainFireEvents(); std::vector<WeaponFiredEvent> drainWeaponFiredEvents();
// Returns the pending schematic choices (empty if no drop is pending). // Returns the pending schematic choices (empty if no drop is pending).
const std::vector<SchematicChoiceOption>& getPendingSchematicChoices() const; const std::vector<SchematicChoiceOption>& getPendingSchematicChoices() const;
@@ -165,6 +165,6 @@ private:
std::unique_ptr<WaveSystem> m_waveSystem; std::unique_ptr<WaveSystem> m_waveSystem;
std::unique_ptr<CombatSystem> m_combatSystem; std::unique_ptr<CombatSystem> m_combatSystem;
std::vector<FireEvent> m_fireEvents; std::vector<WeaponFiredEvent> m_weaponFiredEvents;
std::vector<SchematicChoiceOption> m_pendingSchematicChoices; std::vector<SchematicChoiceOption> m_pendingSchematicChoices;
}; };

View File

@@ -10,7 +10,7 @@
#include "ConfigLoader.h" #include "ConfigLoader.h"
#include "EntityAdmin.h" #include "EntityAdmin.h"
#include "FactionComponent.h" #include "FactionComponent.h"
#include "FireEvent.h" #include "WeaponFiredEvent.h"
#include "HealthComponent.h" #include "HealthComponent.h"
#include "HqProxyComponent.h" #include "HqProxyComponent.h"
#include "ModuleOwnerComponent.h" #include "ModuleOwnerComponent.h"
@@ -111,7 +111,7 @@ TEST_CASE("CombatSystem: ship fires when cooldown=0 and target in range", "[comb
const float hpBefore = f.admin.get<HealthComponent>(player).hp; const float hpBefore = f.admin.get<HealthComponent>(player).hp;
std::vector<FireEvent> events; std::vector<WeaponFiredEvent> events;
f.combat.tick(0, f.admin, f.buildings, events); f.combat.tick(0, f.admin, f.buildings, events);
f.combat.applyPendingDamage(5, f.admin); f.combat.applyPendingDamage(5, f.admin);
@@ -135,16 +135,16 @@ TEST_CASE("CombatSystem: cooldown prevents firing before it expires", "[combat]"
f.admin.get<WeaponComponent>(wc).cooldownTicks = 3.0f; // override to 3 f.admin.get<WeaponComponent>(wc).cooldownTicks = 3.0f; // override to 3
} }
auto enemyFiredIn = [&enemy](const std::vector<FireEvent>& evts) auto enemyFiredIn = [&enemy](const std::vector<WeaponFiredEvent>& evts)
{ {
for (const FireEvent& evt : evts) for (const WeaponFiredEvent& evt : evts)
{ {
if (evt.shooter == enemy) { return true; } if (evt.shooter == enemy) { return true; }
} }
return false; return false;
}; };
std::vector<FireEvent> events; std::vector<WeaponFiredEvent> events;
f.combat.tick(0, f.admin, f.buildings, events); f.combat.tick(0, f.admin, f.buildings, events);
REQUIRE_FALSE(enemyFiredIn(events)); REQUIRE_FALSE(enemyFiredIn(events));
@@ -165,7 +165,7 @@ TEST_CASE("CombatSystem: no fire when target is out of range", "[combat]")
const entt::entity player = f.ships.spawn(combatDef->id, 1, QVector2D(500.0f, 0.0f), false); const entt::entity player = f.ships.spawn(combatDef->id, 1, QVector2D(500.0f, 0.0f), false);
f.wireEnemyTarget(enemy, player); f.wireEnemyTarget(enemy, player);
std::vector<FireEvent> events; std::vector<WeaponFiredEvent> events;
f.combat.tick(0, f.admin, f.buildings, events); f.combat.tick(0, f.admin, f.buildings, events);
REQUIRE(events.empty()); REQUIRE(events.empty());
} }
@@ -204,9 +204,9 @@ TEST_CASE("CombatSystem: player station fires at enemy ship in range", "[combat]
sim.tick(); sim.tick();
const std::vector<FireEvent> events = sim.drainFireEvents(); const std::vector<WeaponFiredEvent> events = sim.drainWeaponFiredEvents();
bool stationFired = false; bool stationFired = false;
for (const FireEvent& evt : events) for (const WeaponFiredEvent& evt : events)
{ {
if (evt.shooter == stationEntity) { stationFired = true; } if (evt.shooter == stationEntity) { stationFired = true; }
} }
@@ -242,9 +242,9 @@ TEST_CASE("CombatSystem: enemy station fires at player ship in range", "[combat]
sim.tick(); sim.tick();
const std::vector<FireEvent> events = sim.drainFireEvents(); const std::vector<WeaponFiredEvent> events = sim.drainWeaponFiredEvents();
bool stationFired = false; bool stationFired = false;
for (const FireEvent& evt : events) for (const WeaponFiredEvent& evt : events)
{ {
if (evt.shooter == stationEntity) { stationFired = true; } if (evt.shooter == stationEntity) { stationFired = true; }
} }
@@ -280,9 +280,9 @@ TEST_CASE("CombatSystem: player ship fires at enemy station in range", "[combat]
sim.tick(); sim.tick();
const std::vector<FireEvent> events = sim.drainFireEvents(); const std::vector<WeaponFiredEvent> events = sim.drainWeaponFiredEvents();
bool playerFiredAtStation = false; bool playerFiredAtStation = false;
for (const FireEvent& evt : events) for (const WeaponFiredEvent& evt : events)
{ {
if (evt.shooter == playerShip && evt.target == stationEntity) if (evt.shooter == playerShip && evt.target == stationEntity)
{ {
@@ -308,7 +308,7 @@ TEST_CASE("CombatSystem: damage not applied before impact tick", "[combat]")
const float hpBefore = f.admin.get<HealthComponent>(player).hp; const float hpBefore = f.admin.get<HealthComponent>(player).hp;
std::vector<FireEvent> events; std::vector<WeaponFiredEvent> events;
f.combat.tick(0, f.admin, f.buildings, events); f.combat.tick(0, f.admin, f.buildings, events);
for (Tick t = 1; t < 5; ++t) for (Tick t = 1; t < 5; ++t)
@@ -330,7 +330,7 @@ TEST_CASE("CombatSystem: damage applied exactly at impact tick", "[combat]")
const float hpBefore = f.admin.get<HealthComponent>(player).hp; const float hpBefore = f.admin.get<HealthComponent>(player).hp;
std::vector<FireEvent> events; std::vector<WeaponFiredEvent> events;
f.combat.tick(0, f.admin, f.buildings, events); f.combat.tick(0, f.admin, f.buildings, events);
f.combat.applyPendingDamage(5, f.admin); f.combat.applyPendingDamage(5, f.admin);
@@ -347,7 +347,7 @@ TEST_CASE("CombatSystem: damage silently dropped if target already dead", "[comb
const entt::entity player = f.ships.spawn(combatDef->id, 1, QVector2D(4.0f, 5.0f), false); const entt::entity player = f.ships.spawn(combatDef->id, 1, QVector2D(4.0f, 5.0f), false);
f.wireEnemyTarget(enemy, player); f.wireEnemyTarget(enemy, player);
std::vector<FireEvent> events; std::vector<WeaponFiredEvent> events;
f.combat.tick(0, f.admin, f.buildings, events); f.combat.tick(0, f.admin, f.buildings, events);
f.ships.despawn(player); f.ships.despawn(player);
@@ -370,7 +370,7 @@ TEST_CASE("CombatSystem: damage still applied if shooter already dead", "[combat
const float hpBefore = f.admin.get<HealthComponent>(player).hp; const float hpBefore = f.admin.get<HealthComponent>(player).hp;
std::vector<FireEvent> events; std::vector<WeaponFiredEvent> events;
f.combat.tick(0, f.admin, f.buildings, events); f.combat.tick(0, f.admin, f.buildings, events);
f.ships.despawn(enemy); f.ships.despawn(enemy);

View File

@@ -43,22 +43,22 @@ TEST_CASE("Simulation::tick 10 times yields currentTick == 10", "[simulation]")
REQUIRE(sim.currentTick() == 10); REQUIRE(sim.currentTick() == 10);
} }
TEST_CASE("Simulation::drainFireEvents returns empty initially", "[simulation]") TEST_CASE("Simulation::drainWeaponFiredEvents returns empty initially", "[simulation]")
{ {
Simulation sim(loadConfig()); Simulation sim(loadConfig());
REQUIRE(sim.drainFireEvents().empty()); REQUIRE(sim.drainWeaponFiredEvents().empty());
} }
TEST_CASE("Simulation::drainFireEvents clears queue on drain", "[simulation]") TEST_CASE("Simulation::drainWeaponFiredEvents clears queue on drain", "[simulation]")
{ {
Simulation sim(loadConfig()); Simulation sim(loadConfig());
// First drain: empty. // First drain: empty.
sim.drainFireEvents(); sim.drainWeaponFiredEvents();
// Second drain must also be empty (not a double-return). // Second drain must also be empty (not a double-return).
REQUIRE(sim.drainFireEvents().empty()); REQUIRE(sim.drainWeaponFiredEvents().empty());
} }
TEST_CASE("Simulation::hasSchematicChoicesPending returns false initially", "[simulation]") TEST_CASE("Simulation::hasSchematicChoicesPending returns false initially", "[simulation]")

View File

@@ -12,8 +12,11 @@
#include <QScrollArea> #include <QScrollArea>
#include <QVBoxLayout> #include <QVBoxLayout>
#include "BlueprintPlacementRequestedEvent.h"
#include "BlueprintSerializer.h" #include "BlueprintSerializer.h"
#include "BuildingBlocksChangedEvent.h" #include "BuildingBlocksChangedEvent.h"
#include "EventManager.h"
#include "ExitBlueprintModeRequestedEvent.h"
#include "Building.h" #include "Building.h"
#include "BuildingSystem.h" #include "BuildingSystem.h"
@@ -62,12 +65,12 @@ BlueprintPanel::BlueprintPanel(Simulation* sim, const GameConfig* config, QWidge
connect(m_saveBtn, &QPushButton::clicked, this, &BlueprintPanel::onSaveClicked); connect(m_saveBtn, &QPushButton::clicked, this, &BlueprintPanel::onSaveClicked);
connect(m_loadBtn, &QPushButton::clicked, this, &BlueprintPanel::onLoadClicked); connect(m_loadBtn, &QPushButton::clicked, this, &BlueprintPanel::onLoadClicked);
registerForEvent(); registerForEvents();
} }
BlueprintPanel::~BlueprintPanel() BlueprintPanel::~BlueprintPanel()
{ {
unregisterForEvent(); unregisterForEvents();
} }
void BlueprintPanel::onSelectionChanged(const std::vector<BuildingId>& ids) void BlueprintPanel::onSelectionChanged(const std::vector<BuildingId>& ids)
@@ -114,7 +117,8 @@ void BlueprintPanel::onDeleteBlueprintClicked(int index)
if (m_activeIndex == index) if (m_activeIndex == index)
{ {
m_activeIndex = -1; m_activeIndex = -1;
emit exitBlueprintModeRequested(); EventManager::getInstance()->sendEventImmediately(
std::make_shared<ExitBlueprintModeRequestedEvent>());
} }
else if (m_activeIndex > index) else if (m_activeIndex > index)
{ {
@@ -131,7 +135,8 @@ void BlueprintPanel::onBlueprintButtonClicked(int index)
if (m_activeIndex == index) if (m_activeIndex == index)
{ {
clearActiveBlueprintButton(); clearActiveBlueprintButton();
emit exitBlueprintModeRequested(); EventManager::getInstance()->sendEventImmediately(
std::make_shared<ExitBlueprintModeRequestedEvent>());
return; return;
} }
@@ -142,7 +147,8 @@ void BlueprintPanel::onBlueprintButtonClicked(int index)
m_activeIndex = index; m_activeIndex = index;
m_blueprintButtons[static_cast<std::size_t>(index)]->setChecked(true); m_blueprintButtons[static_cast<std::size_t>(index)]->setChecked(true);
emit blueprintPlacementRequested(m_blueprints[static_cast<std::size_t>(index)]); EventManager::getInstance()->sendEventImmediately(
std::make_shared<BlueprintPlacementRequestedEvent>(m_blueprints[static_cast<std::size_t>(index)]));
} }
Blueprint BlueprintPanel::createBlueprintFromSelection() const Blueprint BlueprintPanel::createBlueprintFromSelection() const
@@ -312,7 +318,8 @@ void BlueprintPanel::onLoadClicked()
if (m_activeIndex >= 0) if (m_activeIndex >= 0)
{ {
emit exitBlueprintModeRequested(); EventManager::getInstance()->sendEventImmediately(
std::make_shared<ExitBlueprintModeRequestedEvent>());
m_activeIndex = -1; m_activeIndex = -1;
} }
@@ -350,3 +357,13 @@ void BlueprintPanel::refreshButtonStates()
canAfford || m_activeIndex == i); canAfford || m_activeIndex == i);
} }
} }
void BlueprintPanel::handleEvent(std::shared_ptr<const SelectionChangedEvent> event)
{
onSelectionChanged(event->ids);
}
void BlueprintPanel::handleEvent(std::shared_ptr<const BlueprintModeExitedEvent> /*event*/)
{
clearActiveBlueprintButton();
}

View File

@@ -5,10 +5,12 @@
#include <QWidget> #include <QWidget>
#include "Blueprint.h" #include "Blueprint.h"
#include "BlueprintModeExitedEvent.h"
#include "BuildingBlocksChangedEvent.h" #include "BuildingBlocksChangedEvent.h"
#include "BuildingId.h" #include "BuildingId.h"
#include "EventHandler.h" #include "EventHandler.h"
#include "GameConfig.h" #include "GameConfig.h"
#include "SelectionChangedEvent.h"
#include "Tick.h" #include "Tick.h"
class Simulation; class Simulation;
@@ -17,7 +19,9 @@ class QScrollArea;
class QVBoxLayout; class QVBoxLayout;
class BlueprintPanel : public QWidget, class BlueprintPanel : public QWidget,
public EventHandler<BuildingBlocksChangedEvent> public CombinedEventHandler<BuildingBlocksChangedEvent,
SelectionChangedEvent,
BlueprintModeExitedEvent>
{ {
Q_OBJECT Q_OBJECT
@@ -25,16 +29,10 @@ public:
BlueprintPanel(Simulation* sim, const GameConfig* config, QWidget* parent = nullptr); BlueprintPanel(Simulation* sim, const GameConfig* config, QWidget* parent = nullptr);
~BlueprintPanel() override; ~BlueprintPanel() override;
public slots:
void onSelectionChanged(const std::vector<BuildingId>& ids);
void clearActiveBlueprintButton();
signals:
void blueprintPlacementRequested(Blueprint blueprint);
void exitBlueprintModeRequested();
private: private:
void handleEvent(std::shared_ptr<const BuildingBlocksChangedEvent> event) override; void handleEvent(std::shared_ptr<const BuildingBlocksChangedEvent> event) override;
void handleEvent(std::shared_ptr<const SelectionChangedEvent> event) override;
void handleEvent(std::shared_ptr<const BlueprintModeExitedEvent> event) override;
private slots: private slots:
void onCreateClicked(); void onCreateClicked();
@@ -44,6 +42,8 @@ private slots:
void onLoadClicked(); void onLoadClicked();
private: private:
void onSelectionChanged(const std::vector<BuildingId>& ids);
void clearActiveBlueprintButton();
Blueprint createBlueprintFromSelection() const; Blueprint createBlueprintFromSelection() const;
int computeBlueprintCost(const Blueprint& bp) const; int computeBlueprintCost(const Blueprint& bp) const;
void rebuildButtons(); void rebuildButtons();

View File

@@ -7,7 +7,11 @@
#include <QSignalMapper> #include <QSignalMapper>
#include "BuildingType.h" #include "BuildingType.h"
#include "BuildingTypeSelectedEvent.h"
#include "DemolishModeToggleRequestedEvent.h"
#include "DisplayName.h" #include "DisplayName.h"
#include "EventManager.h"
#include "ExitBuilderModeRequestedEvent.h"
BuildButtonGrid::BuildButtonGrid(const GameConfig* config, QWidget* parent) BuildButtonGrid::BuildButtonGrid(const GameConfig* config, QWidget* parent)
@@ -58,7 +62,17 @@ BuildButtonGrid::BuildButtonGrid(const GameConfig* config, QWidget* parent)
m_demolishButton->setCheckable(true); m_demolishButton->setCheckable(true);
m_demolishButton->setFixedHeight(48); m_demolishButton->setFixedHeight(48);
layout->addWidget(m_demolishButton, row, col); layout->addWidget(m_demolishButton, row, col);
connect(m_demolishButton, SIGNAL(clicked()), this, SIGNAL(demolishModeToggleRequested())); connect(m_demolishButton, &QPushButton::clicked, this, [this]() {
EventManager::getInstance()->sendEventImmediately(
std::make_shared<DemolishModeToggleRequestedEvent>());
});
registerForEvents();
}
BuildButtonGrid::~BuildButtonGrid()
{
unregisterForEvents();
} }
void BuildButtonGrid::updateAffordability(int buildingBlocks) void BuildButtonGrid::updateAffordability(int buildingBlocks)
@@ -72,11 +86,6 @@ void BuildButtonGrid::updateAffordability(int buildingBlocks)
} }
} }
void BuildButtonGrid::setDemolishModeActive(bool active)
{
m_demolishButton->setChecked(active);
}
void BuildButtonGrid::clearActiveButton() void BuildButtonGrid::clearActiveButton()
{ {
if (m_activeIndex >= 0 && m_activeIndex < static_cast<int>(m_buttons.size())) if (m_activeIndex >= 0 && m_activeIndex < static_cast<int>(m_buttons.size()))
@@ -96,7 +105,8 @@ void BuildButtonGrid::onBuildButton(int index)
if (m_activeIndex == index) if (m_activeIndex == index)
{ {
clearActiveButton(); clearActiveButton();
emit builderModeExited(); EventManager::getInstance()->sendEventImmediately(
std::make_shared<ExitBuilderModeRequestedEvent>());
return; return;
} }
@@ -107,5 +117,16 @@ void BuildButtonGrid::onBuildButton(int index)
m_activeIndex = index; m_activeIndex = index;
m_buttons[static_cast<std::size_t>(index)]->setChecked(true); m_buttons[static_cast<std::size_t>(index)]->setChecked(true);
emit buildingTypeSelected(m_types[static_cast<std::size_t>(index)]); EventManager::getInstance()->sendEventImmediately(
std::make_shared<BuildingTypeSelectedEvent>(m_types[static_cast<std::size_t>(index)]));
}
void BuildButtonGrid::handleEvent(std::shared_ptr<const BuilderModeExitedEvent> /*event*/)
{
clearActiveButton();
}
void BuildButtonGrid::handleEvent(std::shared_ptr<const DemolishModeChangedEvent> event)
{
m_demolishButton->setChecked(event->active);
} }

View File

@@ -5,28 +5,30 @@
#include <QWidget> #include <QWidget>
#include "BuilderModeExitedEvent.h"
#include "BuildingType.h" #include "BuildingType.h"
#include "DemolishModeChangedEvent.h"
#include "EventHandler.h"
#include "GameConfig.h" #include "GameConfig.h"
class QPushButton; class QPushButton;
class BuildButtonGrid : public QWidget class BuildButtonGrid : public QWidget,
public CombinedEventHandler<BuilderModeExitedEvent,
DemolishModeChangedEvent>
{ {
Q_OBJECT Q_OBJECT
public: public:
BuildButtonGrid(const GameConfig* config, QWidget* parent = nullptr); BuildButtonGrid(const GameConfig* config, QWidget* parent = nullptr);
~BuildButtonGrid() override;
void updateAffordability(int buildingBlocks); void updateAffordability(int buildingBlocks);
void clearActiveButton(); void clearActiveButton();
signals: private:
void buildingTypeSelected(BuildingType type); void handleEvent(std::shared_ptr<const BuilderModeExitedEvent> event) override;
void builderModeExited(); void handleEvent(std::shared_ptr<const DemolishModeChangedEvent> event) override;
void demolishModeToggleRequested();
public slots:
void setDemolishModeActive(bool active);
private slots: private slots:
void onBuildButton(int index); void onBuildButton(int index);

View File

@@ -20,14 +20,17 @@
#include "BeltSystem.h" #include "BeltSystem.h"
#include "Building.h" #include "Building.h"
#include "BuildingSystem.h" #include "BuildingSystem.h"
#include "DemolishModeChangedEvent.h"
#include "EntityHitTest.h" #include "EntityHitTest.h"
#include "EntitySelectedEvent.h" #include "EntitySelectedEvent.h"
#include "EventManager.h" #include "EventManager.h"
#include "FacingComponent.h" #include "FacingComponent.h"
#include "FactionComponent.h" #include "FactionComponent.h"
#include "GameOverEvent.h"
#include "HealthComponent.h" #include "HealthComponent.h"
#include "PositionComponent.h" #include "PositionComponent.h"
#include "ScrapSystem.h" #include "ScrapSystem.h"
#include "SelectionChangedEvent.h"
#include "SensorRangeComponent.h" #include "SensorRangeComponent.h"
#include "ShipIdentityComponent.h" #include "ShipIdentityComponent.h"
#include "ShipSystem.h" #include "ShipSystem.h"
@@ -35,8 +38,11 @@
#include "StationBodyComponent.h" #include "StationBodyComponent.h"
#include "SurfaceMask.h" #include "SurfaceMask.h"
#include "Tick.h" #include "Tick.h"
#include "EscapeMenuRequestedEvent.h"
#include "TracePrintRequestedEvent.h" #include "TracePrintRequestedEvent.h"
#include "BossWaveUpdatedEvent.h" #include "BossWaveUpdatedEvent.h"
#include "BuilderModeExitedEvent.h"
#include "BlueprintModeExitedEvent.h"
#include "BuildingBlocksChangedEvent.h" #include "BuildingBlocksChangedEvent.h"
#include "GameSpeedChangedEvent.h" #include "GameSpeedChangedEvent.h"
#include "SchematicChoicesAvailableEvent.h" #include "SchematicChoicesAvailableEvent.h"
@@ -115,6 +121,13 @@ GameWorldView::GameWorldView(Simulation* sim, const GameConfig* config,
connect(m_renderTimer, &QTimer::timeout, this, &GameWorldView::onFrame); connect(m_renderTimer, &QTimer::timeout, this, &GameWorldView::onFrame);
m_renderTimer->start(); m_renderTimer->start();
m_frameTimer.start(); m_frameTimer.start();
registerForEvents();
}
GameWorldView::~GameWorldView()
{
unregisterForEvents();
} }
void GameWorldView::initializeGL() void GameWorldView::initializeGL()
@@ -137,31 +150,13 @@ void GameWorldView::onFrame()
} }
} }
// Drain fire events → active beams // Emit fire events via EventManager
{ {
const std::vector<FireEvent> fires = m_sim->drainFireEvents(); const std::vector<WeaponFiredEvent> fires = m_sim->drainWeaponFiredEvents();
for (const FireEvent& fe : fires) for (const WeaponFiredEvent& fe : fires)
{ {
float maxRadius = 0.125f; EventManager::getInstance()->sendEventImmediately(
if (m_sim->admin().isValid(fe.target) std::make_shared<WeaponFiredEvent>(fe));
&& m_sim->admin().hasAll<StationBodyComponent>(fe.target))
{
const StationBodyComponent& sb = m_sim->admin().get<StationBodyComponent>(fe.target);
const int shorter = std::min(sb.footprint.width(), sb.footprint.height());
maxRadius = shorter / 2.0f;
}
std::uniform_real_distribution<float> angleDist(0.0f, 6.28318530f);
std::uniform_real_distribution<float> radiusDist(0.0f, maxRadius);
const float angle = angleDist(m_rng);
const float radius = radiusDist(m_rng);
ActiveBeam beam;
beam.event = fe;
beam.emittedWallMs = m_wallMs;
beam.targetOffset = QVector2D(radius * std::cos(angle),
radius * std::sin(angle));
m_activeBeams.push_back(beam);
} }
} }
@@ -232,7 +227,8 @@ void GameWorldView::onFrame()
{ {
m_gameOverShown = true; m_gameOverShown = true;
m_gameSpeedMultiplier = 0.0; m_gameSpeedMultiplier = 0.0;
emit gameOver(); EventManager::getInstance()->sendEventImmediately(
std::make_shared<GameOverEvent>());
} }
update(); update();
@@ -1140,7 +1136,8 @@ void GameWorldView::keyPressEvent(QKeyEvent* event)
} }
break; break;
case Qt::Key_Escape: case Qt::Key_Escape:
emit escapeMenuRequested(); EventManager::getInstance()->sendEventImmediately(
std::make_shared<EscapeMenuRequestedEvent>());
break; break;
case Qt::Key_Backspace: case Qt::Key_Backspace:
toggleDemolishMode(); toggleDemolishMode();
@@ -1227,7 +1224,8 @@ void GameWorldView::mousePressEvent(QMouseEvent* event)
if (hitEntity != entt::null) if (hitEntity != entt::null)
{ {
m_selectedBuildingIds.clear(); m_selectedBuildingIds.clear();
emit selectionChanged(m_selectedBuildingIds); EventManager::getInstance()->sendEventImmediately(
std::make_shared<SelectionChangedEvent>(m_selectedBuildingIds));
m_selectedEntity = hitEntity; m_selectedEntity = hitEntity;
EventManager::getInstance()->sendEventImmediately( EventManager::getInstance()->sendEventImmediately(
std::make_shared<EntitySelectedEvent>(hitEntity)); std::make_shared<EntitySelectedEvent>(hitEntity));
@@ -1264,14 +1262,16 @@ void GameWorldView::mousePressEvent(QMouseEvent* event)
{ {
m_selectedBuildingIds = { id }; m_selectedBuildingIds = { id };
} }
emit selectionChanged(m_selectedBuildingIds); EventManager::getInstance()->sendEventImmediately(
std::make_shared<SelectionChangedEvent>(m_selectedBuildingIds));
} }
else else
{ {
if (!(event->modifiers() & Qt::ControlModifier)) if (!(event->modifiers() & Qt::ControlModifier))
{ {
m_selectedBuildingIds.clear(); m_selectedBuildingIds.clear();
emit selectionChanged(m_selectedBuildingIds); EventManager::getInstance()->sendEventImmediately(
std::make_shared<SelectionChangedEvent>(m_selectedBuildingIds));
} }
m_boxSelecting = true; m_boxSelecting = true;
m_boxStartTile = tile; m_boxStartTile = tile;
@@ -1370,12 +1370,13 @@ void GameWorldView::mouseReleaseEvent(QMouseEvent* event)
if (!found) { m_selectedBuildingIds.push_back(id); } if (!found) { m_selectedBuildingIds.push_back(id); }
} }
} }
emit selectionChanged(m_selectedBuildingIds); EventManager::getInstance()->sendEventImmediately(
std::make_shared<SelectionChangedEvent>(m_selectedBuildingIds));
} }
} }
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
// Slots // Methods (formerly slots)
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
void GameWorldView::toggleDemolishMode() void GameWorldView::toggleDemolishMode()
@@ -1391,7 +1392,8 @@ void GameWorldView::toggleDemolishMode()
if (m_blueprintMode.has_value()) { exitBlueprintMode(); } if (m_blueprintMode.has_value()) { exitBlueprintMode(); }
m_demolishMode = true; m_demolishMode = true;
} }
emit demolishModeChanged(m_demolishMode); EventManager::getInstance()->sendEventImmediately(
std::make_shared<DemolishModeChangedEvent>(m_demolishMode));
} }
void GameWorldView::enterBuilderMode(BuildingType type) void GameWorldView::enterBuilderMode(BuildingType type)
@@ -1401,14 +1403,16 @@ void GameWorldView::enterBuilderMode(BuildingType type)
m_ghostValid = false; m_ghostValid = false;
m_demolishMode = false; m_demolishMode = false;
m_blueprintMode.reset(); m_blueprintMode.reset();
emit demolishModeChanged(false); EventManager::getInstance()->sendEventImmediately(
std::make_shared<DemolishModeChangedEvent>(false));
} }
void GameWorldView::enterBlueprintMode(Blueprint blueprint) void GameWorldView::enterBlueprintMode(Blueprint blueprint)
{ {
if (m_builderType.has_value()) { exitBuilderMode(); } if (m_builderType.has_value()) { exitBuilderMode(); }
m_demolishMode = false; m_demolishMode = false;
emit demolishModeChanged(false); EventManager::getInstance()->sendEventImmediately(
std::make_shared<DemolishModeChangedEvent>(false));
m_blueprintGhostTile = m_ghostTile; m_blueprintGhostTile = m_ghostTile;
m_blueprintMode = std::move(blueprint); m_blueprintMode = std::move(blueprint);
} }
@@ -1416,7 +1420,8 @@ void GameWorldView::enterBlueprintMode(Blueprint blueprint)
void GameWorldView::exitBlueprintMode() void GameWorldView::exitBlueprintMode()
{ {
m_blueprintMode.reset(); m_blueprintMode.reset();
emit blueprintModeExited(); EventManager::getInstance()->sendEventImmediately(
std::make_shared<BlueprintModeExitedEvent>());
} }
void GameWorldView::exitBuilderMode() void GameWorldView::exitBuilderMode()
@@ -1424,7 +1429,8 @@ void GameWorldView::exitBuilderMode()
m_builderType.reset(); m_builderType.reset();
m_beltDragTiles.clear(); m_beltDragTiles.clear();
m_dragging = false; m_dragging = false;
emit builderModeExited(); EventManager::getInstance()->sendEventImmediately(
std::make_shared<BuilderModeExitedEvent>());
} }
double GameWorldView::gameSpeed() const double GameWorldView::gameSpeed() const
@@ -1454,7 +1460,8 @@ void GameWorldView::resetForNewGame()
m_ghostValid = false; m_ghostValid = false;
m_demolishMode = false; m_demolishMode = false;
m_demolishHoverBuildingId = kInvalidBuildingId; m_demolishHoverBuildingId = kInvalidBuildingId;
emit demolishModeChanged(false); EventManager::getInstance()->sendEventImmediately(
std::make_shared<DemolishModeChangedEvent>(false));
m_selectedBuildingIds.clear(); m_selectedBuildingIds.clear();
m_boxSelecting = false; m_boxSelecting = false;
m_scrollXTiles = 0.0f; m_scrollXTiles = 0.0f;
@@ -1466,7 +1473,66 @@ void GameWorldView::resetForNewGame()
m_lastBlocks = -1; m_lastBlocks = -1;
m_lastBossCounter = -1; m_lastBossCounter = -1;
m_lastBossCountdown = Tick(-1); m_lastBossCountdown = Tick(-1);
emit selectionChanged({}); EventManager::getInstance()->sendEventImmediately(
std::make_shared<SelectionChangedEvent>(std::vector<BuildingId>{}));
setGameSpeed(1.0); setGameSpeed(1.0);
update(); update();
} }
// ---------------------------------------------------------------------------
// Event handlers
// ---------------------------------------------------------------------------
void GameWorldView::handleEvent(std::shared_ptr<const WeaponFiredEvent> event)
{
float maxRadius = 0.125f;
if (m_sim->admin().isValid(event->target)
&& m_sim->admin().hasAll<StationBodyComponent>(event->target))
{
const StationBodyComponent& sb = m_sim->admin().get<StationBodyComponent>(event->target);
const int shorter = std::min(sb.footprint.width(), sb.footprint.height());
maxRadius = shorter / 2.0f;
}
std::uniform_real_distribution<float> angleDist(0.0f, 6.28318530f);
std::uniform_real_distribution<float> radiusDist(0.0f, maxRadius);
const float angle = angleDist(m_rng);
const float radius = radiusDist(m_rng);
ActiveBeam beam;
beam.event = *event;
beam.emittedWallMs = m_wallMs;
beam.targetOffset = QVector2D(radius * std::cos(angle),
radius * std::sin(angle));
m_activeBeams.push_back(beam);
}
void GameWorldView::handleEvent(std::shared_ptr<const BuildingTypeSelectedEvent> event)
{
enterBuilderMode(event->type);
}
void GameWorldView::handleEvent(std::shared_ptr<const ExitBuilderModeRequestedEvent> /*event*/)
{
exitBuilderMode();
}
void GameWorldView::handleEvent(std::shared_ptr<const DemolishModeToggleRequestedEvent> /*event*/)
{
toggleDemolishMode();
}
void GameWorldView::handleEvent(std::shared_ptr<const BlueprintPlacementRequestedEvent> event)
{
enterBlueprintMode(event->blueprint);
}
void GameWorldView::handleEvent(std::shared_ptr<const ExitBlueprintModeRequestedEvent> /*event*/)
{
exitBlueprintMode();
}
void GameWorldView::handleEvent(std::shared_ptr<const SpeedChangeRequestedEvent> event)
{
setGameSpeed(event->multiplier);
}

View File

@@ -13,10 +13,20 @@
#include <QVector2D> #include <QVector2D>
#include "Blueprint.h" #include "Blueprint.h"
#include "SchematicChoiceOption.h" #include "BlueprintModeExitedEvent.h"
#include "BlueprintPlacementRequestedEvent.h"
#include "BuilderModeExitedEvent.h"
#include "BuildingType.h" #include "BuildingType.h"
#include "BuildingTypeSelectedEvent.h"
#include "BuildingId.h" #include "BuildingId.h"
#include "FireEvent.h" #include "DemolishModeChangedEvent.h"
#include "DemolishModeToggleRequestedEvent.h"
#include "EventHandler.h"
#include "ExitBlueprintModeRequestedEvent.h"
#include "ExitBuilderModeRequestedEvent.h"
#include "WeaponFiredEvent.h"
#include "SchematicChoiceOption.h"
#include "SpeedChangeRequestedEvent.h"
#include "entt/entity/entity.hpp" #include "entt/entity/entity.hpp"
#include "EntitySelectedEvent.h" #include "EntitySelectedEvent.h"
@@ -38,32 +48,24 @@ struct QPointCompare
} }
}; };
class GameWorldView : public QOpenGLWidget class GameWorldView : public QOpenGLWidget,
public CombinedEventHandler<WeaponFiredEvent,
BuildingTypeSelectedEvent,
ExitBuilderModeRequestedEvent,
DemolishModeToggleRequestedEvent,
BlueprintPlacementRequestedEvent,
ExitBlueprintModeRequestedEvent,
SpeedChangeRequestedEvent>
{ {
Q_OBJECT Q_OBJECT
public: public:
GameWorldView(Simulation* sim, const GameConfig* config, GameWorldView(Simulation* sim, const GameConfig* config,
const VisualsConfig* visuals, QWidget* parent = nullptr); const VisualsConfig* visuals, QWidget* parent = nullptr);
~GameWorldView() override;
signals:
void selectionChanged(const std::vector<BuildingId>& ids);
void gameOver();
void builderModeExited();
void blueprintModeExited();
void escapeMenuRequested();
void demolishModeChanged(bool active);
public:
double gameSpeed() const; double gameSpeed() const;
void resetFrameTimer(); void resetFrameTimer();
public slots:
void enterBuilderMode(BuildingType type);
void exitBuilderMode();
void enterBlueprintMode(Blueprint blueprint);
void exitBlueprintMode();
void toggleDemolishMode();
void setGameSpeed(double multiplier); void setGameSpeed(double multiplier);
void resetForNewGame(); void resetForNewGame();
@@ -80,6 +82,14 @@ private slots:
void onFrame(); void onFrame();
private: private:
void handleEvent(std::shared_ptr<const WeaponFiredEvent> 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 drawTiles(QPainter& painter); void drawTiles(QPainter& painter);
void drawBuildings(QPainter& painter); void drawBuildings(QPainter& painter);
void drawStations(QPainter& painter); void drawStations(QPainter& painter);
@@ -119,9 +129,15 @@ private:
void stepSpeed(int delta); void stepSpeed(int delta);
void placeAtTile(QPoint tile); void placeAtTile(QPoint tile);
void enterBuilderMode(BuildingType type);
void exitBuilderMode();
void enterBlueprintMode(Blueprint blueprint);
void exitBlueprintMode();
void toggleDemolishMode();
struct ActiveBeam struct ActiveBeam
{ {
FireEvent event; WeaponFiredEvent event;
qint64 emittedWallMs; qint64 emittedWallMs;
QVector2D targetOffset; QVector2D targetOffset;
}; };

View File

@@ -8,6 +8,8 @@
#include <QPushButton> #include <QPushButton>
#include <QSignalMapper> #include <QSignalMapper>
#include "EventManager.h"
#include "SpeedChangeRequestedEvent.h"
#include "Tick.h" #include "Tick.h"
const double HeaderBar::kSpeeds[] = { 0.0, 0.5, 1.0, 2.0, 10.0 }; const double HeaderBar::kSpeeds[] = { 0.0, 0.5, 1.0, 2.0, 10.0 };
@@ -90,6 +92,7 @@ void HeaderBar::onSpeedButton(int index)
{ {
if (index >= 0 && index < kSpeedCount) if (index >= 0 && index < kSpeedCount)
{ {
emit speedChanged(kSpeeds[index]); EventManager::getInstance()->sendEventImmediately(
std::make_shared<SpeedChangeRequestedEvent>(kSpeeds[index]));
} }
} }

View File

@@ -26,9 +26,6 @@ public:
explicit HeaderBar(QWidget* parent = nullptr); explicit HeaderBar(QWidget* parent = nullptr);
~HeaderBar() override; ~HeaderBar() override;
signals:
void speedChanged(double multiplier);
private slots: private slots:
void onSpeedButton(int index); void onSpeedButton(int index);

View File

@@ -53,52 +53,6 @@ MainWindow::MainWindow(Simulation* sim, const std::string& configDir, QWidget* p
bottomLayout->addWidget(m_buildButtonGrid, 1); bottomLayout->addWidget(m_buildButtonGrid, 1);
bottomLayout->addWidget(m_blueprintPanel, 1); bottomLayout->addWidget(m_blueprintPanel, 1);
// Signals: game world → other panels
connect(m_gameWorldView, &GameWorldView::selectionChanged,
m_selectedBuildingPanel, &SelectedBuildingPanel::onSelectionChanged);
connect(m_gameWorldView, &GameWorldView::gameOver,
this, &MainWindow::onGameOver);
connect(m_gameWorldView, &GameWorldView::escapeMenuRequested,
this, &MainWindow::onEscapeMenuRequested);
connect(m_selectedBuildingPanel, &SelectedBuildingPanel::layoutDialogRequested,
this, &MainWindow::onLayoutDialogRequested);
// Signals: build grid → game world
connect(m_buildButtonGrid, &BuildButtonGrid::buildingTypeSelected,
m_gameWorldView, &GameWorldView::enterBuilderMode);
connect(m_buildButtonGrid, &BuildButtonGrid::builderModeExited,
m_gameWorldView, &GameWorldView::exitBuilderMode);
connect(m_gameWorldView, &GameWorldView::builderModeExited,
m_buildButtonGrid, &BuildButtonGrid::clearActiveButton);
connect(m_buildButtonGrid, &BuildButtonGrid::demolishModeToggleRequested,
m_gameWorldView, &GameWorldView::toggleDemolishMode);
connect(m_gameWorldView, &GameWorldView::demolishModeChanged,
m_buildButtonGrid, &BuildButtonGrid::setDemolishModeActive);
// Signals: blueprint panel ↔ game world
connect(m_gameWorldView, &GameWorldView::selectionChanged,
m_blueprintPanel, &BlueprintPanel::onSelectionChanged);
connect(m_blueprintPanel, &BlueprintPanel::blueprintPlacementRequested,
m_gameWorldView, &GameWorldView::enterBlueprintMode);
connect(m_blueprintPanel, &BlueprintPanel::exitBlueprintModeRequested,
m_gameWorldView, &GameWorldView::exitBlueprintMode);
connect(m_gameWorldView, &GameWorldView::blueprintModeExited,
m_blueprintPanel, &BlueprintPanel::clearActiveBlueprintButton);
// Signals: header bar → game world
connect(m_headerBar, &HeaderBar::speedChanged,
m_gameWorldView, &GameWorldView::setGameSpeed);
m_gameWorldView->setFocus(); m_gameWorldView->setFocus();
connect(qApp, &QApplication::focusChanged, this, [this](QWidget*, QWidget* newWidget) { connect(qApp, &QApplication::focusChanged, this, [this](QWidget*, QWidget* newWidget) {
@@ -191,7 +145,7 @@ void MainWindow::handleEvent(std::shared_ptr<const SchematicChoicesAvailableEven
m_gameWorldView->resetFrameTimer(); m_gameWorldView->resetFrameTimer();
} }
void MainWindow::onEscapeMenuRequested() void MainWindow::handleEvent(std::shared_ptr<const EscapeMenuRequestedEvent> /*event*/)
{ {
const double prevSpeed = m_gameWorldView->gameSpeed(); const double prevSpeed = m_gameWorldView->gameSpeed();
m_gameWorldView->setGameSpeed(0.0); m_gameWorldView->setGameSpeed(0.0);
@@ -235,12 +189,12 @@ void MainWindow::onEscapeMenuRequested()
} }
} }
void MainWindow::onLayoutDialogRequested(BuildingId shipyardId) void MainWindow::handleEvent(std::shared_ptr<const LayoutDialogRequestedEvent> event)
{ {
const double prevSpeed = m_gameWorldView->gameSpeed(); const double prevSpeed = m_gameWorldView->gameSpeed();
m_gameWorldView->setGameSpeed(0.0); m_gameWorldView->setGameSpeed(0.0);
const Building* b = m_sim->buildings().findBuilding(shipyardId); const Building* b = m_sim->buildings().findBuilding(event->shipyardId);
if (!b) if (!b)
{ {
m_gameWorldView->setGameSpeed(prevSpeed); m_gameWorldView->setGameSpeed(prevSpeed);
@@ -272,14 +226,14 @@ void MainWindow::onLayoutDialogRequested(BuildingId shipyardId)
this); this);
if (dialog.exec() == QDialog::Accepted && dialog.result().has_value()) if (dialog.exec() == QDialog::Accepted && dialog.result().has_value())
{ {
m_sim->buildings().setShipLayout(shipyardId, *dialog.result()); m_sim->buildings().setShipLayout(event->shipyardId, *dialog.result());
} }
m_gameWorldView->setGameSpeed(prevSpeed); m_gameWorldView->setGameSpeed(prevSpeed);
m_gameWorldView->resetFrameTimer(); m_gameWorldView->resetFrameTimer();
} }
void MainWindow::onGameOver() void MainWindow::handleEvent(std::shared_ptr<const GameOverEvent> /*event*/)
{ {
const Tick tick = m_sim->currentTick(); const Tick tick = m_sim->currentTick();
const int totalSeconds = static_cast<int>(ticksToSeconds(tick)); const int totalSeconds = static_cast<int>(ticksToSeconds(tick));

View File

@@ -7,7 +7,10 @@
#include "BuildingBlocksChangedEvent.h" #include "BuildingBlocksChangedEvent.h"
#include "BuildingId.h" #include "BuildingId.h"
#include "EscapeMenuRequestedEvent.h"
#include "EventHandler.h" #include "EventHandler.h"
#include "GameOverEvent.h"
#include "LayoutDialogRequestedEvent.h"
#include "SchematicChoicesAvailableEvent.h" #include "SchematicChoicesAvailableEvent.h"
#include "ShipLayoutBlueprint.h" #include "ShipLayoutBlueprint.h"
#include "Tick.h" #include "Tick.h"
@@ -24,7 +27,10 @@ class QResizeEvent;
class MainWindow : public QWidget, class MainWindow : public QWidget,
public CombinedEventHandler<BuildingBlocksChangedEvent, public CombinedEventHandler<BuildingBlocksChangedEvent,
SchematicChoicesAvailableEvent> SchematicChoicesAvailableEvent,
GameOverEvent,
EscapeMenuRequestedEvent,
LayoutDialogRequestedEvent>
{ {
Q_OBJECT Q_OBJECT
@@ -39,13 +45,11 @@ protected:
private: private:
void handleEvent(std::shared_ptr<const BuildingBlocksChangedEvent> event) override; void handleEvent(std::shared_ptr<const BuildingBlocksChangedEvent> event) override;
void handleEvent(std::shared_ptr<const SchematicChoicesAvailableEvent> event) override; void handleEvent(std::shared_ptr<const SchematicChoicesAvailableEvent> event) override;
void handleEvent(std::shared_ptr<const GameOverEvent> event) override;
void handleEvent(std::shared_ptr<const EscapeMenuRequestedEvent> event) override;
void handleEvent(std::shared_ptr<const LayoutDialogRequestedEvent> event) override;
void layoutPanels(); void layoutPanels();
private slots:
void onGameOver();
void onEscapeMenuRequested();
void onLayoutDialogRequested(BuildingId shipyardId);
private: private:
std::string m_configDir; std::string m_configDir;
VisualsConfig m_visuals; VisualsConfig m_visuals;

View File

@@ -16,6 +16,7 @@
#include "DynamicBodyComponent.h" #include "DynamicBodyComponent.h"
#include "EntityAdmin.h" #include "EntityAdmin.h"
#include "EntitySelectedEvent.h" #include "EntitySelectedEvent.h"
#include "EventManager.h"
#include "FactionComponent.h" #include "FactionComponent.h"
#include "HealthComponent.h" #include "HealthComponent.h"
#include "ModuleOwnerComponent.h" #include "ModuleOwnerComponent.h"
@@ -28,6 +29,7 @@
#include "BuildingSystem.h" #include "BuildingSystem.h"
#include "BuildingType.h" #include "BuildingType.h"
#include "ItemType.h" #include "ItemType.h"
#include "LayoutDialogRequestedEvent.h"
#include "ModulesConfig.h" #include "ModulesConfig.h"
#include "Rotation.h" #include "Rotation.h"
#include "ShipLayoutPreview.h" #include "ShipLayoutPreview.h"
@@ -139,7 +141,8 @@ SelectedBuildingPanel::SelectedBuildingPanel(Simulation* sim,
connect(m_configureLayoutBtn, &QPushButton::clicked, this, [this]() { connect(m_configureLayoutBtn, &QPushButton::clicked, this, [this]() {
if (m_singleBuildingId != kInvalidBuildingId) if (m_singleBuildingId != kInvalidBuildingId)
{ {
emit layoutDialogRequested(m_singleBuildingId); EventManager::getInstance()->sendEventImmediately(
std::make_shared<LayoutDialogRequestedEvent>(m_singleBuildingId));
} }
}); });
connect(m_filterAList, &QListWidget::itemChanged, connect(m_filterAList, &QListWidget::itemChanged,
@@ -861,3 +864,8 @@ void SelectedBuildingPanel::clearEntityDisplay()
m_entityStatsPanel->hide(); m_entityStatsPanel->hide();
m_stationStatsLabel->hide(); m_stationStatsLabel->hide();
} }
void SelectedBuildingPanel::handleEvent(std::shared_ptr<const SelectionChangedEvent> event)
{
onSelectionChanged(event->ids);
}

View File

@@ -15,6 +15,7 @@
#include "EventHandler.h" #include "EventHandler.h"
#include "GameConfig.h" #include "GameConfig.h"
#include "RecipesConfig.h" #include "RecipesConfig.h"
#include "SelectionChangedEvent.h"
#include "ShipLayout.h" #include "ShipLayout.h"
#include "ShipsConfig.h" #include "ShipsConfig.h"
#include "Tick.h" #include "Tick.h"
@@ -30,7 +31,9 @@ class QPushButton;
class QVBoxLayout; class QVBoxLayout;
class SelectedBuildingPanel : public QWidget, class SelectedBuildingPanel : public QWidget,
public CombinedEventHandler<TickAdvancedEvent, EntitySelectedEvent> public CombinedEventHandler<TickAdvancedEvent,
EntitySelectedEvent,
SelectionChangedEvent>
{ {
Q_OBJECT Q_OBJECT
@@ -39,15 +42,10 @@ public:
QWidget* parent = nullptr); QWidget* parent = nullptr);
~SelectedBuildingPanel() override; ~SelectedBuildingPanel() override;
signals:
void layoutDialogRequested(BuildingId shipyardId);
public slots:
void onSelectionChanged(const std::vector<BuildingId>& ids);
private: private:
void handleEvent(std::shared_ptr<const TickAdvancedEvent> event) override; void handleEvent(std::shared_ptr<const TickAdvancedEvent> event) override;
void handleEvent(std::shared_ptr<const EntitySelectedEvent> event) override; void handleEvent(std::shared_ptr<const EntitySelectedEvent> event) override;
void handleEvent(std::shared_ptr<const SelectionChangedEvent> event) override;
private slots: private slots:
void onRecipeChanged(int comboIndex); void onRecipeChanged(int comboIndex);
@@ -55,6 +53,7 @@ private slots:
void onSplitterFilterChanged(); void onSplitterFilterChanged();
private: private:
void onSelectionChanged(const std::vector<BuildingId>& ids);
void rebuild(); void rebuild();
void clearContent(); void clearContent();
void buildEmpty(); void buildEmpty();