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:
2026-07-01 20:27:29 +00:00
committed by mlangkabel
parent d74ba5bfad
commit 0a7e9a34ef
20 changed files with 516 additions and 63 deletions

View File

@@ -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);

View File

@@ -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;
};

View File

@@ -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)

View File

@@ -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;

View File

@@ -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();
}
}

View File

@@ -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;

View File

@@ -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);