Implement config-driven unlock groups so that multiple things can be unlocked at once (including buildings)

This commit is contained in:
2026-07-22 21:34:59 +02:00
parent a8a6a04f1e
commit a9082c57f3
36 changed files with 1005 additions and 543 deletions

View File

@@ -82,6 +82,7 @@ BuildButtonGrid::BuildButtonGrid(Simulation* sim, const GameConfig* config, QWid
std::make_shared<DemolishModeToggleRequestedEvent>());
});
updateVisibility();
registerForEvents();
}
@@ -119,6 +120,17 @@ void BuildButtonGrid::updateAffordability()
}
}
void BuildButtonGrid::updateVisibility()
{
// A locked building type's button is hidden until its unlock group is awarded
// (REQ-LOCK-BUILDING). Buttons keep their index so hotkeys/affordability stay
// stable; they simply appear once the type is unlocked.
for (std::size_t i = 0; i < m_buttons.size(); ++i)
{
m_buttons[i]->setVisible(m_sim->isBuildingUnlocked(m_types[i]));
}
}
void BuildButtonGrid::clearActiveButton()
{
if (m_activeIndex)
@@ -165,6 +177,12 @@ void BuildButtonGrid::handleEvent(std::shared_ptr<const BuildingBlocksChangedEve
updateAffordability();
}
void BuildButtonGrid::handleEvent(std::shared_ptr<const UnlockedBuildingsChangedEvent> /*event*/)
{
updateVisibility();
updateAffordability();
}
void BuildButtonGrid::handleEvent(std::shared_ptr<const DemolishModeChangedEvent> event)
{
m_demolishButton->setChecked(event->active);
@@ -176,9 +194,10 @@ void BuildButtonGrid::handleEvent(std::shared_ptr<const BuildHotkeyPressedEvent>
{
if (m_types[i] == event->type)
{
// Equivalent to clicking the build button: a disabled (unaffordable)
// button cannot be clicked, so the hotkey is likewise inert.
if (m_buttons[i]->isEnabled())
// Equivalent to clicking the build button: a disabled (unaffordable) or
// hidden (locked, REQ-LOCK-BUILDING) button cannot be clicked, so the
// hotkey is likewise inert.
if (m_buttons[i]->isEnabled() && m_buttons[i]->isVisible())
{
onBuildButton(static_cast<int>(i));
}

View File

@@ -13,6 +13,7 @@
#include "DemolishModeChangedEvent.h"
#include "EventHandler.h"
#include "GameConfig.h"
#include "UnlockedBuildingsChangedEvent.h"
class QPushButton;
class Simulation;
@@ -21,7 +22,8 @@ class BuildButtonGrid : public QWidget,
public CombinedEventHandler<BuilderModeExitedEvent,
DemolishModeChangedEvent,
BuildHotkeyPressedEvent,
BuildingBlocksChangedEvent>
BuildingBlocksChangedEvent,
UnlockedBuildingsChangedEvent>
{
Q_OBJECT
@@ -37,10 +39,15 @@ private:
// be afforded, it exits builder mode so the button does not stay selected.
void updateAffordability();
// Re-evaluates which build buttons are visible from the current per-building
// unlock state (REQ-LOCK-BUILDING); a locked building type's button is hidden.
void updateVisibility();
void handleEvent(std::shared_ptr<const BuilderModeExitedEvent> event) override;
void handleEvent(std::shared_ptr<const DemolishModeChangedEvent> event) override;
void handleEvent(std::shared_ptr<const BuildHotkeyPressedEvent> event) override;
void handleEvent(std::shared_ptr<const BuildingBlocksChangedEvent> event) override;
void handleEvent(std::shared_ptr<const UnlockedBuildingsChangedEvent> event) override;
private slots:
void onBuildButton(int index);

View File

@@ -67,6 +67,7 @@
#include "BuilderModeExitedEvent.h"
#include "BlueprintModeExitedEvent.h"
#include "BuildingBlocksChangedEvent.h"
#include "UnlockedBuildingsChangedEvent.h"
#include "ExpansionCostChangedEvent.h"
#include "GameSpeedChangedEvent.h"
#include "SchematicChoicesAvailableEvent.h"
@@ -367,6 +368,20 @@ void GameWorldView::onFrame()
EventManager::getInstance()->sendEventImmediately(
std::make_shared<BossWaveUpdatedEvent>(newBoss, newCountdown));
}
// Unlocked building set changes only after a drop is applied or on Restart
// (REQ-LOCK-BUILDING); the build grid rebuilds its visible buttons.
int newUnlockedBuildingCount = 0;
for (const BuildingDef& def : m_sim->getConfig().buildings.buildings)
{
if (m_sim->isBuildingUnlocked(def.type)) { ++newUnlockedBuildingCount; }
}
if (newUnlockedBuildingCount != m_lastUnlockedBuildingCount)
{
m_lastUnlockedBuildingCount = newUnlockedBuildingCount;
EventManager::getInstance()->sendEventImmediately(
std::make_shared<UnlockedBuildingsChangedEvent>());
}
}
// Sim-state polls are input sources for the live game; in replay these are
@@ -801,13 +816,18 @@ void GameWorldView::placeBlueprintAtTile(QPoint center)
for (const BlueprintBuilding& bb : bp.buildings)
{
// Locked building types are excluded from this placement entirely
// (REQ-LOCK-BUILDING, REQ-LOCK-UI-BLUEPRINT): not validity-checked here.
if (!m_sim->isBuildingUnlocked(bb.type)) { continue; }
if (!isValidPlacement(bb.type, center + bb.offset, bb.rotation)) { return; }
}
// Cost only applies to buildings that are genuinely new (not rotate-in-place).
// Cost only applies to buildings that are genuinely new (not rotate-in-place),
// and excludes locked building types (REQ-LOCK-UI-BLUEPRINT).
int totalCost = 0;
for (const BlueprintBuilding& bb : bp.buildings)
{
if (!m_sim->isBuildingUnlocked(bb.type)) { continue; }
if (m_sim->getBuildings().findRotateInPlaceTarget(
bb.type, center + bb.offset, bb.rotation).has_value())
{
@@ -820,6 +840,7 @@ void GameWorldView::placeBlueprintAtTile(QPoint center)
for (const BlueprintBuilding& bb : bp.buildings)
{
if (!m_sim->isBuildingUnlocked(bb.type)) { continue; }
const QPoint anchor = center + bb.offset;
const std::optional<BuildingId> rotateTarget =
m_sim->getBuildings().findRotateInPlaceTarget(bb.type, anchor, bb.rotation);
@@ -1946,6 +1967,9 @@ void GameWorldView::drawOverlays(QPainter& painter)
{
for (const BlueprintBuilding& bb : m_blueprintMode->buildings)
{
// Locked building types are omitted from the blueprint (REQ-LOCK-BUILDING,
// REQ-LOCK-UI-BLUEPRINT), so they are not ghosted either.
if (!m_sim->isBuildingUnlocked(bb.type)) { continue; }
const QPoint anchor = m_blueprintGhostTile + bb.offset;
const bool valid = isValidPlacement(bb.type, anchor, bb.rotation);
drawBuildingGhost(painter, bb.type, anchor, bb.rotation, valid,

View File

@@ -351,4 +351,5 @@ private:
int m_lastBossCounter = -1;
Tick m_lastBossCountdown = Tick(-1);
int m_lastArtifactCount = -1;
int m_lastUnlockedBuildingCount = -1;
};

View File

@@ -21,6 +21,18 @@ const RecipeDef* findRecipe(const RecipesConfig& recipes, const std::string& id)
return nullptr;
}
QString grantKindLabel(SchematicType type)
{
switch (type)
{
case SchematicType::Ship: return SchematicChoiceDialog::tr("Ship");
case SchematicType::Module: return SchematicChoiceDialog::tr("Module");
case SchematicType::Building: return SchematicChoiceDialog::tr("Building");
case SchematicType::Recipe: return SchematicChoiceDialog::tr("Recipe");
}
return QString();
}
} // namespace
SchematicChoiceDialog::SchematicChoiceDialog(
@@ -63,7 +75,7 @@ SchematicChoiceDialog::SchematicChoiceDialog(
nameLabel->setAlignment(Qt::AlignCenter);
cardLayout->addWidget(nameLabel);
if (option.type == SchematicType::Artifact)
if (option.isArtifact)
{
QLabel* artifactTypeLabel = new QLabel(tr("Artifact"), card);
artifactTypeLabel->setAlignment(Qt::AlignCenter);
@@ -71,22 +83,22 @@ SchematicChoiceDialog::SchematicChoiceDialog(
}
else
{
QString typeText;
if (option.type == SchematicType::Ship)
QLabel* grantsHeaderLabel = new QLabel(tr("Unlocks:"), card);
QFont grantsHeaderFont = grantsHeaderLabel->font();
grantsHeaderFont.setBold(true);
grantsHeaderLabel->setFont(grantsHeaderFont);
grantsHeaderLabel->setAlignment(Qt::AlignCenter);
cardLayout->addWidget(grantsHeaderLabel);
for (const GrantedSchematic& grant : option.grantedItems)
{
typeText = tr("Ship");
QLabel* grantLabel = new QLabel(
grantKindLabel(grant.type) + ": "
+ QString::fromStdString(grant.displayName),
card);
grantLabel->setAlignment(Qt::AlignCenter);
cardLayout->addWidget(grantLabel);
}
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);
QLabel* unlocksHeaderLabel = new QLabel(tr("Unlocks recipes:"), card);
QFont unlocksHeaderFont = unlocksHeaderLabel->font();