move blueprints out of the sidebar into Ctrl+C / Ctrl+V dialogs

This commit is contained in:
2026-08-06 19:03:59 +02:00
parent 2cbcf1554f
commit 18cfe238f6
22 changed files with 1021 additions and 400 deletions

View File

@@ -0,0 +1,11 @@
#pragma once
#include "Event.h"
// Ctrl+C: save the current building selection as a named blueprint
// (REQ-UI-BLUEPRINT-CREATE, REQ-UI-HOTKEYS). MainWindow owns the modal flow, because
// it is the only widget that can pause the game and raise the dim overlay; whether
// anything placeable is selected is decided there, not by the key handler.
class BlueprintSaveRequestedEvent : public Event
{
};

View File

@@ -0,0 +1,11 @@
#pragma once
#include "Event.h"
// Ctrl+V: open the blueprint selection dialog (REQ-UI-BLUEPRINT-DIALOG,
// REQ-UI-HOTKEYS). The Ctrl+C path does not go through this event -- it opens the
// dialog directly so one pause scope and one dim scope span both dialogs
// (REQ-UI-MODAL-DIM).
class BlueprintSelectionRequestedEvent : public Event
{
};

View File

@@ -15,6 +15,8 @@ SET(HDRS
${CMAKE_CURRENT_SOURCE_DIR}/UnlockedBuildingsChangedEvent.h
${CMAKE_CURRENT_SOURCE_DIR}/BuilderModeExitedEvent.h
${CMAKE_CURRENT_SOURCE_DIR}/BlueprintModeExitedEvent.h
${CMAKE_CURRENT_SOURCE_DIR}/BlueprintSaveRequestedEvent.h
${CMAKE_CURRENT_SOURCE_DIR}/BlueprintSelectionRequestedEvent.h
${CMAKE_CURRENT_SOURCE_DIR}/EscapeMenuRequestedEvent.h
${CMAKE_CURRENT_SOURCE_DIR}/PanDirectionChangedEvent.h
${CMAKE_CURRENT_SOURCE_DIR}/PauseToggleRequestedEvent.h

View File

@@ -3,10 +3,14 @@
#include <algorithm>
#include <climits>
#include <cstddef>
#include <map>
#include "BeltSystem.h"
#include "Building.h"
#include "BuildingsConfig.h"
#include "BuildingSystem.h"
#include "DisplayName.h"
#include "Simulation.h"
namespace
@@ -49,6 +53,21 @@ std::optional<SelectedBuilding> resolvePlaceable(const Simulation& sim, Building
resolved.bodyCells = building ? &building->bodyCells : &site->bodyCells;
return resolved;
}
// Position of a building type in buildings.toml, which is the order the build button
// bar lays out its buttons (REQ-UI-BUILD-BAR) and so the tie-break order for a
// blueprint's contents line. A type with no config entry sorts after every known one.
std::size_t configOrderIndex(BuildingType type, const BuildingsConfig& buildings)
{
for (std::size_t i = 0; i < buildings.buildings.size(); ++i)
{
if (buildings.buildings[i].type == type)
{
return i;
}
}
return buildings.buildings.size();
}
} // namespace
std::optional<BuildingConfig> readBuildingConfig(const Simulation& sim, BuildingId id)
@@ -166,3 +185,56 @@ bool selectionHasPlaceableBuilding(const Simulation& sim,
}
return false;
}
std::vector<BlueprintContentEntry> summarizeBlueprintContents(
const Blueprint& blueprint, const BuildingsConfig& buildings)
{
std::map<BuildingType, int> counts;
for (const BlueprintBuilding& building : blueprint.buildings)
{
++counts[building.type];
}
// The config index is carried through the sort so the comparator stays a strict
// total order; the enum value is the final tie-break, which only two config-less
// types could ever reach.
struct RankedType
{
BuildingType type;
int count;
std::size_t order;
};
std::vector<RankedType> ranked;
ranked.reserve(counts.size());
for (const std::pair<const BuildingType, int>& entry : counts)
{
ranked.push_back({entry.first, entry.second,
configOrderIndex(entry.first, buildings)});
}
std::sort(ranked.begin(), ranked.end(),
[](const RankedType& left, const RankedType& right)
{
if (left.count != right.count) { return left.count > right.count; }
if (left.order != right.order) { return left.order < right.order; }
return static_cast<int>(left.type) < static_cast<int>(right.type);
});
std::vector<BlueprintContentEntry> summary;
summary.reserve(ranked.size());
for (const RankedType& entry : ranked)
{
summary.push_back({toDisplayName(buildingTypeId(entry.type)), entry.count});
}
return summary;
}
int computeBlueprintCost(const Blueprint& blueprint, const BuildingsConfig& buildings)
{
int total = 0;
for (const BlueprintBuilding& building : blueprint.buildings)
{
const BuildingDef* def = buildings.findBuildingDef(building.type);
if (def) { total += def->cost; }
}
return total;
}

View File

@@ -11,6 +11,7 @@
#include "ShipLayout.h"
class Simulation;
struct BuildingsConfig;
// The user-configurable settings of a single building or construction site: the
// selected recipe / ship schematic, the shipyard module layout, and (for
@@ -49,6 +50,29 @@ Blueprint captureBlueprintFromSelection(const Simulation& sim,
const std::vector<BuildingId>& selectedIds);
// True if any selected id refers to a player-placeable building or construction site
// (the enable condition for the Create Blueprint button, REQ-UI-BLUEPRINT-CREATE).
// (the condition under which Ctrl+C opens the blueprint save dialog,
// REQ-UI-BLUEPRINT-CREATE).
bool selectionHasPlaceableBuilding(const Simulation& sim,
const std::vector<BuildingId>& selectedIds);
// One "<building name> x <count>" entry of a blueprint card's contents line
// (REQ-UI-BLUEPRINT-CARD).
struct BlueprintContentEntry
{
std::string buildingName;
int count;
};
// Summarizes what a blueprint holds: one entry per building type it contains, ordered
// by descending count with ties broken by the order the types appear in buildings.toml
// (which is the order of the build button bar, REQ-UI-BUILD-BAR). Building types absent
// from the config sort last. Derived display data for the blueprint card, kept here so
// it is unit-testable rather than buried in the dialog (REQ-UI-BLUEPRINT-CARD).
std::vector<BlueprintContentEntry> summarizeBlueprintContents(
const Blueprint& blueprint, const BuildingsConfig& buildings);
// Plain sum of the placement cost of every building in the blueprint
// (REQ-UI-BLUEPRINT-CARD). Distinct from the total charged on placement
// (REQ-UI-BLUEPRINT-PLACE), which additionally excludes locked building types and
// rotate-in-place targets; see GameWorldView's placement path.
int computeBlueprintCost(const Blueprint& blueprint, const BuildingsConfig& buildings);