schematic selection dialog

This commit is contained in:
2026-06-13 12:00:05 +02:00
parent 1641189b75
commit 49f7129bd5
25 changed files with 453 additions and 268 deletions

View File

@@ -10,7 +10,8 @@ SET(HDRS
${CMAKE_CURRENT_SOURCE_DIR}/ItemType.h
${CMAKE_CURRENT_SOURCE_DIR}/Item.h
${CMAKE_CURRENT_SOURCE_DIR}/Port.h
${CMAKE_CURRENT_SOURCE_DIR}/SchematicDropEvent.h
${CMAKE_CURRENT_SOURCE_DIR}/SchematicChoiceOption.h
${CMAKE_CURRENT_SOURCE_DIR}/DisplayName.h
PARENT_SCOPE
)
@@ -18,6 +19,7 @@ SET(SRCS
${SRCS}
${CMAKE_CURRENT_SOURCE_DIR}/BuildingType.cpp
${CMAKE_CURRENT_SOURCE_DIR}/EntityAdmin.cpp
${CMAKE_CURRENT_SOURCE_DIR}/DisplayName.cpp
PARENT_SCOPE
)

View File

@@ -0,0 +1,27 @@
#include "DisplayName.h"
#include <cctype>
std::string toDisplayName(const std::string& id)
{
std::string result;
bool nextUpper = true;
for (char c : id)
{
if (c == '_')
{
result += ' ';
nextUpper = true;
}
else if (nextUpper)
{
result += static_cast<char>(std::toupper(static_cast<unsigned char>(c)));
nextUpper = false;
}
else
{
result += c;
}
}
return result;
}

View File

@@ -0,0 +1,5 @@
#pragma once
#include <string>
std::string toDisplayName(const std::string& id);

View File

@@ -0,0 +1,22 @@
#pragma once
#include <string>
enum class SchematicType
{
Ship,
Module,
Recipe
};
// One option presented to the player in the schematic choice dialog
// (REQ-DEF-SCHEMATIC-DROP). Built by the simulation when enemy stations are
// destroyed; the UI reads these to populate the dialog.
struct SchematicChoiceOption
{
std::string schematicId;
SchematicType type;
std::string displayName;
bool isNewUnlock;
int targetLevel;
};

View File

@@ -1,15 +0,0 @@
#pragma once
#include <string>
// Emitted in tick step 9 (Deaths & loot) when a destroyed enemy-defence-station
// set awards a schematic (REQ-DEF-SCHEMATIC-DROP). The UI renders a toast
// (REQ-UI-SCHEMATIC-TOAST); wasNewUnlock chooses between the "unlocked" and
// "level -> N" wording. isModuleSchematic selects ship vs. module toast text.
struct SchematicDropEvent
{
std::string schematicId; // matches ShipDef::id or ModuleDef::id in the config.
int newLevel;
bool wasNewUnlock;
bool isModuleSchematic;
};

View File

@@ -6,6 +6,7 @@ SET(HDRS
${CMAKE_CURRENT_SOURCE_DIR}/EntitySelectedEvent.h
${CMAKE_CURRENT_SOURCE_DIR}/GameSpeedChangedEvent.h
${CMAKE_CURRENT_SOURCE_DIR}/BossWaveUpdatedEvent.h
${CMAKE_CURRENT_SOURCE_DIR}/SchematicChoicesAvailableEvent.h
PARENT_SCOPE
)

View File

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

View File

