Compare commits
15 Commits
master
...
993325d97c
| Author | SHA1 | Date | |
|---|---|---|---|
| 993325d97c | |||
| 75f306f650 | |||
| 0eb9c97e5d | |||
| 28d0416458 | |||
| 59fde8dbbc | |||
| 5355f9f77d | |||
| f678dab387 | |||
| ebee62166d | |||
| 3c549a160c | |||
| dc58f6ea32 | |||
| 5bd804601c | |||
| 92a4f02cef | |||
| 84d32b6c16 | |||
| d31ff68ab7 | |||
| b722955f7e |
@@ -59,4 +59,18 @@ struct ModuleDef
|
||||
struct ModulesConfig
|
||||
{
|
||||
std::vector<ModuleDef> modules;
|
||||
|
||||
// Returns the definition for the given module id, or nullptr if the id has
|
||||
// no entry in modules.toml.
|
||||
const ModuleDef* findModuleDef(const std::string& id) const
|
||||
{
|
||||
for (const ModuleDef& def : modules)
|
||||
{
|
||||
if (def.id == id)
|
||||
{
|
||||
return &def;
|
||||
}
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
};
|
||||
|
||||
@@ -47,4 +47,32 @@ struct RecipeDef
|
||||
struct RecipesConfig
|
||||
{
|
||||
std::vector<RecipeDef> recipes;
|
||||
|
||||
// Returns the definition for the given recipe id, or nullptr if the id has
|
||||
// no entry in recipes.toml.
|
||||
const RecipeDef* findRecipeDef(const std::string& id) const
|
||||
{
|
||||
for (const RecipeDef& recipe : recipes)
|
||||
{
|
||||
if (recipe.id == id)
|
||||
{
|
||||
return &recipe;
|
||||
}
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
// Same, but additionally requires the recipe to belong to the given building
|
||||
// type — recipe ids are only unique per building type.
|
||||
const RecipeDef* findRecipeDef(const std::string& id, BuildingType building) const
|
||||
{
|
||||
for (const RecipeDef& recipe : recipes)
|
||||
{
|
||||
if (recipe.id == id && recipe.building == building)
|
||||
{
|
||||
return &recipe;
|
||||
}
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
};
|
||||
|
||||
@@ -49,4 +49,18 @@ struct ShipDef
|
||||
struct ShipsConfig
|
||||
{
|
||||
std::vector<ShipDef> ships;
|
||||
|
||||
// Returns the definition for the given ship schematic id, or nullptr if the
|
||||
// id has no entry in ships.toml.
|
||||
const ShipDef* findShipDef(const std::string& id) const
|
||||
{
|
||||
for (const ShipDef& def : ships)
|
||||
{
|
||||
if (def.id == id)
|
||||
{
|
||||
return &def;
|
||||
}
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
};
|
||||
|
||||
@@ -45,11 +45,11 @@ entt::entity EntityAdmin::spawnShip(QVector2D position, float hp, float maxHp,
|
||||
const std::string& schematicId, bool isEnemy)
|
||||
{
|
||||
entt::entity entity = createEntity();
|
||||
add<PositionComponent>(entity, PositionComponent{position});
|
||||
add<HealthComponent>(entity, HealthComponent{hp, maxHp});
|
||||
add<FactionComponent>(entity, FactionComponent{isEnemy});
|
||||
add<FacingComponent>(entity, FacingComponent{0.0f});
|
||||
add<DynamicBodyComponent>(entity, DynamicBodyComponent{
|
||||
addComponent<PositionComponent>(entity, PositionComponent{position});
|
||||
addComponent<HealthComponent>(entity, HealthComponent{hp, maxHp});
|
||||
addComponent<FactionComponent>(entity, FactionComponent{isEnemy});
|
||||
addComponent<FacingComponent>(entity, FacingComponent{0.0f});
|
||||
addComponent<DynamicBodyComponent>(entity, DynamicBodyComponent{
|
||||
maxSpeed_tpt,
|
||||
mainAcceleration_tptt,
|
||||
maneuveringAcceleration_tptt,
|
||||
@@ -60,9 +60,9 @@ entt::entity EntityAdmin::spawnShip(QVector2D position, float hp, float maxHp,
|
||||
QVector2D(0.0f, 0.0f), // linearAcceleration_tptt
|
||||
0.0f // angularAcceleration_rptt
|
||||
});
|
||||
add<SensorRangeComponent>(entity, SensorRangeComponent{sensorRange_tiles});
|
||||
add<ShipIdentityComponent>(entity, ShipIdentityComponent{schematicId});
|
||||
add<MovementIntentComponent>(entity, MovementIntentComponent{0, QVector2D(0.0f, 0.0f)});
|
||||
addComponent<SensorRangeComponent>(entity, SensorRangeComponent{sensorRange_tiles});
|
||||
addComponent<ShipIdentityComponent>(entity, ShipIdentityComponent{schematicId});
|
||||
addComponent<MovementIntentComponent>(entity, MovementIntentComponent{0, QVector2D(0.0f, 0.0f)});
|
||||
return entity;
|
||||
}
|
||||
|
||||
@@ -73,28 +73,28 @@ entt::entity EntityAdmin::spawnStation(QPoint anchor, QSize footprint,
|
||||
entt::entity entity = createEntity();
|
||||
QVector2D center(anchor.x() + footprint.width() / 2.0f,
|
||||
anchor.y() + footprint.height() / 2.0f);
|
||||
add<PositionComponent>(entity, PositionComponent{center});
|
||||
add<HealthComponent>(entity, HealthComponent{hp, maxHp});
|
||||
add<FactionComponent>(entity, FactionComponent{isEnemy});
|
||||
add<StationBodyComponent>(entity, StationBodyComponent{anchor, footprint, bodyCells});
|
||||
addComponent<PositionComponent>(entity, PositionComponent{center});
|
||||
addComponent<HealthComponent>(entity, HealthComponent{hp, maxHp});
|
||||
addComponent<FactionComponent>(entity, FactionComponent{isEnemy});
|
||||
addComponent<StationBodyComponent>(entity, StationBodyComponent{anchor, footprint, bodyCells});
|
||||
return entity;
|
||||
}
|
||||
|
||||
entt::entity EntityAdmin::spawnDebris(QVector2D position, int amount, Tick despawnAt)
|
||||
{
|
||||
entt::entity entity = createEntity();
|
||||
add<PositionComponent>(entity, PositionComponent{position});
|
||||
add<DebrisComponent>(entity, DebrisComponent{amount});
|
||||
add<DespawnAtComponent>(entity, DespawnAtComponent{despawnAt});
|
||||
addComponent<PositionComponent>(entity, PositionComponent{position});
|
||||
addComponent<DebrisComponent>(entity, DebrisComponent{amount});
|
||||
addComponent<DespawnAtComponent>(entity, DespawnAtComponent{despawnAt});
|
||||
return entity;
|
||||
}
|
||||
|
||||
entt::entity EntityAdmin::spawnHqProxy(QVector2D position, float hp, float maxHp)
|
||||
{
|
||||
entt::entity entity = createEntity();
|
||||
add<PositionComponent>(entity, PositionComponent{position});
|
||||
add<HealthComponent>(entity, HealthComponent{hp, maxHp});
|
||||
add<FactionComponent>(entity, FactionComponent{false});
|
||||
add<HqProxyComponent>(entity);
|
||||
addComponent<PositionComponent>(entity, PositionComponent{position});
|
||||
addComponent<HealthComponent>(entity, HealthComponent{hp, maxHp});
|
||||
addComponent<FactionComponent>(entity, FactionComponent{false});
|
||||
addComponent<HqProxyComponent>(entity);
|
||||
return entity;
|
||||
}
|
||||
|
||||
@@ -73,9 +73,6 @@ public:
|
||||
private:
|
||||
entt::entity createEntity();
|
||||
|
||||
template <typename T, typename... Args>
|
||||
void add(entt::entity entity, Args&&... args);
|
||||
|
||||
entt::registry m_registry;
|
||||
};
|
||||
|
||||
@@ -133,10 +130,4 @@ void EntityAdmin::removeComponent(entt::entity entity)
|
||||
m_registry.remove<T>(entity);
|
||||
}
|
||||
|
||||
template <typename T, typename... Args>
|
||||
void EntityAdmin::add(entt::entity entity, Args&&... args)
|
||||
{
|
||||
m_registry.emplace<T>(entity, std::forward<Args>(args)...);
|
||||
}
|
||||
|
||||
#endif // ENTITY_ADMIN_H
|
||||
|
||||
@@ -5,8 +5,10 @@ SET(HDRS
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/ai/AttackEvaluator.h
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/ai/AttackExecutor.h
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/ai/BehaviorTargeting.h
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/ai/Centroid.h
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/ai/DeliverScrapEvaluator.h
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/ai/DeliverScrapExecutor.h
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/ai/OrbitAndAssignExecutor.h
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/ai/RallyEvaluator.h
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/ai/RallyExecutor.h
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/ai/RepairEvaluator.h
|
||||
|
||||
@@ -41,35 +41,11 @@ ShipSystem::ShipSystem(const GameConfig& config, EntityAdmin& admin)
|
||||
{
|
||||
}
|
||||
|
||||
const ShipDef* ShipSystem::findShipDef(const std::string& schematicId) const
|
||||
{
|
||||
for (const ShipDef& def : m_config.ships.ships)
|
||||
{
|
||||
if (def.id == schematicId)
|
||||
{
|
||||
return &def;
|
||||
}
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
const ModuleDef* ShipSystem::findModuleDef(const std::string& id) const
|
||||
{
|
||||
for (const ModuleDef& def : m_config.modules.modules)
|
||||
{
|
||||
if (def.id == id)
|
||||
{
|
||||
return &def;
|
||||
}
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
entt::entity ShipSystem::spawn(const std::string& schematicId,
|
||||
QVector2D position, bool isEnemy,
|
||||
const std::optional<ShipLayoutConfig>& layout)
|
||||
{
|
||||
const ShipDef* def = findShipDef(schematicId);
|
||||
const ShipDef* def = m_config.ships.findShipDef(schematicId);
|
||||
assert(def != nullptr);
|
||||
|
||||
const float tickRate = static_cast<float>(kTickRateHz);
|
||||
@@ -116,7 +92,7 @@ entt::entity ShipSystem::spawn(const std::string& schematicId,
|
||||
|
||||
for (const PlacedModule& pm : modules)
|
||||
{
|
||||
const ModuleDef* modDef = findModuleDef(pm.moduleId);
|
||||
const ModuleDef* modDef = m_config.modules.findModuleDef(pm.moduleId);
|
||||
if (!modDef) { throw std::runtime_error("unknown module id '" + pm.moduleId + "'"); }
|
||||
|
||||
if (modDef->weaponCapability)
|
||||
@@ -184,7 +160,7 @@ entt::entity ShipSystem::spawn(const std::string& schematicId,
|
||||
|
||||
for (const PlacedModule& pm : modules)
|
||||
{
|
||||
const ModuleDef* modDef = findModuleDef(pm.moduleId);
|
||||
const ModuleDef* modDef = m_config.modules.findModuleDef(pm.moduleId);
|
||||
if (!modDef) { throw std::runtime_error("unknown module id '" + pm.moduleId + "'"); }
|
||||
|
||||
for (const ModuleStatModifier& sm : modDef->statModifiers)
|
||||
|
||||
@@ -38,9 +38,6 @@ public:
|
||||
void setRetreatEnabled(bool enabled);
|
||||
|
||||
private:
|
||||
const ShipDef* findShipDef(const std::string& schematicId) const;
|
||||
const ModuleDef* findModuleDef(const std::string& id) const;
|
||||
|
||||
const GameConfig& m_config;
|
||||
EntityAdmin& m_admin;
|
||||
QVector2D m_rallyPoint;
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
|
||||
#include "AdvanceBehavior.h"
|
||||
#include "BehaviorKind.h"
|
||||
#include "Centroid.h"
|
||||
#include "EntityAdmin.h"
|
||||
#include "FactionComponent.h"
|
||||
#include "HealthComponent.h"
|
||||
@@ -16,28 +17,6 @@
|
||||
#include "StationBodyComponent.h"
|
||||
#include "tracing.h"
|
||||
|
||||
namespace
|
||||
{
|
||||
// Accumulates positions to produce their centroid (the center between them).
|
||||
struct Centroid
|
||||
{
|
||||
QVector2D sum;
|
||||
int count = 0;
|
||||
|
||||
void add(const QVector2D& point)
|
||||
{
|
||||
sum += point;
|
||||
count += 1;
|
||||
}
|
||||
|
||||
std::optional<QVector2D> value() const
|
||||
{
|
||||
if (count == 0) { return std::nullopt; }
|
||||
return sum / static_cast<float>(count);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
void AdvanceExecutor::execute(EntityAdmin& admin)
|
||||
{
|
||||
TRACE();
|
||||
|
||||
@@ -2,12 +2,8 @@
|
||||
|
||||
#include "AttackBehavior.h"
|
||||
#include "BehaviorKind.h"
|
||||
#include "DynamicBodyComponent.h"
|
||||
#include "EntityAdmin.h"
|
||||
#include "ModuleOwnerComponent.h"
|
||||
#include "MovementIntentComponent.h"
|
||||
#include "PositionComponent.h"
|
||||
#include "SelectedBehaviorComponent.h"
|
||||
#include "OrbitAndAssignExecutor.h"
|
||||
#include "tracing.h"
|
||||
#include "WeaponComponent.h"
|
||||
|
||||
@@ -15,55 +11,7 @@ void AttackExecutor::execute(EntityAdmin& admin)
|
||||
{
|
||||
TRACE();
|
||||
|
||||
// Ships: move toward the behavior target.
|
||||
admin.forEach<AttackBehavior, SelectedBehaviorComponent, PositionComponent,
|
||||
MovementIntentComponent>(
|
||||
[&](entt::entity /*e*/, const AttackBehavior& attack,
|
||||
const SelectedBehaviorComponent& selected, const PositionComponent& pos,
|
||||
MovementIntentComponent& intent)
|
||||
{
|
||||
if (selected.winner != BehaviorKind::Attack) { return; }
|
||||
if (!attack.currentTarget) { return; }
|
||||
|
||||
const entt::entity t = *attack.currentTarget;
|
||||
QVector2D center = pos.value;
|
||||
float radius = 0.0f;
|
||||
QVector2D centerVelocity;
|
||||
if (admin.isValid(t) && admin.hasAll<PositionComponent>(t))
|
||||
{
|
||||
center = admin.get<PositionComponent>(t).value;
|
||||
radius = attack.orbitRadius_tiles;
|
||||
if (admin.hasAll<DynamicBodyComponent>(t))
|
||||
{
|
||||
centerVelocity = admin.get<DynamicBodyComponent>(t).velocity_tpt;
|
||||
}
|
||||
}
|
||||
intent = MovementIntentComponent{true, center, radius, centerVelocity};
|
||||
});
|
||||
|
||||
// Weapons: assign the behavior target only if it is within this weapon's range.
|
||||
admin.forEach<WeaponComponent, ModuleOwnerComponent>(
|
||||
[&](entt::entity /*we*/, WeaponComponent& weapon, const ModuleOwnerComponent& owner)
|
||||
{
|
||||
if (!admin.hasAll<AttackBehavior, SelectedBehaviorComponent>(owner.owner))
|
||||
{
|
||||
return;
|
||||
}
|
||||
const SelectedBehaviorComponent& selected =
|
||||
admin.get<SelectedBehaviorComponent>(owner.owner);
|
||||
if (selected.winner != BehaviorKind::Attack) { return; }
|
||||
|
||||
const AttackBehavior& attack = admin.get<AttackBehavior>(owner.owner);
|
||||
if (!attack.currentTarget) { return; }
|
||||
|
||||
const entt::entity t = *attack.currentTarget;
|
||||
if (!admin.isValid(t) || !admin.hasAll<PositionComponent>(t)) { return; }
|
||||
|
||||
const QVector2D ownerPos = admin.get<PositionComponent>(owner.owner).value;
|
||||
const float dist = (admin.get<PositionComponent>(t).value - ownerPos).length();
|
||||
if (dist <= weapon.range_tiles)
|
||||
{
|
||||
weapon.currentTarget = t;
|
||||
}
|
||||
});
|
||||
// Orbit the attack target and hand it to every weapon that can reach it
|
||||
// (REQ-SHP-ORBIT).
|
||||
executeOrbitAndAssign<AttackBehavior, WeaponComponent>(admin, BehaviorKind::Attack);
|
||||
}
|
||||
|
||||
26
src/lib/ecs/system/ai/Centroid.h
Normal file
26
src/lib/ecs/system/ai/Centroid.h
Normal file
@@ -0,0 +1,26 @@
|
||||
#pragma once
|
||||
|
||||
#include <optional>
|
||||
|
||||
#include <QVector2D>
|
||||
|
||||
// Accumulates positions to produce their centroid (the center between them).
|
||||
// Shared by the behavior executors that steer toward the middle of a group of
|
||||
// entities (AdvanceExecutor: defence stations; StandbyExecutor: friendly ships).
|
||||
struct Centroid
|
||||
{
|
||||
QVector2D sum;
|
||||
int count = 0;
|
||||
|
||||
void add(const QVector2D& point)
|
||||
{
|
||||
sum += point;
|
||||
count += 1;
|
||||
}
|
||||
|
||||
std::optional<QVector2D> value() const
|
||||
{
|
||||
if (count == 0) { return std::nullopt; }
|
||||
return sum / static_cast<float>(count);
|
||||
}
|
||||
};
|
||||
89
src/lib/ecs/system/ai/OrbitAndAssignExecutor.h
Normal file
89
src/lib/ecs/system/ai/OrbitAndAssignExecutor.h
Normal file
@@ -0,0 +1,89 @@
|
||||
#pragma once
|
||||
|
||||
#include <QVector2D>
|
||||
|
||||
#include "entt/entity/entity.hpp"
|
||||
|
||||
#include "BehaviorKind.h"
|
||||
#include "DynamicBodyComponent.h"
|
||||
#include "EntityAdmin.h"
|
||||
#include "ModuleOwnerComponent.h"
|
||||
#include "MovementIntentComponent.h"
|
||||
#include "PositionComponent.h"
|
||||
#include "SelectedBehaviorComponent.h"
|
||||
|
||||
// Shared executor body for the behaviors that orbit a single target entity and then
|
||||
// hand that target to the ship's in-range modules (REQ-SHP-ORBIT): Attack (with
|
||||
// WeaponComponent) and Repair (with RepairToolComponent).
|
||||
//
|
||||
// Two passes, in this order — the order and the exact sequence of component writes
|
||||
// are load-bearing for determinism (see the Tick Order section of
|
||||
// docs/architecture.md):
|
||||
// 1. Ships that have `Behavior` and won with `kind` write their MovementIntent to
|
||||
// orbit the behavior's target at the behavior's orbit radius. A target that is
|
||||
// gone (or has no position) degenerates to "hold position": the ship's own
|
||||
// position with a zero radius.
|
||||
// 2. Modules of type `ModuleComponent` whose owner won with `kind` adopt the
|
||||
// behavior's target, but only when it lies within that module's own range.
|
||||
// Out-of-range modules keep whatever target they already had, which
|
||||
// CombatSystem/RepairSystem re-validate.
|
||||
//
|
||||
// `Behavior` must expose `std::optional<entt::entity> currentTarget` and
|
||||
// `float orbitRadius_tiles`; `ModuleComponent` must expose `float range_tiles` and
|
||||
// `std::optional<entt::entity> currentTarget`.
|
||||
template <typename Behavior, typename ModuleComponent>
|
||||
void executeOrbitAndAssign(EntityAdmin& admin, BehaviorKind kind)
|
||||
{
|
||||
// Ships: move toward the behavior target.
|
||||
admin.forEach<Behavior, SelectedBehaviorComponent, PositionComponent,
|
||||
MovementIntentComponent>(
|
||||
[&](entt::entity /*e*/, const Behavior& behavior,
|
||||
const SelectedBehaviorComponent& selected, const PositionComponent& pos,
|
||||
MovementIntentComponent& intent)
|
||||
{
|
||||
if (selected.winner != kind) { return; }
|
||||
if (!behavior.currentTarget) { return; }
|
||||
|
||||
const entt::entity t = *behavior.currentTarget;
|
||||
QVector2D center = pos.value;
|
||||
float radius = 0.0f;
|
||||
QVector2D centerVelocity;
|
||||
if (admin.isValid(t) && admin.hasAll<PositionComponent>(t))
|
||||
{
|
||||
center = admin.get<PositionComponent>(t).value;
|
||||
radius = behavior.orbitRadius_tiles;
|
||||
if (admin.hasAll<DynamicBodyComponent>(t))
|
||||
{
|
||||
centerVelocity = admin.get<DynamicBodyComponent>(t).velocity_tpt;
|
||||
}
|
||||
}
|
||||
intent = MovementIntentComponent{true, center, radius, centerVelocity};
|
||||
});
|
||||
|
||||
// Modules: assign the behavior target only if it is within this module's range.
|
||||
admin.forEach<ModuleComponent, ModuleOwnerComponent>(
|
||||
[&](entt::entity /*me*/, ModuleComponent& module,
|
||||
const ModuleOwnerComponent& owner)
|
||||
{
|
||||
if (!admin.hasAll<Behavior, SelectedBehaviorComponent>(owner.owner))
|
||||
{
|
||||
return;
|
||||
}
|
||||
const SelectedBehaviorComponent& selected =
|
||||
admin.get<SelectedBehaviorComponent>(owner.owner);
|
||||
if (selected.winner != kind) { return; }
|
||||
|
||||
const Behavior& behavior = admin.get<Behavior>(owner.owner);
|
||||
if (!behavior.currentTarget) { return; }
|
||||
|
||||
const entt::entity t = *behavior.currentTarget;
|
||||
if (!admin.isValid(t) || !admin.hasAll<PositionComponent>(t)) { return; }
|
||||
|
||||
const QVector2D ownerPos = admin.get<PositionComponent>(owner.owner).value;
|
||||
const float dist = (admin.get<PositionComponent>(t).value - ownerPos).length();
|
||||
if (dist <= module.range_tiles)
|
||||
{
|
||||
module.currentTarget = t;
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -1,69 +1,17 @@
|
||||
#include "RepairExecutor.h"
|
||||
|
||||
#include "BehaviorKind.h"
|
||||
#include "DynamicBodyComponent.h"
|
||||
#include "EntityAdmin.h"
|
||||
#include "ModuleOwnerComponent.h"
|
||||
#include "MovementIntentComponent.h"
|
||||
#include "PositionComponent.h"
|
||||
#include "OrbitAndAssignExecutor.h"
|
||||
#include "RepairBehavior.h"
|
||||
#include "RepairToolComponent.h"
|
||||
#include "SelectedBehaviorComponent.h"
|
||||
#include "tracing.h"
|
||||
|
||||
void RepairExecutor::execute(EntityAdmin& admin)
|
||||
{
|
||||
TRACE();
|
||||
|
||||
// Ships: move toward the repair target.
|
||||
admin.forEach<RepairBehavior, SelectedBehaviorComponent, PositionComponent,
|
||||
MovementIntentComponent>(
|
||||
[&](entt::entity /*e*/, const RepairBehavior& repair,
|
||||
const SelectedBehaviorComponent& selected, const PositionComponent& pos,
|
||||
MovementIntentComponent& intent)
|
||||
{
|
||||
if (selected.winner != BehaviorKind::Repair) { return; }
|
||||
if (!repair.currentTarget) { return; }
|
||||
|
||||
const entt::entity t = *repair.currentTarget;
|
||||
QVector2D center = pos.value;
|
||||
float radius = 0.0f;
|
||||
QVector2D centerVelocity;
|
||||
if (admin.isValid(t) && admin.hasAll<PositionComponent>(t))
|
||||
{
|
||||
center = admin.get<PositionComponent>(t).value;
|
||||
radius = repair.orbitRadius_tiles;
|
||||
if (admin.hasAll<DynamicBodyComponent>(t))
|
||||
{
|
||||
centerVelocity = admin.get<DynamicBodyComponent>(t).velocity_tpt;
|
||||
}
|
||||
}
|
||||
intent = MovementIntentComponent{true, center, radius, centerVelocity};
|
||||
});
|
||||
|
||||
// Repair tools: prefer the behavior target if it is within tool range.
|
||||
admin.forEach<RepairToolComponent, ModuleOwnerComponent>(
|
||||
[&](entt::entity /*re*/, RepairToolComponent& tool, const ModuleOwnerComponent& owner)
|
||||
{
|
||||
if (!admin.hasAll<RepairBehavior, SelectedBehaviorComponent>(owner.owner))
|
||||
{
|
||||
return;
|
||||
}
|
||||
const SelectedBehaviorComponent& selected =
|
||||
admin.get<SelectedBehaviorComponent>(owner.owner);
|
||||
if (selected.winner != BehaviorKind::Repair) { return; }
|
||||
|
||||
const RepairBehavior& repair = admin.get<RepairBehavior>(owner.owner);
|
||||
if (!repair.currentTarget) { return; }
|
||||
|
||||
const entt::entity t = *repair.currentTarget;
|
||||
if (!admin.isValid(t) || !admin.hasAll<PositionComponent>(t)) { return; }
|
||||
|
||||
const QVector2D ownerPos = admin.get<PositionComponent>(owner.owner).value;
|
||||
const float dist = (admin.get<PositionComponent>(t).value - ownerPos).length();
|
||||
if (dist <= tool.range_tiles)
|
||||
{
|
||||
tool.currentTarget = t;
|
||||
}
|
||||
});
|
||||
// Orbit the repair target and hand it to every repair tool that can reach it
|
||||
// (REQ-SHP-ORBIT).
|
||||
executeOrbitAndAssign<RepairBehavior, RepairToolComponent>(admin, BehaviorKind::Repair);
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
#include <QVector2D>
|
||||
|
||||
#include "BehaviorKind.h"
|
||||
#include "Centroid.h"
|
||||
#include "EntityAdmin.h"
|
||||
#include "FactionComponent.h"
|
||||
#include "HealthComponent.h"
|
||||
@@ -16,28 +17,6 @@
|
||||
#include "StationBodyComponent.h"
|
||||
#include "tracing.h"
|
||||
|
||||
namespace
|
||||
{
|
||||
// Accumulates positions to produce their centroid (the center between them).
|
||||
struct Centroid
|
||||
{
|
||||
QVector2D sum;
|
||||
int count = 0;
|
||||
|
||||
void add(const QVector2D& point)
|
||||
{
|
||||
sum += point;
|
||||
count += 1;
|
||||
}
|
||||
|
||||
std::optional<QVector2D> value() const
|
||||
{
|
||||
if (count == 0) { return std::nullopt; }
|
||||
return sum / static_cast<float>(count);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
void StandbyExecutor::execute(EntityAdmin& admin)
|
||||
{
|
||||
TRACE();
|
||||
|
||||
@@ -95,55 +95,6 @@ BuildingSystem::BuildingSystem(const GameConfig& config,
|
||||
// Private helpers
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
const BuildingDef* BuildingSystem::findBuildingDef(BuildingType type) const
|
||||
{
|
||||
for (const BuildingDef& def : m_config.buildings.buildings)
|
||||
{
|
||||
if (def.type == type)
|
||||
{
|
||||
return &def;
|
||||
}
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
const RecipeDef* BuildingSystem::findRecipe(const std::string& id,
|
||||
BuildingType type) const
|
||||
{
|
||||
for (const RecipeDef& recipe : m_config.recipes.recipes)
|
||||
{
|
||||
if (recipe.id == id && recipe.building == type)
|
||||
{
|
||||
return &recipe;
|
||||
}
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
const ShipDef* BuildingSystem::findShipDef(const std::string& id) const
|
||||
{
|
||||
for (const ShipDef& def : m_config.ships.ships)
|
||||
{
|
||||
if (def.id == id)
|
||||
{
|
||||
return &def;
|
||||
}
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
const ModuleDef* BuildingSystem::findModuleDef(const std::string& id) const
|
||||
{
|
||||
for (const ModuleDef& def : m_config.modules.modules)
|
||||
{
|
||||
if (def.id == id)
|
||||
{
|
||||
return &def;
|
||||
}
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
void BuildingSystem::initBuffers(Building& b, const RecipeDef& recipe) const
|
||||
{
|
||||
b.inputBuffer.counts.clear();
|
||||
@@ -237,7 +188,7 @@ void BuildingSystem::initShipyardBuffers(Building& b) const
|
||||
b.inputBuffer.caps.clear();
|
||||
b.outputBuffer.items.clear();
|
||||
b.outputBuffer.capacity = 0;
|
||||
const ShipDef* def = findShipDef(b.recipeId);
|
||||
const ShipDef* def = m_config.ships.findShipDef(b.recipeId);
|
||||
if (!def)
|
||||
{
|
||||
return;
|
||||
@@ -252,7 +203,7 @@ void BuildingSystem::initShipyardBuffers(Building& b) const
|
||||
{
|
||||
for (const PlacedModule& pm : b.shipLayout->placedModules)
|
||||
{
|
||||
const ModuleDef* modDef = findModuleDef(pm.moduleId);
|
||||
const ModuleDef* modDef = m_config.modules.findModuleDef(pm.moduleId);
|
||||
if (!modDef)
|
||||
{
|
||||
continue;
|
||||
@@ -272,7 +223,7 @@ void BuildingSystem::initSalvageBayBuffer(Building& b) const
|
||||
// Salvage Bay has no recipe-driven buffer; its output-buffer holding size for
|
||||
// ship drop-off is config-defined (REQ-BLD-SALVAGE-BAY).
|
||||
b.outputBuffer.items.clear();
|
||||
const BuildingDef* def = findBuildingDef(BuildingType::SalvageBay);
|
||||
const BuildingDef* def = m_config.buildings.findBuildingDef(BuildingType::SalvageBay);
|
||||
b.outputBuffer.capacity =
|
||||
(def && def->outputBufferCapacity) ? *def->outputBufferCapacity : 0;
|
||||
}
|
||||
@@ -345,7 +296,7 @@ std::vector<Port> BuildingSystem::getInputPorts(BuildingId id) const
|
||||
{
|
||||
// A site stores no ports; derive its output ports from the mask (absolute)
|
||||
// and run the same input-edge scan (REQ-BLD-BELT-DRAG, REQ-MAT-INPUT-PORTS).
|
||||
const BuildingDef* def = findBuildingDef(site->type);
|
||||
const BuildingDef* def = m_config.buildings.findBuildingDef(site->type);
|
||||
if (def == nullptr) { return {}; }
|
||||
const ParsedSurfaceMask mask = parseSurfaceMask(def->surfaceMask, site->rotation);
|
||||
std::vector<Port> outputPortsAbsolute;
|
||||
@@ -391,7 +342,7 @@ std::vector<Item> BuildingSystem::rollReprocessingOutput(const RecipeDef& recipe
|
||||
std::optional<BuildingId> BuildingSystem::place(BuildingType type, QPoint anchor,
|
||||
Rotation rotation, Tick currentTick)
|
||||
{
|
||||
const BuildingDef* def = findBuildingDef(type);
|
||||
const BuildingDef* def = m_config.buildings.findBuildingDef(type);
|
||||
assert(def != nullptr);
|
||||
const ParsedSurfaceMask mask = parseSurfaceMask(def->surfaceMask, rotation);
|
||||
|
||||
@@ -455,7 +406,7 @@ bool BuildingSystem::bodyCellsWithinWorldBounds(const std::vector<QPoint>& bodyC
|
||||
bool BuildingSystem::isPlacementValid(BuildingType type, QPoint anchor,
|
||||
Rotation rotation) const
|
||||
{
|
||||
const BuildingDef* def = findBuildingDef(type);
|
||||
const BuildingDef* def = m_config.buildings.findBuildingDef(type);
|
||||
if (def == nullptr)
|
||||
{
|
||||
return false;
|
||||
@@ -510,7 +461,7 @@ int BuildingSystem::deconstruct(BuildingId id, Tick currentTick)
|
||||
{
|
||||
if (it->id == id)
|
||||
{
|
||||
const BuildingDef* def = findBuildingDef(it->type);
|
||||
const BuildingDef* def = m_config.buildings.findBuildingDef(it->type);
|
||||
for (const QPoint& cell : it->bodyCells)
|
||||
{
|
||||
m_tileOccupancy.erase({cell.x(), cell.y()});
|
||||
@@ -646,7 +597,7 @@ void BuildingSystem::setRecipe(BuildingId id, const std::string& recipeId)
|
||||
}
|
||||
else
|
||||
{
|
||||
const RecipeDef* recipe = findRecipe(recipeId, building.type);
|
||||
const RecipeDef* recipe = m_config.recipes.findRecipeDef(recipeId, building.type);
|
||||
if (recipe)
|
||||
{
|
||||
initBuffers(building, *recipe);
|
||||
@@ -701,7 +652,7 @@ BuildingSystem::getSiteSplitterInfo(BuildingId id) const
|
||||
if (site.id != id) { continue; }
|
||||
if (site.type != BuildingType::Splitter) { return std::nullopt; }
|
||||
|
||||
const BuildingDef* def = findBuildingDef(site.type);
|
||||
const BuildingDef* def = m_config.buildings.findBuildingDef(site.type);
|
||||
const ParsedSurfaceMask mask = parseSurfaceMask(
|
||||
def ? def->surfaceMask : std::vector<std::string>{}, site.rotation);
|
||||
if (mask.outputPorts.size() < 2) { return std::nullopt; }
|
||||
@@ -748,7 +699,7 @@ void BuildingSystem::tickConstruction(Tick currentTick)
|
||||
// Guard: if somehow the front site was never started, start it now.
|
||||
if (front.completesAt == 0)
|
||||
{
|
||||
const BuildingDef* def = findBuildingDef(front.type);
|
||||
const BuildingDef* def = m_config.buildings.findBuildingDef(front.type);
|
||||
if (def)
|
||||
{
|
||||
front.completesAt = currentTick + secondsToTicks(def->constructionTimeSeconds);
|
||||
@@ -762,7 +713,7 @@ void BuildingSystem::tickConstruction(Tick currentTick)
|
||||
}
|
||||
|
||||
// Promote construction site to an operational Building.
|
||||
const BuildingDef* def = findBuildingDef(front.type);
|
||||
const BuildingDef* def = m_config.buildings.findBuildingDef(front.type);
|
||||
const ParsedSurfaceMask mask = parseSurfaceMask(
|
||||
def ? def->surfaceMask : std::vector<std::string>{},
|
||||
front.rotation);
|
||||
@@ -809,7 +760,7 @@ void BuildingSystem::tickConstruction(Tick currentTick)
|
||||
}
|
||||
else
|
||||
{
|
||||
const RecipeDef* recipe = findRecipe(building.recipeId, building.type);
|
||||
const RecipeDef* recipe = m_config.recipes.findRecipeDef(building.recipeId, building.type);
|
||||
if (recipe)
|
||||
{
|
||||
initBuffers(building, *recipe);
|
||||
@@ -828,7 +779,8 @@ void BuildingSystem::tickConstruction(Tick currentTick)
|
||||
// Start next queued site if present.
|
||||
if (!m_constructionQueue.empty() && m_constructionQueue.front().completesAt == 0)
|
||||
{
|
||||
const BuildingDef* nextDef = findBuildingDef(m_constructionQueue.front().type);
|
||||
const BuildingDef* nextDef =
|
||||
m_config.buildings.findBuildingDef(m_constructionQueue.front().type);
|
||||
if (nextDef)
|
||||
{
|
||||
m_constructionQueue.front().completesAt =
|
||||
@@ -896,7 +848,7 @@ void BuildingSystem::tickDeconstruction(Tick currentTick)
|
||||
{
|
||||
if (it->id != front.id) { continue; }
|
||||
|
||||
const BuildingDef* def = findBuildingDef(it->type);
|
||||
const BuildingDef* def = m_config.buildings.findBuildingDef(it->type);
|
||||
for (const QPoint& cell : it->bodyCells)
|
||||
{
|
||||
m_tileOccupancy.erase({cell.x(), cell.y()});
|
||||
@@ -1187,7 +1139,7 @@ void BuildingSystem::tickShipyardProduction(Tick currentTick)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
const ShipDef* shipDef = findShipDef(building.recipeId);
|
||||
const ShipDef* shipDef = m_config.ships.findShipDef(building.recipeId);
|
||||
if (!shipDef)
|
||||
{
|
||||
continue;
|
||||
@@ -1252,7 +1204,7 @@ void BuildingSystem::tickShipyardProduction(Tick currentTick)
|
||||
{
|
||||
for (const PlacedModule& pm : building.shipLayout->placedModules)
|
||||
{
|
||||
const ModuleDef* modDef = findModuleDef(pm.moduleId);
|
||||
const ModuleDef* modDef = m_config.modules.findModuleDef(pm.moduleId);
|
||||
if (modDef)
|
||||
{
|
||||
totalTime += modDef->productionTimeSeconds;
|
||||
@@ -1466,7 +1418,7 @@ BuildingSystem::gatherCandidateRecipes(const Building& b) const
|
||||
}
|
||||
else
|
||||
{
|
||||
const RecipeDef* recipe = findRecipe(b.recipeId, b.type);
|
||||
const RecipeDef* recipe = m_config.recipes.findRecipeDef(b.recipeId, b.type);
|
||||
if (recipe)
|
||||
{
|
||||
candidates.push_back(recipe);
|
||||
@@ -1495,7 +1447,7 @@ std::map<std::string, int>
|
||||
BuildingSystem::computeShipyardRequiredMaterials(const Building& b) const
|
||||
{
|
||||
std::map<std::string, int> requiredMaterials;
|
||||
const ShipDef* shipDef = findShipDef(b.recipeId);
|
||||
const ShipDef* shipDef = m_config.ships.findShipDef(b.recipeId);
|
||||
if (!shipDef)
|
||||
{
|
||||
return requiredMaterials;
|
||||
@@ -1508,7 +1460,7 @@ BuildingSystem::computeShipyardRequiredMaterials(const Building& b) const
|
||||
{
|
||||
for (const PlacedModule& pm : b.shipLayout->placedModules)
|
||||
{
|
||||
const ModuleDef* modDef = findModuleDef(pm.moduleId);
|
||||
const ModuleDef* modDef = m_config.modules.findModuleDef(pm.moduleId);
|
||||
if (!modDef)
|
||||
{
|
||||
continue;
|
||||
@@ -1638,7 +1590,7 @@ std::optional<BuildingId> BuildingSystem::findRotateInPlaceTarget(
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
const BuildingDef* def = findBuildingDef(type);
|
||||
const BuildingDef* def = m_config.buildings.findBuildingDef(type);
|
||||
if (!def) { return std::nullopt; }
|
||||
|
||||
const ParsedSurfaceMask mask = parseSurfaceMask(def->surfaceMask, rot);
|
||||
@@ -1698,7 +1650,7 @@ void BuildingSystem::rotateInPlace(BuildingId id, Rotation newRotation)
|
||||
|
||||
b.rotation = newRotation;
|
||||
|
||||
const BuildingDef* def = findBuildingDef(b.type);
|
||||
const BuildingDef* def = m_config.buildings.findBuildingDef(b.type);
|
||||
if (!def) { return; }
|
||||
const ParsedSurfaceMask mask = parseSurfaceMask(def->surfaceMask, newRotation);
|
||||
|
||||
@@ -1719,29 +1671,25 @@ void BuildingSystem::rotateInPlace(BuildingId id, Rotation newRotation)
|
||||
// the new port set (REQ-MAT-INPUT-INTAKE).
|
||||
b.incomingItems.assign(b.inputPorts.size(), {});
|
||||
|
||||
// Re-register with BeltSystem (items on tile are discarded).
|
||||
if (b.type == BuildingType::Belt)
|
||||
// Re-register with BeltSystem (items on tile are discarded). A splitter's
|
||||
// filters live in BeltSystem and would be lost by removeTile, so capture
|
||||
// them first and hand them back to reregisterBeltTile (REQ-BLD-SPLITTER).
|
||||
if (isBeltSubsystemType(b.type))
|
||||
{
|
||||
std::vector<ItemType> splitterFilterA;
|
||||
std::vector<ItemType> splitterFilterB;
|
||||
if (b.type == BuildingType::Splitter)
|
||||
{
|
||||
if (const std::optional<BeltSystem::SplitterInfo> info =
|
||||
m_belts.getSplitterInfo(b.anchor))
|
||||
{
|
||||
splitterFilterA = info->filterA;
|
||||
splitterFilterB = info->filterB;
|
||||
}
|
||||
}
|
||||
|
||||
m_belts.removeTile(b.anchor);
|
||||
m_belts.placeBelt(b.anchor, newRotation);
|
||||
}
|
||||
else if (b.type == BuildingType::Splitter)
|
||||
{
|
||||
m_belts.removeTile(b.anchor);
|
||||
assert(mask.outputPorts.size() >= 2);
|
||||
m_belts.placeSplitter(b.anchor,
|
||||
mask.outputPorts[0].direction,
|
||||
mask.outputPorts[1].direction);
|
||||
}
|
||||
else if (b.type == BuildingType::TunnelEntry)
|
||||
{
|
||||
m_belts.removeTile(b.anchor);
|
||||
m_belts.placeTunnelEntry(b.anchor, newRotation, m_config.world.tunnelMaxDistance_tiles);
|
||||
}
|
||||
else if (b.type == BuildingType::TunnelExit)
|
||||
{
|
||||
m_belts.removeTile(b.anchor);
|
||||
m_belts.placeTunnelExit(b.anchor, newRotation);
|
||||
reregisterBeltTile(b, splitterFilterA, splitterFilterB);
|
||||
}
|
||||
|
||||
return;
|
||||
|
||||
@@ -267,10 +267,6 @@ private:
|
||||
// the status light (REQ-UI-STATUS-LIGHT).
|
||||
bool hasInputsToStart(const Building& b) const;
|
||||
|
||||
const BuildingDef* findBuildingDef(BuildingType type) const;
|
||||
const RecipeDef* findRecipe(const std::string& id, BuildingType type) const;
|
||||
const ShipDef* findShipDef(const std::string& id) const;
|
||||
const ModuleDef* findModuleDef(const std::string& id) const;
|
||||
void initBuffers(Building& b, const RecipeDef& recipe) const;
|
||||
// Buffers for an auto-recipe building (Smelter, Reprocessing Plant): input
|
||||
// caps span the union of every recipe of the building's type; no player
|
||||
|
||||
@@ -21,22 +21,9 @@ ShipStats calculateShipStats(const GameConfig& config,
|
||||
{
|
||||
ShipStats result{};
|
||||
|
||||
const ShipDef* shipDef = nullptr;
|
||||
for (const ShipDef& d : config.ships.ships)
|
||||
{
|
||||
if (d.id == shipId) { shipDef = &d; break; }
|
||||
}
|
||||
const ShipDef* shipDef = config.ships.findShipDef(shipId);
|
||||
if (!shipDef) { return result; }
|
||||
|
||||
auto findModuleDef = [&](const std::string& id) -> const ModuleDef*
|
||||
{
|
||||
for (const ModuleDef& d : config.modules.modules)
|
||||
{
|
||||
if (d.id == id) { return &d; }
|
||||
}
|
||||
return nullptr;
|
||||
};
|
||||
|
||||
const double tileSize = config.world.tileSize_m;
|
||||
|
||||
// --- Base hull stats (convert from SI to display units) ------------------
|
||||
@@ -67,7 +54,7 @@ ShipStats calculateShipStats(const GameConfig& config,
|
||||
|
||||
for (const PlacedModule& pm : modules)
|
||||
{
|
||||
const ModuleDef* def = findModuleDef(pm.moduleId);
|
||||
const ModuleDef* def = config.modules.findModuleDef(pm.moduleId);
|
||||
if (!def) { throw std::runtime_error("unknown module id '" + pm.moduleId + "'"); }
|
||||
|
||||
if (def->weaponCapability)
|
||||
@@ -107,7 +94,7 @@ ShipStats calculateShipStats(const GameConfig& config,
|
||||
|
||||
for (const PlacedModule& pm : modules)
|
||||
{
|
||||
const ModuleDef* def = findModuleDef(pm.moduleId);
|
||||
const ModuleDef* def = config.modules.findModuleDef(pm.moduleId);
|
||||
if (!def) { throw std::runtime_error("unknown module id '" + pm.moduleId + "'"); }
|
||||
|
||||
for (const ModuleStatModifier& sm : def->statModifiers)
|
||||
|
||||
@@ -48,32 +48,7 @@ Simulation::Simulation(GameConfig config, unsigned int seed)
|
||||
m_currentEnemyStationEntities[0] = entt::null;
|
||||
m_currentEnemyStationEntities[1] = entt::null;
|
||||
|
||||
m_buildingSystem = std::make_unique<BuildingSystem>(
|
||||
m_config,
|
||||
m_beltSystem,
|
||||
[this]() { return allocateBuildingId(); },
|
||||
[this](int amount) { m_buildingBlocksStock += amount; },
|
||||
[this](const std::string& id, QVector2D pos,
|
||||
const std::optional<ShipLayoutConfig>& layout) {
|
||||
const std::map<std::string, SchematicState>::const_iterator it =
|
||||
m_schematicLevels.find(id);
|
||||
if (it == m_schematicLevels.end() || !it->second.unlocked)
|
||||
{
|
||||
return;
|
||||
}
|
||||
m_shipSystem->spawn(id, pos, /*isEnemy=*/false, layout);
|
||||
},
|
||||
[this](const std::string& itemId) -> bool { return isItemUnlocked(itemId); },
|
||||
m_rng);
|
||||
m_shipSystem = std::make_unique<ShipSystem>(m_config, m_admin);
|
||||
m_aiSystem = std::make_unique<AiSystem>(m_config);
|
||||
m_movementIntentSystem = std::make_unique<MovementIntentSystem>();
|
||||
m_dynamicBodySystem = std::make_unique<DynamicBodySystem>();
|
||||
m_debrisSystem = std::make_unique<DebrisSystem>(m_admin);
|
||||
m_salvagerSystem = std::make_unique<SalvagerSystem>(m_admin);
|
||||
m_repairSystem = std::make_unique<RepairSystem>(m_admin);
|
||||
m_waveSystem = std::make_unique<WaveSystem>(m_config, m_rng);
|
||||
m_combatSystem = std::make_unique<CombatSystem>(m_config);
|
||||
initializeSubsystems();
|
||||
|
||||
initializeUnlockState();
|
||||
placeInitialStructures();
|
||||
@@ -120,6 +95,14 @@ void Simulation::reset(unsigned int seed)
|
||||
|
||||
m_admin.clear();
|
||||
m_beltSystem = BeltSystem(m_config.world.beltSpeed_tps);
|
||||
initializeSubsystems();
|
||||
|
||||
initializeUnlockState();
|
||||
placeInitialStructures();
|
||||
}
|
||||
|
||||
void Simulation::initializeSubsystems()
|
||||
{
|
||||
m_buildingSystem = std::make_unique<BuildingSystem>(
|
||||
m_config,
|
||||
m_beltSystem,
|
||||
@@ -146,9 +129,6 @@ void Simulation::reset(unsigned int seed)
|
||||
m_repairSystem = std::make_unique<RepairSystem>(m_admin);
|
||||
m_waveSystem = std::make_unique<WaveSystem>(m_config, m_rng);
|
||||
m_combatSystem = std::make_unique<CombatSystem>(m_config);
|
||||
|
||||
initializeUnlockState();
|
||||
placeInitialStructures();
|
||||
}
|
||||
|
||||
void Simulation::initializeUnlockState()
|
||||
|
||||
@@ -165,6 +165,11 @@ private:
|
||||
|
||||
BuildingId allocateBuildingId(); // Strictly increasing; never returns kInvalidBuildingId.
|
||||
|
||||
// (Re-)create every owned subsystem. Shared by the constructor and reset();
|
||||
// the construction order is load-bearing for determinism, so both paths must
|
||||
// go through here. Only called before the first tick of a run.
|
||||
void initializeSubsystems();
|
||||
|
||||
// Populate HQ, player defence stations, and the first enemy station set.
|
||||
void placeInitialStructures();
|
||||
|
||||
|
||||
@@ -335,15 +335,7 @@ double calculateShipThreatCost(const ThreatCostTable& table,
|
||||
const std::string& shipId,
|
||||
const std::vector<PlacedModule>& modules)
|
||||
{
|
||||
const ShipDef* shipDef = nullptr;
|
||||
for (const ShipDef& d : config.ships.ships)
|
||||
{
|
||||
if (d.id == shipId)
|
||||
{
|
||||
shipDef = &d;
|
||||
break;
|
||||
}
|
||||
}
|
||||
const ShipDef* shipDef = config.ships.findShipDef(shipId);
|
||||
if (shipDef == nullptr)
|
||||
{
|
||||
return 0.0;
|
||||
@@ -357,15 +349,7 @@ double calculateShipThreatCost(const ThreatCostTable& table,
|
||||
// Add module production times and material threats.
|
||||
for (const PlacedModule& pm : modules)
|
||||
{
|
||||
const ModuleDef* moduleDef = nullptr;
|
||||
for (const ModuleDef& d : config.modules.modules)
|
||||
{
|
||||
if (d.id == pm.moduleId)
|
||||
{
|
||||
moduleDef = &d;
|
||||
break;
|
||||
}
|
||||
}
|
||||
const ModuleDef* moduleDef = config.modules.findModuleDef(pm.moduleId);
|
||||
if (moduleDef == nullptr)
|
||||
{
|
||||
continue;
|
||||
|
||||
@@ -11,11 +11,7 @@
|
||||
#include "Simulation.h"
|
||||
#include "SimulationTestAccess.h"
|
||||
#include "StationBodyComponent.h"
|
||||
|
||||
static GameConfig loadConfig()
|
||||
{
|
||||
return ConfigLoader::loadFromDirectory(CONFIG_DIR);
|
||||
}
|
||||
#include "TestConfig.h"
|
||||
|
||||
static void killEnemyStations(Simulation& sim)
|
||||
{
|
||||
@@ -51,7 +47,7 @@ static int findArtifactChoiceIndex(const Simulation& sim)
|
||||
TEST_CASE("ArtifactWinCondition: artifact_chance_formula and artifact_win_count are loaded",
|
||||
"[artifact_win]")
|
||||
{
|
||||
const GameConfig cfg = loadConfig();
|
||||
const GameConfig cfg = loadTestConfig();
|
||||
CHECK(cfg.world.artifacts.artifactWinCount == 3);
|
||||
// 0.05 * x at x=2 should be 0.1
|
||||
CHECK(cfg.world.artifacts.artifactChanceFormula.evaluate(2.0) == Approx(0.1));
|
||||
@@ -64,7 +60,7 @@ TEST_CASE("ArtifactWinCondition: artifact_chance_formula and artifact_win_count
|
||||
TEST_CASE("ArtifactWinCondition: artifact count is 0 and isWon is false at game start",
|
||||
"[artifact_win]")
|
||||
{
|
||||
const Simulation sim(loadConfig());
|
||||
const Simulation sim(loadTestConfig());
|
||||
CHECK(sim.getArtifactCount() == 0);
|
||||
CHECK_FALSE(sim.isWon());
|
||||
}
|
||||
@@ -76,7 +72,7 @@ TEST_CASE("ArtifactWinCondition: artifact count is 0 and isWon is false at game
|
||||
TEST_CASE("ArtifactWinCondition: artifact option appears when chance formula returns 1",
|
||||
"[artifact_win]")
|
||||
{
|
||||
GameConfig cfg = loadConfig();
|
||||
GameConfig cfg = loadTestConfig();
|
||||
cfg.world.artifacts.artifactChanceFormula = Formula::compile("1");
|
||||
Simulation sim(std::move(cfg));
|
||||
|
||||
@@ -95,7 +91,7 @@ TEST_CASE("ArtifactWinCondition: artifact option appears when chance formula ret
|
||||
TEST_CASE("ArtifactWinCondition: at most 2 schematic options accompany the artifact",
|
||||
"[artifact_win]")
|
||||
{
|
||||
GameConfig cfg = loadConfig();
|
||||
GameConfig cfg = loadTestConfig();
|
||||
cfg.world.artifacts.artifactChanceFormula = Formula::compile("1");
|
||||
Simulation sim(std::move(cfg));
|
||||
|
||||
@@ -112,7 +108,7 @@ TEST_CASE("ArtifactWinCondition: at most 2 schematic options accompany the artif
|
||||
TEST_CASE("ArtifactWinCondition: no artifact option when chance formula returns 0",
|
||||
"[artifact_win]")
|
||||
{
|
||||
GameConfig cfg = loadConfig();
|
||||
GameConfig cfg = loadTestConfig();
|
||||
cfg.world.artifacts.artifactChanceFormula = Formula::compile("0");
|
||||
Simulation sim(std::move(cfg));
|
||||
|
||||
@@ -133,7 +129,7 @@ TEST_CASE("ArtifactWinCondition: no artifact option when chance formula returns
|
||||
TEST_CASE("ArtifactWinCondition: selecting artifact increments artifact count",
|
||||
"[artifact_win]")
|
||||
{
|
||||
GameConfig cfg = loadConfig();
|
||||
GameConfig cfg = loadTestConfig();
|
||||
cfg.world.artifacts.artifactChanceFormula = Formula::compile("1");
|
||||
Simulation sim(std::move(cfg));
|
||||
|
||||
@@ -151,7 +147,7 @@ TEST_CASE("ArtifactWinCondition: selecting artifact increments artifact count",
|
||||
TEST_CASE("ArtifactWinCondition: selecting a non-artifact option does not increment artifact count",
|
||||
"[artifact_win]")
|
||||
{
|
||||
GameConfig cfg = loadConfig();
|
||||
GameConfig cfg = loadTestConfig();
|
||||
cfg.world.artifacts.artifactChanceFormula = Formula::compile("1");
|
||||
Simulation sim(std::move(cfg));
|
||||
|
||||
@@ -176,7 +172,7 @@ TEST_CASE("ArtifactWinCondition: selecting a non-artifact option does not increm
|
||||
TEST_CASE("ArtifactWinCondition: isWon becomes true when artifact count reaches win count",
|
||||
"[artifact_win]")
|
||||
{
|
||||
GameConfig cfg = loadConfig();
|
||||
GameConfig cfg = loadTestConfig();
|
||||
cfg.world.artifacts.artifactChanceFormula = Formula::compile("1");
|
||||
cfg.world.artifacts.artifactWinCount = 1;
|
||||
Simulation sim(std::move(cfg));
|
||||
@@ -196,7 +192,7 @@ TEST_CASE("ArtifactWinCondition: isWon becomes true when artifact count reaches
|
||||
TEST_CASE("ArtifactWinCondition: isWon stays false when artifact count is below win count",
|
||||
"[artifact_win]")
|
||||
{
|
||||
GameConfig cfg = loadConfig();
|
||||
GameConfig cfg = loadTestConfig();
|
||||
cfg.world.artifacts.artifactChanceFormula = Formula::compile("1");
|
||||
cfg.world.artifacts.artifactWinCount = 2;
|
||||
Simulation sim(std::move(cfg));
|
||||
@@ -212,7 +208,7 @@ TEST_CASE("ArtifactWinCondition: isWon stays false when artifact count is below
|
||||
TEST_CASE("ArtifactWinCondition: isWon becomes true after collecting required number of artifacts",
|
||||
"[artifact_win]")
|
||||
{
|
||||
GameConfig cfg = loadConfig();
|
||||
GameConfig cfg = loadTestConfig();
|
||||
cfg.world.artifacts.artifactChanceFormula = Formula::compile("1");
|
||||
cfg.world.artifacts.artifactWinCount = 2;
|
||||
Simulation sim(std::move(cfg));
|
||||
@@ -237,7 +233,7 @@ TEST_CASE("ArtifactWinCondition: isWon becomes true after collecting required nu
|
||||
TEST_CASE("ArtifactWinCondition: reset clears artifact count and win state",
|
||||
"[artifact_win]")
|
||||
{
|
||||
GameConfig cfg = loadConfig();
|
||||
GameConfig cfg = loadTestConfig();
|
||||
cfg.world.artifacts.artifactChanceFormula = Formula::compile("1");
|
||||
cfg.world.artifacts.artifactWinCount = 1;
|
||||
Simulation sim(std::move(cfg));
|
||||
|
||||
@@ -45,16 +45,12 @@
|
||||
#include "ShipLayout.h"
|
||||
#include "ShipSystem.h"
|
||||
#include "Tick.h"
|
||||
#include "TestConfig.h"
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Fixture
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
static GameConfig loadConfig()
|
||||
{
|
||||
return ConfigLoader::loadFromDirectory(CONFIG_DIR);
|
||||
}
|
||||
|
||||
struct Fixture
|
||||
{
|
||||
GameConfig cfg;
|
||||
@@ -75,7 +71,7 @@ struct Fixture
|
||||
std::vector<BeamFiredEvent> beamEvents;
|
||||
|
||||
explicit Fixture()
|
||||
: cfg(loadConfig())
|
||||
: cfg(loadTestConfig())
|
||||
, belts(cfg.world.beltSpeed_tps)
|
||||
, nextBuildingId(1)
|
||||
, stock(0)
|
||||
|
||||
@@ -20,6 +20,7 @@
|
||||
#include "SimulationTestAccess.h"
|
||||
#include "SurfaceMask.h"
|
||||
#include "Tick.h"
|
||||
#include "TestConfig.h"
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Helpers that mirror the production implementations under test.
|
||||
@@ -147,11 +148,6 @@ static void applyRotationCCW(Blueprint& bp, const GameConfig& cfg)
|
||||
}
|
||||
}
|
||||
|
||||
static GameConfig loadConfig()
|
||||
{
|
||||
return ConfigLoader::loadFromDirectory(CONFIG_DIR);
|
||||
}
|
||||
|
||||
// Mirrors BlueprintPanel::createBlueprintFromSelection's player-placeable filter:
|
||||
// building types absent from buildings.toml (HQ, stations) or with playerPlaceable=false
|
||||
// are silently excluded before the bounding-box center and offsets are computed.
|
||||
@@ -312,7 +308,7 @@ TEST_CASE("Blueprint: non-axis-aligned offset rotates correctly", "[blueprint]")
|
||||
|
||||
TEST_CASE("Blueprint: CW constellation rotation updates offset and building rotation", "[blueprint]")
|
||||
{
|
||||
const GameConfig cfg = loadConfig();
|
||||
const GameConfig cfg = loadTestConfig();
|
||||
// Building one tile to the right, facing East.
|
||||
Blueprint bp;
|
||||
bp.name = "test";
|
||||
@@ -332,7 +328,7 @@ TEST_CASE("Blueprint: CW constellation rotation updates offset and building rota
|
||||
|
||||
TEST_CASE("Blueprint: CCW constellation rotation updates offset and building rotation", "[blueprint]")
|
||||
{
|
||||
const GameConfig cfg = loadConfig();
|
||||
const GameConfig cfg = loadTestConfig();
|
||||
Blueprint bp;
|
||||
bp.name = "test";
|
||||
BlueprintBuilding bb;
|
||||
@@ -351,7 +347,7 @@ TEST_CASE("Blueprint: CCW constellation rotation updates offset and building rot
|
||||
|
||||
TEST_CASE("Blueprint: four CW rotations restore offset and building rotation", "[blueprint]")
|
||||
{
|
||||
const GameConfig cfg = loadConfig();
|
||||
const GameConfig cfg = loadTestConfig();
|
||||
Blueprint bp;
|
||||
bp.name = "test";
|
||||
BlueprintBuilding bb;
|
||||
@@ -371,7 +367,7 @@ TEST_CASE("Blueprint: four CW rotations restore offset and building rotation", "
|
||||
|
||||
TEST_CASE("Blueprint: multi-building constellation rotates symmetrically CW", "[blueprint]")
|
||||
{
|
||||
const GameConfig cfg = loadConfig();
|
||||
const GameConfig cfg = loadTestConfig();
|
||||
// Two buildings left and right of center; after CW they should be above and below.
|
||||
Blueprint bp;
|
||||
bp.name = "test";
|
||||
@@ -398,7 +394,7 @@ TEST_CASE("Blueprint: multi-building constellation rotates symmetrically CW", "[
|
||||
|
||||
TEST_CASE("Blueprint: CW rotation keeps belt adjacent to miner output port", "[blueprint]")
|
||||
{
|
||||
const GameConfig cfg = loadConfig();
|
||||
const GameConfig cfg = loadTestConfig();
|
||||
|
||||
// East miner: anchor (0,0), body cells (0,0),(1,0),(0,1).
|
||||
// Output port indicator '>' at (1,1) → port tile (1,1), direction East.
|
||||
@@ -436,7 +432,7 @@ TEST_CASE("Blueprint: CW rotation keeps belt adjacent to miner output port", "[b
|
||||
|
||||
TEST_CASE("Blueprint: CCW rotation keeps belt adjacent to miner output port", "[blueprint]")
|
||||
{
|
||||
const GameConfig cfg = loadConfig();
|
||||
const GameConfig cfg = loadTestConfig();
|
||||
|
||||
Blueprint bp;
|
||||
bp.name = "test";
|
||||
@@ -473,7 +469,7 @@ TEST_CASE("Blueprint: CCW rotation keeps belt adjacent to miner output port", "[
|
||||
TEST_CASE("Blueprint creation: non-player-placeable building alone yields empty blueprint",
|
||||
"[blueprint]")
|
||||
{
|
||||
const GameConfig cfg = loadConfig();
|
||||
const GameConfig cfg = loadTestConfig();
|
||||
|
||||
// Hq has no entry in buildings.toml, so it is treated as non-player-placeable.
|
||||
const BuildingSpec hq{ QPoint(-5, 0), {QPoint(-5, 0)}, BuildingType::Hq, Rotation::East };
|
||||
@@ -485,7 +481,7 @@ TEST_CASE("Blueprint creation: non-player-placeable building alone yields empty
|
||||
TEST_CASE("Blueprint creation: mixed selection keeps only player-placeable buildings",
|
||||
"[blueprint]")
|
||||
{
|
||||
const GameConfig cfg = loadConfig();
|
||||
const GameConfig cfg = loadTestConfig();
|
||||
|
||||
const BuildingSpec belt{ QPoint(-5, 0), {QPoint(-5, 0)}, BuildingType::Belt, Rotation::East };
|
||||
const BuildingSpec hq { QPoint(-3, 0), {QPoint(-3, 0)}, BuildingType::Hq, Rotation::East };
|
||||
@@ -498,7 +494,7 @@ TEST_CASE("Blueprint creation: mixed selection keeps only player-placeable build
|
||||
TEST_CASE("Blueprint creation: bounding box ignores non-player-placeable buildings",
|
||||
"[blueprint]")
|
||||
{
|
||||
const GameConfig cfg = loadConfig();
|
||||
const GameConfig cfg = loadTestConfig();
|
||||
|
||||
// Belt at (-5, 0). HQ at (-3, 0) — excluded from the blueprint.
|
||||
// If HQ were included: bboxX = [-5, -3], center.x = -4, belt offset = -1.
|
||||
@@ -520,7 +516,7 @@ TEST_CASE("Blueprint placement: buildings land at anchor + offset from cursor",
|
||||
// Simulate placing a two-belt blueprint with offsets (-1, 0) and (+1, 0)
|
||||
// at cursor tile (-5, 0). Expected anchors: (-6, 0) and (-4, 0).
|
||||
// (Belt surface_mask ["A>"] — body at relative (0,0), port at (1,0).)
|
||||
Simulation sim(loadConfig());
|
||||
Simulation sim(loadTestConfig());
|
||||
|
||||
const QPoint cursor(-5, 0);
|
||||
const QPoint offsetA(-1, 0);
|
||||
@@ -540,7 +536,7 @@ TEST_CASE("Blueprint placement: buildings land at anchor + offset from cursor",
|
||||
|
||||
TEST_CASE("Blueprint placement: cost is deducted for each building in sequence", "[blueprint]")
|
||||
{
|
||||
Simulation sim(loadConfig());
|
||||
Simulation sim(loadTestConfig());
|
||||
|
||||
// Find belt cost from config (belt cost = 2 in test config).
|
||||
int beltCost = 0;
|
||||
@@ -563,7 +559,7 @@ TEST_CASE("Blueprint placement: cost is deducted for each building in sequence",
|
||||
TEST_CASE("Blueprint placement: insufficient blocks returns kInvalidBuildingId and deducts nothing",
|
||||
"[blueprint]")
|
||||
{
|
||||
Simulation sim(loadConfig());
|
||||
Simulation sim(loadTestConfig());
|
||||
|
||||
// Find miner cost (15 in test config) — expensive enough to exhaust a small stock.
|
||||
int minerCost = 0;
|
||||
@@ -594,7 +590,7 @@ TEST_CASE("Blueprint placement: insufficient blocks returns kInvalidBuildingId a
|
||||
TEST_CASE("Simulation: tryPlaceBuilding rejects terrain-invalid placement and charges nothing",
|
||||
"[blueprint]")
|
||||
{
|
||||
Simulation sim(loadConfig());
|
||||
Simulation sim(loadTestConfig());
|
||||
const int startBlocks = sim.getBuildingBlocksStock();
|
||||
|
||||
// A miner is all-asteroid; placing it in space (x >= 0) violates the terrain
|
||||
@@ -610,7 +606,7 @@ TEST_CASE("Simulation: tryPlaceBuilding rejects terrain-invalid placement and ch
|
||||
TEST_CASE("Simulation: tryPlaceBuilding accepts a valid asteroid spot, occupies tiles, charges cost",
|
||||
"[blueprint]")
|
||||
{
|
||||
Simulation sim(loadConfig());
|
||||
Simulation sim(loadTestConfig());
|
||||
|
||||
int minerCost = 0;
|
||||
for (const BuildingDef& def : sim.getConfig().buildings.buildings)
|
||||
@@ -663,7 +659,7 @@ TEST_CASE("Blueprint: building with no recipe has empty recipeId", "[blueprint]"
|
||||
|
||||
TEST_CASE("Blueprint placement: setRecipe on construction site stores recipe", "[blueprint]")
|
||||
{
|
||||
Simulation sim(loadConfig());
|
||||
Simulation sim(loadTestConfig());
|
||||
|
||||
// Miner body cells: (0,0),(1,0),(0,1) — all at x < 0, valid for asteroid.
|
||||
const BuildingId id = SimulationTestAccess::place(sim,BuildingType::Miner, QPoint(-2, 0), Rotation::East).value();
|
||||
@@ -679,7 +675,7 @@ TEST_CASE("Blueprint placement: setRecipe on construction site stores recipe", "
|
||||
TEST_CASE("Blueprint placement: recipe transfers to building after construction completes",
|
||||
"[blueprint]")
|
||||
{
|
||||
Simulation sim(loadConfig());
|
||||
Simulation sim(loadTestConfig());
|
||||
|
||||
const BuildingId id = SimulationTestAccess::place(sim,BuildingType::Miner, QPoint(-2, 0), Rotation::East).value();
|
||||
REQUIRE(id != kInvalidBuildingId);
|
||||
@@ -705,7 +701,7 @@ TEST_CASE("Blueprint placement: recipe transfers to building after construction
|
||||
|
||||
TEST_CASE("Blueprint creation: a construction site is captured", "[blueprint]")
|
||||
{
|
||||
Simulation sim(loadConfig());
|
||||
Simulation sim(loadTestConfig());
|
||||
|
||||
// Freshly placed → a ConstructionSite (not ticked to completion). A 1x1 belt keeps
|
||||
// the body-cell bounding-box centered on the anchor, so a single site → zero offset.
|
||||
@@ -724,7 +720,7 @@ TEST_CASE("Blueprint creation: a construction site is captured", "[blueprint]")
|
||||
|
||||
TEST_CASE("Blueprint creation: a construction site's recipe is captured", "[blueprint]")
|
||||
{
|
||||
Simulation sim(loadConfig());
|
||||
Simulation sim(loadTestConfig());
|
||||
|
||||
const BuildingId id =
|
||||
SimulationTestAccess::place(sim, BuildingType::Miner, QPoint(-2, 0), Rotation::East).value();
|
||||
@@ -740,7 +736,7 @@ TEST_CASE("Blueprint creation: a construction site's recipe is captured", "[blue
|
||||
TEST_CASE("Blueprint creation: mixed operational building and construction site are both captured",
|
||||
"[blueprint]")
|
||||
{
|
||||
Simulation sim(loadConfig());
|
||||
Simulation sim(loadTestConfig());
|
||||
|
||||
// Building A: place, configure, and tick to completion so it is operational.
|
||||
const BuildingId idA =
|
||||
@@ -772,7 +768,7 @@ TEST_CASE("Blueprint creation: mixed operational building and construction site
|
||||
|
||||
TEST_CASE("Blueprint creation: selectionHasPlaceableBuilding sees a construction site", "[blueprint]")
|
||||
{
|
||||
Simulation sim(loadConfig());
|
||||
Simulation sim(loadTestConfig());
|
||||
|
||||
REQUIRE_FALSE(selectionHasPlaceableBuilding(sim, {}));
|
||||
|
||||
@@ -787,7 +783,7 @@ TEST_CASE("Blueprint placement: interceptor schematic is unlocked at game start"
|
||||
{
|
||||
// "interceptor" has unlock_at_station_level = -1 in the test config.
|
||||
// This confirms the guard in placeBlueprintAtTile passes for start-unlocked schematics.
|
||||
Simulation sim(loadConfig());
|
||||
Simulation sim(loadTestConfig());
|
||||
REQUIRE(sim.isSchematicUnlocked("interceptor"));
|
||||
}
|
||||
|
||||
@@ -796,7 +792,7 @@ TEST_CASE("Blueprint placement: repair_ship schematic is locked at game start",
|
||||
// "repair_ship" has unlock_at_station_level = 0 in the test config.
|
||||
// This confirms the guard in placeBlueprintAtTile blocks locked schematics,
|
||||
// leaving the shipyard's schematic unset.
|
||||
Simulation sim(loadConfig());
|
||||
Simulation sim(loadTestConfig());
|
||||
REQUIRE_FALSE(sim.isSchematicUnlocked("repair_ship"));
|
||||
}
|
||||
|
||||
@@ -842,7 +838,7 @@ TEST_CASE("Blueprint: building without layout has nullopt shipLayout", "[bluepri
|
||||
|
||||
TEST_CASE("Blueprint placement: setShipLayout on construction site stores layout", "[blueprint]")
|
||||
{
|
||||
Simulation sim(loadConfig());
|
||||
Simulation sim(loadTestConfig());
|
||||
|
||||
// Shipyard surface_mask ["AAAS>","AAAS "] with Rotation::East:
|
||||
// A-tiles at (-3,0),(-2,0),(-1,0),(-3,1),(-2,1),(-1,1) — all x < 0, valid asteroid tiles.
|
||||
@@ -869,7 +865,7 @@ TEST_CASE("Blueprint placement: setShipLayout on construction site stores layout
|
||||
TEST_CASE("Blueprint placement: ship layout transfers to building after construction completes",
|
||||
"[blueprint]")
|
||||
{
|
||||
Simulation sim(loadConfig());
|
||||
Simulation sim(loadTestConfig());
|
||||
|
||||
const BuildingId id = SimulationTestAccess::place(sim,BuildingType::Shipyard, QPoint(-3, 0), Rotation::East).value();
|
||||
REQUIRE(id != kInvalidBuildingId);
|
||||
|
||||
@@ -13,6 +13,7 @@
|
||||
#include "ShipsConfig.h"
|
||||
#include "Simulation.h"
|
||||
#include "SimulationTestAccess.h"
|
||||
#include "TestConfig.h"
|
||||
|
||||
// readBuildingConfig underpins the copy-settings gesture (REQ-BLD-COPY-CONFIG):
|
||||
// it extracts a building's recipe / schematic / layout / splitter filters so they
|
||||
@@ -21,11 +22,6 @@
|
||||
|
||||
namespace
|
||||
{
|
||||
GameConfig loadConfig()
|
||||
{
|
||||
return ConfigLoader::loadFromDirectory(CONFIG_DIR);
|
||||
}
|
||||
|
||||
const BuildingDef* findDef(const GameConfig& cfg, BuildingType type)
|
||||
{
|
||||
for (const BuildingDef& def : cfg.buildings.buildings)
|
||||
@@ -66,8 +62,8 @@ const ShipDef* findAvailableSchematic(const GameConfig& cfg)
|
||||
|
||||
TEST_CASE("readBuildingConfig returns a miner's selected recipe", "[copyconfig]")
|
||||
{
|
||||
const GameConfig cfg = loadConfig();
|
||||
Simulation sim(loadConfig(), 7);
|
||||
const GameConfig cfg = loadTestConfig();
|
||||
Simulation sim(loadTestConfig(), 7);
|
||||
|
||||
const BuildingId id = placeOperational(sim, cfg, BuildingType::Miner, QPoint(0, 0));
|
||||
SimulationTestAccess::buildings(sim).setRecipe(id, "mine_iron_ore");
|
||||
@@ -84,8 +80,8 @@ TEST_CASE("readBuildingConfig returns a miner's selected recipe", "[copyconfig]"
|
||||
TEST_CASE("readBuildingConfig leaves recipe unset when nothing is selected",
|
||||
"[copyconfig]")
|
||||
{
|
||||
const GameConfig cfg = loadConfig();
|
||||
Simulation sim(loadConfig(), 7);
|
||||
const GameConfig cfg = loadTestConfig();
|
||||
Simulation sim(loadTestConfig(), 7);
|
||||
|
||||
const BuildingId id = placeOperational(sim, cfg, BuildingType::Assembler, QPoint(0, 0));
|
||||
|
||||
@@ -99,8 +95,8 @@ TEST_CASE("readBuildingConfig leaves recipe unset when nothing is selected",
|
||||
TEST_CASE("readBuildingConfig returns a shipyard's schematic and layout",
|
||||
"[copyconfig]")
|
||||
{
|
||||
const GameConfig cfg = loadConfig();
|
||||
Simulation sim(loadConfig(), 7);
|
||||
const GameConfig cfg = loadTestConfig();
|
||||
Simulation sim(loadTestConfig(), 7);
|
||||
|
||||
const ShipDef* schematic = findAvailableSchematic(cfg);
|
||||
REQUIRE(schematic != nullptr);
|
||||
@@ -119,8 +115,8 @@ TEST_CASE("readBuildingConfig returns a shipyard's schematic and layout",
|
||||
|
||||
TEST_CASE("readBuildingConfig reads a queued construction site", "[copyconfig]")
|
||||
{
|
||||
const GameConfig cfg = loadConfig();
|
||||
Simulation sim(loadConfig(), 7);
|
||||
const GameConfig cfg = loadTestConfig();
|
||||
Simulation sim(loadTestConfig(), 7);
|
||||
|
||||
// A placed miner enters the construction queue as a site (not yet operational).
|
||||
const BuildingId id =
|
||||
@@ -140,6 +136,6 @@ TEST_CASE("readBuildingConfig reads a queued construction site", "[copyconfig]")
|
||||
|
||||
TEST_CASE("readBuildingConfig returns nullopt for an unknown id", "[copyconfig]")
|
||||
{
|
||||
Simulation sim(loadConfig(), 7);
|
||||
Simulation sim(loadTestConfig(), 7);
|
||||
CHECK_FALSE(readBuildingConfig(sim, kInvalidBuildingId).has_value());
|
||||
}
|
||||
|
||||
@@ -19,16 +19,12 @@
|
||||
#include "Port.h"
|
||||
#include "Rotation.h"
|
||||
#include "Tick.h"
|
||||
#include "TestConfig.h"
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Fixture helpers
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
static GameConfig loadConfig()
|
||||
{
|
||||
return ConfigLoader::loadFromDirectory(CONFIG_DIR);
|
||||
}
|
||||
|
||||
static Item makeItem(const std::string& id)
|
||||
{
|
||||
Item item;
|
||||
@@ -86,7 +82,7 @@ static std::vector<Item> outputSideItems(const Building& b)
|
||||
// Owns a BuildingSystem and its dependencies for placement-bounds tests.
|
||||
struct PlacementFixture
|
||||
{
|
||||
GameConfig cfg = loadConfig();
|
||||
GameConfig cfg = loadTestConfig();
|
||||
BeltSystem belts{cfg.world.beltSpeed_tps};
|
||||
int stock = 0;
|
||||
std::mt19937 rng{0};
|
||||
@@ -110,7 +106,7 @@ struct PlacementFixture
|
||||
|
||||
TEST_CASE("BuildingSystem: place miner occupies expected body tiles", "[building]")
|
||||
{
|
||||
const GameConfig cfg = loadConfig();
|
||||
const GameConfig cfg = loadTestConfig();
|
||||
BeltSystem belts(cfg.world.beltSpeed_tps);
|
||||
int stock = 0;
|
||||
std::mt19937 rng(0);
|
||||
@@ -218,7 +214,7 @@ TEST_CASE("BuildingSystem: isPlacementValid enforces terrain and world bounds",
|
||||
TEST_CASE("BuildingSystem: placing a belt registers it with BeltSystem after construction",
|
||||
"[building]")
|
||||
{
|
||||
const GameConfig cfg = loadConfig();
|
||||
const GameConfig cfg = loadTestConfig();
|
||||
BeltSystem belts(cfg.world.beltSpeed_tps);
|
||||
int stock = 0;
|
||||
std::mt19937 rng(0);
|
||||
@@ -247,7 +243,7 @@ TEST_CASE("BuildingSystem: placing a belt registers it with BeltSystem after con
|
||||
|
||||
TEST_CASE("BuildingSystem: placed building enters construction queue", "[building]")
|
||||
{
|
||||
const GameConfig cfg = loadConfig();
|
||||
const GameConfig cfg = loadTestConfig();
|
||||
BeltSystem belts(cfg.world.beltSpeed_tps);
|
||||
int stock = 0;
|
||||
std::mt19937 rng(0);
|
||||
@@ -289,7 +285,7 @@ TEST_CASE("BuildingSystem: deconstructing a construction site removes it instant
|
||||
TEST_CASE("BuildingSystem: first queued building starts construction immediately",
|
||||
"[building]")
|
||||
{
|
||||
const GameConfig cfg = loadConfig();
|
||||
const GameConfig cfg = loadTestConfig();
|
||||
BeltSystem belts(cfg.world.beltSpeed_tps);
|
||||
int stock = 0;
|
||||
std::mt19937 rng(0);
|
||||
@@ -307,7 +303,7 @@ TEST_CASE("BuildingSystem: first queued building starts construction immediately
|
||||
|
||||
TEST_CASE("BuildingSystem: second queued building waits (completesAt == 0)", "[building]")
|
||||
{
|
||||
const GameConfig cfg = loadConfig();
|
||||
const GameConfig cfg = loadTestConfig();
|
||||
BeltSystem belts(cfg.world.beltSpeed_tps);
|
||||
int stock = 0;
|
||||
std::mt19937 rng(0);
|
||||
@@ -329,7 +325,7 @@ TEST_CASE("BuildingSystem: second queued building waits (completesAt == 0)", "[b
|
||||
|
||||
TEST_CASE("BuildingSystem: construction completes after configured duration", "[building]")
|
||||
{
|
||||
const GameConfig cfg = loadConfig();
|
||||
const GameConfig cfg = loadTestConfig();
|
||||
BeltSystem belts(cfg.world.beltSpeed_tps);
|
||||
int stock = 0;
|
||||
std::mt19937 rng(0);
|
||||
@@ -495,7 +491,7 @@ TEST_CASE("BuildingSystem: splitter filters survive a queue/un-queue round-trip"
|
||||
|
||||
TEST_CASE("BuildingSystem: second building starts after first completes", "[building]")
|
||||
{
|
||||
const GameConfig cfg = loadConfig();
|
||||
const GameConfig cfg = loadTestConfig();
|
||||
BeltSystem belts(cfg.world.beltSpeed_tps);
|
||||
int stock = 0;
|
||||
std::mt19937 rng(0);
|
||||
@@ -525,7 +521,7 @@ TEST_CASE("BuildingSystem: second building starts after first completes", "[buil
|
||||
|
||||
TEST_CASE("BuildingSystem: miner produces iron_ore after recipe duration", "[building]")
|
||||
{
|
||||
const GameConfig cfg = loadConfig();
|
||||
const GameConfig cfg = loadTestConfig();
|
||||
BeltSystem belts(cfg.world.beltSpeed_tps);
|
||||
int stock = 0;
|
||||
std::mt19937 rng(0);
|
||||
@@ -558,7 +554,7 @@ TEST_CASE("BuildingSystem: miner produces iron_ore after recipe duration", "[bui
|
||||
|
||||
TEST_CASE("BuildingSystem: miner output buffer stalls when full", "[building]")
|
||||
{
|
||||
const GameConfig cfg = loadConfig();
|
||||
const GameConfig cfg = loadTestConfig();
|
||||
BeltSystem belts(cfg.world.beltSpeed_tps);
|
||||
int stock = 0;
|
||||
std::mt19937 rng(0);
|
||||
@@ -598,7 +594,7 @@ TEST_CASE("BuildingSystem: miner output buffer stalls when full", "[building]")
|
||||
|
||||
TEST_CASE("BuildingSystem: productionBuildingCount excludes construction sites", "[building]")
|
||||
{
|
||||
const GameConfig cfg = loadConfig();
|
||||
const GameConfig cfg = loadTestConfig();
|
||||
BeltSystem belts(cfg.world.beltSpeed_tps);
|
||||
int stock = 0;
|
||||
std::mt19937 rng(0);
|
||||
@@ -638,7 +634,7 @@ TEST_CASE("BuildingSystem: productionBuildingCount excludes construction sites",
|
||||
TEST_CASE("BuildingSystem: activeProductionBuildingCount tracks production cycle state",
|
||||
"[building]")
|
||||
{
|
||||
const GameConfig cfg = loadConfig();
|
||||
const GameConfig cfg = loadTestConfig();
|
||||
BeltSystem belts(cfg.world.beltSpeed_tps);
|
||||
int stock = 0;
|
||||
std::mt19937 rng(0);
|
||||
@@ -679,7 +675,7 @@ TEST_CASE("BuildingSystem: activeProductionBuildingCount tracks production cycle
|
||||
TEST_CASE("BuildingSystem: smelter input buffer fills from adjacent west-flowing belt",
|
||||
"[building]")
|
||||
{
|
||||
const GameConfig cfg = loadConfig();
|
||||
const GameConfig cfg = loadTestConfig();
|
||||
// Fast belt so items are immediately available for peek/take.
|
||||
BeltSystem belts(static_cast<double>(kTickRateHz));
|
||||
int stock = 0;
|
||||
@@ -722,7 +718,7 @@ TEST_CASE("BuildingSystem: smelter input buffer fills from adjacent west-flowing
|
||||
TEST_CASE("BuildingSystem: accepted input travels inward before entering the buffer",
|
||||
"[building]")
|
||||
{
|
||||
const GameConfig cfg = loadConfig();
|
||||
const GameConfig cfg = loadTestConfig();
|
||||
BeltSystem belts(static_cast<double>(kTickRateHz)); // fast belt: 1 tile/tick
|
||||
int stock = 0;
|
||||
std::mt19937 rng(0);
|
||||
@@ -763,7 +759,7 @@ TEST_CASE("BuildingSystem: accepted input travels inward before entering the buf
|
||||
TEST_CASE("BuildingSystem: input reservation caps buffered plus in-transit at the cap",
|
||||
"[building]")
|
||||
{
|
||||
const GameConfig cfg = loadConfig();
|
||||
const GameConfig cfg = loadTestConfig();
|
||||
BeltSystem belts(static_cast<double>(kTickRateHz)); // fast belt
|
||||
int stock = 0;
|
||||
std::mt19937 rng(0);
|
||||
@@ -805,7 +801,7 @@ TEST_CASE("BuildingSystem: input reservation caps buffered plus in-transit at th
|
||||
TEST_CASE("BuildingSystem: smelter auto-smelts ore without a recipe selection",
|
||||
"[building]")
|
||||
{
|
||||
const GameConfig cfg = loadConfig();
|
||||
const GameConfig cfg = loadTestConfig();
|
||||
BeltSystem belts(static_cast<double>(kTickRateHz));
|
||||
int stock = 0;
|
||||
std::mt19937 rng(0);
|
||||
@@ -851,7 +847,7 @@ TEST_CASE("BuildingSystem: smelter auto-smelts ore without a recipe selection",
|
||||
TEST_CASE("BuildingSystem: smelter runs a satisfiable recipe while an incomplete batch waits",
|
||||
"[building]")
|
||||
{
|
||||
const GameConfig cfg = loadConfig();
|
||||
const GameConfig cfg = loadTestConfig();
|
||||
BeltSystem belts(static_cast<double>(kTickRateHz));
|
||||
int stock = 0;
|
||||
std::mt19937 rng(0);
|
||||
@@ -905,7 +901,7 @@ TEST_CASE("BuildingSystem: smelter runs a satisfiable recipe while an incomplete
|
||||
|
||||
TEST_CASE("BuildingSystem: miner output buffer drains onto adjacent belt", "[building]")
|
||||
{
|
||||
const GameConfig cfg = loadConfig();
|
||||
const GameConfig cfg = loadTestConfig();
|
||||
BeltSystem belts(static_cast<double>(kTickRateHz));
|
||||
int stock = 0;
|
||||
std::mt19937 rng(0);
|
||||
@@ -944,7 +940,7 @@ TEST_CASE("BuildingSystem: miner output buffer drains onto adjacent belt", "[bui
|
||||
TEST_CASE("BuildingSystem: output port couples directly into an adjacent input port",
|
||||
"[building]")
|
||||
{
|
||||
const GameConfig cfg = loadConfig();
|
||||
const GameConfig cfg = loadTestConfig();
|
||||
BeltSystem belts(cfg.world.beltSpeed_tps);
|
||||
int stock = 0;
|
||||
std::mt19937 rng(0);
|
||||
@@ -984,7 +980,7 @@ TEST_CASE("BuildingSystem: output port couples directly into an adjacent input p
|
||||
TEST_CASE("BuildingSystem: direct coupling to a non-consumer leaves the item stuck",
|
||||
"[building]")
|
||||
{
|
||||
const GameConfig cfg = loadConfig();
|
||||
const GameConfig cfg = loadTestConfig();
|
||||
BeltSystem belts(cfg.world.beltSpeed_tps);
|
||||
int stock = 0;
|
||||
std::mt19937 rng(0);
|
||||
@@ -1023,7 +1019,7 @@ TEST_CASE("BuildingSystem: direct coupling to a non-consumer leaves the item stu
|
||||
TEST_CASE("BuildingSystem: setRecipe clears output buffer and active production",
|
||||
"[building]")
|
||||
{
|
||||
const GameConfig cfg = loadConfig();
|
||||
const GameConfig cfg = loadTestConfig();
|
||||
BeltSystem belts(static_cast<double>(kTickRateHz));
|
||||
int stock = 0;
|
||||
std::mt19937 rng(0);
|
||||
@@ -1066,7 +1062,7 @@ TEST_CASE("BuildingSystem: setRecipe clears output buffer and active production"
|
||||
TEST_CASE("BuildingSystem: reprocessing plant output buffer capacity equals max output per roll",
|
||||
"[building]")
|
||||
{
|
||||
const GameConfig cfg = loadConfig();
|
||||
const GameConfig cfg = loadTestConfig();
|
||||
BeltSystem belts(cfg.world.beltSpeed_tps);
|
||||
int stock = 0;
|
||||
std::mt19937 rng(0);
|
||||
@@ -1097,7 +1093,7 @@ TEST_CASE("BuildingSystem: reprocessing plant output buffer capacity equals max
|
||||
TEST_CASE("BuildingSystem: reprocessing plant produces one cycle output then stalls",
|
||||
"[building]")
|
||||
{
|
||||
const GameConfig cfg = loadConfig();
|
||||
const GameConfig cfg = loadTestConfig();
|
||||
BeltSystem belts(static_cast<double>(kTickRateHz));
|
||||
int stock = 0;
|
||||
// Seed chosen so first roll produces 2-item output (iron_ingot), filling buffer.
|
||||
@@ -1156,7 +1152,7 @@ TEST_CASE("BuildingSystem: reprocessing plant produces one cycle output then sta
|
||||
TEST_CASE("BuildingSystem: findRotateInPlaceTarget returns nullopt when tile is empty",
|
||||
"[building][rotate-in-place]")
|
||||
{
|
||||
const GameConfig cfg = loadConfig();
|
||||
const GameConfig cfg = loadTestConfig();
|
||||
BeltSystem belts(cfg.world.beltSpeed_tps);
|
||||
int stock = 0;
|
||||
std::mt19937 rng(0);
|
||||
@@ -1175,7 +1171,7 @@ TEST_CASE("BuildingSystem: findRotateInPlaceTarget returns nullopt when tile is
|
||||
TEST_CASE("BuildingSystem: findRotateInPlaceTarget returns the site id for a queued belt (same type, different rotation)",
|
||||
"[building][rotate-in-place]")
|
||||
{
|
||||
const GameConfig cfg = loadConfig();
|
||||
const GameConfig cfg = loadTestConfig();
|
||||
BeltSystem belts(cfg.world.beltSpeed_tps);
|
||||
int stock = 0;
|
||||
std::mt19937 rng(0);
|
||||
@@ -1198,7 +1194,7 @@ TEST_CASE("BuildingSystem: findRotateInPlaceTarget returns the site id for a que
|
||||
TEST_CASE("BuildingSystem: findRotateInPlaceTarget returns the building id for a completed operational belt",
|
||||
"[building][rotate-in-place]")
|
||||
{
|
||||
const GameConfig cfg = loadConfig();
|
||||
const GameConfig cfg = loadTestConfig();
|
||||
BeltSystem belts(cfg.world.beltSpeed_tps);
|
||||
int stock = 0;
|
||||
std::mt19937 rng(0);
|
||||
@@ -1225,7 +1221,7 @@ TEST_CASE("BuildingSystem: findRotateInPlaceTarget returns the building id for a
|
||||
TEST_CASE("BuildingSystem: findRotateInPlaceTarget returns nullopt when building type differs",
|
||||
"[building][rotate-in-place]")
|
||||
{
|
||||
const GameConfig cfg = loadConfig();
|
||||
const GameConfig cfg = loadTestConfig();
|
||||
BeltSystem belts(cfg.world.beltSpeed_tps);
|
||||
int stock = 0;
|
||||
std::mt19937 rng(0);
|
||||
@@ -1247,7 +1243,7 @@ TEST_CASE("BuildingSystem: findRotateInPlaceTarget returns nullopt when building
|
||||
TEST_CASE("BuildingSystem: findRotateInPlaceTarget never rotates a tunnel in place",
|
||||
"[building][rotate-in-place]")
|
||||
{
|
||||
const GameConfig cfg = loadConfig();
|
||||
const GameConfig cfg = loadTestConfig();
|
||||
BeltSystem belts(cfg.world.beltSpeed_tps);
|
||||
int stock = 0;
|
||||
std::mt19937 rng(0);
|
||||
@@ -1273,7 +1269,7 @@ TEST_CASE("BuildingSystem: findRotateInPlaceTarget never rotates a tunnel in pla
|
||||
TEST_CASE("BuildingSystem: findRotateInPlaceTarget returns nullopt when footprints only partially overlap",
|
||||
"[building][rotate-in-place]")
|
||||
{
|
||||
const GameConfig cfg = loadConfig();
|
||||
const GameConfig cfg = loadTestConfig();
|
||||
BeltSystem belts(cfg.world.beltSpeed_tps);
|
||||
int stock = 0;
|
||||
std::mt19937 rng(0);
|
||||
@@ -1297,7 +1293,7 @@ TEST_CASE("BuildingSystem: findRotateInPlaceTarget returns nullopt when footprin
|
||||
TEST_CASE("BuildingSystem: findRotateInPlaceTarget works for a symmetric multi-tile building with rotated ghost",
|
||||
"[building][rotate-in-place]")
|
||||
{
|
||||
const GameConfig cfg = loadConfig();
|
||||
const GameConfig cfg = loadTestConfig();
|
||||
BeltSystem belts(cfg.world.beltSpeed_tps);
|
||||
int stock = 0;
|
||||
std::mt19937 rng(0);
|
||||
@@ -1326,7 +1322,7 @@ TEST_CASE("BuildingSystem: findRotateInPlaceTarget works for a symmetric multi-t
|
||||
TEST_CASE("BuildingSystem: rotateInPlace updates the rotation field of a construction site",
|
||||
"[building][rotate-in-place]")
|
||||
{
|
||||
const GameConfig cfg = loadConfig();
|
||||
const GameConfig cfg = loadTestConfig();
|
||||
BeltSystem belts(cfg.world.beltSpeed_tps);
|
||||
int stock = 0;
|
||||
std::mt19937 rng(0);
|
||||
@@ -1349,7 +1345,7 @@ TEST_CASE("BuildingSystem: rotateInPlace updates the rotation field of a constru
|
||||
TEST_CASE("BuildingSystem: rotateInPlace preserves the construction progress of a queued site",
|
||||
"[building][rotate-in-place]")
|
||||
{
|
||||
const GameConfig cfg = loadConfig();
|
||||
const GameConfig cfg = loadTestConfig();
|
||||
BeltSystem belts(cfg.world.beltSpeed_tps);
|
||||
int stock = 0;
|
||||
std::mt19937 rng(0);
|
||||
@@ -1373,7 +1369,7 @@ TEST_CASE("BuildingSystem: rotateInPlace preserves the construction progress of
|
||||
TEST_CASE("BuildingSystem: rotateInPlace updates rotation and output port direction on an operational building",
|
||||
"[building][rotate-in-place]")
|
||||
{
|
||||
const GameConfig cfg = loadConfig();
|
||||
const GameConfig cfg = loadTestConfig();
|
||||
BeltSystem belts(cfg.world.beltSpeed_tps);
|
||||
int stock = 0;
|
||||
std::mt19937 rng(0);
|
||||
@@ -1404,7 +1400,7 @@ TEST_CASE("BuildingSystem: rotateInPlace updates rotation and output port direct
|
||||
TEST_CASE("BuildingSystem: rotateInPlace re-registers a belt tile with BeltSystem so it still accepts items",
|
||||
"[building][rotate-in-place]")
|
||||
{
|
||||
const GameConfig cfg = loadConfig();
|
||||
const GameConfig cfg = loadTestConfig();
|
||||
BeltSystem belts(cfg.world.beltSpeed_tps);
|
||||
int stock = 0;
|
||||
std::mt19937 rng(0);
|
||||
@@ -1427,6 +1423,36 @@ TEST_CASE("BuildingSystem: rotateInPlace re-registers a belt tile with BeltSyste
|
||||
REQUIRE(belts.tryPutItem(QPoint(0, 0), makeItem("iron_ore")));
|
||||
}
|
||||
|
||||
TEST_CASE("BuildingSystem: rotateInPlace preserves the output filters of a splitter "
|
||||
"(REQ-BLD-SPLITTER)", "[building][rotate-in-place]")
|
||||
{
|
||||
PlacementFixture f;
|
||||
|
||||
const QPoint tile(5, 5);
|
||||
const BuildingId id = f.bs.place(BuildingType::Splitter, tile, Rotation::East, 0).value();
|
||||
|
||||
// Run until construction completes, so the splitter is registered with BeltSystem.
|
||||
Tick tick = 0;
|
||||
while (f.bs.getAllBuildings().empty() && tick < 100000)
|
||||
{
|
||||
runTicks(f.bs, f.belts, 1, tick);
|
||||
}
|
||||
REQUIRE(f.bs.getAllBuildings().size() == 1);
|
||||
|
||||
const std::vector<ItemType> filterA{ ItemType{"iron_ore"} };
|
||||
const std::vector<ItemType> filterB{ ItemType{"copper_ore"} };
|
||||
f.belts.setSplitterFilters(tile, filterA, filterB);
|
||||
|
||||
f.bs.rotateInPlace(id, Rotation::North);
|
||||
|
||||
// The tile is re-registered with BeltSystem carrying the filters it had before
|
||||
// the rotation — rotating must not reset a configured splitter to "accept all".
|
||||
const std::optional<BeltSystem::SplitterInfo> info = f.belts.getSplitterInfo(tile);
|
||||
REQUIRE(info.has_value());
|
||||
REQUIRE(info->filterA == filterA);
|
||||
REQUIRE(info->filterB == filterB);
|
||||
}
|
||||
|
||||
TEST_CASE("BuildingSystem: splitter filters configured on a construction site carry over "
|
||||
"to the built splitter (REQ-BLD-SITE-CONFIG)", "[building]")
|
||||
{
|
||||
|
||||
@@ -2,6 +2,8 @@
|
||||
add_files(
|
||||
TEST_FILES
|
||||
|
||||
TestConfig.h
|
||||
|
||||
test.cpp
|
||||
FormulaTest.cpp
|
||||
ConfigLoaderTest.cpp
|
||||
|
||||
@@ -22,11 +22,7 @@
|
||||
#include "StationBodyComponent.h"
|
||||
#include "Tick.h"
|
||||
#include "WeaponComponent.h"
|
||||
|
||||
static GameConfig loadConfig()
|
||||
{
|
||||
return ConfigLoader::loadFromDirectory(CONFIG_DIR);
|
||||
}
|
||||
#include "TestConfig.h"
|
||||
|
||||
static const ShipDef* findCombatShip(const GameConfig& cfg)
|
||||
{
|
||||
@@ -64,7 +60,7 @@ struct CombatFixture
|
||||
CombatSystem combat;
|
||||
|
||||
explicit CombatFixture()
|
||||
: cfg(loadConfig())
|
||||
: cfg(loadTestConfig())
|
||||
, rng(42)
|
||||
, nextBuildingId(1)
|
||||
, belts(cfg.world.beltSpeed_tps)
|
||||
@@ -178,7 +174,7 @@ TEST_CASE("CombatSystem: no fire when target is out of range", "[combat]")
|
||||
|
||||
TEST_CASE("CombatSystem: player station fires at enemy ship in range", "[combat]")
|
||||
{
|
||||
Simulation sim(loadConfig(), 42);
|
||||
Simulation sim(loadTestConfig(), 42);
|
||||
|
||||
// Find the player station entity via ECS.
|
||||
entt::entity stationEntity = entt::null;
|
||||
@@ -217,7 +213,7 @@ TEST_CASE("CombatSystem: player station fires at enemy ship in range", "[combat]
|
||||
|
||||
TEST_CASE("CombatSystem: enemy station fires at player ship in range", "[combat]")
|
||||
{
|
||||
Simulation sim(loadConfig(), 42);
|
||||
Simulation sim(loadTestConfig(), 42);
|
||||
|
||||
entt::entity stationEntity = entt::null;
|
||||
QVector2D stationCenter;
|
||||
@@ -255,7 +251,7 @@ TEST_CASE("CombatSystem: enemy station fires at player ship in range", "[combat]
|
||||
|
||||
TEST_CASE("CombatSystem: player ship fires at enemy station in range", "[combat]")
|
||||
{
|
||||
Simulation sim(loadConfig(), 42);
|
||||
Simulation sim(loadTestConfig(), 42);
|
||||
|
||||
entt::entity stationEntity = entt::null;
|
||||
QVector2D stationCenter;
|
||||
@@ -388,7 +384,7 @@ TEST_CASE("CombatSystem: damage still applied if shooter already dead", "[combat
|
||||
|
||||
TEST_CASE("CombatSystem: dead ship is removed after tick step 9", "[combat]")
|
||||
{
|
||||
Simulation sim(loadConfig(), 42);
|
||||
Simulation sim(loadTestConfig(), 42);
|
||||
|
||||
const ShipDef* combatDef = findCombatShip(sim.getConfig());
|
||||
REQUIRE(combatDef != nullptr);
|
||||
@@ -405,7 +401,7 @@ TEST_CASE("CombatSystem: dead ship is removed after tick step 9", "[combat]")
|
||||
|
||||
TEST_CASE("CombatSystem: scrap is spawned on ship death", "[combat]")
|
||||
{
|
||||
Simulation sim(loadConfig(), 42);
|
||||
Simulation sim(loadTestConfig(), 42);
|
||||
|
||||
// Scrap dropped on death is derived from the ship's as-built threat cost
|
||||
// (REQ-RES-DEBRIS-DROP): round(threat * scrap_per_threat). The interceptor's
|
||||
@@ -424,7 +420,7 @@ TEST_CASE("CombatSystem: scrap is spawned on ship death", "[combat]")
|
||||
|
||||
TEST_CASE("CombatSystem: HQ death sets game over", "[combat]")
|
||||
{
|
||||
Simulation sim(loadConfig(), 42);
|
||||
Simulation sim(loadTestConfig(), 42);
|
||||
|
||||
// Damage the HQ proxy entity (has HqProxy + Health).
|
||||
sim.getAdmin().forEach<HqProxyComponent, HealthComponent>(
|
||||
|
||||
@@ -11,14 +11,7 @@
|
||||
#include "Simulation.h"
|
||||
#include "SimulationTestAccess.h"
|
||||
#include "Tick.h"
|
||||
|
||||
namespace
|
||||
{
|
||||
GameConfig loadConfig()
|
||||
{
|
||||
return ConfigLoader::loadFromDirectory(CONFIG_DIR);
|
||||
}
|
||||
} // namespace
|
||||
#include "TestConfig.h"
|
||||
|
||||
// The command chokepoint (Simulation::apply) must produce exactly the same state
|
||||
// as driving the underlying mutators directly — that equivalence is what lets a
|
||||
@@ -26,8 +19,8 @@ GameConfig loadConfig()
|
||||
|
||||
TEST_CASE("apply(PlaceBuildingCommand) matches direct placement", "[command]")
|
||||
{
|
||||
Simulation viaCommand(loadConfig(), 99);
|
||||
Simulation viaDirect(loadConfig(), 99);
|
||||
Simulation viaCommand(loadTestConfig(), 99);
|
||||
Simulation viaDirect(loadTestConfig(), 99);
|
||||
|
||||
PlaceBuildingCommand command;
|
||||
command.type = BuildingType::Miner;
|
||||
@@ -42,8 +35,8 @@ TEST_CASE("apply(PlaceBuildingCommand) matches direct placement", "[command]")
|
||||
|
||||
TEST_CASE("apply(PlaceBuildingCommand) with recipe matches place-then-setRecipe", "[command]")
|
||||
{
|
||||
Simulation viaCommand(loadConfig(), 99);
|
||||
Simulation viaDirect(loadConfig(), 99);
|
||||
Simulation viaCommand(loadTestConfig(), 99);
|
||||
Simulation viaDirect(loadTestConfig(), 99);
|
||||
|
||||
PlaceBuildingCommand command;
|
||||
command.type = BuildingType::Miner;
|
||||
@@ -61,8 +54,8 @@ TEST_CASE("apply(PlaceBuildingCommand) with recipe matches place-then-setRecipe"
|
||||
|
||||
TEST_CASE("apply(DeconstructCommand) matches direct deconstruct", "[command]")
|
||||
{
|
||||
Simulation viaCommand(loadConfig(), 99);
|
||||
Simulation viaDirect(loadConfig(), 99);
|
||||
Simulation viaCommand(loadTestConfig(), 99);
|
||||
Simulation viaDirect(loadTestConfig(), 99);
|
||||
|
||||
const BuildingId idA =
|
||||
SimulationTestAccess::place(viaCommand, BuildingType::Miner, QPoint(-3, 0), Rotation::East).value();
|
||||
@@ -81,8 +74,8 @@ TEST_CASE("apply(DeconstructCommand) matches direct deconstruct", "[command]")
|
||||
|
||||
TEST_CASE("apply(CancelDeconstructionCommand) matches direct cancelDeconstruction", "[command]")
|
||||
{
|
||||
Simulation viaCommand(loadConfig(), 99);
|
||||
Simulation viaDirect(loadConfig(), 99);
|
||||
Simulation viaCommand(loadTestConfig(), 99);
|
||||
Simulation viaDirect(loadTestConfig(), 99);
|
||||
|
||||
const BuildingId idA =
|
||||
SimulationTestAccess::place(viaCommand, BuildingType::Miner, QPoint(-3, 0), Rotation::East).value();
|
||||
@@ -110,8 +103,8 @@ TEST_CASE("apply(CancelDeconstructionCommand) matches direct cancelDeconstructio
|
||||
|
||||
TEST_CASE("CommandManager drains queued commands in FIFO order through apply", "[command]")
|
||||
{
|
||||
Simulation viaManager(loadConfig(), 99);
|
||||
Simulation viaDirect(loadConfig(), 99);
|
||||
Simulation viaManager(loadTestConfig(), 99);
|
||||
Simulation viaDirect(loadTestConfig(), 99);
|
||||
|
||||
CommandManager manager(viaManager);
|
||||
|
||||
|
||||
@@ -11,14 +11,10 @@
|
||||
#include "SimulationTestAccess.h"
|
||||
#include "StateChecksum.h"
|
||||
#include "Tick.h"
|
||||
#include "TestConfig.h"
|
||||
|
||||
namespace
|
||||
{
|
||||
GameConfig loadConfig()
|
||||
{
|
||||
return ConfigLoader::loadFromDirectory(CONFIG_DIR);
|
||||
}
|
||||
|
||||
constexpr int kScriptTicks = 2000;
|
||||
|
||||
// Runs a fixed scripted session and returns the full-state checksum after every
|
||||
@@ -26,7 +22,7 @@ constexpr int kScriptTicks = 2000;
|
||||
// otherwise lets waves/combat run so the RNG stream and ECS state are exercised.
|
||||
std::vector<std::uint64_t> runScriptedSession(unsigned int seed)
|
||||
{
|
||||
Simulation sim(loadConfig(), seed);
|
||||
Simulation sim(loadTestConfig(), seed);
|
||||
|
||||
// Tick 0: a miner feeding a short belt line on the asteroid.
|
||||
SimulationTestAccess::place(sim, BuildingType::Miner, QPoint(-3, 0), Rotation::East);
|
||||
@@ -121,8 +117,8 @@ TEST_CASE("fingerprintRng: equal states match, advanced states differ", "[determ
|
||||
|
||||
TEST_CASE("Simulation::rngFingerprint is stable for equal seeds", "[determinism]")
|
||||
{
|
||||
const Simulation a(loadConfig(), 777);
|
||||
const Simulation b(loadConfig(), 777);
|
||||
const Simulation a(loadTestConfig(), 777);
|
||||
const Simulation b(loadTestConfig(), 777);
|
||||
|
||||
REQUIRE(a.getRngFingerprint() == b.getRngFingerprint());
|
||||
}
|
||||
|
||||
@@ -2,11 +2,7 @@
|
||||
|
||||
#include "ConfigLoader.h"
|
||||
#include "ModulesConfig.h"
|
||||
|
||||
static GameConfig loadConfig()
|
||||
{
|
||||
return ConfigLoader::loadFromDirectory(CONFIG_DIR);
|
||||
}
|
||||
#include "TestConfig.h"
|
||||
|
||||
static const ModuleDef* findModule(const GameConfig& cfg, const std::string& id)
|
||||
{
|
||||
@@ -28,7 +24,7 @@ static const ModuleStatModifier* findModifier(const ModuleDef& def, const std::s
|
||||
|
||||
TEST_CASE("ConfigLoader: loadModules parses modules.toml", "[config][modules]")
|
||||
{
|
||||
const GameConfig cfg = loadConfig();
|
||||
const GameConfig cfg = loadTestConfig();
|
||||
REQUIRE(cfg.modules.modules.size() >= 2);
|
||||
|
||||
const ModuleDef& armor = cfg.modules.modules[0];
|
||||
@@ -49,7 +45,7 @@ TEST_CASE("ConfigLoader: loadModules parses modules.toml", "[config][modules]")
|
||||
|
||||
TEST_CASE("ConfigLoader: loadModules parses additive modifiers", "[config][modules]")
|
||||
{
|
||||
const GameConfig cfg = loadConfig();
|
||||
const GameConfig cfg = loadTestConfig();
|
||||
REQUIRE(cfg.modules.modules.size() >= 2);
|
||||
|
||||
const ModuleDef& sensor = cfg.modules.modules[1];
|
||||
@@ -62,7 +58,7 @@ TEST_CASE("ConfigLoader: loadModules parses additive modifiers", "[config][modul
|
||||
|
||||
TEST_CASE("ConfigLoader: multiplicative modifier with unit suffix is parsed (weapon_primer)", "[config][modules]")
|
||||
{
|
||||
const GameConfig cfg = loadConfig();
|
||||
const GameConfig cfg = loadTestConfig();
|
||||
const ModuleDef* primer = findModule(cfg, "weapon_primer");
|
||||
REQUIRE(primer != nullptr);
|
||||
REQUIRE(primer->statModifiers.size() == 1);
|
||||
@@ -74,7 +70,7 @@ TEST_CASE("ConfigLoader: multiplicative modifier with unit suffix is parsed (wea
|
||||
|
||||
TEST_CASE("ConfigLoader: weapon_stabilizer parses two multiplicative weapon modifiers", "[config][modules]")
|
||||
{
|
||||
const GameConfig cfg = loadConfig();
|
||||
const GameConfig cfg = loadTestConfig();
|
||||
const ModuleDef* stab = findModule(cfg, "weapon_stabilizer");
|
||||
REQUIRE(stab != nullptr);
|
||||
REQUIRE(stab->statModifiers.size() == 2);
|
||||
@@ -92,7 +88,7 @@ TEST_CASE("ConfigLoader: weapon_stabilizer parses two multiplicative weapon modi
|
||||
|
||||
TEST_CASE("ConfigLoader: afterburner parses multiplicative speed and additive main_acceleration", "[config][modules]")
|
||||
{
|
||||
const GameConfig cfg = loadConfig();
|
||||
const GameConfig cfg = loadTestConfig();
|
||||
const ModuleDef* ab = findModule(cfg, "afterburner");
|
||||
REQUIRE(ab != nullptr);
|
||||
REQUIRE(ab->statModifiers.size() == 2);
|
||||
@@ -110,7 +106,7 @@ TEST_CASE("ConfigLoader: afterburner parses multiplicative speed and additive ma
|
||||
|
||||
TEST_CASE("ConfigLoader: maneuvering_thrusters parses multiplicative speed and additive maneuvering_acceleration", "[config][modules]")
|
||||
{
|
||||
const GameConfig cfg = loadConfig();
|
||||
const GameConfig cfg = loadTestConfig();
|
||||
const ModuleDef* mt = findModule(cfg, "maneuvering_thrusters");
|
||||
REQUIRE(mt != nullptr);
|
||||
REQUIRE(mt->statModifiers.size() == 2);
|
||||
@@ -128,7 +124,7 @@ TEST_CASE("ConfigLoader: maneuvering_thrusters parses multiplicative speed and a
|
||||
|
||||
TEST_CASE("ConfigLoader: loadShips parses layout field", "[config][ships]")
|
||||
{
|
||||
const GameConfig cfg = loadConfig();
|
||||
const GameConfig cfg = loadTestConfig();
|
||||
REQUIRE(!cfg.ships.ships.empty());
|
||||
|
||||
const ShipDef& ship = cfg.ships.ships[0];
|
||||
|
||||
@@ -12,11 +12,7 @@
|
||||
#include "Simulation.h"
|
||||
#include "SimulationTestAccess.h"
|
||||
#include "StationBodyComponent.h"
|
||||
|
||||
static GameConfig loadConfig()
|
||||
{
|
||||
return ConfigLoader::loadFromDirectory(CONFIG_DIR);
|
||||
}
|
||||
#include "TestConfig.h"
|
||||
|
||||
// Zeros the HP of both enemy defence stations and advances one tick so that
|
||||
// tickDeathsAndLoot fires, triggering the push and schematic choices.
|
||||
@@ -62,7 +58,7 @@ static bool awaitRecipeUnlock(Simulation& sim, const std::string& recipeId,
|
||||
|
||||
TEST_CASE("RecipeSchematic: unlocked_at_start = true parsed correctly", "[recipe_schematic]")
|
||||
{
|
||||
const GameConfig cfg = loadConfig();
|
||||
const GameConfig cfg = loadTestConfig();
|
||||
const auto it = std::find_if(cfg.recipes.recipes.begin(), cfg.recipes.recipes.end(),
|
||||
[](const RecipeDef& r) { return r.id == "premium_circuit"; });
|
||||
REQUIRE(it != cfg.recipes.recipes.end());
|
||||
@@ -71,7 +67,7 @@ TEST_CASE("RecipeSchematic: unlocked_at_start = true parsed correctly", "[recipe
|
||||
|
||||
TEST_CASE("RecipeSchematic: a gated recipe is granted by an unlock group", "[recipe_schematic]")
|
||||
{
|
||||
const GameConfig cfg = loadConfig();
|
||||
const GameConfig cfg = loadTestConfig();
|
||||
const bool granted = std::any_of(cfg.unlocks.groups.begin(), cfg.unlocks.groups.end(),
|
||||
[](const UnlockGroupDef& g)
|
||||
{
|
||||
@@ -82,7 +78,7 @@ TEST_CASE("RecipeSchematic: a gated recipe is granted by an unlock group", "[rec
|
||||
|
||||
TEST_CASE("RecipeSchematic: an untagged assembler recipe has unlocked_at_start = false", "[recipe_schematic]")
|
||||
{
|
||||
const GameConfig cfg = loadConfig();
|
||||
const GameConfig cfg = loadTestConfig();
|
||||
const auto it = std::find_if(cfg.recipes.recipes.begin(), cfg.recipes.recipes.end(),
|
||||
[](const RecipeDef& r) { return r.id == "circuit_board"; });
|
||||
REQUIRE(it != cfg.recipes.recipes.end());
|
||||
@@ -96,21 +92,21 @@ TEST_CASE("RecipeSchematic: an untagged assembler recipe has unlocked_at_start =
|
||||
TEST_CASE("RecipeSchematic: recipe with unlock_at_station_level = -1 is unlocked at game start",
|
||||
"[recipe_schematic]")
|
||||
{
|
||||
const Simulation sim(loadConfig());
|
||||
const Simulation sim(loadTestConfig());
|
||||
REQUIRE(sim.isRecipeUnlocked("premium_circuit"));
|
||||
}
|
||||
|
||||
TEST_CASE("RecipeSchematic: recipe with unlock_at_station_level = 0 is locked at game start",
|
||||
"[recipe_schematic]")
|
||||
{
|
||||
const Simulation sim(loadConfig());
|
||||
const Simulation sim(loadTestConfig());
|
||||
REQUIRE_FALSE(sim.isRecipeUnlocked("quick_circuit"));
|
||||
}
|
||||
|
||||
TEST_CASE("RecipeSchematic: recipe with unlock_at_station_level = 1 is locked at game start",
|
||||
"[recipe_schematic]")
|
||||
{
|
||||
const Simulation sim(loadConfig());
|
||||
const Simulation sim(loadTestConfig());
|
||||
REQUIRE_FALSE(sim.isRecipeUnlocked("advanced_circuit"));
|
||||
}
|
||||
|
||||
@@ -123,7 +119,7 @@ TEST_CASE("RecipeSchematic: -1 recipe seeds its output item into the implicit un
|
||||
{
|
||||
// premium_circuit is not needed by any ship or module schematic, so it can
|
||||
// only reach the implicit set via the -1 recipe seed in Phase 1.
|
||||
const Simulation sim(loadConfig());
|
||||
const Simulation sim(loadTestConfig());
|
||||
REQUIRE(sim.isItemUnlocked("premium_circuit"));
|
||||
}
|
||||
|
||||
@@ -132,7 +128,7 @@ TEST_CASE("RecipeSchematic: -1 recipe's inputs are in the implicit unlock set",
|
||||
{
|
||||
// premium_circuit takes circuit_board as input; that item was already
|
||||
// implicitly unlocked by ship schematics, so it must remain unlocked.
|
||||
const Simulation sim(loadConfig());
|
||||
const Simulation sim(loadTestConfig());
|
||||
REQUIRE(sim.isItemUnlocked("circuit_board"));
|
||||
}
|
||||
|
||||
@@ -142,7 +138,7 @@ TEST_CASE("RecipeSchematic: locked recipe's unique input is not in the implicit
|
||||
// exotic_alloy has unlock_at_station_level = 0 (locked at start) and takes
|
||||
// exotic_ore as input. exotic_ore is only reachable through this locked
|
||||
// recipe, so it must not appear in the implicit set.
|
||||
const Simulation sim(loadConfig());
|
||||
const Simulation sim(loadTestConfig());
|
||||
REQUIRE_FALSE(sim.isItemUnlocked("exotic_ore"));
|
||||
}
|
||||
|
||||
@@ -152,7 +148,7 @@ TEST_CASE("RecipeSchematic: locked recipe's output item is not in the implicit u
|
||||
// exotic_alloy is produced only by the locked recipe of the same name and
|
||||
// is not needed by any schematic, so neither the item nor the recipe should
|
||||
// be unlocked at game start.
|
||||
const Simulation sim(loadConfig());
|
||||
const Simulation sim(loadTestConfig());
|
||||
REQUIRE_FALSE(sim.isItemUnlocked("exotic_alloy"));
|
||||
REQUIRE_FALSE(sim.isRecipeUnlocked("exotic_alloy"));
|
||||
}
|
||||
@@ -162,7 +158,7 @@ TEST_CASE("RecipeSchematic: normal implicit unlock is unaffected for untagged as
|
||||
{
|
||||
// circuit_board carries no unlock_at_station_level and is needed by ships
|
||||
// that start unlocked, so it must still be implicitly unlocked.
|
||||
const Simulation sim(loadConfig());
|
||||
const Simulation sim(loadTestConfig());
|
||||
REQUIRE(sim.isRecipeUnlocked("circuit_board"));
|
||||
}
|
||||
|
||||
@@ -176,7 +172,7 @@ TEST_CASE("RecipeSchematic: eligible recipe schematic is eventually awarded on s
|
||||
// quick_circuit has unlock_at_station_level = 0 and produces circuit_board
|
||||
// (already implicitly unlocked), so it is eligible from the first station
|
||||
// destruction. With up to 150 trials it must be awarded at least once.
|
||||
Simulation sim(loadConfig());
|
||||
Simulation sim(loadTestConfig());
|
||||
REQUIRE(awaitRecipeUnlock(sim, "quick_circuit"));
|
||||
}
|
||||
|
||||
@@ -185,7 +181,7 @@ TEST_CASE("RecipeSchematic: an implicitly-gated recipe with no unlock group is n
|
||||
{
|
||||
// exotic_alloy is in no unlock group and its output/inputs are unreachable
|
||||
// via the item graph, so it can never be dropped or implicitly unlocked.
|
||||
Simulation sim(loadConfig());
|
||||
Simulation sim(loadTestConfig());
|
||||
for (int i = 0; i < 50; ++i)
|
||||
{
|
||||
killEnemyStationsAndApply(sim);
|
||||
@@ -198,7 +194,7 @@ TEST_CASE("RecipeSchematic: recipe with level > destroyed station level is not a
|
||||
{
|
||||
// advanced_circuit has unlock_at_station_level = 1. Destroying a single
|
||||
// level-0 station set must not award it regardless of the RNG outcome.
|
||||
Simulation sim(loadConfig());
|
||||
Simulation sim(loadTestConfig());
|
||||
killEnemyStationsAndApply(sim);
|
||||
REQUIRE_FALSE(sim.isRecipeUnlocked("advanced_circuit"));
|
||||
}
|
||||
@@ -208,14 +204,14 @@ TEST_CASE("RecipeSchematic: recipe with higher level is awarded once eligible st
|
||||
{
|
||||
// After enough destructions to pass station level 1, advanced_circuit must
|
||||
// eventually be awarded.
|
||||
Simulation sim(loadConfig());
|
||||
Simulation sim(loadTestConfig());
|
||||
REQUIRE(awaitRecipeUnlock(sim, "advanced_circuit", 300));
|
||||
}
|
||||
|
||||
TEST_CASE("RecipeSchematic: awarded recipe schematic stays unlocked and is not awarded again",
|
||||
"[recipe_schematic]")
|
||||
{
|
||||
Simulation sim(loadConfig());
|
||||
Simulation sim(loadTestConfig());
|
||||
awaitRecipeUnlock(sim, "quick_circuit");
|
||||
REQUIRE(sim.isRecipeUnlocked("quick_circuit"));
|
||||
|
||||
@@ -231,7 +227,7 @@ TEST_CASE("RecipeSchematic: awarded recipe schematic stays unlocked and is not a
|
||||
TEST_CASE("RecipeSchematic: recipe schematic can appear in pending choices",
|
||||
"[recipe_schematic]")
|
||||
{
|
||||
Simulation sim(loadConfig());
|
||||
Simulation sim(loadTestConfig());
|
||||
|
||||
bool foundRecipeChoice = false;
|
||||
for (int i = 0; i < 150 && !foundRecipeChoice; ++i)
|
||||
@@ -260,7 +256,7 @@ TEST_CASE("RecipeSchematic: recipe schematic can appear in pending choices",
|
||||
TEST_CASE("RecipeSchematic: reset re-locks a previously awarded recipe schematic",
|
||||
"[recipe_schematic]")
|
||||
{
|
||||
Simulation sim(loadConfig());
|
||||
Simulation sim(loadTestConfig());
|
||||
awaitRecipeUnlock(sim, "quick_circuit");
|
||||
REQUIRE(sim.isRecipeUnlocked("quick_circuit"));
|
||||
|
||||
@@ -272,7 +268,7 @@ TEST_CASE("RecipeSchematic: reset re-locks a previously awarded recipe schematic
|
||||
TEST_CASE("RecipeSchematic: reset keeps -1 recipes unlocked and their seed items accessible",
|
||||
"[recipe_schematic]")
|
||||
{
|
||||
Simulation sim(loadConfig());
|
||||
Simulation sim(loadTestConfig());
|
||||
sim.reset();
|
||||
|
||||
REQUIRE(sim.isRecipeUnlocked("premium_circuit"));
|
||||
@@ -286,7 +282,7 @@ TEST_CASE("RecipeSchematic: reset keeps -1 recipes unlocked and their seed items
|
||||
TEST_CASE("RecipeSchematic: newlyUnlockedRecipeIds is sorted, deduplicated, and empty for level-ups",
|
||||
"[recipe_schematic]")
|
||||
{
|
||||
Simulation sim(loadConfig());
|
||||
Simulation sim(loadTestConfig());
|
||||
|
||||
for (int i = 0; i < 100; ++i)
|
||||
{
|
||||
@@ -310,8 +306,8 @@ TEST_CASE("RecipeSchematic: newlyUnlockedRecipeIds is sorted, deduplicated, and
|
||||
TEST_CASE("RecipeSchematic: newlyUnlockedRecipeIds matches recipes that actually become unlocked",
|
||||
"[recipe_schematic]")
|
||||
{
|
||||
Simulation sim(loadConfig());
|
||||
const GameConfig cfg = loadConfig();
|
||||
Simulation sim(loadTestConfig());
|
||||
const GameConfig cfg = loadTestConfig();
|
||||
|
||||
auto unlockedTrackedRecipeIds = [&]()
|
||||
{
|
||||
@@ -365,7 +361,7 @@ TEST_CASE("SchematicDrop: an owned ship schematic is never offered again",
|
||||
{
|
||||
// repair_ship has unlock_at_station_level = 0 in the test config, so it is
|
||||
// locked at start and becomes eligible once a station set is destroyed.
|
||||
Simulation sim(loadConfig());
|
||||
Simulation sim(loadTestConfig());
|
||||
REQUIRE_FALSE(sim.isSchematicUnlocked("repair_ship"));
|
||||
|
||||
bool wasUnlocked = false;
|
||||
|
||||
@@ -16,14 +16,10 @@
|
||||
#include "ReplayRecorder.h"
|
||||
#include "Rotation.h"
|
||||
#include "Simulation.h"
|
||||
#include "TestConfig.h"
|
||||
|
||||
namespace
|
||||
{
|
||||
GameConfig loadConfig()
|
||||
{
|
||||
return ConfigLoader::loadFromDirectory(CONFIG_DIR);
|
||||
}
|
||||
|
||||
std::string tempOutputDir()
|
||||
{
|
||||
return (QDir::tempPath() + "/dota_factory_replay_playback_test").toStdString();
|
||||
@@ -148,7 +144,7 @@ TEST_CASE("a recorded run replays to byte-identical state with no desync", "[rep
|
||||
std::string replayPath;
|
||||
std::uint64_t recordedFinalChecksum = 0;
|
||||
{
|
||||
Simulation rec(loadConfig(), seed);
|
||||
Simulation rec(loadTestConfig(), seed);
|
||||
CommandManager manager(rec);
|
||||
std::unique_ptr<ReplayRecorder> recorder =
|
||||
std::make_unique<ReplayRecorder>(CONFIG_DIR, tempOutputDir());
|
||||
@@ -179,7 +175,7 @@ TEST_CASE("a recorded run replays to byte-identical state with no desync", "[rep
|
||||
REQUIRE_FALSE(parsed->entries.empty());
|
||||
|
||||
// --- Replay it. ---
|
||||
Simulation play(loadConfig(), parsed->header.seed);
|
||||
Simulation play(loadTestConfig(), parsed->header.seed);
|
||||
ReplayPlayer player(play, parsed->entries);
|
||||
player.start();
|
||||
while (!player.isFinished())
|
||||
@@ -201,7 +197,7 @@ TEST_CASE("ReplayPlayer reports a desync when a checksum is corrupted", "[replay
|
||||
|
||||
std::string replayPath;
|
||||
{
|
||||
Simulation rec(loadConfig(), seed);
|
||||
Simulation rec(loadTestConfig(), seed);
|
||||
CommandManager manager(rec);
|
||||
std::unique_ptr<ReplayRecorder> recorder =
|
||||
std::make_unique<ReplayRecorder>(CONFIG_DIR, tempOutputDir());
|
||||
@@ -228,7 +224,7 @@ TEST_CASE("ReplayPlayer reports a desync when a checksum is corrupted", "[replay
|
||||
}
|
||||
REQUIRE(corrupted);
|
||||
|
||||
Simulation play(loadConfig(), parsed->header.seed);
|
||||
Simulation play(loadTestConfig(), parsed->header.seed);
|
||||
ReplayPlayer player(play, parsed->entries);
|
||||
player.start();
|
||||
while (!player.isFinished())
|
||||
@@ -250,7 +246,7 @@ TEST_CASE("a long recorded run (through waves and combat) replays with no desync
|
||||
std::string replayPath;
|
||||
std::uint64_t recordedFinalChecksum = 0;
|
||||
{
|
||||
Simulation rec(loadConfig(), seed);
|
||||
Simulation rec(loadTestConfig(), seed);
|
||||
CommandManager manager(rec);
|
||||
std::unique_ptr<ReplayRecorder> recorder =
|
||||
std::make_unique<ReplayRecorder>(CONFIG_DIR, tempOutputDir());
|
||||
@@ -275,7 +271,7 @@ TEST_CASE("a long recorded run (through waves and combat) replays with no desync
|
||||
const std::optional<ParsedReplay> parsed = readReplayFile(replayPath);
|
||||
REQUIRE(parsed.has_value());
|
||||
|
||||
Simulation play(loadConfig(), parsed->header.seed);
|
||||
Simulation play(loadTestConfig(), parsed->header.seed);
|
||||
ReplayPlayer player(play, parsed->entries);
|
||||
player.start();
|
||||
while (!player.isFinished())
|
||||
|
||||
@@ -16,6 +16,7 @@
|
||||
#include "GameConfig.h"
|
||||
#include "ReplayRecorder.h"
|
||||
#include "Simulation.h"
|
||||
#include "TestConfig.h"
|
||||
|
||||
namespace
|
||||
{
|
||||
@@ -144,7 +145,7 @@ TEST_CASE("ReplayRecorder writes a well-formed file", "[replay]")
|
||||
|
||||
TEST_CASE("CommandManager records commands and an initial checksum on drain", "[replay]")
|
||||
{
|
||||
Simulation sim(ConfigLoader::loadFromDirectory(CONFIG_DIR), 7u);
|
||||
Simulation sim(loadTestConfig(), 7u);
|
||||
CommandManager manager(sim);
|
||||
|
||||
std::unique_ptr<ReplayRecorder> recorder =
|
||||
@@ -187,7 +188,7 @@ TEST_CASE("ReplayRecorder startNewRun rolls to a new file", "[replay]")
|
||||
|
||||
TEST_CASE("CommandManager rolls the replay file on a Reset command", "[replay]")
|
||||
{
|
||||
Simulation sim(ConfigLoader::loadFromDirectory(CONFIG_DIR), 1u);
|
||||
Simulation sim(loadTestConfig(), 1u);
|
||||
CommandManager manager(sim);
|
||||
|
||||
std::unique_ptr<ReplayRecorder> recorder =
|
||||
@@ -197,7 +198,7 @@ TEST_CASE("CommandManager rolls the replay file on a Reset command", "[replay]")
|
||||
const std::string firstPath = recorderPtr->getCurrentFilePath();
|
||||
|
||||
std::shared_ptr<ResetCommand> reset = std::make_shared<ResetCommand>();
|
||||
reset->config = std::make_shared<GameConfig>(ConfigLoader::loadFromDirectory(CONFIG_DIR));
|
||||
reset->config = std::make_shared<GameConfig>(loadTestConfig());
|
||||
reset->seed = 999u;
|
||||
manager.enqueue(reset);
|
||||
manager.drain();
|
||||
|
||||
@@ -22,11 +22,7 @@
|
||||
#include "SimulationTestAccess.h"
|
||||
#include "Tick.h"
|
||||
#include "WeaponComponent.h"
|
||||
|
||||
static GameConfig loadConfig()
|
||||
{
|
||||
return ConfigLoader::loadFromDirectory(CONFIG_DIR);
|
||||
}
|
||||
#include "TestConfig.h"
|
||||
|
||||
static const ShipDef* findSchematic(const GameConfig& cfg, const std::string& id)
|
||||
{
|
||||
@@ -108,7 +104,7 @@ static void fillMaterials(Simulation& sim, BuildingId yardId,
|
||||
|
||||
TEST_CASE("Ship spawn: no modules leaves base stats unchanged", "[modules]")
|
||||
{
|
||||
Simulation sim(loadConfig(), 42);
|
||||
Simulation sim(loadTestConfig(), 42);
|
||||
const ShipDef* def = findSchematic(sim.getConfig(), "interceptor");
|
||||
REQUIRE(def != nullptr);
|
||||
|
||||
@@ -123,7 +119,7 @@ TEST_CASE("Ship spawn: no modules leaves base stats unchanged", "[modules]")
|
||||
|
||||
TEST_CASE("Ship spawn: multiplicative HP module applies correctly", "[modules]")
|
||||
{
|
||||
Simulation sim(loadConfig(), 42);
|
||||
Simulation sim(loadTestConfig(), 42);
|
||||
const ShipDef* def = findSchematic(sim.getConfig(), "interceptor");
|
||||
REQUIRE(def != nullptr);
|
||||
|
||||
@@ -148,7 +144,7 @@ TEST_CASE("Ship spawn: multiplicative HP module applies correctly", "[modules]")
|
||||
|
||||
TEST_CASE("Ship spawn: additive sensor module applies correctly", "[modules]")
|
||||
{
|
||||
Simulation sim(loadConfig(), 42);
|
||||
Simulation sim(loadTestConfig(), 42);
|
||||
const ShipDef* def = findSchematic(sim.getConfig(), "interceptor");
|
||||
REQUIRE(def != nullptr);
|
||||
|
||||
@@ -173,7 +169,7 @@ TEST_CASE("Ship spawn: additive sensor module applies correctly", "[modules]")
|
||||
|
||||
TEST_CASE("Ship spawn: multiple modules stack correctly", "[modules]")
|
||||
{
|
||||
Simulation sim(loadConfig(), 42);
|
||||
Simulation sim(loadTestConfig(), 42);
|
||||
const ShipDef* def = findSchematic(sim.getConfig(), "interceptor");
|
||||
REQUIRE(def != nullptr);
|
||||
|
||||
@@ -206,7 +202,7 @@ TEST_CASE("Ship spawn: multiple modules stack correctly", "[modules]")
|
||||
TEST_CASE("Shipyard: setShipLayout reinitializes buffers with module materials",
|
||||
"[modules][shipyard]")
|
||||
{
|
||||
Simulation sim(loadConfig(), 42);
|
||||
Simulation sim(loadTestConfig(), 42);
|
||||
const BuildingDef* yardDef = findShipyardDef(sim.getConfig());
|
||||
REQUIRE(yardDef != nullptr);
|
||||
|
||||
@@ -233,7 +229,7 @@ TEST_CASE("Shipyard: setShipLayout reinitializes buffers with module materials",
|
||||
TEST_CASE("Shipyard: setShipLayout cancels in-progress production",
|
||||
"[modules][shipyard]")
|
||||
{
|
||||
Simulation sim(loadConfig(), 42);
|
||||
Simulation sim(loadTestConfig(), 42);
|
||||
const ShipDef* def = findSchematic(sim.getConfig(), "interceptor");
|
||||
REQUIRE(def != nullptr);
|
||||
const BuildingDef* yardDef = findShipyardDef(sim.getConfig());
|
||||
@@ -269,7 +265,7 @@ TEST_CASE("Shipyard: setShipLayout cancels in-progress production",
|
||||
TEST_CASE("Shipyard: builds a bare hull when no layout is configured",
|
||||
"[modules][shipyard]")
|
||||
{
|
||||
Simulation sim(loadConfig(), 42);
|
||||
Simulation sim(loadTestConfig(), 42);
|
||||
const ShipDef* def = findSchematic(sim.getConfig(), "interceptor");
|
||||
REQUIRE(def != nullptr);
|
||||
// The schematic carries a weapon in its (wave-only) default loadout. This
|
||||
@@ -312,7 +308,7 @@ TEST_CASE("Shipyard: builds a bare hull when no layout is configured",
|
||||
|
||||
TEST_CASE("Shipyard: setRecipe clears ship layout", "[modules][shipyard]")
|
||||
{
|
||||
Simulation sim(loadConfig(), 42);
|
||||
Simulation sim(loadTestConfig(), 42);
|
||||
const BuildingDef* yardDef = findShipyardDef(sim.getConfig());
|
||||
REQUIRE(yardDef != nullptr);
|
||||
|
||||
@@ -341,7 +337,7 @@ TEST_CASE("Shipyard: setRecipe clears ship layout", "[modules][shipyard]")
|
||||
TEST_CASE("Shipyard: setRecipe with unchanged recipe keeps ship layout",
|
||||
"[modules][shipyard]")
|
||||
{
|
||||
Simulation sim(loadConfig(), 42);
|
||||
Simulation sim(loadTestConfig(), 42);
|
||||
const BuildingDef* yardDef = findShipyardDef(sim.getConfig());
|
||||
REQUIRE(yardDef != nullptr);
|
||||
|
||||
@@ -376,7 +372,7 @@ TEST_CASE("Shipyard: setRecipe with unchanged recipe keeps ship layout",
|
||||
|
||||
TEST_CASE("Ship spawn: weapon_primer multiplies attack rate in simulation", "[modules]")
|
||||
{
|
||||
Simulation sim(loadConfig(), 42);
|
||||
Simulation sim(loadTestConfig(), 42);
|
||||
const ShipDef* def = findSchematic(sim.getConfig(), "interceptor");
|
||||
REQUIRE(def != nullptr);
|
||||
|
||||
@@ -401,7 +397,7 @@ TEST_CASE("Ship spawn: weapon_primer multiplies attack rate in simulation", "[mo
|
||||
|
||||
TEST_CASE("Ship spawn: weapon_stabilizer multiplies attack range in simulation", "[modules]")
|
||||
{
|
||||
Simulation sim(loadConfig(), 42);
|
||||
Simulation sim(loadTestConfig(), 42);
|
||||
const ShipDef* def = findSchematic(sim.getConfig(), "interceptor");
|
||||
REQUIRE(def != nullptr);
|
||||
|
||||
@@ -428,7 +424,7 @@ TEST_CASE("Ship spawn: weapon_stabilizer multiplies attack range in simulation",
|
||||
|
||||
TEST_CASE("Ship spawn: afterburner additive main_acceleration is converted m/s² to tiles/tick", "[modules]")
|
||||
{
|
||||
Simulation sim(loadConfig(), 42);
|
||||
Simulation sim(loadTestConfig(), 42);
|
||||
const ShipDef* def = findSchematic(sim.getConfig(), "interceptor");
|
||||
REQUIRE(def != nullptr);
|
||||
|
||||
@@ -453,7 +449,7 @@ TEST_CASE("Ship spawn: afterburner additive main_acceleration is converted m/s²
|
||||
|
||||
TEST_CASE("Ship spawn: maneuvering_thrusters additive maneuvering_acceleration is converted m/s² to tiles/tick", "[modules]")
|
||||
{
|
||||
Simulation sim(loadConfig(), 42);
|
||||
Simulation sim(loadTestConfig(), 42);
|
||||
const ShipDef* def = findSchematic(sim.getConfig(), "interceptor");
|
||||
REQUIRE(def != nullptr);
|
||||
|
||||
@@ -482,7 +478,7 @@ TEST_CASE("Ship spawn: maneuvering_thrusters additive maneuvering_acceleration i
|
||||
|
||||
TEST_CASE("calculateShipStats: weapon_primer multiplies attack rate in stats view", "[modules]")
|
||||
{
|
||||
const GameConfig cfg = loadConfig();
|
||||
const GameConfig cfg = loadTestConfig();
|
||||
const ShipDef* def = findSchematic(cfg, "interceptor");
|
||||
REQUIRE(def != nullptr);
|
||||
|
||||
@@ -506,7 +502,7 @@ TEST_CASE("calculateShipStats: weapon_primer multiplies attack rate in stats vie
|
||||
|
||||
TEST_CASE("calculateShipStats: weapon_stabilizer multiplies attack range in stats view", "[modules]")
|
||||
{
|
||||
const GameConfig cfg = loadConfig();
|
||||
const GameConfig cfg = loadTestConfig();
|
||||
const ShipDef* def = findSchematic(cfg, "interceptor");
|
||||
REQUIRE(def != nullptr);
|
||||
|
||||
@@ -532,7 +528,7 @@ TEST_CASE("calculateShipStats: weapon_stabilizer multiplies attack range in stat
|
||||
|
||||
TEST_CASE("calculateShipStats: afterburner additive main_acceleration is converted m/s² to tiles/s²", "[modules]")
|
||||
{
|
||||
const GameConfig cfg = loadConfig();
|
||||
const GameConfig cfg = loadTestConfig();
|
||||
const ShipDef* def = findSchematic(cfg, "interceptor");
|
||||
REQUIRE(def != nullptr);
|
||||
|
||||
@@ -556,7 +552,7 @@ TEST_CASE("calculateShipStats: afterburner additive main_acceleration is convert
|
||||
|
||||
TEST_CASE("calculateShipStats: maneuvering_thrusters additive maneuvering_acceleration is converted m/s² to tiles/s²", "[modules]")
|
||||
{
|
||||
const GameConfig cfg = loadConfig();
|
||||
const GameConfig cfg = loadTestConfig();
|
||||
const ShipDef* def = findSchematic(cfg, "interceptor");
|
||||
REQUIRE(def != nullptr);
|
||||
|
||||
|
||||
@@ -29,11 +29,7 @@
|
||||
#include "ShipSystem.h"
|
||||
#include "Tick.h"
|
||||
#include "WeaponComponent.h"
|
||||
|
||||
static GameConfig loadConfig()
|
||||
{
|
||||
return ConfigLoader::loadFromDirectory(CONFIG_DIR);
|
||||
}
|
||||
#include "TestConfig.h"
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Helpers
|
||||
@@ -91,7 +87,7 @@ TEST_CASE("ShipSystem: interceptor spawn has weapon child and attack behavior, n
|
||||
"[ship]")
|
||||
{
|
||||
EntityAdmin admin;
|
||||
const GameConfig cfg = loadConfig();
|
||||
const GameConfig cfg = loadTestConfig();
|
||||
ShipSystem ss(cfg, admin);
|
||||
|
||||
const entt::entity e = ss.spawn("interceptor", QVector2D(0.0f, 0.0f));
|
||||
@@ -115,7 +111,7 @@ TEST_CASE("ShipSystem: interceptor spawn has weapon child and attack behavior, n
|
||||
TEST_CASE("ShipSystem: enemy combat ship has no rally or retreat behavior", "[ship]")
|
||||
{
|
||||
EntityAdmin admin;
|
||||
const GameConfig cfg = loadConfig();
|
||||
const GameConfig cfg = loadTestConfig();
|
||||
ShipSystem ss(cfg, admin);
|
||||
|
||||
const entt::entity e = ss.spawn("interceptor", QVector2D(0.0f, 0.0f), /*isEnemy=*/true);
|
||||
@@ -129,7 +125,7 @@ TEST_CASE("ShipSystem: enemy combat ship has no rally or retreat behavior", "[sh
|
||||
TEST_CASE("ShipSystem: setRetreatEnabled(false) suppresses player retreat behavior", "[ship]")
|
||||
{
|
||||
EntityAdmin admin;
|
||||
const GameConfig cfg = loadConfig();
|
||||
const GameConfig cfg = loadTestConfig();
|
||||
ShipSystem ss(cfg, admin);
|
||||
ss.setRetreatEnabled(false);
|
||||
|
||||
@@ -144,7 +140,7 @@ TEST_CASE("ShipSystem: setRetreatEnabled(false) suppresses player retreat behavi
|
||||
TEST_CASE("ShipSystem: interceptor level 1 stats match config formulas", "[ship]")
|
||||
{
|
||||
EntityAdmin admin;
|
||||
const GameConfig cfg = loadConfig();
|
||||
const GameConfig cfg = loadTestConfig();
|
||||
ShipSystem ss(cfg, admin);
|
||||
|
||||
const entt::entity e = ss.spawn("interceptor", QVector2D(0.0f, 0.0f));
|
||||
@@ -166,7 +162,7 @@ TEST_CASE("ShipSystem: interceptor level 1 stats match config formulas", "[ship]
|
||||
TEST_CASE("ShipSystem: interceptor hp matches config value", "[ship]")
|
||||
{
|
||||
EntityAdmin admin;
|
||||
const GameConfig cfg = loadConfig();
|
||||
const GameConfig cfg = loadTestConfig();
|
||||
ShipSystem ss(cfg, admin);
|
||||
|
||||
const entt::entity e = ss.spawn("interceptor", QVector2D(0.0f, 0.0f));
|
||||
@@ -178,7 +174,7 @@ TEST_CASE("ShipSystem: interceptor hp matches config value", "[ship]")
|
||||
TEST_CASE("ShipSystem: interceptor maxSpeed_tpt matches config value / tileSize / kTickRateHz", "[ship]")
|
||||
{
|
||||
EntityAdmin admin;
|
||||
const GameConfig cfg = loadConfig();
|
||||
const GameConfig cfg = loadTestConfig();
|
||||
ShipSystem ss(cfg, admin);
|
||||
|
||||
const entt::entity e = ss.spawn("interceptor", QVector2D(0.0f, 0.0f));
|
||||
@@ -196,7 +192,7 @@ TEST_CASE("ShipSystem: salvage_ship spawn with salvage module has cargo child an
|
||||
"[ship]")
|
||||
{
|
||||
EntityAdmin admin;
|
||||
const GameConfig cfg = loadConfig();
|
||||
const GameConfig cfg = loadTestConfig();
|
||||
ShipSystem ss(cfg, admin);
|
||||
|
||||
const ShipLayoutConfig layout = makeSingleModuleLayout("salvager");
|
||||
@@ -212,7 +208,7 @@ TEST_CASE("ShipSystem: salvage_ship spawn with salvage module has cargo child an
|
||||
TEST_CASE("ShipSystem: salvage_ship cargo capacity matches config", "[ship]")
|
||||
{
|
||||
EntityAdmin admin;
|
||||
const GameConfig cfg = loadConfig();
|
||||
const GameConfig cfg = loadTestConfig();
|
||||
ShipSystem ss(cfg, admin);
|
||||
|
||||
const ShipLayoutConfig layout = makeSingleModuleLayout("salvager");
|
||||
@@ -237,7 +233,7 @@ TEST_CASE("ShipSystem: repair_ship spawn with repair module has repair child and
|
||||
"[ship]")
|
||||
{
|
||||
EntityAdmin admin;
|
||||
const GameConfig cfg = loadConfig();
|
||||
const GameConfig cfg = loadTestConfig();
|
||||
ShipSystem ss(cfg, admin);
|
||||
|
||||
const ShipLayoutConfig layout = makeSingleModuleLayout("repair_tool");
|
||||
@@ -252,7 +248,7 @@ TEST_CASE("ShipSystem: repair_ship spawn with repair module has repair child and
|
||||
TEST_CASE("ShipSystem: repair_ship level 1 repair stats match config formulas", "[ship]")
|
||||
{
|
||||
EntityAdmin admin;
|
||||
const GameConfig cfg = loadConfig();
|
||||
const GameConfig cfg = loadTestConfig();
|
||||
ShipSystem ss(cfg, admin);
|
||||
|
||||
const ShipLayoutConfig layout = makeSingleModuleLayout("repair_tool");
|
||||
@@ -277,7 +273,7 @@ TEST_CASE("ShipSystem: repair_ship level 1 repair stats match config formulas",
|
||||
TEST_CASE("ShipSystem: spawned ships are valid entities", "[ship]")
|
||||
{
|
||||
EntityAdmin admin;
|
||||
const GameConfig cfg = loadConfig();
|
||||
const GameConfig cfg = loadTestConfig();
|
||||
ShipSystem ss(cfg, admin);
|
||||
|
||||
const entt::entity e1 = ss.spawn("interceptor", QVector2D(0.0f, 0.0f));
|
||||
@@ -292,7 +288,7 @@ TEST_CASE("ShipSystem: spawned ships are valid entities", "[ship]")
|
||||
TEST_CASE("ShipSystem: despawn removes the ship and its weapon children", "[ship]")
|
||||
{
|
||||
EntityAdmin admin;
|
||||
const GameConfig cfg = loadConfig();
|
||||
const GameConfig cfg = loadTestConfig();
|
||||
ShipSystem ss(cfg, admin);
|
||||
|
||||
const entt::entity e = ss.spawn("interceptor", QVector2D(0.0f, 0.0f));
|
||||
|
||||
@@ -16,11 +16,7 @@
|
||||
#include "Simulation.h"
|
||||
#include "SimulationTestAccess.h"
|
||||
#include "Tick.h"
|
||||
|
||||
static GameConfig loadConfig()
|
||||
{
|
||||
return ConfigLoader::loadFromDirectory(CONFIG_DIR);
|
||||
}
|
||||
#include "TestConfig.h"
|
||||
|
||||
// A ship starts unlocked iff no unlock group grants it (REQ-LOCK-EXPLICIT).
|
||||
static bool startsUnlocked(const GameConfig& cfg, const std::string& shipId)
|
||||
@@ -98,7 +94,7 @@ static void fillMaterials(Simulation& sim, BuildingId yardId, const ShipDef& def
|
||||
TEST_CASE("Shipyard: spawns a player ship after production cycle completes",
|
||||
"[shipyard]")
|
||||
{
|
||||
Simulation sim(loadConfig(), 42);
|
||||
Simulation sim(loadTestConfig(), 42);
|
||||
|
||||
const ShipDef* def = findAvailableSchematic(sim.getConfig());
|
||||
REQUIRE(def != nullptr);
|
||||
@@ -143,7 +139,7 @@ TEST_CASE("Shipyard: spawns a player ship after production cycle completes",
|
||||
|
||||
TEST_CASE("Shipyard: does not spawn without a schematic set", "[shipyard]")
|
||||
{
|
||||
Simulation sim(loadConfig(), 42);
|
||||
Simulation sim(loadTestConfig(), 42);
|
||||
|
||||
const BuildingDef* yardDef = findShipyardDef(sim.getConfig());
|
||||
REQUIRE(yardDef != nullptr);
|
||||
@@ -159,7 +155,7 @@ TEST_CASE("Shipyard: does not spawn without a schematic set", "[shipyard]")
|
||||
|
||||
TEST_CASE("Shipyard: does not spawn with insufficient materials", "[shipyard]")
|
||||
{
|
||||
Simulation sim(loadConfig(), 42);
|
||||
Simulation sim(loadTestConfig(), 42);
|
||||
|
||||
const ShipDef* def = findAvailableSchematic(sim.getConfig());
|
||||
REQUIRE(def != nullptr);
|
||||
@@ -183,7 +179,7 @@ TEST_CASE("Shipyard: does not spawn with insufficient materials", "[shipyard]")
|
||||
|
||||
TEST_CASE("Shipyard: spawns a second ship after materials replenished", "[shipyard]")
|
||||
{
|
||||
Simulation sim(loadConfig(), 42);
|
||||
Simulation sim(loadTestConfig(), 42);
|
||||
|
||||
const ShipDef* def = findAvailableSchematic(sim.getConfig());
|
||||
REQUIRE(def != nullptr);
|
||||
|
||||
@@ -5,11 +5,7 @@
|
||||
#include "Simulation.h"
|
||||
#include "Tick.h"
|
||||
#include "TickDriver.h"
|
||||
|
||||
static GameConfig loadConfig()
|
||||
{
|
||||
return ConfigLoader::loadFromDirectory(CONFIG_DIR);
|
||||
}
|
||||
#include "TestConfig.h"
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Simulation
|
||||
@@ -17,14 +13,14 @@ static GameConfig loadConfig()
|
||||
|
||||
TEST_CASE("Simulation::currentTick starts at 0", "[simulation]")
|
||||
{
|
||||
const Simulation sim(loadConfig());
|
||||
const Simulation sim(loadTestConfig());
|
||||
|
||||
REQUIRE(sim.getCurrentTick() == 0);
|
||||
}
|
||||
|
||||
TEST_CASE("Simulation::tick increments currentTick by 1", "[simulation]")
|
||||
{
|
||||
Simulation sim(loadConfig());
|
||||
Simulation sim(loadTestConfig());
|
||||
|
||||
sim.tick();
|
||||
|
||||
@@ -33,7 +29,7 @@ TEST_CASE("Simulation::tick increments currentTick by 1", "[simulation]")
|
||||
|
||||
TEST_CASE("Simulation::tick 10 times yields currentTick == 10", "[simulation]")
|
||||
{
|
||||
Simulation sim(loadConfig());
|
||||
Simulation sim(loadTestConfig());
|
||||
|
||||
for (int i = 0; i < 10; ++i)
|
||||
{
|
||||
@@ -45,14 +41,14 @@ TEST_CASE("Simulation::tick 10 times yields currentTick == 10", "[simulation]")
|
||||
|
||||
TEST_CASE("Simulation::drainBeamFiredEvents returns empty initially", "[simulation]")
|
||||
{
|
||||
Simulation sim(loadConfig());
|
||||
Simulation sim(loadTestConfig());
|
||||
|
||||
REQUIRE(sim.drainBeamFiredEvents().empty());
|
||||
}
|
||||
|
||||
TEST_CASE("Simulation::drainBeamFiredEvents clears queue on drain", "[simulation]")
|
||||
{
|
||||
Simulation sim(loadConfig());
|
||||
Simulation sim(loadTestConfig());
|
||||
|
||||
// First drain: empty.
|
||||
sim.drainBeamFiredEvents();
|
||||
@@ -63,7 +59,7 @@ TEST_CASE("Simulation::drainBeamFiredEvents clears queue on drain", "[simulation
|
||||
|
||||
TEST_CASE("Simulation::hasSchematicChoicesPending returns false initially", "[simulation]")
|
||||
{
|
||||
Simulation sim(loadConfig());
|
||||
Simulation sim(loadTestConfig());
|
||||
|
||||
REQUIRE_FALSE(sim.hasSchematicChoicesPending());
|
||||
}
|
||||
|
||||
15
src/test/TestConfig.h
Normal file
15
src/test/TestConfig.h
Normal file
@@ -0,0 +1,15 @@
|
||||
#pragma once
|
||||
|
||||
#include "ConfigLoader.h"
|
||||
#include "GameConfig.h"
|
||||
|
||||
// Loads the test fixture config set (CONFIG_DIR points at bin/test/data/config
|
||||
// for the test target). Shared by every test that needs a full GameConfig, so
|
||||
// the one-liner is not repeated per translation unit.
|
||||
//
|
||||
// Like SimulationTestAccess.h this header lives under src/test and is
|
||||
// deliberately off the lib/ui/app include path.
|
||||
inline GameConfig loadTestConfig()
|
||||
{
|
||||
return ConfigLoader::loadFromDirectory(CONFIG_DIR);
|
||||
}
|
||||
@@ -2,15 +2,11 @@
|
||||
|
||||
#include "ConfigLoader.h"
|
||||
#include "ThreatCostCalculator.h"
|
||||
|
||||
static GameConfig loadConfig()
|
||||
{
|
||||
return ConfigLoader::loadFromDirectory(CONFIG_DIR);
|
||||
}
|
||||
#include "TestConfig.h"
|
||||
|
||||
TEST_CASE("ThreatCostCalculator: miner item threat equals duration", "[threat]")
|
||||
{
|
||||
const GameConfig cfg = loadConfig();
|
||||
const GameConfig cfg = loadTestConfig();
|
||||
const ThreatCostTable& table = cfg.threatCosts;
|
||||
|
||||
CHECK(table.itemThreat.at("iron_ore") == Approx(1.0));
|
||||
@@ -19,7 +15,7 @@ TEST_CASE("ThreatCostCalculator: miner item threat equals duration", "[threat]")
|
||||
|
||||
TEST_CASE("ThreatCostCalculator: smelter item threat includes input costs", "[threat]")
|
||||
{
|
||||
const GameConfig cfg = loadConfig();
|
||||
const GameConfig cfg = loadTestConfig();
|
||||
const ThreatCostTable& table = cfg.threatCosts;
|
||||
|
||||
// iron_ingot: duration 2.0 + iron_ore(1.0) * 2 = 4.0
|
||||
@@ -30,7 +26,7 @@ TEST_CASE("ThreatCostCalculator: smelter item threat includes input costs", "[th
|
||||
|
||||
TEST_CASE("ThreatCostCalculator: assembler takes max across recipes", "[threat]")
|
||||
{
|
||||
const GameConfig cfg = loadConfig();
|
||||
const GameConfig cfg = loadTestConfig();
|
||||
const ThreatCostTable& table = cfg.threatCosts;
|
||||
|
||||
// circuit_board has three non-reprocessing recipes:
|
||||
@@ -43,7 +39,7 @@ TEST_CASE("ThreatCostCalculator: assembler takes max across recipes", "[threat]"
|
||||
|
||||
TEST_CASE("ThreatCostCalculator: scrap threat is 1 / scrap_per_threat", "[threat]")
|
||||
{
|
||||
const GameConfig cfg = loadConfig();
|
||||
const GameConfig cfg = loadTestConfig();
|
||||
const ThreatCostTable& table = cfg.threatCosts;
|
||||
|
||||
// REQ-THREAT-SCRAP: scrap threat is the constant 1 / world.scrap_per_threat.
|
||||
@@ -54,7 +50,7 @@ TEST_CASE("ThreatCostCalculator: scrap threat is 1 / scrap_per_threat", "[threat
|
||||
|
||||
TEST_CASE("ThreatCostCalculator: reprocessing-only item threat", "[threat]")
|
||||
{
|
||||
const GameConfig cfg = loadConfig();
|
||||
const GameConfig cfg = loadTestConfig();
|
||||
const ThreatCostTable& table = cfg.threatCosts;
|
||||
|
||||
// advanced_alloy: reprocessing recipe with scrap*5, duration 3.0, probability 0.1
|
||||
@@ -64,7 +60,7 @@ TEST_CASE("ThreatCostCalculator: reprocessing-only item threat", "[threat]")
|
||||
|
||||
TEST_CASE("ThreatCostCalculator: ship threat with default modules", "[threat]")
|
||||
{
|
||||
const GameConfig cfg = loadConfig();
|
||||
const GameConfig cfg = loadTestConfig();
|
||||
const ThreatCostTable& table = cfg.threatCosts;
|
||||
|
||||
// interceptor: 10 + iron_ingot(4)*3 + circuit_board(28)*1 + laser_cannon(5 + 4*1) = 59.0
|
||||
@@ -80,7 +76,7 @@ TEST_CASE("ThreatCostCalculator: ship threat with default modules", "[threat]")
|
||||
|
||||
TEST_CASE("ThreatCostCalculator: ship threat with custom modules", "[threat]")
|
||||
{
|
||||
const GameConfig cfg = loadConfig();
|
||||
const GameConfig cfg = loadTestConfig();
|
||||
const ThreatCostTable& table = cfg.threatCosts;
|
||||
|
||||
// interceptor base: 10 + iron_ingot(4)*3 + circuit_board(28)*1 = 50.0
|
||||
@@ -101,7 +97,7 @@ TEST_CASE("ThreatCostCalculator: ship threat with custom modules", "[threat]")
|
||||
|
||||
TEST_CASE("ThreatCostCalculator: unknown ship returns zero", "[threat]")
|
||||
{
|
||||
const GameConfig cfg = loadConfig();
|
||||
const GameConfig cfg = loadTestConfig();
|
||||
const ThreatCostTable& table = cfg.threatCosts;
|
||||
|
||||
double threat = calculateShipThreatCost(table, cfg, "nonexistent_ship", {});
|
||||
@@ -113,7 +109,7 @@ TEST_CASE("ThreatCostCalculator: unknown ship returns zero", "[threat]")
|
||||
// be excluded. iron_ingot threat must not be inflated by the scrap path.
|
||||
TEST_CASE("ThreatCostCalculator: scrap-consuming recipe excluded when scrap-free recipe exists", "[threat]")
|
||||
{
|
||||
const GameConfig cfg = loadConfig();
|
||||
const GameConfig cfg = loadTestConfig();
|
||||
const ThreatCostTable& table = cfg.threatCosts;
|
||||
|
||||
// scrap_iron recipe: duration=1.0, 1 scrap (threat=1.0) -> 1 iron_ingot.
|
||||
@@ -132,7 +128,7 @@ TEST_CASE("ThreatCostCalculator: scrap-consuming recipe excluded when scrap-free
|
||||
// Per-unit threat = (3.0 + iron_ore(1.0)*1) / 2 = 4.0 / 2 = 2.0.
|
||||
TEST_CASE("ThreatCostCalculator: per-unit division by output amount", "[threat]")
|
||||
{
|
||||
const GameConfig cfg = loadConfig();
|
||||
const GameConfig cfg = loadTestConfig();
|
||||
const ThreatCostTable& table = cfg.threatCosts;
|
||||
|
||||
CHECK(table.itemThreat.at("dual_wire") == Approx(2.0));
|
||||
@@ -146,7 +142,7 @@ TEST_CASE("ThreatCostCalculator: per-unit division by output amount", "[threat]"
|
||||
// downstream_product = 2.0 + 80.0*1 = 82.0.
|
||||
TEST_CASE("ThreatCostCalculator: downstream-of-reprocessing item resolves via fixpoint", "[threat]")
|
||||
{
|
||||
const GameConfig cfg = loadConfig();
|
||||
const GameConfig cfg = loadTestConfig();
|
||||
const ThreatCostTable& table = cfg.threatCosts;
|
||||
|
||||
CHECK(table.itemThreat.at("advanced_alloy") == Approx(80.0));
|
||||
@@ -161,7 +157,7 @@ TEST_CASE("ThreatCostCalculator: downstream-of-reprocessing item resolves via fi
|
||||
// expected threat = max(2.0, 29.0) = 29.0, not 2.0.
|
||||
TEST_CASE("ThreatCostCalculator: staggered recipes committed only when all computable", "[threat]")
|
||||
{
|
||||
const GameConfig cfg = loadConfig();
|
||||
const GameConfig cfg = loadTestConfig();
|
||||
const ThreatCostTable& table = cfg.threatCosts;
|
||||
|
||||
CHECK(table.itemThreat.at("staggered_item") == Approx(29.0));
|
||||
|
||||
@@ -19,15 +19,11 @@
|
||||
#include "SimulationTestAccess.h"
|
||||
#include "StationBodyComponent.h"
|
||||
#include "UnlocksConfig.h"
|
||||
#include "TestConfig.h"
|
||||
|
||||
namespace
|
||||
{
|
||||
|
||||
GameConfig loadConfig()
|
||||
{
|
||||
return ConfigLoader::loadFromDirectory(CONFIG_DIR);
|
||||
}
|
||||
|
||||
void killEnemyStations(Simulation& sim)
|
||||
{
|
||||
sim.getAdmin().forEach<StationBodyComponent, FactionComponent, HealthComponent>(
|
||||
@@ -42,7 +38,7 @@ void killEnemyStations(Simulation& sim)
|
||||
// bay together, gated to the given station level.
|
||||
GameConfig configWithSalvageGroup(int stationLevel)
|
||||
{
|
||||
GameConfig cfg = loadConfig();
|
||||
GameConfig cfg = loadTestConfig();
|
||||
cfg.unlocks.groups.clear();
|
||||
cfg.unlocks.groups.push_back(
|
||||
UnlockGroupDef{"salvage_operations", stationLevel, {}, {}, {"salvager"}, {"salvage_bay"}, {}});
|
||||
|
||||
@@ -22,15 +22,11 @@
|
||||
#include "SimulationTestAccess.h"
|
||||
#include "StationBodyComponent.h"
|
||||
#include "UnlocksConfig.h"
|
||||
#include "TestConfig.h"
|
||||
|
||||
namespace
|
||||
{
|
||||
|
||||
GameConfig loadConfig()
|
||||
{
|
||||
return ConfigLoader::loadFromDirectory(CONFIG_DIR);
|
||||
}
|
||||
|
||||
UnlockGroupDef& findGroup(GameConfig& cfg, const std::string& id)
|
||||
{
|
||||
for (UnlockGroupDef& group : cfg.unlocks.groups)
|
||||
@@ -85,7 +81,7 @@ TEST_CASE("UnlockPrereq: a group gated behind a locked ship is withheld from the
|
||||
// quick_circuit (recipe group, level 0) would normally be eligible at the
|
||||
// first station destruction. Gate it behind the repair_ship group (level 0,
|
||||
// locked at game start).
|
||||
GameConfig cfg = loadConfig();
|
||||
GameConfig cfg = loadTestConfig();
|
||||
findGroup(cfg, "quick_circuit").requiredGroupIds = {"repair_ship"};
|
||||
|
||||
Simulation sim(std::move(cfg), 123);
|
||||
@@ -103,7 +99,7 @@ TEST_CASE("UnlockPrereq: a group gated behind a locked ship is withheld from the
|
||||
TEST_CASE("UnlockPrereq: the gated group becomes eligible once its prerequisite is awarded",
|
||||
"[unlock_prereq]")
|
||||
{
|
||||
GameConfig cfg = loadConfig();
|
||||
GameConfig cfg = loadTestConfig();
|
||||
findGroup(cfg, "quick_circuit").requiredGroupIds = {"repair_ship"};
|
||||
|
||||
Simulation sim(std::move(cfg), 123);
|
||||
@@ -126,7 +122,7 @@ TEST_CASE("UnlockPrereq: a module group gated behind another module is withheld
|
||||
{
|
||||
// Make laser_cannon and armor_plate (both start-unlocked in the test config)
|
||||
// lockable via new unlock groups, with armor_plate gated behind laser_cannon.
|
||||
GameConfig cfg = loadConfig();
|
||||
GameConfig cfg = loadTestConfig();
|
||||
cfg.unlocks.groups.push_back(
|
||||
UnlockGroupDef{"laser_cannon", 0, {}, {}, {"laser_cannon"}, {}, {}});
|
||||
cfg.unlocks.groups.push_back(
|
||||
|
||||
@@ -25,11 +25,7 @@
|
||||
#include "Tick.h"
|
||||
#include "ThreatCostCalculator.h"
|
||||
#include "WaveSystem.h"
|
||||
|
||||
static GameConfig loadConfig()
|
||||
{
|
||||
return ConfigLoader::loadFromDirectory(CONFIG_DIR);
|
||||
}
|
||||
#include "TestConfig.h"
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Threat accumulation
|
||||
@@ -37,7 +33,7 @@ static GameConfig loadConfig()
|
||||
|
||||
TEST_CASE("WaveSystem: threat accumulates at boss wave counter rate", "[wave]")
|
||||
{
|
||||
const GameConfig cfg = loadConfig();
|
||||
const GameConfig cfg = loadTestConfig();
|
||||
std::mt19937 rng(42);
|
||||
WaveSystem ws(cfg, rng);
|
||||
|
||||
@@ -55,7 +51,7 @@ TEST_CASE("WaveSystem: threat accumulates at boss wave counter rate", "[wave]")
|
||||
TEST_CASE("WaveSystem: threatAccumulationRate matches the rate formula outside quiet windows",
|
||||
"[wave]")
|
||||
{
|
||||
const GameConfig cfg = loadConfig();
|
||||
const GameConfig cfg = loadTestConfig();
|
||||
std::mt19937 rng(42);
|
||||
WaveSystem ws(cfg, rng);
|
||||
|
||||
@@ -65,7 +61,7 @@ TEST_CASE("WaveSystem: threatAccumulationRate matches the rate formula outside q
|
||||
|
||||
TEST_CASE("WaveSystem: threatAccumulationRate is 0 during a quiet window", "[wave]")
|
||||
{
|
||||
GameConfig cfg = loadConfig();
|
||||
GameConfig cfg = loadTestConfig();
|
||||
// Start with the boss countdown already at the pre-boss quiet threshold.
|
||||
cfg.world.waves.bossCountdownSeconds = cfg.world.waves.bossQuietBeforeSeconds;
|
||||
std::mt19937 rng(42);
|
||||
@@ -83,7 +79,7 @@ TEST_CASE("WaveSystem: threatAccumulationRate is 0 during a quiet window", "[wav
|
||||
|
||||
TEST_CASE("WaveSystem: generation starts at 0 and increments on station destruction", "[wave]")
|
||||
{
|
||||
const GameConfig cfg = loadConfig();
|
||||
const GameConfig cfg = loadTestConfig();
|
||||
std::mt19937 rng(42);
|
||||
WaveSystem ws(cfg, rng);
|
||||
|
||||
@@ -100,7 +96,7 @@ TEST_CASE("WaveSystem: generation starts at 0 and increments on station destruct
|
||||
|
||||
TEST_CASE("WaveSystem: Simulation pre-places HQ + 2 player + 2 enemy stations", "[wave]")
|
||||
{
|
||||
const Simulation sim(loadConfig(), 42);
|
||||
const Simulation sim(loadTestConfig(), 42);
|
||||
|
||||
// HQ is still a Building (for belt integration).
|
||||
int hqCount = 0;
|
||||
@@ -126,7 +122,7 @@ TEST_CASE("WaveSystem: Simulation pre-places HQ + 2 player + 2 enemy stations",
|
||||
|
||||
TEST_CASE("WaveSystem: HQ has correct initial HP from config", "[wave]")
|
||||
{
|
||||
const Simulation sim(loadConfig(), 42);
|
||||
const Simulation sim(loadTestConfig(), 42);
|
||||
|
||||
const float expectedHp =
|
||||
static_cast<float>(sim.getConfig().stations.hq.hpFormula.evaluate(0.0));
|
||||
@@ -145,7 +141,7 @@ TEST_CASE("WaveSystem: HQ has correct initial HP from config", "[wave]")
|
||||
|
||||
TEST_CASE("WaveSystem: HQ anchor is at asteroid right edge", "[wave]")
|
||||
{
|
||||
const Simulation sim(loadConfig(), 42);
|
||||
const Simulation sim(loadTestConfig(), 42);
|
||||
|
||||
for (const Building& b : sim.getBuildings().getAllBuildings())
|
||||
{
|
||||
@@ -162,7 +158,7 @@ TEST_CASE("WaveSystem: HQ anchor is at asteroid right edge", "[wave]")
|
||||
|
||||
TEST_CASE("WaveSystem: player stations have weapon set", "[wave]")
|
||||
{
|
||||
Simulation sim(loadConfig(), 42);
|
||||
Simulation sim(loadTestConfig(), 42);
|
||||
|
||||
int armedPlayerStations = 0;
|
||||
sim.getAdmin().forEach<WeaponComponent, ModuleOwnerComponent>(
|
||||
@@ -183,7 +179,7 @@ TEST_CASE("WaveSystem: player stations have weapon set", "[wave]")
|
||||
|
||||
TEST_CASE("WaveSystem: enemy stations have weapon set", "[wave]")
|
||||
{
|
||||
Simulation sim(loadConfig(), 42);
|
||||
Simulation sim(loadTestConfig(), 42);
|
||||
|
||||
int armedEnemyStations = 0;
|
||||
sim.getAdmin().forEach<WeaponComponent, ModuleOwnerComponent>(
|
||||
@@ -208,7 +204,7 @@ TEST_CASE("WaveSystem: enemy stations have weapon set", "[wave]")
|
||||
|
||||
TEST_CASE("WaveSystem: enemy ships spawn after the initial gap elapses", "[wave]")
|
||||
{
|
||||
Simulation sim(loadConfig(), 42);
|
||||
Simulation sim(loadTestConfig(), 42);
|
||||
|
||||
// The maximum gap is gapMaxSeconds = 45s → 1350 ticks.
|
||||
// Run 1500 ticks to guarantee at least one wave has triggered.
|
||||
@@ -234,7 +230,7 @@ TEST_CASE("WaveSystem: enemy ships spawn after the initial gap elapses", "[wave]
|
||||
|
||||
TEST_CASE("WaveSystem: all ships have positive dynamic threat cost", "[wave]")
|
||||
{
|
||||
const GameConfig cfg = loadConfig();
|
||||
const GameConfig cfg = loadTestConfig();
|
||||
|
||||
for (const ShipDef& def : cfg.ships.ships)
|
||||
{
|
||||
@@ -250,7 +246,7 @@ TEST_CASE("WaveSystem: all ships have positive dynamic threat cost", "[wave]")
|
||||
|
||||
TEST_CASE("WaveSystem: destroying both enemy stations triggers a push", "[wave]")
|
||||
{
|
||||
Simulation sim(loadConfig(), 42);
|
||||
Simulation sim(loadTestConfig(), 42);
|
||||
|
||||
// Damage both enemy stations to 0.
|
||||
sim.getAdmin().forEach<StationBodyComponent, FactionComponent, HealthComponent>(
|
||||
@@ -273,7 +269,7 @@ TEST_CASE("WaveSystem: destroying both enemy stations triggers a push", "[wave]"
|
||||
|
||||
TEST_CASE("WaveSystem: push generates pending schematic choices", "[wave]")
|
||||
{
|
||||
Simulation sim(loadConfig(), 42);
|
||||
Simulation sim(loadTestConfig(), 42);
|
||||
|
||||
sim.getAdmin().forEach<StationBodyComponent, FactionComponent, HealthComponent>(
|
||||
[](entt::entity /*e*/, const StationBodyComponent& /*sb*/, const FactionComponent& f, HealthComponent& h)
|
||||
@@ -291,7 +287,7 @@ TEST_CASE("WaveSystem: push generates pending schematic choices", "[wave]")
|
||||
|
||||
TEST_CASE("WaveSystem: push schematic choices have valid ids", "[wave]")
|
||||
{
|
||||
Simulation sim(loadConfig(), 42);
|
||||
Simulation sim(loadTestConfig(), 42);
|
||||
|
||||
sim.getAdmin().forEach<StationBodyComponent, FactionComponent, HealthComponent>(
|
||||
[](entt::entity /*e*/, const StationBodyComponent& /*sb*/, const FactionComponent& f, HealthComponent& h)
|
||||
@@ -339,7 +335,7 @@ TEST_CASE("WaveSystem: push schematic choices have valid ids", "[wave]")
|
||||
|
||||
TEST_CASE("WaveSystem: schematic choices have no duplicates", "[wave]")
|
||||
{
|
||||
Simulation sim(loadConfig(), 42);
|
||||
Simulation sim(loadTestConfig(), 42);
|
||||
|
||||
sim.getAdmin().forEach<StationBodyComponent, FactionComponent, HealthComponent>(
|
||||
[](entt::entity /*e*/, const StationBodyComponent& /*sb*/, const FactionComponent& f, HealthComponent& h)
|
||||
@@ -359,7 +355,7 @@ TEST_CASE("WaveSystem: schematic choices have no duplicates", "[wave]")
|
||||
|
||||
TEST_CASE("WaveSystem: applySchematicChoice clears pending and applies", "[wave]")
|
||||
{
|
||||
Simulation sim(loadConfig(), 42);
|
||||
Simulation sim(loadTestConfig(), 42);
|
||||
|
||||
sim.getAdmin().forEach<StationBodyComponent, FactionComponent, HealthComponent>(
|
||||
[](entt::entity /*e*/, const StationBodyComponent& /*sb*/, const FactionComponent& f, HealthComponent& h)
|
||||
@@ -375,7 +371,7 @@ TEST_CASE("WaveSystem: applySchematicChoice clears pending and applies", "[wave]
|
||||
|
||||
TEST_CASE("WaveSystem: push places new enemy stations further right", "[wave]")
|
||||
{
|
||||
Simulation sim(loadConfig(), 42);
|
||||
Simulation sim(loadTestConfig(), 42);
|
||||
|
||||
// Record the X position of the initial enemy stations.
|
||||
int initialX = std::numeric_limits<int>::min();
|
||||
|
||||
@@ -184,13 +184,12 @@ namespace
|
||||
|
||||
BuildButtonGrid::BuildButtonGrid(Simulation* sim, const GameConfig* config,
|
||||
const std::string& iconDir,
|
||||
const std::string& itemsIconDir, QWidget* parent)
|
||||
ItemIconCache* itemIcons, QWidget* parent)
|
||||
: QWidget(parent)
|
||||
, m_sim(sim)
|
||||
, m_config(config)
|
||||
, m_iconDir(iconDir)
|
||||
, m_itemIcons(std::make_unique<ItemIconCache>(
|
||||
QString::fromStdString(itemsIconDir)))
|
||||
, m_itemIcons(itemIcons)
|
||||
{
|
||||
QGridLayout* layout = new QGridLayout(this);
|
||||
layout->setSpacing(4);
|
||||
|
||||
@@ -32,11 +32,12 @@ class BuildButtonGrid : public QWidget,
|
||||
|
||||
public:
|
||||
// iconDir is the directory holding the per-building "<id>.svg" chip icons
|
||||
// (REQ-UI-BUILD-GRID); itemsIconDir holds the per-item icons and supplies the
|
||||
// building_block icon shown in each button's cost (REQ-UI-BUILD-COST). Both are
|
||||
// read from disk at runtime, like the config files.
|
||||
// (REQ-UI-BUILD-GRID), read from disk at runtime like the config files.
|
||||
// itemIcons is the window-wide per-item icon cache and supplies the
|
||||
// building_block icon shown in each button's cost (REQ-UI-BUILD-COST). Not
|
||||
// owned; must outlive this widget.
|
||||
BuildButtonGrid(Simulation* sim, const GameConfig* config,
|
||||
const std::string& iconDir, const std::string& itemsIconDir,
|
||||
const std::string& iconDir, ItemIconCache* itemIcons,
|
||||
QWidget* parent = nullptr);
|
||||
~BuildButtonGrid() override;
|
||||
|
||||
@@ -65,7 +66,7 @@ private:
|
||||
Simulation* m_sim;
|
||||
const GameConfig* m_config;
|
||||
std::string m_iconDir;
|
||||
std::unique_ptr<ItemIconCache> m_itemIcons;
|
||||
ItemIconCache* m_itemIcons; // Not owned; lives in MainWindow.
|
||||
std::vector<BuildingType> m_types;
|
||||
std::vector<QPushButton*> m_buttons;
|
||||
std::map<BuildingType, int> m_costs;
|
||||
|
||||
@@ -4,6 +4,7 @@ SET(HDRS
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/VisualsLoader.h
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/MainWindow.h
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/ModalDimOverlay.h
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/ModalPauseScope.h
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/GameWorldView.h
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/HeaderBar.h
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/BuildButtonGrid.h
|
||||
|
||||
@@ -197,11 +197,13 @@ QColor statusLightFill(ProductionStatus status, const StatusLightVisuals& sl)
|
||||
|
||||
GameWorldView::GameWorldView(Simulation* sim, const GameConfig* config,
|
||||
const VisualsConfig* visuals, const std::string& configDir,
|
||||
const ParsedReplay* replay, QWidget* parent)
|
||||
ItemIconCache* itemIcons, const ParsedReplay* replay,
|
||||
QWidget* parent)
|
||||
: QOpenGLWidget(parent)
|
||||
, m_sim(sim)
|
||||
, m_config(config)
|
||||
, m_visuals(visuals)
|
||||
, m_itemIcons(itemIcons)
|
||||
, m_commandManager(*sim)
|
||||
, m_gameSpeedMultiplier(1.0)
|
||||
, m_prevNonZeroSpeed(1.0)
|
||||
@@ -223,11 +225,6 @@ GameWorldView::GameWorldView(Simulation* sim, const GameConfig* config,
|
||||
|
||||
loadBuildingIcons(configDir);
|
||||
|
||||
// Item icons live beside the config dir, mirroring the building icons
|
||||
// (REQ-UI-ITEM-ICON, REQ-UI-WORLD-ICON).
|
||||
m_itemIcons = std::make_unique<ItemIconCache>(QDir::cleanPath(
|
||||
QString::fromStdString(configDir) + "/../icons/items"));
|
||||
|
||||
m_renderTimer = new QTimer(this);
|
||||
m_renderTimer->setInterval(16);
|
||||
connect(m_renderTimer, &QTimer::timeout, this, &GameWorldView::onFrame);
|
||||
@@ -669,15 +666,6 @@ void GameWorldView::clampScroll()
|
||||
// Placement helpers
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
const BuildingDef* GameWorldView::findBuildingDef(BuildingType type) const
|
||||
{
|
||||
for (const BuildingDef& def : m_config->buildings.buildings)
|
||||
{
|
||||
if (def.type == type) { return &def; }
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
bool GameWorldView::isValidPlacement(BuildingType type, QPoint anchor,
|
||||
Rotation rot) const
|
||||
{
|
||||
@@ -689,7 +677,7 @@ bool GameWorldView::isValidPlacement(BuildingType type, QPoint anchor,
|
||||
return false;
|
||||
}
|
||||
|
||||
const BuildingDef* def = findBuildingDef(type);
|
||||
const BuildingDef* def = m_config->buildings.findBuildingDef(type);
|
||||
if (!def) { return false; }
|
||||
const ParsedSurfaceMask parsed = parseSurfaceMask(def->surfaceMask, rot);
|
||||
|
||||
@@ -892,7 +880,7 @@ void GameWorldView::placeBlueprintAtTile(QPoint center)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
const BuildingDef* def = findBuildingDef(bb.type);
|
||||
const BuildingDef* def = m_config->buildings.findBuildingDef(bb.type);
|
||||
if (def) { totalCost += def->cost; }
|
||||
}
|
||||
if (m_sim->getBuildingBlocksStock() < totalCost) { return; }
|
||||
@@ -969,29 +957,39 @@ BuildingType GameWorldView::effectiveBuilderType() const
|
||||
return inTunnelMode() ? m_tunnelGhostType : *m_builderType;
|
||||
}
|
||||
|
||||
std::map<std::pair<int, int>, TunnelTileInfo> GameWorldView::collectTunnelTiles() const
|
||||
TunnelTileMap GameWorldView::collectTunnelTiles() const
|
||||
{
|
||||
// Index every tunnel entry/exit — built or still a construction site — by its
|
||||
// single-cell tile, so a just-placed tunnel (not yet constructed) is matchable
|
||||
// (REQ-BLD-TUNNEL-MODE, REQ-BLD-TUNNEL-SELECT-HIGHLIGHT).
|
||||
std::map<std::pair<int, int>, TunnelTileInfo> tunnels;
|
||||
TunnelTileMap tunnels;
|
||||
for (const Building& b : m_sim->getBuildings().getAllBuildings())
|
||||
{
|
||||
if (b.type == BuildingType::TunnelEntry || b.type == BuildingType::TunnelExit)
|
||||
{
|
||||
tunnels[{b.anchor.x(), b.anchor.y()}] = TunnelTileInfo{b.type, b.rotation};
|
||||
tunnels[b.anchor] = TunnelTileInfo{b.type, b.rotation};
|
||||
}
|
||||
}
|
||||
for (const ConstructionSite& s : m_sim->getBuildings().getAllSites())
|
||||
{
|
||||
if (s.type == BuildingType::TunnelEntry || s.type == BuildingType::TunnelExit)
|
||||
{
|
||||
tunnels[{s.anchor.x(), s.anchor.y()}] = TunnelTileInfo{s.type, s.rotation};
|
||||
tunnels[s.anchor] = TunnelTileInfo{s.type, s.rotation};
|
||||
}
|
||||
}
|
||||
return tunnels;
|
||||
}
|
||||
|
||||
TunnelLookup GameWorldView::makeTunnelLookup(const TunnelTileMap& tunnels)
|
||||
{
|
||||
return [&tunnels](QPoint tile) -> std::optional<TunnelTileInfo>
|
||||
{
|
||||
const TunnelTileMap::const_iterator it = tunnels.find(tile);
|
||||
if (it == tunnels.end()) { return std::nullopt; }
|
||||
return it->second;
|
||||
};
|
||||
}
|
||||
|
||||
void GameWorldView::updateTunnelGhost()
|
||||
{
|
||||
m_tunnelGhostType = BuildingType::TunnelEntry;
|
||||
@@ -1004,14 +1002,8 @@ void GameWorldView::updateTunnelGhost()
|
||||
return;
|
||||
}
|
||||
|
||||
const std::map<std::pair<int, int>, TunnelTileInfo> tunnels = collectTunnelTiles();
|
||||
const TunnelLookup lookup = [&tunnels](QPoint tile) -> std::optional<TunnelTileInfo>
|
||||
{
|
||||
const std::map<std::pair<int, int>, TunnelTileInfo>::const_iterator it =
|
||||
tunnels.find({tile.x(), tile.y()});
|
||||
if (it == tunnels.end()) { return std::nullopt; }
|
||||
return it->second;
|
||||
};
|
||||
const TunnelTileMap tunnels = collectTunnelTiles();
|
||||
const TunnelLookup lookup = makeTunnelLookup(tunnels);
|
||||
|
||||
const TunnelCompletion completion =
|
||||
resolveTunnelCompletion(lookup, m_ghostTile, m_ghostRotation,
|
||||
@@ -1133,7 +1125,7 @@ std::vector<GameWorldView::BeltDragResolved> GameWorldView::resolveBeltDragPath(
|
||||
std::vector<BeltDragResolved> resolved;
|
||||
resolved.reserve(m_beltDragPath.size());
|
||||
|
||||
const BuildingDef* def = findBuildingDef(BuildingType::Belt);
|
||||
const BuildingDef* def = m_config->buildings.findBuildingDef(BuildingType::Belt);
|
||||
const int beltCost = (def != nullptr) ? def->cost : 0;
|
||||
const int stock = m_sim->getBuildingBlocksStock();
|
||||
int spent = 0;
|
||||
@@ -1405,7 +1397,7 @@ void GameWorldView::drawBuildings(QPainter& painter)
|
||||
painter.setBrush(Qt::NoBrush);
|
||||
painter.drawRect(bboxRect);
|
||||
|
||||
const BuildingDef* siteDef = findBuildingDef(s.type);
|
||||
const BuildingDef* siteDef = m_config->buildings.findBuildingDef(s.type);
|
||||
if (siteDef)
|
||||
{
|
||||
// Glyph + progress percentage
|
||||
@@ -1988,21 +1980,15 @@ void GameWorldView::drawSelectedTunnelConnections(QPainter& painter)
|
||||
{
|
||||
if (m_selectedBuildingIds.empty()) { return; }
|
||||
|
||||
const std::map<std::pair<int, int>, TunnelTileInfo> tunnels = collectTunnelTiles();
|
||||
const TunnelTileMap tunnels = collectTunnelTiles();
|
||||
if (tunnels.empty()) { return; }
|
||||
|
||||
const TunnelLookup lookup = [&tunnels](QPoint tile) -> std::optional<TunnelTileInfo>
|
||||
{
|
||||
const std::map<std::pair<int, int>, TunnelTileInfo>::const_iterator it =
|
||||
tunnels.find({tile.x(), tile.y()});
|
||||
if (it == tunnels.end()) { return std::nullopt; }
|
||||
return it->second;
|
||||
};
|
||||
const TunnelLookup lookup = makeTunnelLookup(tunnels);
|
||||
|
||||
// Collect the tiles to highlight in a set so a connection selected from both ends
|
||||
// (or overlapping runs) is filled exactly once — filling a semi-transparent green
|
||||
// twice would darken it (REQ-BLD-TUNNEL-SELECT-HIGHLIGHT).
|
||||
std::set<std::pair<int, int>> highlightTiles;
|
||||
std::set<QPoint, QPointCompare> highlightTiles;
|
||||
for (const BuildingId id : m_selectedBuildingIds)
|
||||
{
|
||||
std::optional<QPoint> anchor;
|
||||
@@ -2032,15 +2018,15 @@ void GameWorldView::drawSelectedTunnelConnections(QPainter& painter)
|
||||
(delta.y() > 0) - (delta.y() < 0));
|
||||
for (QPoint t = *anchor; ; t += stepDir)
|
||||
{
|
||||
highlightTiles.insert({t.x(), t.y()});
|
||||
highlightTiles.insert(t);
|
||||
if (t == *partner) { break; }
|
||||
}
|
||||
}
|
||||
|
||||
const QColor green = m_visuals->overlays.tunnelPreview;
|
||||
for (const std::pair<int, int>& tile : highlightTiles)
|
||||
for (const QPoint& tile : highlightTiles)
|
||||
{
|
||||
painter.fillRect(tileRect(QPoint(tile.first, tile.second)), green);
|
||||
painter.fillRect(tileRect(tile), green);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2177,7 +2163,7 @@ void GameWorldView::drawBuildingGhost(QPainter& painter, BuildingType type,
|
||||
QPoint anchorTile, Rotation rotation,
|
||||
bool valid, bool showPortTargetGlyphs)
|
||||
{
|
||||
const BuildingDef* def = findBuildingDef(type);
|
||||
const BuildingDef* def = m_config->buildings.findBuildingDef(type);
|
||||
if (!def) { return; }
|
||||
|
||||
const std::map<BuildingType, BuildingVisuals>::const_iterator it =
|
||||
@@ -2960,7 +2946,7 @@ void GameWorldView::rotateGhost(bool clockwise)
|
||||
{
|
||||
for (BlueprintBuilding& bb : m_blueprintMode->buildings)
|
||||
{
|
||||
const BuildingDef* def = findBuildingDef(bb.type);
|
||||
const BuildingDef* def = m_config->buildings.findBuildingDef(bb.type);
|
||||
if (!def) { continue; }
|
||||
const ParsedSurfaceMask mask = parseSurfaceMask(def->surfaceMask, bb.rotation);
|
||||
int minX = INT_MAX, minY = INT_MAX;
|
||||
@@ -3259,7 +3245,7 @@ void GameWorldView::enqueuePlaceBuilding(BuildingType type, QPoint anchor, Rotat
|
||||
|
||||
bool GameWorldView::canAfford(BuildingType type) const
|
||||
{
|
||||
const BuildingDef* def = findBuildingDef(type);
|
||||
const BuildingDef* def = m_config->buildings.findBuildingDef(type);
|
||||
if (!def) { return false; }
|
||||
return m_sim->getBuildingBlocksStock() >= def->cost;
|
||||
}
|
||||
|
||||
@@ -67,6 +67,9 @@ struct QPointCompare
|
||||
}
|
||||
};
|
||||
|
||||
// Tunnel entries/exits indexed by their single-cell tile (REQ-BLD-TUNNEL-MODE).
|
||||
using TunnelTileMap = std::map<QPoint, TunnelTileInfo, QPointCompare>;
|
||||
|
||||
class GameWorldView : public QOpenGLWidget,
|
||||
public CombinedEventHandler<BeamFiredEvent,
|
||||
BuildingTypeSelectedEvent,
|
||||
@@ -80,9 +83,12 @@ class GameWorldView : public QOpenGLWidget,
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
// itemIcons is the window-wide per-item icon cache (REQ-UI-ITEM-ICON); not
|
||||
// owned, must outlive this widget.
|
||||
GameWorldView(Simulation* sim, const GameConfig* config,
|
||||
const VisualsConfig* visuals, const std::string& configDir,
|
||||
const ParsedReplay* replay, QWidget* parent = nullptr);
|
||||
ItemIconCache* itemIcons, const ParsedReplay* replay,
|
||||
QWidget* parent = nullptr);
|
||||
~GameWorldView() override;
|
||||
|
||||
double getGameSpeed() const;
|
||||
@@ -182,7 +188,6 @@ private:
|
||||
void clampScroll();
|
||||
|
||||
bool isValidPlacement(BuildingType type, QPoint anchor, Rotation rot) const;
|
||||
const BuildingDef* findBuildingDef(BuildingType type) const;
|
||||
std::optional<BuildingId> buildingAtTile(QPoint tile) const;
|
||||
std::optional<BuildingId> siteAtTile(QPoint tile) const;
|
||||
// Ids of all buildings and construction sites whose footprint intersects
|
||||
@@ -243,7 +248,10 @@ private:
|
||||
void updateTunnelGhost();
|
||||
// Indexes every tunnel entry/exit — built or still a construction site — by its
|
||||
// single-cell tile. Shared by the placement preview and the selection highlight.
|
||||
std::map<std::pair<int, int>, TunnelTileInfo> collectTunnelTiles() const;
|
||||
TunnelTileMap collectTunnelTiles() const;
|
||||
// Wraps a tunnel tile index in the lookup functor the TunnelCompletion helpers
|
||||
// take. The returned functor references `tunnels`, which must outlive it.
|
||||
static TunnelLookup makeTunnelLookup(const TunnelTileMap& tunnels);
|
||||
// Draws the green connection highlight for every selected tunnel end that has a
|
||||
// matching end (REQ-BLD-TUNNEL-SELECT-HIGHLIGHT).
|
||||
void drawSelectedTunnelConnections(QPainter& painter);
|
||||
@@ -304,9 +312,10 @@ private:
|
||||
};
|
||||
std::map<BuildingType, BuildingIconRenderers> m_buildingIcons;
|
||||
|
||||
// Per-item icon cache (REQ-UI-ITEM-ICON), loaded from <configDir>/../icons/items.
|
||||
// Shared draw path for belt and port items; pixmaps are cached per target size.
|
||||
std::unique_ptr<ItemIconCache> m_itemIcons;
|
||||
// Per-item icon cache (REQ-UI-ITEM-ICON), shared window-wide and owned by
|
||||
// MainWindow. Shared draw path for belt and port items; pixmaps are cached
|
||||
// per target size.
|
||||
ItemIconCache* m_itemIcons;
|
||||
|
||||
// Funnels all player input into the single Simulation::apply chokepoint.
|
||||
CommandManager m_commandManager;
|
||||
|
||||
@@ -17,6 +17,7 @@
|
||||
#include "EventManager.h"
|
||||
#include "IconCaption.h"
|
||||
#include "ItemIconCache.h"
|
||||
#include "Simulation.h"
|
||||
#include "SpeedChangeRequestedEvent.h"
|
||||
#include "Tick.h"
|
||||
|
||||
@@ -30,11 +31,11 @@ namespace
|
||||
const double HeaderBar::kSpeeds[] = { 0.0, 0.5, 1.0, 2.0, 10.0 };
|
||||
const int HeaderBar::kSpeedCount = 5;
|
||||
|
||||
HeaderBar::HeaderBar(const GameConfig* config, const std::string& itemsIconDir,
|
||||
QWidget* parent)
|
||||
HeaderBar::HeaderBar(const Simulation* sim, const GameConfig* config,
|
||||
ItemIconCache* itemIcons, QWidget* parent)
|
||||
: QWidget(parent)
|
||||
, m_itemIcons(std::make_unique<ItemIconCache>(
|
||||
QString::fromStdString(itemsIconDir)))
|
||||
, m_itemIcons(itemIcons)
|
||||
, m_sim(sim)
|
||||
{
|
||||
QHBoxLayout* layout = new QHBoxLayout(this);
|
||||
layout->setContentsMargins(8, 4, 8, 4);
|
||||
@@ -115,16 +116,14 @@ void HeaderBar::handleEvent(std::shared_ptr<const TickAdvancedEvent> event)
|
||||
.arg(totalSeconds % 60, 2, 10, QChar('0')));
|
||||
}
|
||||
|
||||
void HeaderBar::handleEvent(std::shared_ptr<const BuildingBlocksChangedEvent> event)
|
||||
void HeaderBar::handleEvent(std::shared_ptr<const BuildingBlocksChangedEvent> /*event*/)
|
||||
{
|
||||
m_blocks = event->blocks;
|
||||
updateBlocksLabel();
|
||||
updateExpandButton();
|
||||
}
|
||||
|
||||
void HeaderBar::handleEvent(std::shared_ptr<const ExpansionCostChangedEvent> event)
|
||||
void HeaderBar::handleEvent(std::shared_ptr<const ExpansionCostChangedEvent> /*event*/)
|
||||
{
|
||||
m_expansionCost = event->cost;
|
||||
updateExpandButton();
|
||||
}
|
||||
|
||||
@@ -138,32 +137,37 @@ QPixmap HeaderBar::blockIcon() const
|
||||
|
||||
void HeaderBar::updateBlocksLabel()
|
||||
{
|
||||
const int blocks = m_sim->getBuildingBlocksStock();
|
||||
|
||||
const QPixmap icon = blockIcon();
|
||||
if (icon.isNull())
|
||||
{
|
||||
// Fallback text form when no building_block icon exists (REQ-UI-BLOCKS-ICON).
|
||||
m_blocksLabel->setText(tr("Stock: %1 Blocks").arg(m_blocks));
|
||||
m_blocksLabel->setText(tr("Stock: %1 Blocks").arg(blocks));
|
||||
return;
|
||||
}
|
||||
m_blocksLabel->setPixmap(renderCaptionWithIcon(
|
||||
tr("Stock: %1").arg(m_blocks), icon, font(),
|
||||
tr("Stock: %1").arg(blocks), icon, font(),
|
||||
m_blocksLabel->palette().color(QPalette::WindowText)));
|
||||
}
|
||||
|
||||
void HeaderBar::updateExpandButton()
|
||||
{
|
||||
m_expandButton->setEnabled(m_blocks >= m_expansionCost);
|
||||
const int blocks = m_sim->getBuildingBlocksStock();
|
||||
const int expansionCost = m_sim->getCurrentExpansionCost();
|
||||
|
||||
m_expandButton->setEnabled(blocks >= expansionCost);
|
||||
|
||||
const QPixmap icon = blockIcon();
|
||||
if (icon.isNull())
|
||||
{
|
||||
// Fallback text form when no building_block icon exists (REQ-UI-EXPAND-BUTTON).
|
||||
m_expandButton->setIcon(QIcon());
|
||||
m_expandButton->setText(tr("Expand: %1 Blocks").arg(m_expansionCost));
|
||||
m_expandButton->setText(tr("Expand: %1 Blocks").arg(expansionCost));
|
||||
return;
|
||||
}
|
||||
|
||||
const QString text = tr("Expand: %1").arg(m_expansionCost);
|
||||
const QString text = tr("Expand: %1").arg(expansionCost);
|
||||
const QPalette& pal = m_expandButton->palette();
|
||||
const QPixmap normal = renderCaptionWithIcon(
|
||||
text, icon, m_expandButton->font(), pal.color(QPalette::ButtonText));
|
||||
|
||||
@@ -20,6 +20,7 @@
|
||||
class QLabel;
|
||||
class QPushButton;
|
||||
class ItemIconCache;
|
||||
class Simulation;
|
||||
|
||||
class HeaderBar : public QWidget,
|
||||
public CombinedEventHandler<TickAdvancedEvent,
|
||||
@@ -32,11 +33,11 @@ class HeaderBar : public QWidget,
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
// itemsIconDir holds the per-item icon SVGs (REQ-UI-ITEM-ICON); used to show the
|
||||
// building_block icon in the stock display and expand button (REQ-UI-BLOCKS-ICON,
|
||||
// REQ-UI-EXPAND-BUTTON).
|
||||
HeaderBar(const GameConfig* config, const std::string& itemsIconDir,
|
||||
QWidget* parent = nullptr);
|
||||
// itemIcons is the window-wide per-item icon cache (REQ-UI-ITEM-ICON); used to
|
||||
// show the building_block icon in the stock display and expand button
|
||||
// (REQ-UI-BLOCKS-ICON, REQ-UI-EXPAND-BUTTON). Not owned; must outlive this widget.
|
||||
HeaderBar(const Simulation* sim, const GameConfig* config,
|
||||
ItemIconCache* itemIcons, QWidget* parent = nullptr);
|
||||
~HeaderBar() override;
|
||||
|
||||
private slots:
|
||||
@@ -54,9 +55,10 @@ private:
|
||||
// expansion cost and building block stock (REQ-UI-EXPAND-BUTTON).
|
||||
void updateExpandButton();
|
||||
|
||||
// Refreshes the building blocks stock display from m_blocks: `Stock: <n>` with
|
||||
// the building_block icon after it, or the `Stock: <n> Blocks` text fallback when
|
||||
// no icon file exists (REQ-UI-BLOCKS-ICON).
|
||||
// Refreshes the building blocks stock display from the simulation:
|
||||
// `Stock: <n>` with the building_block icon after it, or the
|
||||
// `Stock: <n> Blocks` text fallback when no icon file exists
|
||||
// (REQ-UI-BLOCKS-ICON).
|
||||
void updateBlocksLabel();
|
||||
|
||||
// The building_block icon at the header's text height, or a null pixmap when no
|
||||
@@ -71,10 +73,11 @@ private:
|
||||
QPushButton* m_expandButton;
|
||||
std::vector<QPushButton*> m_speedButtons;
|
||||
|
||||
std::unique_ptr<ItemIconCache> m_itemIcons;
|
||||
ItemIconCache* m_itemIcons; // Not owned; lives in MainWindow.
|
||||
|
||||
int m_blocks = 0;
|
||||
int m_expansionCost = 0;
|
||||
// The simulation is the single source of truth for the block stock and the
|
||||
// expansion cost; the change events are only refresh signals.
|
||||
const Simulation* m_sim;
|
||||
|
||||
static const double kSpeeds[];
|
||||
static const int kSpeedCount;
|
||||
|
||||
@@ -27,6 +27,8 @@
|
||||
#include "SelectedBuildingPanel.h"
|
||||
#include "ShipLayoutBlueprintSerializer.h"
|
||||
#include "ShipLayoutDialog.h"
|
||||
#include "ItemIconCache.h"
|
||||
#include "ModalPauseScope.h"
|
||||
#include "Simulation.h"
|
||||
#include "Tick.h"
|
||||
#include "VisualsLoader.h"
|
||||
@@ -47,10 +49,13 @@ MainWindow::MainWindow(Simulation* sim, const std::string& configDir,
|
||||
const std::string itemsIconDir = QDir::cleanPath(
|
||||
QString::fromStdString(m_configDir) + "/../icons/items").toStdString();
|
||||
|
||||
m_headerBar = new HeaderBar(&sim->getConfig(), itemsIconDir, this);
|
||||
m_itemIcons = std::make_unique<ItemIconCache>(
|
||||
QString::fromStdString(itemsIconDir));
|
||||
|
||||
m_headerBar = new HeaderBar(sim, &sim->getConfig(), m_itemIcons.get(), this);
|
||||
|
||||
m_gameWorldView = new GameWorldView(sim, &sim->getConfig(), &m_visuals, m_configDir,
|
||||
m_replay.get(), this);
|
||||
m_itemIcons.get(), m_replay.get(), this);
|
||||
|
||||
m_sidePanel = new QWidget(this);
|
||||
QVBoxLayout* sideLayout = new QVBoxLayout(m_sidePanel);
|
||||
@@ -63,7 +68,7 @@ MainWindow::MainWindow(Simulation* sim, const std::string& configDir,
|
||||
QString::fromStdString(m_configDir) + "/../icons/buildings").toStdString();
|
||||
|
||||
m_selectedBuildingPanel = new SelectedBuildingPanel(sim, &sim->getConfig(), m_sidePanel);
|
||||
m_buildButtonGrid = new BuildButtonGrid(sim, &sim->getConfig(), iconDir, itemsIconDir, m_sidePanel);
|
||||
m_buildButtonGrid = new BuildButtonGrid(sim, &sim->getConfig(), iconDir, m_itemIcons.get(), m_sidePanel);
|
||||
m_blueprintPanel = new BlueprintPanel(sim, &sim->getConfig(), m_sidePanel);
|
||||
|
||||
sideLayout->addWidget(m_selectedBuildingPanel, 1);
|
||||
@@ -163,8 +168,7 @@ void MainWindow::layoutPanels()
|
||||
|
||||
void MainWindow::handleEvent(std::shared_ptr<const SchematicChoicesAvailableEvent> event)
|
||||
{
|
||||
const double prevSpeed = m_gameWorldView->getGameSpeed();
|
||||
m_gameWorldView->setGameSpeed(0.0);
|
||||
ModalPauseScope pause(*m_gameWorldView);
|
||||
|
||||
ModalDimScope dim(*m_dimOverlay);
|
||||
SchematicChoiceDialog dialog(event->choices, m_sim->getConfig().recipes, this);
|
||||
@@ -175,15 +179,11 @@ void MainWindow::handleEvent(std::shared_ptr<const SchematicChoicesAvailableEven
|
||||
command->choiceIndex = dialog.getChosenIndex();
|
||||
EventManager::getInstance()->sendEventImmediately(
|
||||
std::make_shared<CommandRequestedEvent>(command));
|
||||
|
||||
m_gameWorldView->setGameSpeed(prevSpeed);
|
||||
m_gameWorldView->resetFrameTimer();
|
||||
}
|
||||
|
||||
void MainWindow::handleEvent(std::shared_ptr<const EscapeMenuRequestedEvent> /*event*/)
|
||||
{
|
||||
const double prevSpeed = m_gameWorldView->getGameSpeed();
|
||||
m_gameWorldView->setGameSpeed(0.0);
|
||||
ModalPauseScope pause(*m_gameWorldView);
|
||||
|
||||
ModalDimScope dim(*m_dimOverlay);
|
||||
QMessageBox box(this);
|
||||
@@ -197,39 +197,47 @@ void MainWindow::handleEvent(std::shared_ptr<const EscapeMenuRequestedEvent> /*e
|
||||
QAbstractButton* clicked = box.clickedButton();
|
||||
if (clicked == restartBtn)
|
||||
{
|
||||
std::shared_ptr<GameConfig> newConfig;
|
||||
try
|
||||
std::optional<GameConfig> newConfig = reloadConfig();
|
||||
if (!newConfig.has_value())
|
||||
{
|
||||
newConfig = std::make_shared<GameConfig>(
|
||||
ConfigLoader::loadFromDirectory(m_configDir));
|
||||
VisualsConfig newVisuals = VisualsLoader::load(m_configDir + "/visuals.toml");
|
||||
m_visuals = std::move(newVisuals);
|
||||
m_dimOverlay->setDimColor(m_visuals.overlays.modalDim);
|
||||
}
|
||||
catch (const std::exception& e)
|
||||
{
|
||||
QMessageBox::critical(this, tr("Config Error"),
|
||||
tr("Failed to reload config:\n%1").arg(e.what()));
|
||||
m_gameWorldView->setGameSpeed(prevSpeed);
|
||||
m_gameWorldView->resetFrameTimer();
|
||||
return;
|
||||
}
|
||||
// Restart is a command boundary; the view resets when the drain applies
|
||||
// it (see GameWorldView::onFrame). A fresh random seed starts a new run.
|
||||
// resetForNewGame() sets the speed for the new run, so the pre-restart
|
||||
// speed is deliberately not restored here.
|
||||
pause.release();
|
||||
std::shared_ptr<ResetCommand> command = std::make_shared<ResetCommand>();
|
||||
command->config = std::move(newConfig);
|
||||
command->config = std::make_shared<GameConfig>(std::move(*newConfig));
|
||||
command->seed = std::random_device{}();
|
||||
EventManager::getInstance()->sendEventImmediately(
|
||||
std::make_shared<CommandRequestedEvent>(command));
|
||||
}
|
||||
else if (clicked == quitBtn)
|
||||
{
|
||||
pause.release();
|
||||
close();
|
||||
}
|
||||
else
|
||||
}
|
||||
|
||||
std::optional<GameConfig> MainWindow::reloadConfig()
|
||||
{
|
||||
// Config is reloaded from disk on every restart (REQ-CFG-RELOAD); a malformed
|
||||
// file must not leave the window half-updated, so the visuals are only applied
|
||||
// once both files have parsed.
|
||||
try
|
||||
{
|
||||
m_gameWorldView->setGameSpeed(prevSpeed);
|
||||
m_gameWorldView->resetFrameTimer();
|
||||
GameConfig newConfig = ConfigLoader::loadFromDirectory(m_configDir);
|
||||
VisualsConfig newVisuals = VisualsLoader::load(m_configDir + "/visuals.toml");
|
||||
m_visuals = std::move(newVisuals);
|
||||
m_dimOverlay->setDimColor(m_visuals.overlays.modalDim);
|
||||
return newConfig;
|
||||
}
|
||||
catch (const std::exception& e)
|
||||
{
|
||||
QMessageBox::critical(this, tr("Config Error"),
|
||||
tr("Failed to reload config:\n%1").arg(e.what()));
|
||||
return std::nullopt;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -237,8 +245,7 @@ void MainWindow::openShipLayoutDialog(BuildingId shipyardId,
|
||||
const std::string& schematicId,
|
||||
const ShipLayoutConfig& currentLayout)
|
||||
{
|
||||
const double prevSpeed = m_gameWorldView->getGameSpeed();
|
||||
m_gameWorldView->setGameSpeed(0.0);
|
||||
ModalPauseScope pause(*m_gameWorldView);
|
||||
|
||||
std::set<std::string> unlockedModuleIds;
|
||||
for (const ModuleDef& def : m_sim->getConfig().modules.modules)
|
||||
@@ -264,9 +271,6 @@ void MainWindow::openShipLayoutDialog(BuildingId shipyardId,
|
||||
EventManager::getInstance()->sendEventImmediately(
|
||||
std::make_shared<CommandRequestedEvent>(command));
|
||||
}
|
||||
|
||||
m_gameWorldView->setGameSpeed(prevSpeed);
|
||||
m_gameWorldView->resetFrameTimer();
|
||||
}
|
||||
|
||||
void MainWindow::handleEvent(std::shared_ptr<const LayoutDialogRequestedEvent> event)
|
||||
@@ -296,8 +300,7 @@ void MainWindow::handleEvent(std::shared_ptr<const LayoutDialogRequestedEvent> e
|
||||
|
||||
void MainWindow::handleEvent(std::shared_ptr<const RecipeSelectionRequestedEvent> event)
|
||||
{
|
||||
const double prevSpeed = m_gameWorldView->getGameSpeed();
|
||||
m_gameWorldView->setGameSpeed(0.0);
|
||||
ModalPauseScope pause(*m_gameWorldView);
|
||||
|
||||
// A construction site has no Building yet; fall back to its site record so
|
||||
// the recipe/schematic can be chosen before it is built (REQ-BLD-SITE-CONFIG).
|
||||
@@ -306,8 +309,6 @@ void MainWindow::handleEvent(std::shared_ptr<const RecipeSelectionRequestedEvent
|
||||
b ? nullptr : m_sim->getBuildings().findSite(event->buildingId);
|
||||
if (!b && !s)
|
||||
{
|
||||
m_gameWorldView->setGameSpeed(prevSpeed);
|
||||
m_gameWorldView->resetFrameTimer();
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -328,11 +329,7 @@ void MainWindow::handleEvent(std::shared_ptr<const RecipeSelectionRequestedEvent
|
||||
|
||||
bool autoOpenLayout = false;
|
||||
std::string chosenSchematic;
|
||||
// Item icons live beside the config dir, mirroring the buildings icon path
|
||||
// (REQ-UI-ITEM-ICON, REQ-UI-BUILD-ICON).
|
||||
const QString itemIconDir = QDir::cleanPath(
|
||||
QString::fromStdString(m_configDir) + "/../icons/items");
|
||||
RecipeSelectionDialog dialog(options, title, itemIconDir, this);
|
||||
RecipeSelectionDialog dialog(options, title, m_itemIcons.get(), this);
|
||||
if (dialog.exec() == QDialog::Accepted && dialog.getChosenId().has_value())
|
||||
{
|
||||
std::shared_ptr<SetRecipeCommand> command = std::make_shared<SetRecipeCommand>();
|
||||
@@ -350,13 +347,11 @@ void MainWindow::handleEvent(std::shared_ptr<const RecipeSelectionRequestedEvent
|
||||
}
|
||||
}
|
||||
|
||||
m_gameWorldView->setGameSpeed(prevSpeed);
|
||||
m_gameWorldView->resetFrameTimer();
|
||||
|
||||
// The SetRecipeCommand above is queued (drains on a later frame) and clears
|
||||
// the shipyard's layout, so open the dialog with the chosen schematic and an
|
||||
// empty layout rather than reading the not-yet-updated building state. Speed
|
||||
// is already restored so the helper snapshots the real speed to restore.
|
||||
// is restored first so the helper snapshots the real speed to restore.
|
||||
pause.restore();
|
||||
if (autoOpenLayout)
|
||||
{
|
||||
openShipLayoutDialog(event->buildingId, chosenSchematic, ShipLayoutConfig{});
|
||||
@@ -382,24 +377,14 @@ void MainWindow::handleEvent(std::shared_ptr<const GameOverEvent> /*event*/)
|
||||
|
||||
if (box.clickedButton() == restartBtn)
|
||||
{
|
||||
std::shared_ptr<GameConfig> newConfig;
|
||||
try
|
||||
std::optional<GameConfig> newConfig = reloadConfig();
|
||||
if (!newConfig.has_value())
|
||||
{
|
||||
newConfig = std::make_shared<GameConfig>(
|
||||
ConfigLoader::loadFromDirectory(m_configDir));
|
||||
VisualsConfig newVisuals = VisualsLoader::load(m_configDir + "/visuals.toml");
|
||||
m_visuals = std::move(newVisuals);
|
||||
m_dimOverlay->setDimColor(m_visuals.overlays.modalDim);
|
||||
}
|
||||
catch (const std::exception& e)
|
||||
{
|
||||
QMessageBox::critical(this, tr("Config Error"),
|
||||
tr("Failed to reload config:\n%1").arg(e.what()));
|
||||
return;
|
||||
}
|
||||
// Restart is a command boundary; the view resets when the drain applies it.
|
||||
std::shared_ptr<ResetCommand> command = std::make_shared<ResetCommand>();
|
||||
command->config = std::move(newConfig);
|
||||
command->config = std::make_shared<GameConfig>(std::move(*newConfig));
|
||||
command->seed = std::random_device{}();
|
||||
EventManager::getInstance()->sendEventImmediately(
|
||||
std::make_shared<CommandRequestedEvent>(command));
|
||||
@@ -429,21 +414,17 @@ void MainWindow::handleEvent(std::shared_ptr<const WinEvent> /*event*/)
|
||||
|
||||
if (box.clickedButton() == restartBtn)
|
||||
{
|
||||
try
|
||||
std::optional<GameConfig> newConfig = reloadConfig();
|
||||
if (!newConfig.has_value())
|
||||
{
|
||||
GameConfig newConfig = ConfigLoader::loadFromDirectory(m_configDir);
|
||||
VisualsConfig newVisuals = VisualsLoader::load(m_configDir + "/visuals.toml");
|
||||
m_visuals = std::move(newVisuals);
|
||||
m_dimOverlay->setDimColor(m_visuals.overlays.modalDim);
|
||||
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();
|
||||
// Restart is a command boundary; the view resets when the drain applies it.
|
||||
std::shared_ptr<ResetCommand> command = std::make_shared<ResetCommand>();
|
||||
command->config = std::make_shared<GameConfig>(std::move(*newConfig));
|
||||
command->seed = std::random_device{}();
|
||||
EventManager::getInstance()->sendEventImmediately(
|
||||
std::make_shared<CommandRequestedEvent>(command));
|
||||
}
|
||||
else
|
||||
{
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
#pragma once
|
||||
|
||||
#include <memory>
|
||||
#include <optional>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
@@ -9,6 +10,7 @@
|
||||
#include "BuildingId.h"
|
||||
#include "EscapeMenuRequestedEvent.h"
|
||||
#include "EventHandler.h"
|
||||
#include "GameConfig.h"
|
||||
#include "GameOverEvent.h"
|
||||
#include "LayoutDialogRequestedEvent.h"
|
||||
#include "ModalDimOverlay.h"
|
||||
@@ -27,6 +29,7 @@ class HeaderBar;
|
||||
class SelectedBuildingPanel;
|
||||
class BuildButtonGrid;
|
||||
class BlueprintPanel;
|
||||
class ItemIconCache;
|
||||
class QCloseEvent;
|
||||
class QResizeEvent;
|
||||
|
||||
@@ -57,6 +60,12 @@ private:
|
||||
void handleEvent(std::shared_ptr<const LayoutDialogRequestedEvent> event) override;
|
||||
void handleEvent(std::shared_ptr<const RecipeSelectionRequestedEvent> event) override;
|
||||
|
||||
// Reloads the game config and visuals.toml from disk (REQ-CFG-RELOAD), shared
|
||||
// by every restart path. On success the reloaded visuals are applied to this
|
||||
// window and the fresh GameConfig is returned; on failure a modal error dialog
|
||||
// is shown and std::nullopt is returned, leaving the window state untouched.
|
||||
std::optional<GameConfig> reloadConfig();
|
||||
|
||||
// Opens the shipyard layout configuration dialog for the given schematic and
|
||||
// current layout, applying the result via SetShipLayoutCommand (REQ-MOD-UI-DIALOG).
|
||||
void openShipLayoutDialog(BuildingId shipyardId,
|
||||
@@ -68,6 +77,9 @@ private:
|
||||
std::string m_configDir;
|
||||
VisualsConfig m_visuals;
|
||||
Simulation* m_sim;
|
||||
// One per-item icon cache for the whole window (REQ-UI-ITEM-ICON): the header,
|
||||
// build grid, world view, and recipe dialog all rasterize the same SVGs.
|
||||
std::unique_ptr<ItemIconCache> m_itemIcons;
|
||||
GameWorldView* m_gameWorldView;
|
||||
HeaderBar* m_headerBar;
|
||||
SelectedBuildingPanel* m_selectedBuildingPanel;
|
||||
|
||||
55
src/ui/ModalPauseScope.h
Normal file
55
src/ui/ModalPauseScope.h
Normal file
@@ -0,0 +1,55 @@
|
||||
#pragma once
|
||||
|
||||
#include "GameWorldView.h"
|
||||
|
||||
// RAII guard for the "pause the game while a modal is open" idiom (REQ-UI-SPEED).
|
||||
// Constructing it snapshots the current game speed and pauses the game; on scope
|
||||
// exit it restores the snapshotted speed and rebases the render frame timer, so the
|
||||
// wall time the player spent in the dialog is not converted into simulation ticks.
|
||||
//
|
||||
// Pairs with ModalDimScope, which the same call sites use for the dim overlay.
|
||||
//
|
||||
// Two escape hatches for the paths that must not simply restore at scope exit:
|
||||
// restore() — restore now instead of at scope exit, for when more work has to run
|
||||
// at the player's real speed before the scope ends (e.g. a follow-up
|
||||
// dialog that snapshots the speed itself).
|
||||
// release() — abandon the restore entirely, for when the game is about to be
|
||||
// reset or the window closed and the old speed is meaningless.
|
||||
// Both are idempotent and the destructor does nothing once either has run.
|
||||
class ModalPauseScope
|
||||
{
|
||||
public:
|
||||
explicit ModalPauseScope(GameWorldView& view)
|
||||
: m_view(view)
|
||||
, m_previousGameSpeed(view.getGameSpeed())
|
||||
, m_restorePending(true)
|
||||
{
|
||||
m_view.setGameSpeed(0.0);
|
||||
}
|
||||
|
||||
~ModalPauseScope()
|
||||
{
|
||||
restore();
|
||||
}
|
||||
|
||||
void restore()
|
||||
{
|
||||
if (!m_restorePending) { return; }
|
||||
m_restorePending = false;
|
||||
m_view.setGameSpeed(m_previousGameSpeed);
|
||||
m_view.resetFrameTimer();
|
||||
}
|
||||
|
||||
void release()
|
||||
{
|
||||
m_restorePending = false;
|
||||
}
|
||||
|
||||
ModalPauseScope(const ModalPauseScope&) = delete;
|
||||
ModalPauseScope& operator=(const ModalPauseScope&) = delete;
|
||||
|
||||
private:
|
||||
GameWorldView& m_view;
|
||||
double m_previousGameSpeed;
|
||||
bool m_restorePending;
|
||||
};
|
||||
@@ -111,14 +111,12 @@ namespace
|
||||
|
||||
RecipeSelectionDialog::RecipeSelectionDialog(
|
||||
const std::vector<RecipeSelectionOption>& options,
|
||||
const QString& title, const QString& itemIconDir, QWidget* parent)
|
||||
const QString& title, ItemIconCache* itemIcons, QWidget* parent)
|
||||
: QDialog(parent)
|
||||
{
|
||||
setWindowTitle(title);
|
||||
setModal(true);
|
||||
|
||||
ItemIconCache iconCache(itemIconDir);
|
||||
|
||||
QVBoxLayout* mainLayout = new QVBoxLayout(this);
|
||||
QGridLayout* grid = new QGridLayout();
|
||||
mainLayout->addLayout(grid);
|
||||
@@ -131,9 +129,9 @@ RecipeSelectionDialog::RecipeSelectionDialog(
|
||||
QPushButton* button = new QPushButton(this);
|
||||
// Icon-only when the produced item has an icon (REQ-UI-RECIPE-ICON); otherwise
|
||||
// fall back to the caption. The name stays reachable via the tooltip.
|
||||
if (!option.iconItemId.empty() && iconCache.hasIcon(option.iconItemId))
|
||||
if (!option.iconItemId.empty() && itemIcons->hasIcon(option.iconItemId))
|
||||
{
|
||||
button->setIcon(QIcon(iconCache.getPixmap(
|
||||
button->setIcon(QIcon(itemIcons->getPixmap(
|
||||
option.iconItemId, kOptionIconSize.width())));
|
||||
button->setIconSize(kOptionIconSize);
|
||||
}
|
||||
|
||||
@@ -12,6 +12,7 @@
|
||||
struct GameConfig;
|
||||
class Simulation;
|
||||
class QPushButton;
|
||||
class ItemIconCache;
|
||||
|
||||
// One selectable entry in the recipe/schematic selection dialog
|
||||
// (REQ-UI-SELECT-BUTTON). The "(None)" entry uses an empty id.
|
||||
@@ -43,11 +44,11 @@ class RecipeSelectionDialog : public QDialog
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
// itemIconDir is the directory holding per-item icon SVGs (REQ-UI-ITEM-ICON);
|
||||
// used to render recipe options icon-only (REQ-UI-RECIPE-ICON). Options whose
|
||||
// item has no icon file fall back to their caption text.
|
||||
// itemIcons is the window-wide per-item icon cache (REQ-UI-ITEM-ICON); used to
|
||||
// render recipe options icon-only (REQ-UI-RECIPE-ICON). Options whose item has
|
||||
// no icon file fall back to their caption text. Not owned.
|
||||
RecipeSelectionDialog(const std::vector<RecipeSelectionOption>& options,
|
||||
const QString& title, const QString& itemIconDir,
|
||||
const QString& title, ItemIconCache* itemIcons,
|
||||
QWidget* parent = nullptr);
|
||||
|
||||
std::optional<std::string> getChosenId() const;
|
||||
|
||||
@@ -12,15 +12,6 @@
|
||||
namespace
|
||||
{
|
||||
|
||||
const RecipeDef* findRecipe(const RecipesConfig& recipes, const std::string& id)
|
||||
{
|
||||
for (const RecipeDef& recipe : recipes.recipes)
|
||||
{
|
||||
if (recipe.id == id) { return &recipe; }
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
QString grantKindLabel(SchematicType type)
|
||||
{
|
||||
switch (type)
|
||||
@@ -120,7 +111,7 @@ SchematicChoiceDialog::SchematicChoiceDialog(
|
||||
QLabel* recipeLabel = new QLabel(
|
||||
QString::fromStdString(toDisplayName(recipeId)), card);
|
||||
recipeLabel->setAlignment(Qt::AlignCenter);
|
||||
if (const RecipeDef* def = findRecipe(recipes, recipeId))
|
||||
if (const RecipeDef* def = recipes.findRecipeDef(recipeId))
|
||||
{
|
||||
recipeLabel->setToolTip(buildRecipeTooltip(*def));
|
||||
}
|
||||
|
||||
@@ -430,14 +430,7 @@ void SelectedBuildingPanel::refreshBuffers(const Building* b)
|
||||
// the cycle time and progress can be shown (REQ-UI-PRODUCTION-PROGRESS).
|
||||
if (!recipe && isAutoRecipeBuilding(b->type) && b->production.has_value())
|
||||
{
|
||||
for (const RecipeDef& r : m_config->recipes.recipes)
|
||||
{
|
||||
if (r.id == b->production->recipeId && r.building == b->type)
|
||||
{
|
||||
recipe = &r;
|
||||
break;
|
||||
}
|
||||
}
|
||||
recipe = m_config->recipes.findRecipeDef(b->production->recipeId, b->type);
|
||||
}
|
||||
|
||||
QString bufText;
|
||||
@@ -465,18 +458,14 @@ void SelectedBuildingPanel::refreshBuffers(const Building* b)
|
||||
{
|
||||
for (const PlacedModule& pm : b->shipLayout->placedModules)
|
||||
{
|
||||
for (const ModuleDef& modDef : m_config->modules.modules)
|
||||
const ModuleDef* modDef =
|
||||
m_config->modules.findModuleDef(pm.moduleId);
|
||||
if (!modDef) { continue; }
|
||||
for (const RecipeIngredient& ing : modDef->materials)
|
||||
{
|
||||
if (modDef.id == pm.moduleId)
|
||||
if (ing.item == entry.first.id)
|
||||
{
|
||||
for (const RecipeIngredient& ing : modDef.materials)
|
||||
{
|
||||
if (ing.item == entry.first.id)
|
||||
{
|
||||
perCycle += ing.amount;
|
||||
}
|
||||
}
|
||||
break;
|
||||
perCycle += ing.amount;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -545,13 +534,11 @@ void SelectedBuildingPanel::refreshBuffers(const Building* b)
|
||||
{
|
||||
for (const PlacedModule& pm : b->shipLayout->placedModules)
|
||||
{
|
||||
for (const ModuleDef& modDef : m_config->modules.modules)
|
||||
const ModuleDef* modDef =
|
||||
m_config->modules.findModuleDef(pm.moduleId);
|
||||
if (modDef)
|
||||
{
|
||||
if (modDef.id == pm.moduleId)
|
||||
{
|
||||
durationSeconds += modDef.productionTimeSeconds;
|
||||
break;
|
||||
}
|
||||
durationSeconds += modDef->productionTimeSeconds;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -633,21 +620,13 @@ void SelectedBuildingPanel::updateShipyardLayoutWidgets(
|
||||
const RecipeDef* SelectedBuildingPanel::findRecipe(const Building* b) const
|
||||
{
|
||||
if (b->recipeId.empty()) { return nullptr; }
|
||||
for (const RecipeDef& r : m_config->recipes.recipes)
|
||||
{
|
||||
if (r.id == b->recipeId && r.building == b->type) { return &r; }
|
||||
}
|
||||
return nullptr;
|
||||
return m_config->recipes.findRecipeDef(b->recipeId, b->type);
|
||||
}
|
||||
|
||||
const ShipDef* SelectedBuildingPanel::findShipDef(const std::string& id) const
|
||||
{
|
||||
if (id.empty()) { return nullptr; }
|
||||
for (const ShipDef& s : m_config->ships.ships)
|
||||
{
|
||||
if (s.id == id) { return &s; }
|
||||
}
|
||||
return nullptr;
|
||||
return m_config->ships.findShipDef(id);
|
||||
}
|
||||
|
||||
void SelectedBuildingPanel::handleEvent(std::shared_ptr<const TickAdvancedEvent> /*event*/)
|
||||
@@ -1088,15 +1067,14 @@ void SelectedBuildingPanel::buildEntityShip(entt::entity entity)
|
||||
admin.get<SelectedBehaviorComponent>(entity).winner);
|
||||
m_entityStatsPanel->setDebugDrawEnabled(m_debugDraw);
|
||||
|
||||
for (const ShipDef& def : m_config->ships.ships)
|
||||
const ShipDef* schematicDef =
|
||||
m_config->ships.findShipDef(identity.schematicId);
|
||||
if (schematicDef)
|
||||
{
|
||||
if (def.id == identity.schematicId)
|
||||
{
|
||||
double threat = calculateShipThreatCost(
|
||||
m_config->threatCosts, *m_config, def.id, def.defaultModules);
|
||||
m_entityStatsPanel->setThreatCost(threat);
|
||||
break;
|
||||
}
|
||||
const double threat = calculateShipThreatCost(
|
||||
m_config->threatCosts, *m_config, schematicDef->id,
|
||||
schematicDef->defaultModules);
|
||||
m_entityStatsPanel->setThreatCost(threat);
|
||||
}
|
||||
|
||||
m_entityStatsPanel->show();
|
||||
|
||||
@@ -243,14 +243,7 @@ private:
|
||||
{
|
||||
return nullptr;
|
||||
}
|
||||
for (const ModuleDef& def : m_config->modules.modules)
|
||||
{
|
||||
if (def.id == id)
|
||||
{
|
||||
return &def;
|
||||
}
|
||||
}
|
||||
return nullptr;
|
||||
return m_config->modules.findModuleDef(id);
|
||||
}
|
||||
|
||||
std::vector<std::string> rotateMask(const std::vector<std::string>& mask,
|
||||
@@ -426,13 +419,10 @@ ShipLayoutDialog::ShipLayoutDialog(const GameConfig* config,
|
||||
setModal(true);
|
||||
|
||||
// Find the ship's layout grid.
|
||||
for (const ShipDef& def : config->ships.ships)
|
||||
const ShipDef* shipDef = config->ships.findShipDef(shipId);
|
||||
if (shipDef)
|
||||
{
|
||||
if (def.id == shipId)
|
||||
{
|
||||
m_shipLayout = def.layout;
|
||||
break;
|
||||
}
|
||||
m_shipLayout = shipDef->layout;
|
||||
}
|
||||
|
||||
m_rows = static_cast<int>(m_shipLayout.size());
|
||||
@@ -706,11 +696,7 @@ void ShipLayoutDialog::rebuildOccupancy()
|
||||
for (int i = 0; i < static_cast<int>(m_placedModules.size()); ++i)
|
||||
{
|
||||
const PlacedModule& pm = m_placedModules[i];
|
||||
const ModuleDef* def = nullptr;
|
||||
for (const ModuleDef& d : m_config->modules.modules)
|
||||
{
|
||||
if (d.id == pm.moduleId) { def = &d; break; }
|
||||
}
|
||||
const ModuleDef* def = m_config->modules.findModuleDef(pm.moduleId);
|
||||
if (!def)
|
||||
{
|
||||
continue;
|
||||
@@ -807,11 +793,7 @@ void ShipLayoutDialog::loadLayoutBlueprint(const std::vector<PlacedModule>& modu
|
||||
for (const PlacedModule& pm : modules)
|
||||
{
|
||||
// Validate module type exists and is unlocked.
|
||||
const ModuleDef* def = nullptr;
|
||||
for (const ModuleDef& d : m_config->modules.modules)
|
||||
{
|
||||
if (d.id == pm.moduleId) { def = &d; break; }
|
||||
}
|
||||
const ModuleDef* def = m_config->modules.findModuleDef(pm.moduleId);
|
||||
if (!def || m_unlockedModuleIds.count(def->id) == 0) { continue; }
|
||||
|
||||
const std::vector<std::string> mask = rotatedMask(*def, pm.rotation);
|
||||
|
||||
Reference in New Issue
Block a user