Implement config-driven unlock groups so that multiple things can be unlocked at once (including buildings)
This commit is contained in:
@@ -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));
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -351,4 +351,5 @@ private:
|
||||
int m_lastBossCounter = -1;
|
||||
Tick m_lastBossCountdown = Tick(-1);
|
||||
int m_lastArtifactCount = -1;
|
||||
int m_lastUnlockedBuildingCount = -1;
|
||||
};
|
||||
|
||||
@@ -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();
|
||||
|
||||
Reference in New Issue
Block a user