@@ -1,8 +1,10 @@
#include "Simulation.h"
#include <algorithm>
#include <cassert>
#include "AiSystem.h"
#include "DisplayName.h"
#include "BuildingSystem.h"
#include "CombatSystem.h"
#include "DynamicBodySystem.h"
@@ -135,7 +137,7 @@ void Simulation::reset(unsigned int seed)
m_currentEnemyStationEntities[0] = entt::null;
m_currentEnemyStationEntities[1] = entt::null;
m_fireEvents.clear();
m_schematicDropEvents.clear();
m_pendingSchematicChoices.clear();
m_admin.clear();
m_beltSystem = BeltSystem(m_config.world.beltSpeed_tps);
@@ -532,11 +534,11 @@ void Simulation::tickDeathsAndLoot()
const int destroyedLevel = m_waveSystem->generation();
m_waveSystem->onEnemyStationsDestroyed();
placeEnemyStationSet(m_waveSystem->generation());
awardSchematicDrop(destroyedLevel);
generateSchematicChoices(destroyedLevel);
}
}
void Simulation::awardSchematicDrop(int destroyedStationLevel)
void Simulation::generateSchematicChoices(int destroyedStationLevel)
{
enum class DropType { Ship, Module, Recipe };
struct PoolEntry { std::string id; DropType type; };
@@ -572,32 +574,78 @@ void Simulation::awardSchematicDrop(int destroyedStationLevel)
pool.push_back({def.id, DropType::Recipe});
}
std::uniform_int_distribution<int> dist(0, static_cast<int>(pool.size()) - 1);
const PoolEntry& chosen = pool[static_cast<std::size_t>(dist(m_rng))];
if (pool.empty()) { return; }
if (chosen.type == DropType::Recipe)
const int numChoices = std::min(static_cast<int>(pool.size()), 3);
m_pendingSchematicChoices.clear();
for (int i = 0; i < numChoices; ++i)
{
m_unlockedRecipeSchematicIds.insert(chosen.id);
recomputeUnlocked();
std::uniform_int_distribution<int> dist(0, static_cast<int>(pool.size()) - 1 - i);
const int roll = dist(m_rng);
const std::size_t rollIdx = static_cast<std::size_t>(roll);
const std::size_t endIdx = pool.size() - 1 - static_cast<std::size_t>(i);
std::swap(pool[rollIdx], pool[endIdx]);
const PoolEntry& entry = pool[endIdx];
SchematicChoiceOption option;
option.schematicId = entry.id;
if (entry.type == DropType::Ship)
{
option.type = SchematicType::Ship;
option.displayName = toDisplayName(entry.id);
const SchematicState& state = m_schematicLevels.at(entry.id);
option.isNewUnlock = !state.unlocked;
option.targetLevel = state.level + 1;
}
else if (entry.type == DropType::Module)
{
option.type = SchematicType::Module;
option.displayName = toDisplayName(entry.id);
const SchematicState& state = m_moduleSchematicLevels.at(entry.id);
option.isNewUnlock = !state.unlocked;
option.targetLevel = state.level + 1;
}
else
{
option.type = SchematicType::Recipe;
option.isNewUnlock = true;
option.targetLevel = 0;
for (const RecipeDef& def : m_config.recipes.recipes)
{
if (def.id == entry.id && !def.outputs.empty())
{
option.displayName = toDisplayName(def.outputs[0].item);
break;
}
}
}
m_pendingSchematicChoices.push_back(option);
}
}
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::Recipe)
{
m_unlockedRecipeSchematicIds.insert(chosen.schematicId);
}
else
{
SchematicState& state = (chosen.type == DropType::Module)
? m_moduleSchematicLevels.at(chosen.id)
: m_schematicLevels.at(chosen.id);
const bool wasNew = !state.unlocked;
SchematicState& state = (chosen.type == SchematicType::Module)
? m_moduleSchematicLevels.at(chosen.schematicId)
: m_schematicLevels.at(chosen.schematicId);
state.unlocked = true;
state.level += 1;
SchematicDropEvent evt;
evt.schematicId = chosen.id;
evt.newLevel = state.level;
evt.wasNewUnlock = wasNew;
evt.isModuleSchematic = (chosen.type == DropType::Module);
m_schematicDropEvents.push_back(evt);
recomputeUnlocked();
}
recomputeUnlocked();
m_pendingSchematicChoices.clear();
}
// ---------------------------------------------------------------------------
@@ -704,13 +752,17 @@ std::vector<FireEvent> Simulation::drainFireEvents()
return result;
}
std::vector<SchematicDropEvent> Simulation::drainSchematicDropEvents()
const std::vector<SchematicChoiceOption>& Simulation::getPendingSchematicChoices() const
{
std::vector<SchematicDropEvent> result;
result.swap(m_schematicDropEvents);
return result;
return m_pendingSchematicChoices;
}
bool Simulation::hasSchematicChoicesPending() const
{
return !m_pendingSchematicChoices.empty();
}
// ---------------------------------------------------------------------------
// Accessors
// ---------------------------------------------------------------------------

View File

@@ -12,7 +12,7 @@
#include "BeltSystem.h"
#include "EntityAdmin.h"
#include "entt/entity/entity.hpp"
#include "SchematicDropEvent.h"
#include "SchematicChoiceOption.h"
#include "BuildingType.h"
#include "BuildingId.h"
#include "EventHandler.h"
@@ -52,8 +52,16 @@ public:
// internal queue. Call once per rendered frame (REQ-SHP-FIRING-BEAM).
std::vector<FireEvent> drainFireEvents();
// Returns all schematic drop events since the last drain.
std::vector<SchematicDropEvent> drainSchematicDropEvents();
// Returns the pending schematic choices (empty if no drop is pending).
const std::vector<SchematicChoiceOption>& getPendingSchematicChoices() const;
// Returns true if there are pending schematic choices waiting for player input.
bool hasSchematicChoicesPending() const;
// Applies the player's chosen schematic from the pending choices.
// choiceIndex must be in [0, pendingChoices.size()).
// Clears the pending choices after application.
void applySchematicChoice(int choiceIndex);
Tick currentTick() const;
int buildingBlocksStock() const;
@@ -108,8 +116,8 @@ private:
// Tick step 9: remove dead ships and buildings, drop scrap, handle push.
void tickDeathsAndLoot();
// Award a random schematic drop (REQ-DEF-SCHEMATIC-DROP) and emit the event.
void awardSchematicDrop(int destroyedStationLevel);
// Generate up to 3 schematic choices (REQ-DEF-SCHEMATIC-DROP) for the player.
void generateSchematicChoices(int destroyedStationLevel);
GameConfig m_config;
std::mt19937 m_rng;
@@ -158,5 +166,5 @@ private:
std::unique_ptr<CombatSystem> m_combatSystem;
std::vector<FireEvent> m_fireEvents;
std::vector<SchematicDropEvent> m_schematicDropEvents;
std::vector<SchematicChoiceOption> m_pendingSchematicChoices;
};