Add artifact win condition (#3)
Reviewed-on: #3 Co-authored-by: Malte Langkabel <malte.langkabel@gmail.com> Co-committed-by: Malte Langkabel <malte.langkabel@gmail.com>
This commit was merged in pull request #3.
This commit is contained in:
@@ -301,6 +301,9 @@ WorldConfig ConfigLoader::loadWorld(const std::string& path)
|
||||
cfg.targeting.overclaimPenaltyFormula = requireFormula(tbl["targeting"]["overclaim_penalty_formula"], file, "targeting.overclaim_penalty_formula");
|
||||
cfg.targeting.hysteresis = requireDouble(tbl["targeting"]["target_hysteresis"], file, "targeting.target_hysteresis");
|
||||
|
||||
cfg.artifacts.artifactChanceFormula = requireFormula(tbl["artifacts"]["artifact_chance_formula"], file, "artifacts.artifact_chance_formula");
|
||||
cfg.artifacts.artifactWinCount = static_cast<int>(requireInt(tbl["artifacts"]["artifact_win_count"], file, "artifacts.artifact_win_count"));
|
||||
|
||||
return cfg;
|
||||
}
|
||||
|
||||
|
||||
@@ -47,6 +47,13 @@ struct WorldTargeting
|
||||
double hysteresis; // fractional margin a challenger must beat the current target by
|
||||
};
|
||||
|
||||
// Artifact win condition (REQ-WIN-ARTIFACT-COUNT, REQ-WIN-SCREEN).
|
||||
struct WorldArtifacts
|
||||
{
|
||||
Formula artifactChanceFormula; // x = station level, result clamped to [0,1]
|
||||
int artifactWinCount;
|
||||
};
|
||||
|
||||
struct WorldConfig
|
||||
{
|
||||
int heightTiles; // REQ-GW-HEIGHT
|
||||
@@ -65,4 +72,5 @@ struct WorldConfig
|
||||
WorldPush push;
|
||||
WorldWaves waves;
|
||||
WorldTargeting targeting;
|
||||
WorldArtifacts artifacts;
|
||||
};
|
||||
|
||||
@@ -7,7 +7,8 @@ enum class SchematicType
|
||||
{
|
||||
Ship,
|
||||
Module,
|
||||
Recipe
|
||||
Recipe,
|
||||
Artifact
|
||||
};
|
||||
|
||||
// One option presented to the player in the schematic choice dialog
|
||||
|
||||
11
src/lib/eventsystem/event/ArtifactCountChangedEvent.h
Normal file
11
src/lib/eventsystem/event/ArtifactCountChangedEvent.h
Normal file
@@ -0,0 +1,11 @@
|
||||
#pragma once
|
||||
|
||||
#include "Event.h"
|
||||
|
||||
class ArtifactCountChangedEvent : public Event
|
||||
{
|
||||
public:
|
||||
ArtifactCountChangedEvent(int count, int winCount) : count(count), winCount(winCount) {}
|
||||
const int count;
|
||||
const int winCount;
|
||||
};
|
||||
@@ -9,6 +9,8 @@ SET(HDRS
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/SchematicChoicesAvailableEvent.h
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/SelectionChangedEvent.h
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/GameOverEvent.h
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/WinEvent.h
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/ArtifactCountChangedEvent.h
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/BuilderModeExitedEvent.h
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/BlueprintModeExitedEvent.h
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/EscapeMenuRequestedEvent.h
|
||||
|
||||
5
src/lib/eventsystem/event/WinEvent.h
Normal file
5
src/lib/eventsystem/event/WinEvent.h
Normal file
@@ -0,0 +1,5 @@
|
||||
#pragma once
|
||||
|
||||
#include "Event.h"
|
||||
|
||||
class WinEvent : public Event {};
|
||||
@@ -141,6 +141,8 @@ void Simulation::reset(unsigned int seed)
|
||||
m_nextBuildingId = 1;
|
||||
m_buildingBlocksStock = m_config.world.startingBuildingBlocks;
|
||||
m_gameOver = false;
|
||||
m_isWon = false;
|
||||
m_artifactCount = 0;
|
||||
m_hqBuildingId = kInvalidBuildingId;
|
||||
m_hqProxyEntity = entt::null;
|
||||
m_playerStation1Entity = entt::null;
|
||||
@@ -677,7 +679,14 @@ void Simulation::generateSchematicChoices(int destroyedStationLevel)
|
||||
|
||||
if (pool.empty()) { return; }
|
||||
|
||||
const int numChoices = std::min(static_cast<int>(pool.size()), 3);
|
||||
const double artifactChance = std::clamp(
|
||||
m_config.world.artifacts.artifactChanceFormula.evaluate(
|
||||
static_cast<double>(destroyedStationLevel)),
|
||||
0.0, 1.0);
|
||||
std::uniform_real_distribution<double> realDist(0.0, 1.0);
|
||||
const bool artifactRolled = realDist(m_rng) < artifactChance;
|
||||
|
||||
const int numChoices = std::min(static_cast<int>(pool.size()), artifactRolled ? 2 : 3);
|
||||
m_pendingSchematicChoices.clear();
|
||||
|
||||
const std::set<std::string> currentShipIds = getUnlockedShipSchematicIds();
|
||||
@@ -750,6 +759,17 @@ void Simulation::generateSchematicChoices(int destroyedStationLevel)
|
||||
|
||||
m_pendingSchematicChoices.push_back(option);
|
||||
}
|
||||
|
||||
if (artifactRolled)
|
||||
{
|
||||
SchematicChoiceOption artifactOption;
|
||||
artifactOption.schematicId = "";
|
||||
artifactOption.type = SchematicType::Artifact;
|
||||
artifactOption.displayName = "Artifact";
|
||||
artifactOption.isNewUnlock = false;
|
||||
artifactOption.targetLevel = 0;
|
||||
m_pendingSchematicChoices.push_back(std::move(artifactOption));
|
||||
}
|
||||
}
|
||||
|
||||
void Simulation::applySchematicChoice(int choiceIndex)
|
||||
@@ -757,6 +777,17 @@ void Simulation::applySchematicChoice(int choiceIndex)
|
||||
assert(choiceIndex >= 0 && choiceIndex < static_cast<int>(m_pendingSchematicChoices.size()));
|
||||
const SchematicChoiceOption& chosen = m_pendingSchematicChoices[static_cast<std::size_t>(choiceIndex)];
|
||||
|
||||
if (chosen.type == SchematicType::Artifact)
|
||||
{
|
||||
m_artifactCount += 1;
|
||||
if (m_artifactCount >= m_config.world.artifacts.artifactWinCount)
|
||||
{
|
||||
m_isWon = true;
|
||||
}
|
||||
m_pendingSchematicChoices.clear();
|
||||
return;
|
||||
}
|
||||
|
||||
if (chosen.type == SchematicType::Recipe)
|
||||
{
|
||||
m_unlockedRecipeSchematicIds.insert(chosen.schematicId);
|
||||
@@ -961,6 +992,8 @@ unsigned long long Simulation::computeStateChecksum() const
|
||||
hasher.append(m_nextBuildingId);
|
||||
hasher.append(m_buildingBlocksStock);
|
||||
hasher.append(m_gameOver);
|
||||
hasher.append(m_isWon);
|
||||
hasher.append(m_artifactCount);
|
||||
|
||||
// WaveSystem scalar state, reached through existing accessors.
|
||||
hasher.append(threatLevel());
|
||||
@@ -1074,6 +1107,16 @@ bool Simulation::isGameOver() const
|
||||
return m_gameOver;
|
||||
}
|
||||
|
||||
bool Simulation::isWon() const
|
||||
{
|
||||
return m_isWon;
|
||||
}
|
||||
|
||||
int Simulation::artifactCount() const
|
||||
{
|
||||
return m_artifactCount;
|
||||
}
|
||||
|
||||
double Simulation::threatLevel() const
|
||||
{
|
||||
return m_waveSystem->threatLevel();
|
||||
|
||||
@@ -73,6 +73,8 @@ public:
|
||||
unsigned int getSeed() const;
|
||||
int buildingBlocksStock() const;
|
||||
bool isGameOver() const;
|
||||
bool isWon() const;
|
||||
int artifactCount() const;
|
||||
double threatLevel() const;
|
||||
double threatAccumulationRate() const;
|
||||
double maxFactoryProductionThreatRate() const;
|
||||
@@ -165,7 +167,9 @@ private:
|
||||
Tick m_nextDepartureTick;
|
||||
BuildingId m_nextBuildingId;
|
||||
int m_buildingBlocksStock;
|
||||
bool m_gameOver = false;
|
||||
bool m_gameOver = false;
|
||||
bool m_isWon = false;
|
||||
int m_artifactCount = 0;
|
||||
|
||||
// Pre-placed structure IDs.
|
||||
BuildingId m_hqBuildingId; // Building id (for belt integration)
|
||||
|
||||
254
src/test/ArtifactWinConditionTest.cpp
Normal file
254
src/test/ArtifactWinConditionTest.cpp
Normal file
@@ -0,0 +1,254 @@
|
||||
#include <algorithm>
|
||||
|
||||
#include "catch.hpp"
|
||||
|
||||
#include "ConfigLoader.h"
|
||||
#include "FactionComponent.h"
|
||||
#include "Formula.h"
|
||||
#include "GameConfig.h"
|
||||
#include "HealthComponent.h"
|
||||
#include "SchematicChoiceOption.h"
|
||||
#include "Simulation.h"
|
||||
#include "SimulationTestAccess.h"
|
||||
#include "StationBodyComponent.h"
|
||||
|
||||
static GameConfig loadConfig()
|
||||
{
|
||||
return ConfigLoader::loadFromDirectory(CONFIG_DIR);
|
||||
}
|
||||
|
||||
static void killEnemyStations(Simulation& sim)
|
||||
{
|
||||
sim.admin().forEach<StationBodyComponent, FactionComponent, HealthComponent>(
|
||||
[](entt::entity, StationBodyComponent&, FactionComponent& faction, HealthComponent& health)
|
||||
{
|
||||
if (faction.isEnemy)
|
||||
{
|
||||
health.hp = 0.0f;
|
||||
}
|
||||
});
|
||||
sim.tick();
|
||||
}
|
||||
|
||||
// Returns the index of the Artifact option in the pending choices, or -1 if absent.
|
||||
static int findArtifactChoiceIndex(const Simulation& sim)
|
||||
{
|
||||
const auto& choices = sim.getPendingSchematicChoices();
|
||||
for (int i = 0; i < static_cast<int>(choices.size()); ++i)
|
||||
{
|
||||
if (choices[static_cast<std::size_t>(i)].type == SchematicType::Artifact)
|
||||
{
|
||||
return i;
|
||||
}
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Config: new artifact fields are parsed correctly (REQ-WIN-ARTIFACT-COUNT)
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
TEST_CASE("ArtifactWinCondition: artifact_chance_formula and artifact_win_count are loaded",
|
||||
"[artifact_win]")
|
||||
{
|
||||
const GameConfig cfg = loadConfig();
|
||||
CHECK(cfg.world.artifacts.artifactWinCount == 3);
|
||||
// 0.05 * x at x=2 should be 0.1
|
||||
CHECK(cfg.world.artifacts.artifactChanceFormula.evaluate(2.0) == Approx(0.1));
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Simulation initial state
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
TEST_CASE("ArtifactWinCondition: artifact count is 0 and isWon is false at game start",
|
||||
"[artifact_win]")
|
||||
{
|
||||
const Simulation sim(loadConfig());
|
||||
CHECK(sim.artifactCount() == 0);
|
||||
CHECK_FALSE(sim.isWon());
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Artifact option presence in schematic choices
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
TEST_CASE("ArtifactWinCondition: artifact option appears when chance formula returns 1",
|
||||
"[artifact_win]")
|
||||
{
|
||||
GameConfig cfg = loadConfig();
|
||||
cfg.world.artifacts.artifactChanceFormula = Formula::compile("1");
|
||||
Simulation sim(std::move(cfg));
|
||||
|
||||
killEnemyStations(sim);
|
||||
|
||||
REQUIRE(sim.hasSchematicChoicesPending());
|
||||
const auto& choices = sim.getPendingSchematicChoices();
|
||||
|
||||
const long artifactCount = std::count_if(choices.begin(), choices.end(),
|
||||
[](const SchematicChoiceOption& opt) { return opt.type == SchematicType::Artifact; });
|
||||
CHECK(artifactCount == 1);
|
||||
// Exactly 2 schematic picks + 1 artifact (or fewer if pool was small)
|
||||
CHECK(choices.size() <= 3);
|
||||
}
|
||||
|
||||
TEST_CASE("ArtifactWinCondition: at most 2 schematic options accompany the artifact",
|
||||
"[artifact_win]")
|
||||
{
|
||||
GameConfig cfg = loadConfig();
|
||||
cfg.world.artifacts.artifactChanceFormula = Formula::compile("1");
|
||||
Simulation sim(std::move(cfg));
|
||||
|
||||
killEnemyStations(sim);
|
||||
|
||||
REQUIRE(sim.hasSchematicChoicesPending());
|
||||
const auto& choices = sim.getPendingSchematicChoices();
|
||||
|
||||
const long schematicCount = std::count_if(choices.begin(), choices.end(),
|
||||
[](const SchematicChoiceOption& opt) { return opt.type != SchematicType::Artifact; });
|
||||
CHECK(schematicCount <= 2);
|
||||
}
|
||||
|
||||
TEST_CASE("ArtifactWinCondition: no artifact option when chance formula returns 0",
|
||||
"[artifact_win]")
|
||||
{
|
||||
GameConfig cfg = loadConfig();
|
||||
cfg.world.artifacts.artifactChanceFormula = Formula::compile("0");
|
||||
Simulation sim(std::move(cfg));
|
||||
|
||||
for (int i = 0; i < 20; ++i)
|
||||
{
|
||||
killEnemyStations(sim);
|
||||
if (!sim.hasSchematicChoicesPending()) { continue; }
|
||||
|
||||
CHECK(findArtifactChoiceIndex(sim) == -1);
|
||||
SimulationTestAccess::applySchematicChoice(sim,0);
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Selecting the artifact option
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
TEST_CASE("ArtifactWinCondition: selecting artifact increments artifact count",
|
||||
"[artifact_win]")
|
||||
{
|
||||
GameConfig cfg = loadConfig();
|
||||
cfg.world.artifacts.artifactChanceFormula = Formula::compile("1");
|
||||
Simulation sim(std::move(cfg));
|
||||
|
||||
killEnemyStations(sim);
|
||||
REQUIRE(sim.hasSchematicChoicesPending());
|
||||
|
||||
const int index = findArtifactChoiceIndex(sim);
|
||||
REQUIRE(index >= 0);
|
||||
|
||||
SimulationTestAccess::applySchematicChoice(sim,index);
|
||||
|
||||
CHECK(sim.artifactCount() == 1);
|
||||
}
|
||||
|
||||
TEST_CASE("ArtifactWinCondition: selecting a non-artifact option does not increment artifact count",
|
||||
"[artifact_win]")
|
||||
{
|
||||
GameConfig cfg = loadConfig();
|
||||
cfg.world.artifacts.artifactChanceFormula = Formula::compile("1");
|
||||
Simulation sim(std::move(cfg));
|
||||
|
||||
killEnemyStations(sim);
|
||||
REQUIRE(sim.hasSchematicChoicesPending());
|
||||
|
||||
// Pick the first non-artifact choice.
|
||||
const auto& choices = sim.getPendingSchematicChoices();
|
||||
const auto it = std::find_if(choices.begin(), choices.end(),
|
||||
[](const SchematicChoiceOption& opt) { return opt.type != SchematicType::Artifact; });
|
||||
REQUIRE(it != choices.end());
|
||||
|
||||
SimulationTestAccess::applySchematicChoice(sim,static_cast<int>(it - choices.begin()));
|
||||
|
||||
CHECK(sim.artifactCount() == 0);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Win condition (REQ-WIN-ARTIFACT-COUNT)
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
TEST_CASE("ArtifactWinCondition: isWon becomes true when artifact count reaches win count",
|
||||
"[artifact_win]")
|
||||
{
|
||||
GameConfig cfg = loadConfig();
|
||||
cfg.world.artifacts.artifactChanceFormula = Formula::compile("1");
|
||||
cfg.world.artifacts.artifactWinCount = 1;
|
||||
Simulation sim(std::move(cfg));
|
||||
|
||||
CHECK_FALSE(sim.isWon());
|
||||
|
||||
killEnemyStations(sim);
|
||||
REQUIRE(sim.hasSchematicChoicesPending());
|
||||
|
||||
const int index = findArtifactChoiceIndex(sim);
|
||||
REQUIRE(index >= 0);
|
||||
SimulationTestAccess::applySchematicChoice(sim,index);
|
||||
|
||||
CHECK(sim.isWon());
|
||||
}
|
||||
|
||||
TEST_CASE("ArtifactWinCondition: isWon stays false when artifact count is below win count",
|
||||
"[artifact_win]")
|
||||
{
|
||||
GameConfig cfg = loadConfig();
|
||||
cfg.world.artifacts.artifactChanceFormula = Formula::compile("1");
|
||||
cfg.world.artifacts.artifactWinCount = 2;
|
||||
Simulation sim(std::move(cfg));
|
||||
|
||||
killEnemyStations(sim);
|
||||
REQUIRE(sim.hasSchematicChoicesPending());
|
||||
SimulationTestAccess::applySchematicChoice(sim,findArtifactChoiceIndex(sim));
|
||||
|
||||
CHECK(sim.artifactCount() == 1);
|
||||
CHECK_FALSE(sim.isWon());
|
||||
}
|
||||
|
||||
TEST_CASE("ArtifactWinCondition: isWon becomes true after collecting required number of artifacts",
|
||||
"[artifact_win]")
|
||||
{
|
||||
GameConfig cfg = loadConfig();
|
||||
cfg.world.artifacts.artifactChanceFormula = Formula::compile("1");
|
||||
cfg.world.artifacts.artifactWinCount = 2;
|
||||
Simulation sim(std::move(cfg));
|
||||
|
||||
for (int i = 0; i < 2; ++i)
|
||||
{
|
||||
killEnemyStations(sim);
|
||||
REQUIRE(sim.hasSchematicChoicesPending());
|
||||
const int index = findArtifactChoiceIndex(sim);
|
||||
REQUIRE(index >= 0);
|
||||
SimulationTestAccess::applySchematicChoice(sim,index);
|
||||
}
|
||||
|
||||
CHECK(sim.artifactCount() == 2);
|
||||
CHECK(sim.isWon());
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// reset() restores initial state (REQ-CFG-RELOAD)
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
TEST_CASE("ArtifactWinCondition: reset clears artifact count and win state",
|
||||
"[artifact_win]")
|
||||
{
|
||||
GameConfig cfg = loadConfig();
|
||||
cfg.world.artifacts.artifactChanceFormula = Formula::compile("1");
|
||||
cfg.world.artifacts.artifactWinCount = 1;
|
||||
Simulation sim(std::move(cfg));
|
||||
|
||||
killEnemyStations(sim);
|
||||
REQUIRE(sim.hasSchematicChoicesPending());
|
||||
SimulationTestAccess::applySchematicChoice(sim,findArtifactChoiceIndex(sim));
|
||||
REQUIRE(sim.isWon());
|
||||
|
||||
sim.reset();
|
||||
|
||||
CHECK(sim.artifactCount() == 0);
|
||||
CHECK_FALSE(sim.isWon());
|
||||
}
|
||||
@@ -21,6 +21,7 @@ add_files(
|
||||
ShipModuleTest.cpp
|
||||
ThreatCostCalculatorTest.cpp
|
||||
RecipeSchematicTest.cpp
|
||||
ArtifactWinConditionTest.cpp
|
||||
DeterminismTest.cpp
|
||||
CommandTest.cpp
|
||||
ReplayRecorderTest.cpp
|
||||
|
||||
@@ -194,7 +194,11 @@ void GameWorldView::onFrame()
|
||||
static_cast<double>(elapsed), m_gameSpeedMultiplier);
|
||||
for (int i = 0; i < ticks; ++i)
|
||||
{
|
||||
if (m_replayPlayer->isFinished()) { break; }
|
||||
// Stop at the recorded stream's end, or at the run's terminal state:
|
||||
// the real game froze when it was won or lost, so the replay does too
|
||||
// (the recording may run a few ticks past that point).
|
||||
if (m_replayPlayer->isFinished()
|
||||
|| m_sim->isWon() || m_sim->isGameOver()) { break; }
|
||||
m_sim->tick();
|
||||
m_replayPlayer->advanceTo(m_sim->currentTick());
|
||||
}
|
||||
@@ -310,6 +314,14 @@ void GameWorldView::onFrame()
|
||||
m_schematicChoiceShown = false;
|
||||
}
|
||||
|
||||
// Win check
|
||||
if (m_sim->isWon() && !m_winShown)
|
||||
{
|
||||
m_winShown = true;
|
||||
m_gameSpeedMultiplier = 0.0;
|
||||
EventManager::getInstance()->sendEventImmediately(std::make_shared<WinEvent>());
|
||||
}
|
||||
|
||||
// Game over check
|
||||
if (m_sim->isGameOver() && !m_gameOverShown)
|
||||
{
|
||||
@@ -320,6 +332,20 @@ void GameWorldView::onFrame()
|
||||
}
|
||||
}
|
||||
|
||||
// Artifact count is a passive reflection of sim state (not a live-input source
|
||||
// like the schematic/game-over polls above), so it updates during replay too —
|
||||
// the recorded ApplySchematicChoice drives m_sim->artifactCount() forward and
|
||||
// the header bar must track it. Fires on the first frame (count 0 vs the -1
|
||||
// sentinel), which also populates the win-count max (replacing the "0/?" label).
|
||||
const int currentArtifactCount = m_sim->artifactCount();
|
||||
if (currentArtifactCount != m_lastArtifactCount)
|
||||
{
|
||||
m_lastArtifactCount = currentArtifactCount;
|
||||
EventManager::getInstance()->sendEventImmediately(
|
||||
std::make_shared<ArtifactCountChangedEvent>(
|
||||
currentArtifactCount, m_sim->config().world.artifacts.artifactWinCount));
|
||||
}
|
||||
|
||||
update();
|
||||
}
|
||||
|
||||
@@ -1292,7 +1318,11 @@ void GameWorldView::drawReplayOverlay(QPainter& painter)
|
||||
painter.setPen(QColor(255, 220, 80));
|
||||
painter.drawText(QRect(0, 8, width(), 24), Qt::AlignHCenter | Qt::AlignTop, tr("REPLAY"));
|
||||
|
||||
if (m_replayPlayer->isFinished())
|
||||
// The replay is over once the recorded stream is exhausted or the run reached
|
||||
// its terminal state (win / defeat) — matching where playback freezes above.
|
||||
const bool ended = m_replayPlayer->isFinished()
|
||||
|| m_sim->isWon() || m_sim->isGameOver();
|
||||
if (ended)
|
||||
{
|
||||
// Dim the world so the end message reads clearly over it.
|
||||
painter.fillRect(rect(), QColor(0, 0, 0, 140));
|
||||
@@ -1304,6 +1334,16 @@ void GameWorldView::drawReplayOverlay(QPainter& painter)
|
||||
message = tr("Desync at tick %1").arg(static_cast<qlonglong>(*desync));
|
||||
painter.setPen(QColor(255, 90, 90));
|
||||
}
|
||||
else if (m_sim->isWon())
|
||||
{
|
||||
message = tr("Replay ended — Victory");
|
||||
painter.setPen(QColor(120, 255, 120));
|
||||
}
|
||||
else if (m_sim->isGameOver())
|
||||
{
|
||||
message = tr("Replay ended — Defeat");
|
||||
painter.setPen(QColor(255, 255, 255));
|
||||
}
|
||||
else
|
||||
{
|
||||
message = tr("Replay ended");
|
||||
@@ -1779,12 +1819,14 @@ void GameWorldView::resetForNewGame()
|
||||
m_scrollXTiles = 0.0f;
|
||||
m_scrollLeft = false;
|
||||
m_scrollRight = false;
|
||||
m_gameOverShown = false;
|
||||
m_prevNonZeroSpeed = 1.0;
|
||||
m_gameOverShown = false;
|
||||
m_winShown = false;
|
||||
m_prevNonZeroSpeed = 1.0;
|
||||
m_lastTick = Tick(-1);
|
||||
m_lastBlocks = -1;
|
||||
m_lastBossCounter = -1;
|
||||
m_lastBossCountdown = Tick(-1);
|
||||
m_lastArtifactCount = -1;
|
||||
EventManager::getInstance()->sendEventImmediately(
|
||||
std::make_shared<SelectionChangedEvent>(std::vector<BuildingId>{}));
|
||||
setGameSpeed(1.0);
|
||||
|
||||
@@ -26,9 +26,11 @@
|
||||
#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"
|
||||
@@ -219,10 +221,12 @@ private:
|
||||
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_lastBossCounter = -1;
|
||||
Tick m_lastBossCountdown = Tick(-1);
|
||||
int m_lastArtifactCount = -1;
|
||||
};
|
||||
|
||||
@@ -22,11 +22,13 @@ HeaderBar::HeaderBar(QWidget* parent)
|
||||
layout->setContentsMargins(8, 4, 8, 4);
|
||||
layout->setSpacing(8);
|
||||
|
||||
m_timeLabel = new QLabel("00:00", this);
|
||||
m_blocksLabel = new QLabel(tr("Blocks: 0"), this);
|
||||
m_bossLabel = new QLabel(tr("Boss Wave #1 Next boss: 5:00"), this);
|
||||
m_timeLabel = new QLabel("00:00", this);
|
||||
m_blocksLabel = new QLabel(tr("Blocks: 0"), this);
|
||||
m_artifactsLabel = new QLabel(tr("Artifacts: 0/?"), this);
|
||||
m_bossLabel = new QLabel(tr("Boss Wave #1 Next boss: 5:00"), this);
|
||||
layout->addWidget(m_timeLabel);
|
||||
layout->addWidget(m_blocksLabel);
|
||||
layout->addWidget(m_artifactsLabel);
|
||||
layout->addStretch();
|
||||
layout->addWidget(m_bossLabel);
|
||||
|
||||
@@ -88,6 +90,11 @@ void HeaderBar::handleEvent(std::shared_ptr<const BossWaveUpdatedEvent> event)
|
||||
.arg(bossSeconds % 60, 2, 10, QChar('0')));
|
||||
}
|
||||
|
||||
void HeaderBar::handleEvent(std::shared_ptr<const ArtifactCountChangedEvent> event)
|
||||
{
|
||||
m_artifactsLabel->setText(tr("Artifacts: %1/%2").arg(event->count).arg(event->winCount));
|
||||
}
|
||||
|
||||
void HeaderBar::onSpeedButton(int index)
|
||||
{
|
||||
if (index >= 0 && index < kSpeedCount)
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
|
||||
#include <QWidget>
|
||||
|
||||
#include "ArtifactCountChangedEvent.h"
|
||||
#include "BossWaveUpdatedEvent.h"
|
||||
#include "BuildingBlocksChangedEvent.h"
|
||||
#include "EventHandler.h"
|
||||
@@ -18,7 +19,8 @@ class HeaderBar : public QWidget,
|
||||
public CombinedEventHandler<TickAdvancedEvent,
|
||||
BuildingBlocksChangedEvent,
|
||||
GameSpeedChangedEvent,
|
||||
BossWaveUpdatedEvent>
|
||||
BossWaveUpdatedEvent,
|
||||
ArtifactCountChangedEvent>
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
@@ -34,9 +36,11 @@ private:
|
||||
void handleEvent(std::shared_ptr<const BuildingBlocksChangedEvent> event) override;
|
||||
void handleEvent(std::shared_ptr<const GameSpeedChangedEvent> event) override;
|
||||
void handleEvent(std::shared_ptr<const BossWaveUpdatedEvent> event) override;
|
||||
void handleEvent(std::shared_ptr<const ArtifactCountChangedEvent> event) override;
|
||||
|
||||
QLabel* m_timeLabel;
|
||||
QLabel* m_blocksLabel;
|
||||
QLabel* m_artifactsLabel;
|
||||
QLabel* m_bossLabel;
|
||||
std::vector<QPushButton*> m_speedButtons;
|
||||
|
||||
|
||||
@@ -346,3 +346,42 @@ void MainWindow::handleEvent(std::shared_ptr<const GameOverEvent> /*event*/)
|
||||
close();
|
||||
}
|
||||
}
|
||||
|
||||
void MainWindow::handleEvent(std::shared_ptr<const WinEvent> /*event*/)
|
||||
{
|
||||
const Tick tick = m_sim->currentTick();
|
||||
const int totalSeconds = static_cast<int>(ticksToSeconds(tick));
|
||||
const int minutes = totalSeconds / 60;
|
||||
const int seconds = totalSeconds % 60;
|
||||
|
||||
QMessageBox box(this);
|
||||
box.setWindowTitle(tr("Won!"));
|
||||
box.setText(tr("You collected all artifacts!\nSurvival time: %1:%2")
|
||||
.arg(minutes, 2, 10, QChar('0'))
|
||||
.arg(seconds, 2, 10, QChar('0')));
|
||||
QPushButton* restartBtn = box.addButton(tr("Restart"), QMessageBox::AcceptRole);
|
||||
box.addButton(tr("Quit"), QMessageBox::RejectRole);
|
||||
box.exec();
|
||||
|
||||
if (box.clickedButton() == restartBtn)
|
||||
{
|
||||
try
|
||||
{
|
||||
GameConfig newConfig = ConfigLoader::loadFromDirectory(m_configDir);
|
||||
VisualsConfig newVisuals = VisualsLoader::load(m_configDir + "/visuals.toml");
|
||||
m_visuals = std::move(newVisuals);
|
||||
m_sim->reset(std::move(newConfig));
|
||||
}
|
||||
catch (const std::exception& e)
|
||||
{
|
||||
QMessageBox::critical(this, tr("Config Error"),
|
||||
tr("Failed to reload config:\n%1").arg(e.what()));
|
||||
return;
|
||||
}
|
||||
m_gameWorldView->resetForNewGame();
|
||||
}
|
||||
else
|
||||
{
|
||||
close();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,6 +12,7 @@
|
||||
#include "EventHandler.h"
|
||||
#include "GameOverEvent.h"
|
||||
#include "LayoutDialogRequestedEvent.h"
|
||||
#include "WinEvent.h"
|
||||
#include "RecipeSelectionRequestedEvent.h"
|
||||
#include "SchematicChoicesAvailableEvent.h"
|
||||
#include "ShipLayoutBlueprint.h"
|
||||
@@ -32,6 +33,7 @@ class MainWindow : public QWidget,
|
||||
public CombinedEventHandler<BuildingBlocksChangedEvent,
|
||||
SchematicChoicesAvailableEvent,
|
||||
GameOverEvent,
|
||||
WinEvent,
|
||||
EscapeMenuRequestedEvent,
|
||||
LayoutDialogRequestedEvent,
|
||||
RecipeSelectionRequestedEvent>
|
||||
@@ -51,6 +53,7 @@ private:
|
||||
void handleEvent(std::shared_ptr<const BuildingBlocksChangedEvent> 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 WinEvent> event) override;
|
||||
void handleEvent(std::shared_ptr<const EscapeMenuRequestedEvent> event) override;
|
||||
void handleEvent(std::shared_ptr<const LayoutDialogRequestedEvent> event) override;
|
||||
void handleEvent(std::shared_ptr<const RecipeSelectionRequestedEvent> event) override;
|
||||
|
||||
@@ -45,60 +45,69 @@ SchematicChoiceDialog::SchematicChoiceDialog(
|
||||
nameLabel->setAlignment(Qt::AlignCenter);
|
||||
cardLayout->addWidget(nameLabel);
|
||||
|
||||
QString typeText;
|
||||
if (option.type == SchematicType::Ship)
|
||||
if (option.type == SchematicType::Artifact)
|
||||
{
|
||||
typeText = tr("Ship");
|
||||
}
|
||||
else if (option.type == SchematicType::Module)
|
||||
{
|
||||
typeText = tr("Module");
|
||||
QLabel* artifactTypeLabel = new QLabel(tr("Artifact"), card);
|
||||
artifactTypeLabel->setAlignment(Qt::AlignCenter);
|
||||
cardLayout->addWidget(artifactTypeLabel);
|
||||
}
|
||||
else
|
||||
{
|
||||
typeText = tr("Recipe");
|
||||
}
|
||||
QLabel* typeLabel = new QLabel(typeText, card);
|
||||
typeLabel->setAlignment(Qt::AlignCenter);
|
||||
cardLayout->addWidget(typeLabel);
|
||||
|
||||
QString statusText;
|
||||
if (option.isNewUnlock)
|
||||
{
|
||||
statusText = tr("New unlock");
|
||||
}
|
||||
else
|
||||
{
|
||||
statusText = tr("Level up -> %1").arg(option.targetLevel);
|
||||
}
|
||||
QLabel* statusLabel = new QLabel(statusText, card);
|
||||
statusLabel->setAlignment(Qt::AlignCenter);
|
||||
cardLayout->addWidget(statusLabel);
|
||||
|
||||
QLabel* unlocksHeaderLabel = new QLabel(tr("Unlocks recipes for:"), card);
|
||||
QFont unlocksHeaderFont = unlocksHeaderLabel->font();
|
||||
unlocksHeaderFont.setBold(true);
|
||||
unlocksHeaderLabel->setFont(unlocksHeaderFont);
|
||||
unlocksHeaderLabel->setAlignment(Qt::AlignCenter);
|
||||
cardLayout->addWidget(unlocksHeaderLabel);
|
||||
|
||||
QString unlocksText;
|
||||
if (option.newlyUnlockedItemNames.empty())
|
||||
{
|
||||
unlocksText = tr("None");
|
||||
}
|
||||
else
|
||||
{
|
||||
QStringList itemLines;
|
||||
for (const std::string& itemName : option.newlyUnlockedItemNames)
|
||||
QString typeText;
|
||||
if (option.type == SchematicType::Ship)
|
||||
{
|
||||
itemLines << QString::fromStdString(itemName);
|
||||
typeText = tr("Ship");
|
||||
}
|
||||
unlocksText = itemLines.join("\n");
|
||||
else if (option.type == SchematicType::Module)
|
||||
{
|
||||
typeText = tr("Module");
|
||||
}
|
||||
else
|
||||
{
|
||||
typeText = tr("Recipe");
|
||||
}
|
||||
QLabel* typeLabel = new QLabel(typeText, card);
|
||||
typeLabel->setAlignment(Qt::AlignCenter);
|
||||
cardLayout->addWidget(typeLabel);
|
||||
|
||||
QString statusText;
|
||||
if (option.isNewUnlock)
|
||||
{
|
||||
statusText = tr("New unlock");
|
||||
}
|
||||
else
|
||||
{
|
||||
statusText = tr("Level up -> %1").arg(option.targetLevel);
|
||||
}
|
||||
QLabel* statusLabel = new QLabel(statusText, card);
|
||||
statusLabel->setAlignment(Qt::AlignCenter);
|
||||
cardLayout->addWidget(statusLabel);
|
||||
|
||||
QLabel* unlocksHeaderLabel = new QLabel(tr("Unlocks recipes for:"), card);
|
||||
QFont unlocksHeaderFont = unlocksHeaderLabel->font();
|
||||
unlocksHeaderFont.setBold(true);
|
||||
unlocksHeaderLabel->setFont(unlocksHeaderFont);
|
||||
unlocksHeaderLabel->setAlignment(Qt::AlignCenter);
|
||||
cardLayout->addWidget(unlocksHeaderLabel);
|
||||
|
||||
QString unlocksText;
|
||||
if (option.newlyUnlockedItemNames.empty())
|
||||
{
|
||||
unlocksText = tr("None");
|
||||
}
|
||||
else
|
||||
{
|
||||
QStringList itemLines;
|
||||
for (const std::string& itemName : option.newlyUnlockedItemNames)
|
||||
{
|
||||
itemLines << QString::fromStdString(itemName);
|
||||
}
|
||||
unlocksText = itemLines.join("\n");
|
||||
}
|
||||
QLabel* unlocksLabel = new QLabel(unlocksText, card);
|
||||
unlocksLabel->setAlignment(Qt::AlignCenter);
|
||||
cardLayout->addWidget(unlocksLabel);
|
||||
}
|
||||
QLabel* unlocksLabel = new QLabel(unlocksText, card);
|
||||
unlocksLabel->setAlignment(Qt::AlignCenter);
|
||||
cardLayout->addWidget(unlocksLabel);
|
||||
|
||||
QPushButton* selectButton = new QPushButton(tr("Select"), card);
|
||||
cardLayout->addWidget(selectButton);
|
||||
|
||||
Reference in New Issue
Block a user