Remove schematic upgrades and module and ship levels
This commit is contained in:
@@ -231,7 +231,7 @@ void ArenaSimulation::spawnShips()
|
||||
for (int i = 0; i < entry.count; ++i)
|
||||
{
|
||||
const QVector2D pos(xDist(m_rng), yDist(m_rng));
|
||||
m_shipSystem->spawn(entry.schematicId, entry.level, pos, false,
|
||||
m_shipSystem->spawn(entry.schematicId, pos, false,
|
||||
entry.layout);
|
||||
}
|
||||
}
|
||||
@@ -248,7 +248,7 @@ void ArenaSimulation::spawnShips()
|
||||
for (int i = 0; i < entry.count; ++i)
|
||||
{
|
||||
const QVector2D pos(xDist(m_rng), yDist(m_rng));
|
||||
m_shipSystem->spawn(entry.schematicId, entry.level, pos, true,
|
||||
m_shipSystem->spawn(entry.schematicId, pos, true,
|
||||
entry.layout);
|
||||
}
|
||||
}
|
||||
@@ -500,7 +500,7 @@ void ArenaSimulation::updateStatus()
|
||||
{
|
||||
ArenaStatus::Entry entry;
|
||||
entry.displayName = shipEntry.schematicId;
|
||||
entry.level = shipEntry.level;
|
||||
// Ships no longer carry a level (level suffix stays empty).
|
||||
entry.total = shipEntry.count;
|
||||
|
||||
int surviving = 0;
|
||||
@@ -512,7 +512,6 @@ void ArenaSimulation::updateStatus()
|
||||
{
|
||||
if (f.isEnemy == isEnemyTeam
|
||||
&& si.schematicId == shipEntry.schematicId
|
||||
&& si.level == shipEntry.level
|
||||
&& h.hp > 0.0f)
|
||||
{
|
||||
++surviving;
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
#include <atomic>
|
||||
#include <memory>
|
||||
#include <mutex>
|
||||
#include <optional>
|
||||
#include <random>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
@@ -32,7 +33,9 @@ struct ArenaStatus
|
||||
struct Entry
|
||||
{
|
||||
std::string displayName;
|
||||
int level;
|
||||
// Level suffix shown in the widget/inspect display. Set for the HQ and
|
||||
// defence stations; empty for ships, which no longer have a level.
|
||||
std::optional<int> level;
|
||||
int total;
|
||||
int surviving;
|
||||
};
|
||||
|
||||
@@ -126,11 +126,14 @@ void ArenaWidget::updateStatus(const ArenaStatus& status)
|
||||
{
|
||||
lines += "\n";
|
||||
}
|
||||
lines += QString("%1/%2 %3 L%4")
|
||||
lines += QString("%1/%2 %3")
|
||||
.arg(entry.surviving)
|
||||
.arg(entry.total)
|
||||
.arg(QString::fromStdString(entry.displayName))
|
||||
.arg(entry.level);
|
||||
.arg(QString::fromStdString(entry.displayName));
|
||||
if (entry.level.has_value())
|
||||
{
|
||||
lines += QString(" L%1").arg(entry.level.value());
|
||||
}
|
||||
}
|
||||
content->setText(lines);
|
||||
}
|
||||
|
||||
@@ -125,8 +125,6 @@ BalancingConfig loadBalancingConfig(const std::string& path)
|
||||
ArenaShipEntry entry;
|
||||
entry.schematicId = requireString((*shipTbl)["schematic"],
|
||||
sPrefix + ".schematic");
|
||||
entry.level = static_cast<int>(
|
||||
requireInt((*shipTbl)["level"], sPrefix + ".level"));
|
||||
entry.count = static_cast<int>(
|
||||
requireInt((*shipTbl)["count"], sPrefix + ".count"));
|
||||
|
||||
|
||||
@@ -18,7 +18,6 @@ struct ArenaStationEntry
|
||||
struct ArenaShipEntry
|
||||
{
|
||||
std::string schematicId;
|
||||
int level;
|
||||
int count;
|
||||
std::optional<ShipLayoutConfig> layout;
|
||||
};
|
||||
|
||||
@@ -223,11 +223,14 @@ void InspectWindow::updateInfoPanel(const ArenaStatus& status)
|
||||
{
|
||||
lines += "\n";
|
||||
}
|
||||
lines += QString("%1/%2 %3 L%4")
|
||||
lines += QString("%1/%2 %3")
|
||||
.arg(entry.surviving)
|
||||
.arg(entry.total)
|
||||
.arg(QString::fromStdString(entry.displayName))
|
||||
.arg(entry.level);
|
||||
.arg(QString::fromStdString(entry.displayName));
|
||||
if (entry.level.has_value())
|
||||
{
|
||||
lines += QString(" L%1").arg(entry.level.value());
|
||||
}
|
||||
}
|
||||
content->setText(lines);
|
||||
}
|
||||
@@ -256,9 +259,8 @@ void InspectWindow::handleEvent(std::shared_ptr<const EntitySelectedEvent> event
|
||||
const ShipIdentityComponent& identity = admin.get<ShipIdentityComponent>(entity);
|
||||
const HealthComponent& health = admin.get<HealthComponent>(entity);
|
||||
|
||||
m_entityTitleLabel->setText(tr("Ship: %1 (Lv %2)")
|
||||
.arg(QString::fromStdString(identity.schematicId))
|
||||
.arg(identity.level));
|
||||
m_entityTitleLabel->setText(tr("Ship: %1")
|
||||
.arg(QString::fromStdString(identity.schematicId)));
|
||||
m_entityTitleLabel->show();
|
||||
|
||||
const ShipStats stats = buildShipStatsFromEntity(admin, entity);
|
||||
|
||||
@@ -284,7 +284,6 @@ WorldConfig ConfigLoader::loadWorld(const std::string& path)
|
||||
cfg.push.bossAdvanceSeconds = requireDouble(tbl["push"]["boss_advance_seconds"], file, "push.boss_advance_seconds");
|
||||
|
||||
cfg.waves.threatRateFormula = requireFormula(tbl["waves"]["threat_rate_formula"], file, "waves.threat_rate_formula");
|
||||
cfg.waves.shipLevelFormula = requireFormula(tbl["waves"]["ship_level_formula"], file, "waves.ship_level_formula");
|
||||
cfg.waves.gapMinSeconds = requireDouble(tbl["waves"]["gap_min_seconds"], file, "waves.gap_min_seconds");
|
||||
cfg.waves.gapMaxSeconds = requireDouble(tbl["waves"]["gap_max_seconds"], file, "waves.gap_max_seconds");
|
||||
cfg.waves.spawnDurationSeconds = requireDouble(tbl["waves"]["spawn_duration_seconds"], file, "waves.spawn_duration_seconds");
|
||||
@@ -433,8 +432,6 @@ ShipsConfig ConfigLoader::loadShips(const std::string& path)
|
||||
|
||||
const toml::array& materials = requireArray(bpMt["materials"], file, bpPath + ".materials");
|
||||
def.schematic.materials = parseIngredients(materials, file, bpPath + ".materials");
|
||||
def.schematic.playerProductionLevel = static_cast<int>(requireInt(
|
||||
bpMt["player_production_level"], file, bpPath + ".player_production_level"));
|
||||
def.schematic.productionTimeSeconds = requireDouble(
|
||||
bpMt["production_time_seconds"], file, bpPath + ".production_time_seconds");
|
||||
}
|
||||
@@ -444,7 +441,7 @@ ShipsConfig ConfigLoader::loadShips(const std::string& path)
|
||||
const std::string hPath = elemPath + ".health";
|
||||
const toml::table& hTable = requireTable(mt["health"], file, hPath);
|
||||
toml::table& hMt = const_cast<toml::table&>(hTable);
|
||||
def.health.hpFormula = requireFormula(hMt["hp_formula"], file, hPath + ".hp_formula");
|
||||
def.health.hp = static_cast<float>(requireDouble(hMt["hp"], file, hPath + ".hp"));
|
||||
}
|
||||
|
||||
// Movement
|
||||
@@ -452,11 +449,11 @@ ShipsConfig ConfigLoader::loadShips(const std::string& path)
|
||||
const std::string mPath = elemPath + ".movement";
|
||||
const toml::table& mTable = requireTable(mt["movement"], file, mPath);
|
||||
toml::table& mMt = const_cast<toml::table&>(mTable);
|
||||
def.movement.speedFormula = requireFormula(mMt["speed_mps_formula"], file, mPath + ".speed_mps_formula");
|
||||
def.movement.mainAccelerationFormula = requireFormula(mMt["main_acceleration_mpss_formula"], file, mPath + ".main_acceleration_mpss_formula");
|
||||
def.movement.maneuveringAccelerationFormula = requireFormula(mMt["maneuvering_acceleration_mpss_formula"], file, mPath + ".maneuvering_acceleration_mpss_formula");
|
||||
def.movement.angularAccelerationFormula = requireFormula(mMt["angular_acceleration_radpss_formula"], file, mPath + ".angular_acceleration_radpss_formula");
|
||||
def.movement.maxRotationSpeedFormula = requireFormula(mMt["max_rotation_speed_radps_formula"], file, mPath + ".max_rotation_speed_radps_formula");
|
||||
def.movement.speed_mps = static_cast<float>(requireDouble(mMt["speed_mps"], file, mPath + ".speed_mps"));
|
||||
def.movement.mainAcceleration_mpss = static_cast<float>(requireDouble(mMt["main_acceleration_mpss"], file, mPath + ".main_acceleration_mpss"));
|
||||
def.movement.maneuveringAcceleration_mpss = static_cast<float>(requireDouble(mMt["maneuvering_acceleration_mpss"], file, mPath + ".maneuvering_acceleration_mpss"));
|
||||
def.movement.angularAcceleration_radpss = static_cast<float>(requireDouble(mMt["angular_acceleration_radpss"], file, mPath + ".angular_acceleration_radpss"));
|
||||
def.movement.maxRotationSpeed_radps = static_cast<float>(requireDouble(mMt["max_rotation_speed_radps"], file, mPath + ".max_rotation_speed_radps"));
|
||||
}
|
||||
|
||||
// Sensor
|
||||
@@ -464,7 +461,7 @@ ShipsConfig ConfigLoader::loadShips(const std::string& path)
|
||||
const std::string snsPath = elemPath + ".sensor";
|
||||
const toml::table& snsTable = requireTable(mt["sensor"], file, snsPath);
|
||||
toml::table& snsMt = const_cast<toml::table&>(snsTable);
|
||||
def.sensor.sensorRangeFormula = requireFormula(snsMt["sensor_range_m_formula"], file, snsPath + ".sensor_range_m_formula");
|
||||
def.sensor.sensorRange_m = static_cast<float>(requireDouble(snsMt["sensor_range_m"], file, snsPath + ".sensor_range_m"));
|
||||
}
|
||||
|
||||
// Optional: default_modules (REQ-WAV-DEFAULT-MODULES)
|
||||
@@ -577,8 +574,6 @@ ModulesConfig ConfigLoader::loadModules(const std::string& path)
|
||||
def.unlockAtStationLevel = static_cast<int>(
|
||||
mt["unlock_at_station_level"].value_or<int64_t>(-1));
|
||||
def.surfaceMask = requireStringArray(mt["surface_mask"], file, elemPath + ".surface_mask");
|
||||
def.playerProductionLevel = static_cast<int>(requireInt(
|
||||
mt["player_production_level"], file, elemPath + ".player_production_level"));
|
||||
def.productionTimeSeconds = requireDouble(
|
||||
mt["production_time_seconds"], file, elemPath + ".production_time_seconds");
|
||||
def.fillColor = requireString(mt["fill_color"], file, elemPath + ".fill_color");
|
||||
@@ -601,15 +596,15 @@ ModulesConfig ConfigLoader::loadModules(const std::string& path)
|
||||
elemPath + "." + se.category);
|
||||
toml::table& catMt = const_cast<toml::table&>(catTable);
|
||||
|
||||
const std::string addedKey = std::string("added_") + se.stat + se.addedKeySuffix + "_formula";
|
||||
const std::string multipliedKey = std::string("multiplied_") + se.stat + se.addedKeySuffix + "_formula";
|
||||
const std::string addedKey = std::string("added_") + se.stat + se.addedKeySuffix;
|
||||
const std::string multipliedKey = std::string("multiplied_") + se.stat + se.addedKeySuffix;
|
||||
|
||||
if (catMt.contains(addedKey))
|
||||
{
|
||||
ModuleStatModifier mod;
|
||||
mod.stat = se.stat;
|
||||
mod.modifierType = "additive";
|
||||
mod.formula = requireFormula(catMt[addedKey], file,
|
||||
mod.value = requireDouble(catMt[addedKey], file,
|
||||
elemPath + "." + se.category + "." + addedKey);
|
||||
def.statModifiers.push_back(std::move(mod));
|
||||
}
|
||||
@@ -619,7 +614,7 @@ ModulesConfig ConfigLoader::loadModules(const std::string& path)
|
||||
ModuleStatModifier mod;
|
||||
mod.stat = se.stat;
|
||||
mod.modifierType = "multiplicative";
|
||||
mod.formula = requireFormula(catMt[multipliedKey], file,
|
||||
mod.value = requireDouble(catMt[multipliedKey], file,
|
||||
elemPath + "." + se.category + "." + multipliedKey);
|
||||
def.statModifiers.push_back(std::move(mod));
|
||||
}
|
||||
@@ -631,16 +626,16 @@ ModulesConfig ConfigLoader::loadModules(const std::string& path)
|
||||
const std::string wPath = elemPath + ".weapon";
|
||||
const toml::table& wTable = requireTable(mt["weapon"], file, wPath);
|
||||
toml::table& wMt = const_cast<toml::table&>(wTable);
|
||||
if (wMt.contains("damage_formula") || wMt.contains("attack_range_m_formula")
|
||||
|| wMt.contains("attack_rate_hz_formula"))
|
||||
if (wMt.contains("damage") || wMt.contains("attack_range_m")
|
||||
|| wMt.contains("attack_rate_hz"))
|
||||
{
|
||||
ModuleWeaponCapability cap;
|
||||
cap.damageFormula = requireFormula(wMt["damage_formula"],
|
||||
file, wPath + ".damage_formula");
|
||||
cap.attackRangeFormula = requireFormula(wMt["attack_range_m_formula"],
|
||||
file, wPath + ".attack_range_m_formula");
|
||||
cap.attackRateFormula = requireFormula(wMt["attack_rate_hz_formula"],
|
||||
file, wPath + ".attack_rate_hz_formula");
|
||||
cap.damage = static_cast<float>(requireDouble(wMt["damage"],
|
||||
file, wPath + ".damage"));
|
||||
cap.attackRange_m = static_cast<float>(requireDouble(wMt["attack_range_m"],
|
||||
file, wPath + ".attack_range_m"));
|
||||
cap.attackRate_hz = static_cast<float>(requireDouble(wMt["attack_rate_hz"],
|
||||
file, wPath + ".attack_rate_hz"));
|
||||
def.weaponCapability = std::move(cap);
|
||||
}
|
||||
}
|
||||
@@ -651,16 +646,16 @@ ModulesConfig ConfigLoader::loadModules(const std::string& path)
|
||||
const std::string sPath = elemPath + ".salvage";
|
||||
const toml::table& sTable = requireTable(mt["salvage"], file, sPath);
|
||||
toml::table& sMt = const_cast<toml::table&>(sTable);
|
||||
if (sMt.contains("collection_range_m_formula") || sMt.contains("cargo_capacity_formula")
|
||||
|| sMt.contains("collection_rate_hz_formula"))
|
||||
if (sMt.contains("collection_range_m") || sMt.contains("cargo_capacity")
|
||||
|| sMt.contains("collection_rate_hz"))
|
||||
{
|
||||
ModuleSalvageCapability cap;
|
||||
cap.collectionRangeFormula = requireFormula(sMt["collection_range_m_formula"],
|
||||
file, sPath + ".collection_range_m_formula");
|
||||
cap.cargoCapacityFormula = requireFormula(sMt["cargo_capacity_formula"],
|
||||
file, sPath + ".cargo_capacity_formula");
|
||||
cap.collectionRateFormula = requireFormula(sMt["collection_rate_hz_formula"],
|
||||
file, sPath + ".collection_rate_hz_formula");
|
||||
cap.collectionRange_m = static_cast<float>(requireDouble(sMt["collection_range_m"],
|
||||
file, sPath + ".collection_range_m"));
|
||||
cap.cargoCapacity = static_cast<float>(requireDouble(sMt["cargo_capacity"],
|
||||
file, sPath + ".cargo_capacity"));
|
||||
cap.collectionRate_hz = static_cast<float>(requireDouble(sMt["collection_rate_hz"],
|
||||
file, sPath + ".collection_rate_hz"));
|
||||
def.salvageCapability = std::move(cap);
|
||||
}
|
||||
}
|
||||
@@ -671,15 +666,15 @@ ModulesConfig ConfigLoader::loadModules(const std::string& path)
|
||||
const std::string rPath = elemPath + ".repair";
|
||||
const toml::table& rTable = requireTable(mt["repair"], file, rPath);
|
||||
toml::table& rMt = const_cast<toml::table&>(rTable);
|
||||
if (rMt.contains("repair_rate_hz_formula") || rMt.contains("repair_range_m_formula"))
|
||||
if (rMt.contains("repair_rate_hz") || rMt.contains("repair_range_m"))
|
||||
{
|
||||
ModuleRepairCapability cap;
|
||||
cap.repairRateFormula = requireFormula(rMt["repair_rate_hz_formula"],
|
||||
file, rPath + ".repair_rate_hz_formula");
|
||||
cap.repairAmountHpFormula = requireFormula(rMt["repair_amount_hp_formula"],
|
||||
file, rPath + ".repair_amount_hp_formula");
|
||||
cap.repairRangeFormula = requireFormula(rMt["repair_range_m_formula"],
|
||||
file, rPath + ".repair_range_m_formula");
|
||||
cap.repairRate_hz = static_cast<float>(requireDouble(rMt["repair_rate_hz"],
|
||||
file, rPath + ".repair_rate_hz"));
|
||||
cap.repairAmountHp = static_cast<float>(requireDouble(rMt["repair_amount_hp"],
|
||||
file, rPath + ".repair_amount_hp"));
|
||||
cap.repairRange_m = static_cast<float>(requireDouble(rMt["repair_range_m"],
|
||||
file, rPath + ".repair_range_m"));
|
||||
def.repairCapability = std::move(cap);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,7 +4,6 @@
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include "Formula.h"
|
||||
#include "RecipesConfig.h"
|
||||
|
||||
// A single stat modifier contributed by a module instance.
|
||||
@@ -13,29 +12,29 @@ struct ModuleStatModifier
|
||||
{
|
||||
std::string stat; // e.g. "hp", "speed", "sensor_range"
|
||||
std::string modifierType; // "additive" or "multiplicative"
|
||||
Formula formula;
|
||||
double value;
|
||||
};
|
||||
|
||||
// Capability sections — present when the module grants that capability.
|
||||
struct ModuleWeaponCapability
|
||||
{
|
||||
Formula damageFormula;
|
||||
Formula attackRangeFormula;
|
||||
Formula attackRateFormula;
|
||||
float damage;
|
||||
float attackRange_m;
|
||||
float attackRate_hz;
|
||||
};
|
||||
|
||||
struct ModuleSalvageCapability
|
||||
{
|
||||
Formula collectionRangeFormula;
|
||||
Formula cargoCapacityFormula;
|
||||
Formula collectionRateFormula;
|
||||
float collectionRange_m;
|
||||
float cargoCapacity;
|
||||
float collectionRate_hz;
|
||||
};
|
||||
|
||||
struct ModuleRepairCapability
|
||||
{
|
||||
Formula repairRateFormula; // repair cycles per second
|
||||
Formula repairAmountHpFormula; // HP restored per cycle
|
||||
Formula repairRangeFormula;
|
||||
float repairRate_hz; // repair cycles per second
|
||||
float repairAmountHp; // HP restored per cycle
|
||||
float repairRange_m;
|
||||
};
|
||||
|
||||
struct ModuleDef
|
||||
@@ -44,7 +43,6 @@ struct ModuleDef
|
||||
int unlockAtStationLevel;
|
||||
std::vector<std::string> surfaceMask;
|
||||
std::vector<RecipeIngredient> materials;
|
||||
int playerProductionLevel;
|
||||
double productionTimeSeconds;
|
||||
std::string fillColor;
|
||||
std::string glyph;
|
||||
|
||||
@@ -3,36 +3,33 @@
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include "Formula.h"
|
||||
#include "RecipesConfig.h" // for RecipeIngredient
|
||||
#include "ShipLayout.h" // for PlacedModule
|
||||
|
||||
// Build materials and initial per-schematic production level
|
||||
// (REQ-BLD-SHIPYARD, REQ-DEF-SCHEMATIC-DROP).
|
||||
// Build materials and base production time (REQ-BLD-SHIPYARD, REQ-DEF-SCHEMATIC-DROP).
|
||||
struct ShipSchematic
|
||||
{
|
||||
std::vector<RecipeIngredient> materials;
|
||||
int playerProductionLevel;
|
||||
double productionTimeSeconds;
|
||||
};
|
||||
|
||||
struct ShipHealth
|
||||
{
|
||||
Formula hpFormula; // REQ-SHP-STATS
|
||||
float hp; // REQ-SHP-STATS
|
||||
};
|
||||
|
||||
struct ShipMovement
|
||||
{
|
||||
Formula speedFormula; // max linear speed cap, tiles/s (REQ-SHP-STATS, REQ-SHP-MOVEMENT)
|
||||
Formula mainAccelerationFormula; // forward acceleration, tiles/s²
|
||||
Formula maneuveringAccelerationFormula;// omnidirectional acceleration, tiles/s²
|
||||
Formula angularAccelerationFormula; // angular acceleration, rad/s²
|
||||
Formula maxRotationSpeedFormula; // angular velocity cap, rad/s
|
||||
float speed_mps; // max linear speed cap, m/s (REQ-SHP-STATS, REQ-SHP-MOVEMENT)
|
||||
float mainAcceleration_mpss; // forward acceleration, m/s²
|
||||
float maneuveringAcceleration_mpss;// omnidirectional acceleration, m/s²
|
||||
float angularAcceleration_radpss; // angular acceleration, rad/s²
|
||||
float maxRotationSpeed_radps; // angular velocity cap, rad/s
|
||||
};
|
||||
|
||||
struct ShipSensor
|
||||
{
|
||||
Formula sensorRangeFormula; // REQ-SHP-SENSOR, REQ-SHP-STATS
|
||||
float sensorRange_m; // REQ-SHP-SENSOR, REQ-SHP-STATS
|
||||
};
|
||||
|
||||
struct ShipDef
|
||||
|
||||
@@ -29,7 +29,6 @@ struct WorldPush
|
||||
struct WorldWaves
|
||||
{
|
||||
Formula threatRateFormula; // threat/s as a function of boss wave counter x
|
||||
Formula shipLevelFormula; // enemy ship level as a function of boss wave counter x
|
||||
double gapMinSeconds;
|
||||
double gapMaxSeconds;
|
||||
double spawnDurationSeconds;
|
||||
|
||||
@@ -42,7 +42,7 @@ entt::entity EntityAdmin::spawnShip(QVector2D position, float hp, float maxHp,
|
||||
float maxSpeed_tpt, float mainAcceleration_tptt,
|
||||
float maneuveringAcceleration_tptt, float maxAngularAcceleration_rptt,
|
||||
float maxRotationSpeed_rpt, float sensorRange_tiles,
|
||||
int level, const std::string& schematicId, bool isEnemy)
|
||||
const std::string& schematicId, bool isEnemy)
|
||||
{
|
||||
entt::entity entity = createEntity();
|
||||
add<PositionComponent>(entity, PositionComponent{position});
|
||||
@@ -61,7 +61,7 @@ entt::entity EntityAdmin::spawnShip(QVector2D position, float hp, float maxHp,
|
||||
0.0f // angularAcceleration_rptt
|
||||
});
|
||||
add<SensorRangeComponent>(entity, SensorRangeComponent{sensorRange_tiles});
|
||||
add<ShipIdentityComponent>(entity, ShipIdentityComponent{level, schematicId});
|
||||
add<ShipIdentityComponent>(entity, ShipIdentityComponent{schematicId});
|
||||
add<MovementIntentComponent>(entity, MovementIntentComponent{0, QVector2D(0.0f, 0.0f)});
|
||||
return entity;
|
||||
}
|
||||
|
||||
@@ -56,7 +56,7 @@ public:
|
||||
float maxSpeed_tpt, float mainAcceleration_tptt,
|
||||
float maneuveringAcceleration_tptt, float maxAngularAcceleration_rptt,
|
||||
float maxRotationSpeed_rpt, float sensorRange_tiles,
|
||||
int level, const std::string& schematicId, bool isEnemy);
|
||||
const std::string& schematicId, bool isEnemy);
|
||||
|
||||
entt::entity spawnStation(QPoint anchor, QSize footprint,
|
||||
const std::vector<QPoint>& bodyCells,
|
||||
|
||||
@@ -19,8 +19,6 @@ struct SchematicChoiceOption
|
||||
std::string schematicId;
|
||||
SchematicType type;
|
||||
std::string displayName;
|
||||
bool isNewUnlock;
|
||||
int targetLevel;
|
||||
|
||||
// Display names of items produced by recipes that would newly become
|
||||
// implicitly unlocked (REQ-LOCK-IMPLICIT) if this option is selected.
|
||||
|
||||
@@ -4,7 +4,6 @@
|
||||
|
||||
struct ShipIdentityComponent
|
||||
{
|
||||
int level;
|
||||
std::string schematicId;
|
||||
// Scrap dropped on destruction, derived from the ship's as-built threat cost
|
||||
// at spawn time (REQ-RES-SCRAP-DROP).
|
||||
|
||||
@@ -65,43 +65,31 @@ const ModuleDef* ShipSystem::findModuleDef(const std::string& id) const
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
entt::entity ShipSystem::spawn(const std::string& schematicId, int level,
|
||||
entt::entity ShipSystem::spawn(const std::string& schematicId,
|
||||
QVector2D position, bool isEnemy,
|
||||
const std::optional<ShipLayoutConfig>& layout,
|
||||
const std::map<std::string, int>& moduleLevelOverrides)
|
||||
const std::optional<ShipLayoutConfig>& layout)
|
||||
{
|
||||
const ShipDef* def = findShipDef(schematicId);
|
||||
assert(def != nullptr);
|
||||
|
||||
const double x = static_cast<double>(level);
|
||||
const float tickRate = static_cast<float>(kTickRateHz);
|
||||
const float tileSize = static_cast<float>(m_config.world.tileSize_m);
|
||||
|
||||
float hp = static_cast<float>(def->health.hpFormula.evaluate(x));
|
||||
float hp = def->health.hp;
|
||||
float maxHp = hp;
|
||||
float maxSpeed_tpt = static_cast<float>(def->movement.speedFormula.evaluate(x))
|
||||
/ tileSize / tickRate;
|
||||
float mainAcceleration_tptt = static_cast<float>(
|
||||
def->movement.mainAccelerationFormula.evaluate(x))
|
||||
/ tileSize / tickRate;
|
||||
float maneuveringAcceleration_tptt = static_cast<float>(
|
||||
def->movement.maneuveringAccelerationFormula.evaluate(x))
|
||||
float maxSpeed_tpt = def->movement.speed_mps / tileSize / tickRate;
|
||||
float mainAcceleration_tptt = def->movement.mainAcceleration_mpss / tileSize / tickRate;
|
||||
float maneuveringAcceleration_tptt = def->movement.maneuveringAcceleration_mpss
|
||||
/ tileSize / tickRate;
|
||||
float maxAngularAcceleration_rptt = static_cast<float>(
|
||||
def->movement.angularAccelerationFormula.evaluate(x))
|
||||
/ tickRate;
|
||||
float maxRotationSpeed_rpt = static_cast<float>(
|
||||
def->movement.maxRotationSpeedFormula.evaluate(x))
|
||||
/ tickRate;
|
||||
float sensorRange_tiles = static_cast<float>(
|
||||
def->sensor.sensorRangeFormula.evaluate(x))
|
||||
/ tileSize;
|
||||
float maxAngularAcceleration_rptt = def->movement.angularAcceleration_radpss / tickRate;
|
||||
float maxRotationSpeed_rpt = def->movement.maxRotationSpeed_radps / tickRate;
|
||||
float sensorRange_tiles = def->sensor.sensorRange_m / tileSize;
|
||||
|
||||
entt::entity entity = m_admin.spawnShip(
|
||||
position, hp, maxHp,
|
||||
maxSpeed_tpt, mainAcceleration_tptt, maneuveringAcceleration_tptt,
|
||||
maxAngularAcceleration_rptt, maxRotationSpeed_rpt, sensorRange_tiles,
|
||||
level, schematicId, isEnemy);
|
||||
schematicId, isEnemy);
|
||||
|
||||
// Determine module list: configured layout takes precedence over default.
|
||||
const std::vector<PlacedModule>& modules =
|
||||
@@ -131,19 +119,12 @@ entt::entity ShipSystem::spawn(const std::string& schematicId, int level,
|
||||
const ModuleDef* modDef = findModuleDef(pm.moduleId);
|
||||
if (!modDef) { throw std::runtime_error("unknown module id '" + pm.moduleId + "'"); }
|
||||
|
||||
const auto overIt = moduleLevelOverrides.find(pm.moduleId);
|
||||
const double mx = static_cast<double>(
|
||||
overIt != moduleLevelOverrides.end() ? overIt->second : modDef->playerProductionLevel);
|
||||
|
||||
if (modDef->weaponCapability)
|
||||
{
|
||||
WeaponComponent w;
|
||||
w.damage = static_cast<float>(
|
||||
modDef->weaponCapability->damageFormula.evaluate(mx));
|
||||
w.range_tiles = static_cast<float>(
|
||||
modDef->weaponCapability->attackRangeFormula.evaluate(mx)) / tileSize;
|
||||
w.fireRateHz = static_cast<float>(
|
||||
modDef->weaponCapability->attackRateFormula.evaluate(mx));
|
||||
w.damage = modDef->weaponCapability->damage;
|
||||
w.range_tiles = modDef->weaponCapability->attackRange_m / tileSize;
|
||||
w.fireRateHz = modDef->weaponCapability->attackRate_hz;
|
||||
w.cooldownTicks = 0.0f;
|
||||
w.currentTarget = std::nullopt;
|
||||
|
||||
@@ -155,12 +136,11 @@ entt::entity ShipSystem::spawn(const std::string& schematicId, int level,
|
||||
|
||||
if (modDef->salvageCapability)
|
||||
{
|
||||
cargoCapacityBase += modDef->salvageCapability->cargoCapacityFormula.evaluate(mx);
|
||||
cargoCapacityBase += modDef->salvageCapability->cargoCapacity;
|
||||
|
||||
SalvagerComponent salvager;
|
||||
salvager.collectionRange_tiles = static_cast<float>(
|
||||
modDef->salvageCapability->collectionRangeFormula.evaluate(mx)) / tileSize;
|
||||
const double rate = modDef->salvageCapability->collectionRateFormula.evaluate(mx);
|
||||
salvager.collectionRange_tiles = modDef->salvageCapability->collectionRange_m / tileSize;
|
||||
const double rate = modDef->salvageCapability->collectionRate_hz;
|
||||
salvager.collectionIntervalTicks = (rate > 0.0)
|
||||
? static_cast<int>(kTickRateHz / rate + 0.5)
|
||||
: 0;
|
||||
@@ -175,16 +155,13 @@ entt::entity ShipSystem::spawn(const std::string& schematicId, int level,
|
||||
if (modDef->repairCapability)
|
||||
{
|
||||
RepairToolComponent rt;
|
||||
const double repairRateHz =
|
||||
modDef->repairCapability->repairRateFormula.evaluate(mx);
|
||||
const double repairRateHz = modDef->repairCapability->repairRate_hz;
|
||||
rt.repairIntervalTicks = (repairRateHz > 0.0)
|
||||
? static_cast<int>(kTickRateHz / repairRateHz + 0.5)
|
||||
: 0;
|
||||
rt.repairAmountHp = static_cast<float>(
|
||||
modDef->repairCapability->repairAmountHpFormula.evaluate(mx));
|
||||
rt.repairAmountHp = modDef->repairCapability->repairAmountHp;
|
||||
rt.cooldownTicksRemaining = 0;
|
||||
rt.range_tiles = static_cast<float>(
|
||||
modDef->repairCapability->repairRangeFormula.evaluate(mx)) / tileSize;
|
||||
rt.range_tiles = modDef->repairCapability->repairRange_m / tileSize;
|
||||
rt.currentTarget = std::nullopt;
|
||||
|
||||
entt::entity child = m_admin.createModuleEntity();
|
||||
@@ -210,13 +187,9 @@ entt::entity ShipSystem::spawn(const std::string& schematicId, int level,
|
||||
const ModuleDef* modDef = findModuleDef(pm.moduleId);
|
||||
if (!modDef) { throw std::runtime_error("unknown module id '" + pm.moduleId + "'"); }
|
||||
|
||||
const auto overIt2 = moduleLevelOverrides.find(pm.moduleId);
|
||||
const double mx = static_cast<double>(
|
||||
overIt2 != moduleLevelOverrides.end() ? overIt2->second : modDef->playerProductionLevel);
|
||||
|
||||
for (const ModuleStatModifier& sm : modDef->statModifiers)
|
||||
{
|
||||
const double val = sm.formula.evaluate(mx);
|
||||
const double val = sm.value;
|
||||
|
||||
// Route modifier to the correct accumulator by stat category.
|
||||
// weapon/salvage/repair stats go to the corresponding child map;
|
||||
|
||||
@@ -18,10 +18,9 @@ class ShipSystem
|
||||
public:
|
||||
ShipSystem(const GameConfig& config, EntityAdmin& admin);
|
||||
|
||||
entt::entity spawn(const std::string& schematicId, int level, QVector2D position,
|
||||
entt::entity spawn(const std::string& schematicId, QVector2D position,
|
||||
bool isEnemy = false,
|
||||
const std::optional<ShipLayoutConfig>& layout = std::nullopt,
|
||||
const std::map<std::string, int>& moduleLevelOverrides = {});
|
||||
const std::optional<ShipLayoutConfig>& layout = std::nullopt);
|
||||
void despawn(entt::entity entity);
|
||||
|
||||
// Reset all movement intents to inactive before behavior systems run.
|
||||
|
||||
@@ -17,9 +17,7 @@
|
||||
|
||||
ShipStats calculateShipStats(const GameConfig& config,
|
||||
const std::string& shipId,
|
||||
int level,
|
||||
const std::vector<PlacedModule>& modules,
|
||||
const std::map<std::string, int>& moduleLevelOverrides)
|
||||
const std::vector<PlacedModule>& modules)
|
||||
{
|
||||
ShipStats result{};
|
||||
|
||||
@@ -39,24 +37,20 @@ ShipStats calculateShipStats(const GameConfig& config,
|
||||
return nullptr;
|
||||
};
|
||||
|
||||
const double x = static_cast<double>(level);
|
||||
const double tileSize = config.world.tileSize_m;
|
||||
|
||||
// --- Base hull stats (convert from SI to display units) ------------------
|
||||
result.hp = static_cast<float>(
|
||||
shipDef->health.hpFormula.evaluate(x));
|
||||
result.hp = shipDef->health.hp;
|
||||
result.maxSpeed_tps = static_cast<float>(
|
||||
shipDef->movement.speedFormula.evaluate(x) / tileSize);
|
||||
shipDef->movement.speed_mps / tileSize);
|
||||
result.sensorRange_tiles = static_cast<float>(
|
||||
shipDef->sensor.sensorRangeFormula.evaluate(x) / tileSize);
|
||||
shipDef->sensor.sensorRange_m / tileSize);
|
||||
result.mainAcceleration_tpss = static_cast<float>(
|
||||
shipDef->movement.mainAccelerationFormula.evaluate(x) / tileSize);
|
||||
shipDef->movement.mainAcceleration_mpss / tileSize);
|
||||
result.maneuveringAcceleration_tpss = static_cast<float>(
|
||||
shipDef->movement.maneuveringAccelerationFormula.evaluate(x) / tileSize);
|
||||
result.angularAcceleration_radpss = static_cast<float>(
|
||||
shipDef->movement.angularAccelerationFormula.evaluate(x));
|
||||
result.maxRotationSpeed_radps = static_cast<float>(
|
||||
shipDef->movement.maxRotationSpeedFormula.evaluate(x));
|
||||
shipDef->movement.maneuveringAcceleration_mpss / tileSize);
|
||||
result.angularAcceleration_radpss = shipDef->movement.angularAcceleration_radpss;
|
||||
result.maxRotationSpeed_radps = shipDef->movement.maxRotationSpeed_radps;
|
||||
|
||||
// --- Pass 1: base capability stats per module instance -------------------
|
||||
struct WeaponInstance { float damage; float range_tiles; float rate_hz; };
|
||||
@@ -76,33 +70,29 @@ ShipStats calculateShipStats(const GameConfig& config,
|
||||
const ModuleDef* def = findModuleDef(pm.moduleId);
|
||||
if (!def) { throw std::runtime_error("unknown module id '" + pm.moduleId + "'"); }
|
||||
|
||||
const auto overIt = moduleLevelOverrides.find(pm.moduleId);
|
||||
const double mx = static_cast<double>(
|
||||
overIt != moduleLevelOverrides.end() ? overIt->second : def->playerProductionLevel);
|
||||
|
||||
if (def->weaponCapability)
|
||||
{
|
||||
WeaponInstance wi;
|
||||
wi.damage = static_cast<float>(def->weaponCapability->damageFormula.evaluate(mx));
|
||||
wi.range_tiles = static_cast<float>(def->weaponCapability->attackRangeFormula.evaluate(mx) / tileSize);
|
||||
wi.rate_hz = static_cast<float>(def->weaponCapability->attackRateFormula.evaluate(mx));
|
||||
wi.damage = def->weaponCapability->damage;
|
||||
wi.range_tiles = static_cast<float>(def->weaponCapability->attackRange_m / tileSize);
|
||||
wi.rate_hz = def->weaponCapability->attackRate_hz;
|
||||
weaponInstances.push_back(wi);
|
||||
}
|
||||
if (def->salvageCapability)
|
||||
{
|
||||
cargoCapacityBase += def->salvageCapability->cargoCapacityFormula.evaluate(mx);
|
||||
cargoCapacityBase += def->salvageCapability->cargoCapacity;
|
||||
|
||||
SalvageInstance si;
|
||||
si.range_tiles = static_cast<float>(def->salvageCapability->collectionRangeFormula.evaluate(mx) / tileSize);
|
||||
si.rate = static_cast<float>(def->salvageCapability->collectionRateFormula.evaluate(mx));
|
||||
si.range_tiles = static_cast<float>(def->salvageCapability->collectionRange_m / tileSize);
|
||||
si.rate = def->salvageCapability->collectionRate_hz;
|
||||
salvageInstances.push_back(si);
|
||||
}
|
||||
if (def->repairCapability)
|
||||
{
|
||||
RepairInstance ri;
|
||||
ri.rate_hz = static_cast<float>(def->repairCapability->repairRateFormula.evaluate(mx));
|
||||
ri.amount_hp = static_cast<float>(def->repairCapability->repairAmountHpFormula.evaluate(mx));
|
||||
ri.range_tiles = static_cast<float>(def->repairCapability->repairRangeFormula.evaluate(mx) / tileSize);
|
||||
ri.rate_hz = def->repairCapability->repairRate_hz;
|
||||
ri.amount_hp = def->repairCapability->repairAmountHp;
|
||||
ri.range_tiles = static_cast<float>(def->repairCapability->repairRange_m / tileSize);
|
||||
repairInstances.push_back(ri);
|
||||
}
|
||||
}
|
||||
@@ -120,13 +110,9 @@ ShipStats calculateShipStats(const GameConfig& config,
|
||||
const ModuleDef* def = findModuleDef(pm.moduleId);
|
||||
if (!def) { throw std::runtime_error("unknown module id '" + pm.moduleId + "'"); }
|
||||
|
||||
const auto overIt = moduleLevelOverrides.find(pm.moduleId);
|
||||
const double mx = static_cast<double>(
|
||||
overIt != moduleLevelOverrides.end() ? overIt->second : def->playerProductionLevel);
|
||||
|
||||
for (const ModuleStatModifier& sm : def->statModifiers)
|
||||
{
|
||||
const double val = sm.formula.evaluate(mx);
|
||||
const double val = sm.value;
|
||||
|
||||
const bool isWeaponStat = (sm.stat == "damage"
|
||||
|| sm.stat == "attack_range"
|
||||
|
||||
@@ -51,9 +51,7 @@ struct ShipStats
|
||||
|
||||
ShipStats calculateShipStats(const GameConfig& config,
|
||||
const std::string& shipId,
|
||||
int level,
|
||||
const std::vector<PlacedModule>& modules,
|
||||
const std::map<std::string, int>& moduleLevelOverrides = {});
|
||||
const std::vector<PlacedModule>& modules);
|
||||
|
||||
ShipStats buildShipStatsFromEntity(const EntityAdmin& admin, entt::entity shipEntity);
|
||||
|
||||
|
||||
@@ -61,13 +61,7 @@ Simulation::Simulation(GameConfig config, unsigned int seed)
|
||||
{
|
||||
return;
|
||||
}
|
||||
std::map<std::string, int> moduleLevels;
|
||||
for (const auto& [mId, mState] : m_moduleSchematicLevels)
|
||||
{
|
||||
moduleLevels[mId] = mState.level;
|
||||
}
|
||||
m_shipSystem->spawn(id, it->second.level, pos, /*isEnemy=*/false, layout,
|
||||
moduleLevels);
|
||||
m_shipSystem->spawn(id, pos, /*isEnemy=*/false, layout);
|
||||
},
|
||||
[this](const std::string& itemId) -> bool { return isItemUnlocked(itemId); },
|
||||
m_rng);
|
||||
@@ -86,7 +80,6 @@ Simulation::Simulation(GameConfig config, unsigned int seed)
|
||||
{
|
||||
SchematicState state;
|
||||
state.unlocked = (def.unlockAtStationLevel == -1);
|
||||
state.level = (def.unlockAtStationLevel == -1) ? def.schematic.playerProductionLevel : 0;
|
||||
m_schematicLevels[def.id] = state;
|
||||
}
|
||||
|
||||
@@ -95,7 +88,6 @@ Simulation::Simulation(GameConfig config, unsigned int seed)
|
||||
{
|
||||
SchematicState state;
|
||||
state.unlocked = (def.unlockAtStationLevel == -1);
|
||||
state.level = (def.unlockAtStationLevel == -1) ? def.playerProductionLevel : 0;
|
||||
m_moduleSchematicLevels[def.id] = state;
|
||||
}
|
||||
|
||||
@@ -167,13 +159,7 @@ void Simulation::reset(unsigned int seed)
|
||||
{
|
||||
return;
|
||||
}
|
||||
std::map<std::string, int> moduleLevels;
|
||||
for (const auto& [mId, mState] : m_moduleSchematicLevels)
|
||||
{
|
||||
moduleLevels[mId] = mState.level;
|
||||
}
|
||||
m_shipSystem->spawn(id, it->second.level, pos, /*isEnemy=*/false, layout,
|
||||
moduleLevels);
|
||||
m_shipSystem->spawn(id, pos, /*isEnemy=*/false, layout);
|
||||
},
|
||||
[this](const std::string& itemId) -> bool { return isItemUnlocked(itemId); },
|
||||
m_rng);
|
||||
@@ -192,7 +178,6 @@ void Simulation::reset(unsigned int seed)
|
||||
{
|
||||
SchematicState state;
|
||||
state.unlocked = (def.unlockAtStationLevel == -1);
|
||||
state.level = (def.unlockAtStationLevel == -1) ? def.schematic.playerProductionLevel : 0;
|
||||
m_schematicLevels[def.id] = state;
|
||||
}
|
||||
|
||||
@@ -201,7 +186,6 @@ void Simulation::reset(unsigned int seed)
|
||||
{
|
||||
SchematicState state;
|
||||
state.unlocked = (def.unlockAtStationLevel == -1);
|
||||
state.level = (def.unlockAtStationLevel == -1) ? def.playerProductionLevel : 0;
|
||||
m_moduleSchematicLevels[def.id] = state;
|
||||
}
|
||||
|
||||
@@ -643,19 +627,21 @@ void Simulation::generateSchematicChoices(int destroyedStationLevel)
|
||||
struct PoolEntry { std::string id; DropType type; };
|
||||
std::vector<PoolEntry> pool;
|
||||
|
||||
// Owned schematics leave the pool (REQ-DEF-SCHEMATIC-DROP): only offer
|
||||
// ship/module schematics that are gated to a station level in range and not
|
||||
// yet unlocked. Schematics with unlock_at_station_level -1 start unlocked, so
|
||||
// they are always owned and never eligible.
|
||||
for (const ShipDef& def : m_config.ships.ships)
|
||||
{
|
||||
if (def.unlockAtStationLevel == -1 || def.unlockAtStationLevel <= destroyedStationLevel)
|
||||
{
|
||||
pool.push_back({def.id, DropType::Ship});
|
||||
}
|
||||
if (def.unlockAtStationLevel < 0 || def.unlockAtStationLevel > destroyedStationLevel) { continue; }
|
||||
if (m_schematicLevels.at(def.id).unlocked) { continue; }
|
||||
pool.push_back({def.id, DropType::Ship});
|
||||
}
|
||||
for (const ModuleDef& def : m_config.modules.modules)
|
||||
{
|
||||
if (def.unlockAtStationLevel == -1 || def.unlockAtStationLevel <= destroyedStationLevel)
|
||||
{
|
||||
pool.push_back({def.id, DropType::Module});
|
||||
}
|
||||
if (def.unlockAtStationLevel < 0 || def.unlockAtStationLevel > destroyedStationLevel) { continue; }
|
||||
if (m_moduleSchematicLevels.at(def.id).unlocked) { continue; }
|
||||
pool.push_back({def.id, DropType::Module});
|
||||
}
|
||||
for (const RecipeDef& def : m_config.recipes.recipes)
|
||||
{
|
||||
@@ -704,23 +690,15 @@ void Simulation::generateSchematicChoices(int destroyedStationLevel)
|
||||
{
|
||||
option.type = SchematicType::Ship;
|
||||
option.displayName = toDisplayName(entry.id);
|
||||
const SchematicState& state = m_schematicLevels.at(entry.id);
|
||||
option.isNewUnlock = !state.unlocked;
|
||||
option.targetLevel = state.level + 1;
|
||||
}
|
||||
else if (entry.type == DropType::Module)
|
||||
{
|
||||
option.type = SchematicType::Module;
|
||||
option.displayName = toDisplayName(entry.id);
|
||||
const SchematicState& state = m_moduleSchematicLevels.at(entry.id);
|
||||
option.isNewUnlock = !state.unlocked;
|
||||
option.targetLevel = state.level + 1;
|
||||
}
|
||||
else
|
||||
{
|
||||
option.type = SchematicType::Recipe;
|
||||
option.isNewUnlock = true;
|
||||
option.targetLevel = 0;
|
||||
for (const RecipeDef& def : m_config.recipes.recipes)
|
||||
{
|
||||
if (def.id == entry.id && !def.outputs.empty())
|
||||
@@ -736,11 +714,11 @@ void Simulation::generateSchematicChoices(int destroyedStationLevel)
|
||||
std::set<std::string> hypotheticalModuleIds = currentModuleIds;
|
||||
std::set<std::string> hypotheticalRecipeSchematicIds = m_unlockedRecipeSchematicIds;
|
||||
|
||||
if (entry.type == DropType::Ship && option.isNewUnlock)
|
||||
if (entry.type == DropType::Ship)
|
||||
{
|
||||
hypotheticalShipIds.insert(entry.id);
|
||||
}
|
||||
else if (entry.type == DropType::Module && option.isNewUnlock)
|
||||
else if (entry.type == DropType::Module)
|
||||
{
|
||||
hypotheticalModuleIds.insert(entry.id);
|
||||
}
|
||||
@@ -762,8 +740,6 @@ void Simulation::generateSchematicChoices(int destroyedStationLevel)
|
||||
artifactOption.schematicId = "";
|
||||
artifactOption.type = SchematicType::Artifact;
|
||||
artifactOption.displayName = "Artifact";
|
||||
artifactOption.isNewUnlock = false;
|
||||
artifactOption.targetLevel = 0;
|
||||
m_pendingSchematicChoices.push_back(std::move(artifactOption));
|
||||
}
|
||||
}
|
||||
@@ -794,7 +770,6 @@ void Simulation::applySchematicChoice(int choiceIndex)
|
||||
? m_moduleSchematicLevels.at(chosen.schematicId)
|
||||
: m_schematicLevels.at(chosen.schematicId);
|
||||
state.unlocked = true;
|
||||
state.level += 1;
|
||||
}
|
||||
|
||||
recomputeUnlocked();
|
||||
@@ -957,7 +932,6 @@ void Simulation::appendSchematicMap(Hasher& hasher,
|
||||
{
|
||||
hasher.append(entry.first);
|
||||
hasher.append(entry.second.unlocked);
|
||||
hasher.append(entry.second.level);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1050,7 +1024,6 @@ unsigned long long Simulation::computeStateChecksum() const
|
||||
[&hasher](entt::entity entity, const ShipIdentityComponent& c)
|
||||
{
|
||||
hasher.append(static_cast<std::uint32_t>(entity));
|
||||
hasher.append(c.level);
|
||||
hasher.append(c.schematicId);
|
||||
});
|
||||
|
||||
@@ -1148,17 +1121,6 @@ Tick Simulation::normalGapRemainingTicks() const
|
||||
return m_waveSystem->normalGapRemainingTicks();
|
||||
}
|
||||
|
||||
int Simulation::schematicLevel(const std::string& shipId) const
|
||||
{
|
||||
const std::map<std::string, SchematicState>::const_iterator it =
|
||||
m_schematicLevels.find(shipId);
|
||||
if (it == m_schematicLevels.end())
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
return it->second.level;
|
||||
}
|
||||
|
||||
bool Simulation::isSchematicUnlocked(const std::string& shipId) const
|
||||
{
|
||||
const std::map<std::string, SchematicState>::const_iterator it =
|
||||
@@ -1170,17 +1132,6 @@ bool Simulation::isSchematicUnlocked(const std::string& shipId) const
|
||||
return it->second.unlocked;
|
||||
}
|
||||
|
||||
int Simulation::moduleSchematicLevel(const std::string& moduleId) const
|
||||
{
|
||||
const std::map<std::string, SchematicState>::const_iterator it =
|
||||
m_moduleSchematicLevels.find(moduleId);
|
||||
if (it == m_moduleSchematicLevels.end())
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
return it->second.level;
|
||||
}
|
||||
|
||||
bool Simulation::isModuleSchematicUnlocked(const std::string& moduleId) const
|
||||
{
|
||||
const std::map<std::string, SchematicState>::const_iterator it =
|
||||
|
||||
@@ -83,12 +83,10 @@ public:
|
||||
Tick bossCountdownTicks() const;
|
||||
Tick normalGapRemainingTicks() const;
|
||||
|
||||
// Ship schematic state queries.
|
||||
int schematicLevel(const std::string& shipId) const;
|
||||
// Ship schematic state query.
|
||||
bool isSchematicUnlocked(const std::string& shipId) const;
|
||||
|
||||
// Module schematic state queries.
|
||||
int moduleSchematicLevel(const std::string& moduleId) const;
|
||||
// Module schematic state query.
|
||||
bool isModuleSchematicUnlocked(const std::string& moduleId) const;
|
||||
|
||||
// Implicit recipe/item unlock queries (REQ-LOCK-IMPLICIT).
|
||||
@@ -182,7 +180,6 @@ private:
|
||||
struct SchematicState
|
||||
{
|
||||
bool unlocked;
|
||||
int level;
|
||||
};
|
||||
std::map<std::string, SchematicState> m_schematicLevels;
|
||||
std::map<std::string, SchematicState> m_moduleSchematicLevels;
|
||||
|
||||
@@ -55,7 +55,7 @@ void WaveSystem::tickWaveScheduler(Tick currentTick, ShipSystem& ships,
|
||||
{
|
||||
if (currentTick >= entry.spawnAt)
|
||||
{
|
||||
ships.spawn(entry.schematicId, entry.level, entry.position,
|
||||
ships.spawn(entry.schematicId, entry.position,
|
||||
/*isEnemy=*/true, entry.layout);
|
||||
}
|
||||
else
|
||||
@@ -174,11 +174,7 @@ std::vector<WaveSystem::SpawnEntry> WaveSystem::selectWaveShips(double& budget,
|
||||
Tick currentTick,
|
||||
int worldHeightTiles)
|
||||
{
|
||||
const int shipLevel = std::max(1, static_cast<int>(
|
||||
m_config.world.waves.shipLevelFormula.evaluate(
|
||||
static_cast<double>(m_bossWaveCounter))));
|
||||
|
||||
// Build eligible ship list with their costs at the current level.
|
||||
// Build eligible ship list with their (level-independent) threat costs.
|
||||
struct EligibleShip
|
||||
{
|
||||
std::string schematicId;
|
||||
@@ -242,7 +238,6 @@ std::vector<WaveSystem::SpawnEntry> WaveSystem::selectWaveShips(double& budget,
|
||||
|
||||
SpawnEntry entry;
|
||||
entry.schematicId = chosen.schematicId;
|
||||
entry.level = shipLevel;
|
||||
entry.spawnAt = 0; // set below after all picks are done
|
||||
entry.position = QVector2D(xDist(m_rng),
|
||||
static_cast<float>(yDist(m_rng)) + 0.5f);
|
||||
|
||||
@@ -59,7 +59,6 @@ private:
|
||||
struct SpawnEntry
|
||||
{
|
||||
std::string schematicId;
|
||||
int level;
|
||||
Tick spawnAt;
|
||||
QVector2D position;
|
||||
ShipLayoutConfig layout;
|
||||
|
||||
@@ -238,7 +238,7 @@ TEST_CASE("BehaviorSystem: clearMovementIntents resets all ships to inactive",
|
||||
"[behavior]")
|
||||
{
|
||||
Fixture f;
|
||||
const entt::entity e = f.ships.spawn("interceptor", 1, QVector2D(0.0f, 0.0f));
|
||||
const entt::entity e = f.ships.spawn("interceptor", QVector2D(0.0f, 0.0f));
|
||||
|
||||
f.admin.get<MovementIntentComponent>(e) = MovementIntentComponent{true, QVector2D(10.0f, 0.0f)};
|
||||
f.ships.clearMovementIntents();
|
||||
@@ -254,7 +254,7 @@ TEST_CASE("BehaviorSystem: tickMovement advances ship by maxSpeed_tpt toward tar
|
||||
"[behavior]")
|
||||
{
|
||||
Fixture f;
|
||||
const entt::entity e = f.ships.spawn("interceptor", 1, QVector2D(0.0f, 0.0f));
|
||||
const entt::entity e = f.ships.spawn("interceptor", QVector2D(0.0f, 0.0f));
|
||||
|
||||
const float speed = f.admin.get<DynamicBodyComponent>(e).maxSpeed_tpt;
|
||||
f.admin.get<MovementIntentComponent>(e) = MovementIntentComponent{true, QVector2D(100.0f, 0.0f)};
|
||||
@@ -269,7 +269,7 @@ TEST_CASE("BehaviorSystem: tickMovement stops exactly at target without overshoo
|
||||
"[behavior]")
|
||||
{
|
||||
Fixture f;
|
||||
const entt::entity e = f.ships.spawn("interceptor", 1, QVector2D(0.0f, 0.0f));
|
||||
const entt::entity e = f.ships.spawn("interceptor", QVector2D(0.0f, 0.0f));
|
||||
|
||||
const float speed = f.admin.get<DynamicBodyComponent>(e).maxSpeed_tpt;
|
||||
const QVector2D target(speed * 0.5f, 0.0f);
|
||||
@@ -288,7 +288,7 @@ TEST_CASE("BehaviorSystem: tickMovement stops exactly at target without overshoo
|
||||
TEST_CASE("BehaviorSystem: healthy player ship does not retreat", "[behavior]")
|
||||
{
|
||||
Fixture f;
|
||||
const entt::entity e = f.ships.spawn("interceptor", 1, QVector2D(0.0f, 0.0f));
|
||||
const entt::entity e = f.ships.spawn("interceptor", QVector2D(0.0f, 0.0f));
|
||||
f.admin.get<HealthComponent>(e).hp = f.admin.get<HealthComponent>(e).maxHp; // full HP
|
||||
|
||||
f.decide();
|
||||
@@ -301,7 +301,7 @@ TEST_CASE("BehaviorSystem: low-HP player ship retreats toward the rally point",
|
||||
Fixture f;
|
||||
const QVector2D rallyPoint(-50.0f, 0.0f);
|
||||
f.ships.setRallyPoint(rallyPoint);
|
||||
const entt::entity e = f.ships.spawn("interceptor", 1, QVector2D(0.0f, 0.0f));
|
||||
const entt::entity e = f.ships.spawn("interceptor", QVector2D(0.0f, 0.0f));
|
||||
f.admin.get<HealthComponent>(e).hp = f.admin.get<HealthComponent>(e).maxHp * 0.2f; // below threshold
|
||||
|
||||
f.decide();
|
||||
@@ -316,8 +316,8 @@ TEST_CASE("BehaviorSystem: low-HP retreat outranks attacking a nearby enemy", "[
|
||||
Fixture f;
|
||||
const QVector2D rallyPoint(-50.0f, 0.0f);
|
||||
f.ships.setRallyPoint(rallyPoint);
|
||||
const entt::entity player = f.ships.spawn("interceptor", 1, QVector2D(0.0f, 0.0f));
|
||||
f.ships.spawn("interceptor", 1, QVector2D(5.0f, 0.0f), /*isEnemy=*/true);
|
||||
const entt::entity player = f.ships.spawn("interceptor", QVector2D(0.0f, 0.0f));
|
||||
f.ships.spawn("interceptor", QVector2D(5.0f, 0.0f), /*isEnemy=*/true);
|
||||
f.admin.get<HealthComponent>(player).hp = f.admin.get<HealthComponent>(player).maxHp * 0.1f;
|
||||
|
||||
f.decide();
|
||||
@@ -329,7 +329,7 @@ TEST_CASE("BehaviorSystem: low-HP retreat outranks attacking a nearby enemy", "[
|
||||
TEST_CASE("BehaviorSystem: enemy ships never retreat even at low HP", "[behavior]")
|
||||
{
|
||||
Fixture f;
|
||||
const entt::entity enemy = f.ships.spawn("interceptor", 1, QVector2D(0.0f, 0.0f),
|
||||
const entt::entity enemy = f.ships.spawn("interceptor", QVector2D(0.0f, 0.0f),
|
||||
/*isEnemy=*/true);
|
||||
f.admin.get<HealthComponent>(enemy).hp = f.admin.get<HealthComponent>(enemy).maxHp * 0.05f;
|
||||
|
||||
@@ -347,8 +347,8 @@ TEST_CASE("BehaviorSystem: player combat ship acquires nearest enemy ship in ran
|
||||
"[behavior]")
|
||||
{
|
||||
Fixture f;
|
||||
const entt::entity player = f.ships.spawn("interceptor", 1, QVector2D(0.0f, 0.0f));
|
||||
const entt::entity enemy = f.ships.spawn("interceptor", 1, QVector2D(10.0f, 0.0f),
|
||||
const entt::entity player = f.ships.spawn("interceptor", QVector2D(0.0f, 0.0f));
|
||||
const entt::entity enemy = f.ships.spawn("interceptor", QVector2D(10.0f, 0.0f),
|
||||
/*isEnemy=*/true);
|
||||
|
||||
f.decide();
|
||||
@@ -364,8 +364,8 @@ TEST_CASE("BehaviorSystem: player combat ship does not target friendly ships",
|
||||
"[behavior]")
|
||||
{
|
||||
Fixture f;
|
||||
const entt::entity e1 = f.ships.spawn("interceptor", 1, QVector2D(0.0f, 0.0f));
|
||||
f.ships.spawn("interceptor", 1, QVector2D(5.0f, 0.0f)); // also player
|
||||
const entt::entity e1 = f.ships.spawn("interceptor", QVector2D(0.0f, 0.0f));
|
||||
f.ships.spawn("interceptor", QVector2D(5.0f, 0.0f)); // also player
|
||||
|
||||
f.decide();
|
||||
|
||||
@@ -378,8 +378,8 @@ TEST_CASE("BehaviorSystem: player combat ship ignores enemy beyond engagement ra
|
||||
"[behavior]")
|
||||
{
|
||||
Fixture f;
|
||||
const entt::entity player = f.ships.spawn("interceptor", 1, QVector2D(0.0f, 0.0f));
|
||||
f.ships.spawn("interceptor", 1, QVector2D(500.0f, 0.0f), /*isEnemy=*/true);
|
||||
const entt::entity player = f.ships.spawn("interceptor", QVector2D(0.0f, 0.0f));
|
||||
f.ships.spawn("interceptor", QVector2D(500.0f, 0.0f), /*isEnemy=*/true);
|
||||
|
||||
f.decide();
|
||||
|
||||
@@ -398,10 +398,10 @@ TEST_CASE("BehaviorSystem: overclaim penalty steers a ship off a claimed target"
|
||||
SECTION("no claim: the nearer enemy is chosen")
|
||||
{
|
||||
Fixture f;
|
||||
const entt::entity player = f.ships.spawn("interceptor", 1, QVector2D(0.0f, 0.0f));
|
||||
const entt::entity nearEnemy = f.ships.spawn("interceptor", 1, QVector2D(8.0f, 0.0f),
|
||||
const entt::entity player = f.ships.spawn("interceptor", QVector2D(0.0f, 0.0f));
|
||||
const entt::entity nearEnemy = f.ships.spawn("interceptor", QVector2D(8.0f, 0.0f),
|
||||
/*isEnemy=*/true);
|
||||
f.ships.spawn("interceptor", 1, QVector2D(0.0f, 12.0f), /*isEnemy=*/true);
|
||||
f.ships.spawn("interceptor", QVector2D(0.0f, 12.0f), /*isEnemy=*/true);
|
||||
|
||||
f.decide();
|
||||
|
||||
@@ -412,15 +412,15 @@ TEST_CASE("BehaviorSystem: overclaim penalty steers a ship off a claimed target"
|
||||
{
|
||||
const float d = 10.0f;
|
||||
Fixture f;
|
||||
const entt::entity player = f.ships.spawn("interceptor", 1, QVector2D(0.0f, 0.0f));
|
||||
const entt::entity enemyA = f.ships.spawn("interceptor", 1, QVector2D(d, 0.0f),
|
||||
const entt::entity player = f.ships.spawn("interceptor", QVector2D(0.0f, 0.0f));
|
||||
const entt::entity enemyA = f.ships.spawn("interceptor", QVector2D(d, 0.0f),
|
||||
/*isEnemy=*/true);
|
||||
const entt::entity enemyB = f.ships.spawn("interceptor", 1, QVector2D(0.0f, d),
|
||||
const entt::entity enemyB = f.ships.spawn("interceptor", QVector2D(0.0f, d),
|
||||
/*isEnemy=*/true);
|
||||
|
||||
// A second player ship already commits to enemyA, registering a claim, so
|
||||
// the penalised score of enemyA falls below the unclaimed equidistant enemyB.
|
||||
const entt::entity claimant = f.ships.spawn("interceptor", 1, QVector2D(d, 1.0f));
|
||||
const entt::entity claimant = f.ships.spawn("interceptor", QVector2D(d, 1.0f));
|
||||
f.admin.get<AttackBehavior>(claimant).currentTarget = enemyA;
|
||||
|
||||
f.decide();
|
||||
@@ -437,10 +437,10 @@ TEST_CASE("BehaviorSystem: hysteresis keeps a ship on its own claimed target",
|
||||
{
|
||||
const float d = 10.0f;
|
||||
Fixture f;
|
||||
const entt::entity player = f.ships.spawn("interceptor", 1, QVector2D(0.0f, 0.0f));
|
||||
const entt::entity enemyA = f.ships.spawn("interceptor", 1, QVector2D(d, 0.0f),
|
||||
const entt::entity player = f.ships.spawn("interceptor", QVector2D(0.0f, 0.0f));
|
||||
const entt::entity enemyA = f.ships.spawn("interceptor", QVector2D(d, 0.0f),
|
||||
/*isEnemy=*/true);
|
||||
f.ships.spawn("interceptor", 1, QVector2D(0.0f, d), /*isEnemy=*/true);
|
||||
f.ships.spawn("interceptor", QVector2D(0.0f, d), /*isEnemy=*/true);
|
||||
|
||||
// The ship already holds enemyA; enemyB is equidistant. Without self-exclusion
|
||||
// the ship's own claim would penalise enemyA and flip the choice.
|
||||
@@ -459,10 +459,10 @@ TEST_CASE("BehaviorSystem: a ship switches off a heavily overclaimed target",
|
||||
{
|
||||
const float d = 10.0f;
|
||||
Fixture f;
|
||||
const entt::entity player = f.ships.spawn("interceptor", 1, QVector2D(0.0f, 0.0f));
|
||||
const entt::entity enemyA = f.ships.spawn("interceptor", 1, QVector2D(d, 0.0f),
|
||||
const entt::entity player = f.ships.spawn("interceptor", QVector2D(0.0f, 0.0f));
|
||||
const entt::entity enemyA = f.ships.spawn("interceptor", QVector2D(d, 0.0f),
|
||||
/*isEnemy=*/true);
|
||||
const entt::entity enemyB = f.ships.spawn("interceptor", 1, QVector2D(0.0f, d),
|
||||
const entt::entity enemyB = f.ships.spawn("interceptor", QVector2D(0.0f, d),
|
||||
/*isEnemy=*/true);
|
||||
|
||||
f.admin.get<AttackBehavior>(player).currentTarget = enemyA;
|
||||
@@ -471,7 +471,7 @@ TEST_CASE("BehaviorSystem: a ship switches off a heavily overclaimed target",
|
||||
for (int i = 0; i < 5; ++i)
|
||||
{
|
||||
const entt::entity other =
|
||||
f.ships.spawn("interceptor", 1, QVector2D(d, static_cast<float>(2 + i)));
|
||||
f.ships.spawn("interceptor", QVector2D(d, static_cast<float>(2 + i)));
|
||||
f.admin.get<AttackBehavior>(other).currentTarget = enemyA;
|
||||
}
|
||||
|
||||
@@ -488,8 +488,8 @@ TEST_CASE("BehaviorSystem: enemy ship acquires nearest player ship in range",
|
||||
"[behavior]")
|
||||
{
|
||||
Fixture f;
|
||||
const entt::entity player = f.ships.spawn("interceptor", 1, QVector2D(0.0f, 0.0f));
|
||||
const entt::entity enemy = f.ships.spawn("interceptor", 1, QVector2D(10.0f, 0.0f),
|
||||
const entt::entity player = f.ships.spawn("interceptor", QVector2D(0.0f, 0.0f));
|
||||
const entt::entity enemy = f.ships.spawn("interceptor", QVector2D(10.0f, 0.0f),
|
||||
/*isEnemy=*/true);
|
||||
|
||||
f.decide();
|
||||
@@ -505,7 +505,7 @@ TEST_CASE("BehaviorSystem: enemy ship with no target advances leftward",
|
||||
"[behavior]")
|
||||
{
|
||||
Fixture f;
|
||||
const entt::entity enemy = f.ships.spawn("interceptor", 1, QVector2D(100.0f, 0.0f),
|
||||
const entt::entity enemy = f.ships.spawn("interceptor", QVector2D(100.0f, 0.0f),
|
||||
/*isEnemy=*/true);
|
||||
|
||||
f.decide();
|
||||
@@ -526,7 +526,7 @@ TEST_CASE("BehaviorSystem: advancing ship targets center between enemy defence s
|
||||
f.admin.spawnStation(QPoint(1000, 10), QSize(1, 1), body, 100.0f, 100.0f, /*isEnemy=*/true);
|
||||
f.admin.spawnStation(QPoint(1000, 30), QSize(1, 1), body, 100.0f, 100.0f, /*isEnemy=*/true);
|
||||
|
||||
const entt::entity player = f.ships.spawn("interceptor", 1, QVector2D(0.0f, 0.0f),
|
||||
const entt::entity player = f.ships.spawn("interceptor", QVector2D(0.0f, 0.0f),
|
||||
/*isEnemy=*/false);
|
||||
// Player ships rally until departure; drop Rally so Advance is the fallback.
|
||||
f.ships.triggerRallyDeparture();
|
||||
@@ -549,7 +549,7 @@ TEST_CASE("BehaviorSystem: advancing ship falls back to enemy HQ, then off-world
|
||||
const QVector2D hqPos(5.0f, 7.0f);
|
||||
const entt::entity hq = f.admin.spawnHqProxy(hqPos, 100.0f, 100.0f);
|
||||
|
||||
const entt::entity enemy = f.ships.spawn("interceptor", 1, QVector2D(1000.0f, 0.0f),
|
||||
const entt::entity enemy = f.ships.spawn("interceptor", QVector2D(1000.0f, 0.0f),
|
||||
/*isEnemy=*/true);
|
||||
|
||||
f.decide();
|
||||
@@ -576,9 +576,9 @@ TEST_CASE("BehaviorSystem: repair ship orbits damaged friendly ship",
|
||||
{
|
||||
Fixture f;
|
||||
const ShipLayoutConfig repairLayout = makeSingleModuleLayout("repair_tool");
|
||||
const entt::entity repairShip = f.ships.spawn("repair_ship", 1, QVector2D(0.0f, 0.0f),
|
||||
const entt::entity repairShip = f.ships.spawn("repair_ship", QVector2D(0.0f, 0.0f),
|
||||
false, repairLayout);
|
||||
const entt::entity friendly = f.ships.spawn("interceptor", 1, QVector2D(5.0f, 0.0f));
|
||||
const entt::entity friendly = f.ships.spawn("interceptor", QVector2D(5.0f, 0.0f));
|
||||
|
||||
f.admin.get<HealthComponent>(friendly).hp = f.admin.get<HealthComponent>(friendly).maxHp * 0.5f;
|
||||
|
||||
@@ -605,10 +605,10 @@ TEST_CASE("BehaviorSystem: idle repair ship stands by with the fleet instead of
|
||||
{
|
||||
Fixture f;
|
||||
const ShipLayoutConfig repairLayout = makeSingleModuleLayout("repair_tool");
|
||||
const entt::entity repairShip = f.ships.spawn("repair_ship", 1, QVector2D(0.0f, 0.0f),
|
||||
const entt::entity repairShip = f.ships.spawn("repair_ship", QVector2D(0.0f, 0.0f),
|
||||
false, repairLayout);
|
||||
// A healthy ally (nothing to repair) and a far enemy station (no threat in range).
|
||||
const entt::entity ally = f.ships.spawn("interceptor", 1, QVector2D(50.0f, 0.0f));
|
||||
const entt::entity ally = f.ships.spawn("interceptor", QVector2D(50.0f, 0.0f));
|
||||
const std::vector<QPoint> body{QPoint(0, 0)};
|
||||
f.admin.spawnStation(QPoint(1000, 0), QSize(1, 1), body, 100.0f, 100.0f, /*isEnemy=*/true);
|
||||
|
||||
@@ -628,8 +628,8 @@ TEST_CASE("BehaviorSystem: repair ship heals damaged ally within repair range",
|
||||
{
|
||||
Fixture f;
|
||||
const ShipLayoutConfig repairLayout = makeSingleModuleLayout("repair_tool");
|
||||
f.ships.spawn("repair_ship", 1, QVector2D(0.0f, 0.0f), false, repairLayout);
|
||||
const entt::entity friendly = f.ships.spawn("interceptor", 1, QVector2D(1.0f, 0.0f));
|
||||
f.ships.spawn("repair_ship", QVector2D(0.0f, 0.0f), false, repairLayout);
|
||||
const entt::entity friendly = f.ships.spawn("interceptor", QVector2D(1.0f, 0.0f));
|
||||
|
||||
const float initialHp = f.admin.get<HealthComponent>(friendly).maxHp * 0.5f;
|
||||
f.admin.get<HealthComponent>(friendly).hp = initialHp;
|
||||
@@ -644,8 +644,8 @@ TEST_CASE("BehaviorSystem: repair ship does not heal above maxHp", "[behavior]")
|
||||
{
|
||||
Fixture f;
|
||||
const ShipLayoutConfig repairLayout = makeSingleModuleLayout("repair_tool");
|
||||
f.ships.spawn("repair_ship", 1, QVector2D(0.0f, 0.0f), false, repairLayout);
|
||||
const entt::entity friendly = f.ships.spawn("interceptor", 1, QVector2D(1.0f, 0.0f));
|
||||
f.ships.spawn("repair_ship", QVector2D(0.0f, 0.0f), false, repairLayout);
|
||||
const entt::entity friendly = f.ships.spawn("interceptor", QVector2D(1.0f, 0.0f));
|
||||
|
||||
f.admin.get<HealthComponent>(friendly).hp = f.admin.get<HealthComponent>(friendly).maxHp - 0.001f;
|
||||
|
||||
@@ -666,9 +666,9 @@ TEST_CASE("RepairSystem: tool heals the in-range damaged target chosen by the ex
|
||||
{
|
||||
Fixture f;
|
||||
const ShipLayoutConfig repairLayout = makeSingleModuleLayout("repair_tool");
|
||||
const entt::entity repairShip = f.ships.spawn("repair_ship", 1, QVector2D(0.0f, 0.0f),
|
||||
const entt::entity repairShip = f.ships.spawn("repair_ship", QVector2D(0.0f, 0.0f),
|
||||
false, repairLayout);
|
||||
const entt::entity friendly = f.ships.spawn("interceptor", 1, QVector2D(10.0f, 0.0f));
|
||||
const entt::entity friendly = f.ships.spawn("interceptor", QVector2D(10.0f, 0.0f));
|
||||
|
||||
const float initHp = f.admin.get<HealthComponent>(friendly).maxHp * 0.5f;
|
||||
f.admin.get<HealthComponent>(friendly).hp = initHp;
|
||||
@@ -688,12 +688,12 @@ TEST_CASE("RepairSystem: tool falls back to in-range target when its target is o
|
||||
{
|
||||
Fixture f;
|
||||
const ShipLayoutConfig repairLayout = makeSingleModuleLayout("repair_tool");
|
||||
const entt::entity repairShip = f.ships.spawn("repair_ship", 1, QVector2D(0.0f, 0.0f),
|
||||
const entt::entity repairShip = f.ships.spawn("repair_ship", QVector2D(0.0f, 0.0f),
|
||||
false, repairLayout);
|
||||
// out of repair range (80) but in sensor range (200)
|
||||
const entt::entity outOfRange = f.ships.spawn("interceptor", 1, QVector2D(90.0f, 0.0f));
|
||||
const entt::entity outOfRange = f.ships.spawn("interceptor", QVector2D(90.0f, 0.0f));
|
||||
// within repair range
|
||||
const entt::entity fallback = f.ships.spawn("interceptor", 1, QVector2D(20.0f, 0.0f));
|
||||
const entt::entity fallback = f.ships.spawn("interceptor", QVector2D(20.0f, 0.0f));
|
||||
|
||||
const float outInitHp = f.admin.get<HealthComponent>(outOfRange).maxHp * 0.5f;
|
||||
const float fallbackInitHp = f.admin.get<HealthComponent>(fallback).maxHp * 0.5f;
|
||||
@@ -717,10 +717,10 @@ TEST_CASE("RepairSystem: tool falls back when its target is fully healed",
|
||||
{
|
||||
Fixture f;
|
||||
const ShipLayoutConfig repairLayout = makeSingleModuleLayout("repair_tool");
|
||||
const entt::entity repairShip = f.ships.spawn("repair_ship", 1, QVector2D(0.0f, 0.0f),
|
||||
const entt::entity repairShip = f.ships.spawn("repair_ship", QVector2D(0.0f, 0.0f),
|
||||
false, repairLayout);
|
||||
const entt::entity healed = f.ships.spawn("interceptor", 1, QVector2D(10.0f, 0.0f));
|
||||
const entt::entity fallback = f.ships.spawn("interceptor", 1, QVector2D(15.0f, 0.0f));
|
||||
const entt::entity healed = f.ships.spawn("interceptor", QVector2D(10.0f, 0.0f));
|
||||
const entt::entity fallback = f.ships.spawn("interceptor", QVector2D(15.0f, 0.0f));
|
||||
|
||||
f.admin.get<HealthComponent>(healed).hp = f.admin.get<HealthComponent>(healed).maxHp;
|
||||
const float fallbackInitHp = f.admin.get<HealthComponent>(fallback).maxHp * 0.5f;
|
||||
@@ -740,10 +740,10 @@ TEST_CASE("RepairSystem: tool falls back when its target is destroyed",
|
||||
{
|
||||
Fixture f;
|
||||
const ShipLayoutConfig repairLayout = makeSingleModuleLayout("repair_tool");
|
||||
const entt::entity repairShip = f.ships.spawn("repair_ship", 1, QVector2D(0.0f, 0.0f),
|
||||
const entt::entity repairShip = f.ships.spawn("repair_ship", QVector2D(0.0f, 0.0f),
|
||||
false, repairLayout);
|
||||
const entt::entity gone = f.ships.spawn("interceptor", 1, QVector2D(10.0f, 0.0f));
|
||||
const entt::entity fallback = f.ships.spawn("interceptor", 1, QVector2D(15.0f, 0.0f));
|
||||
const entt::entity gone = f.ships.spawn("interceptor", QVector2D(10.0f, 0.0f));
|
||||
const entt::entity fallback = f.ships.spawn("interceptor", QVector2D(15.0f, 0.0f));
|
||||
|
||||
const float fallbackInitHp = f.admin.get<HealthComponent>(fallback).maxHp * 0.5f;
|
||||
f.admin.get<HealthComponent>(fallback).hp = fallbackInitHp;
|
||||
@@ -763,10 +763,10 @@ TEST_CASE("RepairSystem: tool target is cleared when no repairable target is in
|
||||
{
|
||||
Fixture f;
|
||||
const ShipLayoutConfig repairLayout = makeSingleModuleLayout("repair_tool");
|
||||
const entt::entity repairShip = f.ships.spawn("repair_ship", 1, QVector2D(0.0f, 0.0f),
|
||||
const entt::entity repairShip = f.ships.spawn("repair_ship", QVector2D(0.0f, 0.0f),
|
||||
false, repairLayout);
|
||||
// damaged but beyond repair range (80)
|
||||
const entt::entity outOfRange = f.ships.spawn("interceptor", 1, QVector2D(150.0f, 0.0f));
|
||||
const entt::entity outOfRange = f.ships.spawn("interceptor", QVector2D(150.0f, 0.0f));
|
||||
|
||||
const float initHp = f.admin.get<HealthComponent>(outOfRange).maxHp * 0.5f;
|
||||
f.admin.get<HealthComponent>(outOfRange).hp = initHp;
|
||||
@@ -785,9 +785,9 @@ TEST_CASE("RepairSystem: two repair modules both heal the chosen target additive
|
||||
{
|
||||
Fixture f;
|
||||
const ShipLayoutConfig repairLayout = makeTwoModuleLayout("repair_tool");
|
||||
const entt::entity repairShip = f.ships.spawn("repair_ship", 1, QVector2D(0.0f, 0.0f),
|
||||
const entt::entity repairShip = f.ships.spawn("repair_ship", QVector2D(0.0f, 0.0f),
|
||||
false, repairLayout);
|
||||
const entt::entity targetA = f.ships.spawn("interceptor", 1, QVector2D(10.0f, 0.0f));
|
||||
const entt::entity targetA = f.ships.spawn("interceptor", QVector2D(10.0f, 0.0f));
|
||||
|
||||
const float initHp = f.admin.get<HealthComponent>(targetA).maxHp * 0.5f;
|
||||
f.admin.get<HealthComponent>(targetA).hp = initHp;
|
||||
@@ -813,10 +813,10 @@ TEST_CASE("RepairSystem: two modules both fall back and heal the same target",
|
||||
{
|
||||
Fixture f;
|
||||
const ShipLayoutConfig repairLayout = makeTwoModuleLayout("repair_tool");
|
||||
const entt::entity repairShip = f.ships.spawn("repair_ship", 1, QVector2D(0.0f, 0.0f),
|
||||
const entt::entity repairShip = f.ships.spawn("repair_ship", QVector2D(0.0f, 0.0f),
|
||||
false, repairLayout);
|
||||
const entt::entity healed = f.ships.spawn("interceptor", 1, QVector2D(10.0f, 0.0f));
|
||||
const entt::entity targetB = f.ships.spawn("interceptor", 1, QVector2D(20.0f, 0.0f));
|
||||
const entt::entity healed = f.ships.spawn("interceptor", QVector2D(10.0f, 0.0f));
|
||||
const entt::entity targetB = f.ships.spawn("interceptor", QVector2D(20.0f, 0.0f));
|
||||
|
||||
f.admin.get<HealthComponent>(healed).hp = f.admin.get<HealthComponent>(healed).maxHp;
|
||||
const float initHp = f.admin.get<HealthComponent>(targetB).maxHp * 0.5f;
|
||||
@@ -847,7 +847,7 @@ TEST_CASE("RepairSystem: does not crash when a tool's owner is not a repair ship
|
||||
Fixture f;
|
||||
|
||||
// Bare child entity: RepairToolComponent + ModuleOwnerComponent, owner is a combat ship.
|
||||
const entt::entity ownerShip = f.ships.spawn("interceptor", 1, QVector2D(0.0f, 0.0f));
|
||||
const entt::entity ownerShip = f.ships.spawn("interceptor", QVector2D(0.0f, 0.0f));
|
||||
const entt::entity moduleEntity = f.admin.createModuleEntity();
|
||||
RepairToolComponent rt;
|
||||
rt.repairAmountHp = 1.0f;
|
||||
@@ -868,7 +868,7 @@ TEST_CASE("RepairSystem: repair tool does not repair an HQ", "[behavior]")
|
||||
{
|
||||
Fixture f;
|
||||
const ShipLayoutConfig repairLayout = makeSingleModuleLayout("repair_tool");
|
||||
const entt::entity repairShip = f.ships.spawn("repair_ship", 1, QVector2D(0.0f, 0.0f),
|
||||
const entt::entity repairShip = f.ships.spawn("repair_ship", QVector2D(0.0f, 0.0f),
|
||||
false, repairLayout);
|
||||
|
||||
// A damaged, same-faction HQ in repair range — spawned as a station and tagged
|
||||
@@ -890,7 +890,7 @@ TEST_CASE("RepairSystem: repair tool still repairs a damaged defence station", "
|
||||
{
|
||||
Fixture f;
|
||||
const ShipLayoutConfig repairLayout = makeSingleModuleLayout("repair_tool");
|
||||
const entt::entity repairShip = f.ships.spawn("repair_ship", 1, QVector2D(0.0f, 0.0f),
|
||||
const entt::entity repairShip = f.ships.spawn("repair_ship", QVector2D(0.0f, 0.0f),
|
||||
false, repairLayout);
|
||||
|
||||
// A damaged, same-faction defence station (no HQ tag) in repair range.
|
||||
@@ -914,7 +914,7 @@ TEST_CASE("BehaviorSystem: salvage ship orbits nearest scrap", "[behavior]")
|
||||
{
|
||||
Fixture f;
|
||||
const ShipLayoutConfig salvageLayout = makeSingleModuleLayout("salvager");
|
||||
const entt::entity ship = f.ships.spawn("salvage_ship", 1, QVector2D(0.0f, 0.0f),
|
||||
const entt::entity ship = f.ships.spawn("salvage_ship", QVector2D(0.0f, 0.0f),
|
||||
false, salvageLayout);
|
||||
|
||||
const QVector2D scrapPos(100.0f, 0.0f);
|
||||
@@ -937,7 +937,7 @@ TEST_CASE("BehaviorSystem: salvage ship collects scrap on arrival", "[behavior]"
|
||||
{
|
||||
Fixture f;
|
||||
const ShipLayoutConfig salvageLayout = makeSingleModuleLayout("salvager");
|
||||
const entt::entity ship = f.ships.spawn("salvage_ship", 1, QVector2D(0.0f, 0.0f),
|
||||
const entt::entity ship = f.ships.spawn("salvage_ship", QVector2D(0.0f, 0.0f),
|
||||
false, salvageLayout);
|
||||
const entt::entity scrapEntity = f.scraps.spawn(QVector2D(0.0f, 0.0f), 1, 100000);
|
||||
|
||||
@@ -967,7 +967,7 @@ TEST_CASE("BehaviorSystem: full-cargo salvage ship moves toward SalvageBay", "[b
|
||||
REQUIRE(f.buildings.findBuilding(bayId) != nullptr);
|
||||
|
||||
const ShipLayoutConfig salvageLayout = makeSingleModuleLayout("salvager");
|
||||
const entt::entity ship = f.ships.spawn("salvage_ship", 1, QVector2D(5.0f, 0.0f),
|
||||
const entt::entity ship = f.ships.spawn("salvage_ship", QVector2D(5.0f, 0.0f),
|
||||
false, salvageLayout);
|
||||
{
|
||||
REQUIRE(f.admin.isValid(firstSalvageChild(f.admin, ship)));
|
||||
@@ -1001,7 +1001,7 @@ TEST_CASE("SalvagerSystem: module does not collect scrap beyond its collection r
|
||||
// collection_range_m_formula = "50"; scrap at distance 55 must not be collected.
|
||||
Fixture f;
|
||||
const ShipLayoutConfig salvageLayout = makeSingleModuleLayout("salvager");
|
||||
const entt::entity ship = f.ships.spawn("salvage_ship", 1, QVector2D(0.0f, 0.0f),
|
||||
const entt::entity ship = f.ships.spawn("salvage_ship", QVector2D(0.0f, 0.0f),
|
||||
false, salvageLayout);
|
||||
f.scraps.spawn(QVector2D(55.0f, 0.0f), 1, 100000);
|
||||
|
||||
@@ -1016,7 +1016,7 @@ TEST_CASE("SalvagerSystem: module collects scrap within its collection range",
|
||||
// collection_range_m_formula = "50"; scrap at distance 45 must be collected.
|
||||
Fixture f;
|
||||
const ShipLayoutConfig salvageLayout = makeSingleModuleLayout("salvager");
|
||||
const entt::entity ship = f.ships.spawn("salvage_ship", 1, QVector2D(0.0f, 0.0f),
|
||||
const entt::entity ship = f.ships.spawn("salvage_ship", QVector2D(0.0f, 0.0f),
|
||||
false, salvageLayout);
|
||||
f.scraps.spawn(QVector2D(45.0f, 0.0f), 1, 100000);
|
||||
|
||||
@@ -1033,7 +1033,7 @@ TEST_CASE("SalvagerSystem: collection sets cooldown on module", "[behavior]")
|
||||
{
|
||||
Fixture f;
|
||||
const ShipLayoutConfig salvageLayout = makeSingleModuleLayout("salvager");
|
||||
const entt::entity ship = f.ships.spawn("salvage_ship", 1, QVector2D(0.0f, 0.0f),
|
||||
const entt::entity ship = f.ships.spawn("salvage_ship", QVector2D(0.0f, 0.0f),
|
||||
false, salvageLayout);
|
||||
f.scraps.spawn(QVector2D(0.0f, 0.0f), 1, 100000);
|
||||
|
||||
@@ -1051,7 +1051,7 @@ TEST_CASE("SalvagerSystem: module on cooldown does not collect scrap", "[behavio
|
||||
{
|
||||
Fixture f;
|
||||
const ShipLayoutConfig salvageLayout = makeSingleModuleLayout("salvager");
|
||||
const entt::entity ship = f.ships.spawn("salvage_ship", 1, QVector2D(0.0f, 0.0f),
|
||||
const entt::entity ship = f.ships.spawn("salvage_ship", QVector2D(0.0f, 0.0f),
|
||||
false, salvageLayout);
|
||||
f.scraps.spawn(QVector2D(0.0f, 0.0f), 1, 100000);
|
||||
|
||||
@@ -1066,7 +1066,7 @@ TEST_CASE("SalvagerSystem: module collects again after cooldown expires", "[beha
|
||||
{
|
||||
Fixture f;
|
||||
const ShipLayoutConfig salvageLayout = makeSingleModuleLayout("salvager");
|
||||
const entt::entity ship = f.ships.spawn("salvage_ship", 1, QVector2D(0.0f, 0.0f),
|
||||
const entt::entity ship = f.ships.spawn("salvage_ship", QVector2D(0.0f, 0.0f),
|
||||
false, salvageLayout);
|
||||
const entt::entity sc = firstSalvageChild(f.admin, ship);
|
||||
|
||||
@@ -1093,7 +1093,7 @@ TEST_CASE("SalvagerSystem: two salvage modules collect independently in same tic
|
||||
{
|
||||
Fixture f;
|
||||
const ShipLayoutConfig salvageLayout = makeTwoModuleLayout("salvager");
|
||||
const entt::entity ship = f.ships.spawn("salvage_ship", 1, QVector2D(0.0f, 0.0f),
|
||||
const entt::entity ship = f.ships.spawn("salvage_ship", QVector2D(0.0f, 0.0f),
|
||||
false, salvageLayout);
|
||||
|
||||
f.scraps.spawn(QVector2D(0.0f, 0.0f), 1, 100000);
|
||||
@@ -1110,7 +1110,7 @@ TEST_CASE("SalvagerSystem: second salvage module does not collect when first is
|
||||
// One module on cooldown, one ready: only the ready module collects.
|
||||
Fixture f;
|
||||
const ShipLayoutConfig salvageLayout = makeTwoModuleLayout("salvager");
|
||||
const entt::entity ship = f.ships.spawn("salvage_ship", 1, QVector2D(0.0f, 0.0f),
|
||||
const entt::entity ship = f.ships.spawn("salvage_ship", QVector2D(0.0f, 0.0f),
|
||||
false, salvageLayout);
|
||||
|
||||
// Put the first salvage child on cooldown.
|
||||
@@ -1141,7 +1141,7 @@ TEST_CASE("SalvagerSystem: second salvage module does not collect when first is
|
||||
TEST_CASE("SensorRange: sensorRange is populated from config formula at spawn", "[sensor]")
|
||||
{
|
||||
Fixture f;
|
||||
const entt::entity e = f.ships.spawn("interceptor", 1, QVector2D(0.0f, 0.0f));
|
||||
const entt::entity e = f.ships.spawn("interceptor", QVector2D(0.0f, 0.0f));
|
||||
REQUIRE(f.admin.get<SensorRangeComponent>(e).value_tiles == Approx(200.0f));
|
||||
}
|
||||
|
||||
@@ -1152,8 +1152,8 @@ TEST_CASE("SensorRange: sensorRange is populated from config formula at spawn",
|
||||
TEST_CASE("SensorRange: player combat ship acquires enemy just inside sensor range", "[sensor]")
|
||||
{
|
||||
Fixture f;
|
||||
const entt::entity player = f.ships.spawn("interceptor", 1, QVector2D(0.0f, 0.0f));
|
||||
const entt::entity enemy = f.ships.spawn("interceptor", 1, QVector2D(190.0f, 0.0f),
|
||||
const entt::entity player = f.ships.spawn("interceptor", QVector2D(0.0f, 0.0f));
|
||||
const entt::entity enemy = f.ships.spawn("interceptor", QVector2D(190.0f, 0.0f),
|
||||
/*isEnemy=*/true);
|
||||
|
||||
f.decide();
|
||||
@@ -1164,8 +1164,8 @@ TEST_CASE("SensorRange: player combat ship acquires enemy just inside sensor ran
|
||||
TEST_CASE("SensorRange: player combat ship ignores enemy just outside sensor range", "[sensor]")
|
||||
{
|
||||
Fixture f;
|
||||
const entt::entity player = f.ships.spawn("interceptor", 1, QVector2D(0.0f, 0.0f));
|
||||
f.ships.spawn("interceptor", 1, QVector2D(210.0f, 0.0f), /*isEnemy=*/true);
|
||||
const entt::entity player = f.ships.spawn("interceptor", QVector2D(0.0f, 0.0f));
|
||||
f.ships.spawn("interceptor", QVector2D(210.0f, 0.0f), /*isEnemy=*/true);
|
||||
|
||||
f.decide();
|
||||
|
||||
@@ -1175,8 +1175,8 @@ TEST_CASE("SensorRange: player combat ship ignores enemy just outside sensor ran
|
||||
TEST_CASE("SensorRange: enemy ship ignores player just outside sensor range", "[sensor]")
|
||||
{
|
||||
Fixture f;
|
||||
f.ships.spawn("interceptor", 1, QVector2D(0.0f, 0.0f));
|
||||
const entt::entity enemy = f.ships.spawn("interceptor", 1, QVector2D(210.0f, 0.0f),
|
||||
f.ships.spawn("interceptor", QVector2D(0.0f, 0.0f));
|
||||
const entt::entity enemy = f.ships.spawn("interceptor", QVector2D(210.0f, 0.0f),
|
||||
/*isEnemy=*/true);
|
||||
|
||||
f.decide();
|
||||
@@ -1194,9 +1194,9 @@ TEST_CASE("SensorRange: repair ship retreats from enemy within sensor range", "[
|
||||
const QVector2D rallyPoint(-100.0f, 0.0f);
|
||||
f.ships.setRallyPoint(rallyPoint);
|
||||
const ShipLayoutConfig repairLayout = makeSingleModuleLayout("repair_tool");
|
||||
const entt::entity repairShip = f.ships.spawn("repair_ship", 1, QVector2D(0.0f, 0.0f),
|
||||
const entt::entity repairShip = f.ships.spawn("repair_ship", QVector2D(0.0f, 0.0f),
|
||||
false, repairLayout);
|
||||
f.ships.spawn("interceptor", 1, QVector2D(200.0f, 0.0f), /*isEnemy=*/true);
|
||||
f.ships.spawn("interceptor", QVector2D(200.0f, 0.0f), /*isEnemy=*/true);
|
||||
|
||||
f.decide();
|
||||
|
||||
@@ -1208,9 +1208,9 @@ TEST_CASE("SensorRange: repair ship does not retreat from enemy beyond sensor ra
|
||||
{
|
||||
Fixture f;
|
||||
const ShipLayoutConfig repairLayout = makeSingleModuleLayout("repair_tool");
|
||||
const entt::entity repairShip = f.ships.spawn("repair_ship", 1, QVector2D(0.0f, 0.0f),
|
||||
const entt::entity repairShip = f.ships.spawn("repair_ship", QVector2D(0.0f, 0.0f),
|
||||
false, repairLayout);
|
||||
f.ships.spawn("interceptor", 1, QVector2D(300.0f, 0.0f), /*isEnemy=*/true);
|
||||
f.ships.spawn("interceptor", QVector2D(300.0f, 0.0f), /*isEnemy=*/true);
|
||||
|
||||
f.decide();
|
||||
|
||||
@@ -1225,9 +1225,9 @@ TEST_CASE("SensorRange: repair ship does not acquire damaged ally beyond sensor
|
||||
{
|
||||
Fixture f;
|
||||
const ShipLayoutConfig repairLayout = makeSingleModuleLayout("repair_tool");
|
||||
const entt::entity repairShip = f.ships.spawn("repair_ship", 1, QVector2D(0.0f, 0.0f),
|
||||
const entt::entity repairShip = f.ships.spawn("repair_ship", QVector2D(0.0f, 0.0f),
|
||||
false, repairLayout);
|
||||
const entt::entity friendly = f.ships.spawn("interceptor", 1, QVector2D(300.0f, 0.0f));
|
||||
const entt::entity friendly = f.ships.spawn("interceptor", QVector2D(300.0f, 0.0f));
|
||||
f.admin.get<HealthComponent>(friendly).hp = f.admin.get<HealthComponent>(friendly).maxHp * 0.5f;
|
||||
|
||||
f.decide();
|
||||
@@ -1243,7 +1243,7 @@ TEST_CASE("SensorRange: salvage ship ignores scrap beyond sensor range", "[senso
|
||||
{
|
||||
Fixture f;
|
||||
const ShipLayoutConfig salvageLayout = makeSingleModuleLayout("salvager");
|
||||
const entt::entity ship = f.ships.spawn("salvage_ship", 1, QVector2D(0.0f, 0.0f),
|
||||
const entt::entity ship = f.ships.spawn("salvage_ship", QVector2D(0.0f, 0.0f),
|
||||
false, salvageLayout);
|
||||
f.scraps.spawn(QVector2D(300.0f, 0.0f), 1, 100000);
|
||||
|
||||
@@ -1261,8 +1261,8 @@ TEST_CASE("Orbit: combat ship's intent carries the target center and orbit radiu
|
||||
"[orbit]")
|
||||
{
|
||||
Fixture f;
|
||||
const entt::entity player = f.ships.spawn("interceptor", 1, QVector2D(0.0f, 0.0f));
|
||||
const entt::entity enemy = f.ships.spawn("interceptor", 1, QVector2D(10.0f, 0.0f),
|
||||
const entt::entity player = f.ships.spawn("interceptor", QVector2D(0.0f, 0.0f));
|
||||
const entt::entity enemy = f.ships.spawn("interceptor", QVector2D(10.0f, 0.0f),
|
||||
/*isEnemy=*/true);
|
||||
|
||||
f.decide();
|
||||
@@ -1282,7 +1282,7 @@ TEST_CASE("Orbit: rally ship orbits the rally point at the configured rally radi
|
||||
Fixture f;
|
||||
const QVector2D rallyPoint(-50.0f, 0.0f);
|
||||
f.ships.setRallyPoint(rallyPoint);
|
||||
const entt::entity player = f.ships.spawn("interceptor", 1, QVector2D(0.0f, 0.0f));
|
||||
const entt::entity player = f.ships.spawn("interceptor", QVector2D(0.0f, 0.0f));
|
||||
|
||||
f.decide();
|
||||
|
||||
@@ -1298,8 +1298,8 @@ TEST_CASE("Orbit: combat ship settles near the orbit radius and circles a statio
|
||||
{
|
||||
Fixture f;
|
||||
const QVector2D enemyPos(80.0f, 0.0f);
|
||||
const entt::entity player = f.ships.spawn("interceptor", 1, QVector2D(0.0f, 0.0f));
|
||||
const entt::entity enemy = f.ships.spawn("interceptor", 1, enemyPos, /*isEnemy=*/true);
|
||||
const entt::entity player = f.ships.spawn("interceptor", QVector2D(0.0f, 0.0f));
|
||||
const entt::entity enemy = f.ships.spawn("interceptor", enemyPos, /*isEnemy=*/true);
|
||||
|
||||
const float orbitRadius = f.admin.get<AttackBehavior>(player).orbitRadius_tiles;
|
||||
REQUIRE(orbitRadius > 0.0f);
|
||||
|
||||
@@ -107,8 +107,8 @@ TEST_CASE("CombatSystem: ship fires when cooldown=0 and target in range", "[comb
|
||||
const ShipDef* combatDef = findCombatShip(f.cfg);
|
||||
REQUIRE(combatDef != nullptr);
|
||||
|
||||
const entt::entity enemy = f.ships.spawn(combatDef->id, 1, QVector2D(5.0f, 5.0f), true);
|
||||
const entt::entity player = f.ships.spawn(combatDef->id, 1, QVector2D(4.0f, 5.0f), false);
|
||||
const entt::entity enemy = f.ships.spawn(combatDef->id, QVector2D(5.0f, 5.0f), true);
|
||||
const entt::entity player = f.ships.spawn(combatDef->id, QVector2D(4.0f, 5.0f), false);
|
||||
f.wireEnemyTarget(enemy, player);
|
||||
|
||||
const float hpBefore = f.admin.get<HealthComponent>(player).hp;
|
||||
@@ -127,8 +127,8 @@ TEST_CASE("CombatSystem: cooldown prevents firing before it expires", "[combat]"
|
||||
const ShipDef* combatDef = findCombatShip(f.cfg);
|
||||
REQUIRE(combatDef != nullptr);
|
||||
|
||||
const entt::entity enemy = f.ships.spawn(combatDef->id, 1, QVector2D(5.0f, 5.0f), true);
|
||||
const entt::entity player = f.ships.spawn(combatDef->id, 1, QVector2D(4.0f, 5.0f), false);
|
||||
const entt::entity enemy = f.ships.spawn(combatDef->id, QVector2D(5.0f, 5.0f), true);
|
||||
const entt::entity player = f.ships.spawn(combatDef->id, QVector2D(4.0f, 5.0f), false);
|
||||
|
||||
f.wireEnemyTarget(enemy, player);
|
||||
{
|
||||
@@ -163,8 +163,8 @@ TEST_CASE("CombatSystem: no fire when target is out of range", "[combat]")
|
||||
const ShipDef* combatDef = findCombatShip(f.cfg);
|
||||
REQUIRE(combatDef != nullptr);
|
||||
|
||||
const entt::entity enemy = f.ships.spawn(combatDef->id, 1, QVector2D(0.0f, 0.0f), true);
|
||||
const entt::entity player = f.ships.spawn(combatDef->id, 1, QVector2D(500.0f, 0.0f), false);
|
||||
const entt::entity enemy = f.ships.spawn(combatDef->id, QVector2D(0.0f, 0.0f), true);
|
||||
const entt::entity player = f.ships.spawn(combatDef->id, QVector2D(500.0f, 0.0f), false);
|
||||
f.wireEnemyTarget(enemy, player);
|
||||
|
||||
std::vector<BeamFiredEvent> events;
|
||||
@@ -200,7 +200,7 @@ TEST_CASE("CombatSystem: player station fires at enemy ship in range", "[combat]
|
||||
REQUIRE(combatDef != nullptr);
|
||||
|
||||
const entt::entity enemyShip = sim.ships().spawn(
|
||||
combatDef->id, 1,
|
||||
combatDef->id,
|
||||
QVector2D(stationCenter.x() + 1.0f, stationCenter.y()),
|
||||
/*isEnemy=*/true);
|
||||
|
||||
@@ -238,7 +238,7 @@ TEST_CASE("CombatSystem: enemy station fires at player ship in range", "[combat]
|
||||
REQUIRE(combatDef != nullptr);
|
||||
|
||||
sim.ships().spawn(
|
||||
combatDef->id, 1,
|
||||
combatDef->id,
|
||||
QVector2D(stationCenter.x() - 1.0f, stationCenter.y()),
|
||||
/*isEnemy=*/false);
|
||||
|
||||
@@ -276,7 +276,7 @@ TEST_CASE("CombatSystem: player ship fires at enemy station in range", "[combat]
|
||||
REQUIRE(combatDef != nullptr);
|
||||
|
||||
const entt::entity playerShip = sim.ships().spawn(
|
||||
combatDef->id, 1,
|
||||
combatDef->id,
|
||||
QVector2D(stationCenter.x() - 1.0f, stationCenter.y()),
|
||||
/*isEnemy=*/false);
|
||||
|
||||
@@ -304,8 +304,8 @@ TEST_CASE("CombatSystem: damage not applied before impact tick", "[combat]")
|
||||
const ShipDef* combatDef = findCombatShip(f.cfg);
|
||||
REQUIRE(combatDef != nullptr);
|
||||
|
||||
const entt::entity enemy = f.ships.spawn(combatDef->id, 1, QVector2D(5.0f, 5.0f), true);
|
||||
const entt::entity player = f.ships.spawn(combatDef->id, 1, QVector2D(4.0f, 5.0f), false);
|
||||
const entt::entity enemy = f.ships.spawn(combatDef->id, QVector2D(5.0f, 5.0f), true);
|
||||
const entt::entity player = f.ships.spawn(combatDef->id, QVector2D(4.0f, 5.0f), false);
|
||||
f.wireEnemyTarget(enemy, player);
|
||||
|
||||
const float hpBefore = f.admin.get<HealthComponent>(player).hp;
|
||||
@@ -326,8 +326,8 @@ TEST_CASE("CombatSystem: damage applied exactly at impact tick", "[combat]")
|
||||
const ShipDef* combatDef = findCombatShip(f.cfg);
|
||||
REQUIRE(combatDef != nullptr);
|
||||
|
||||
const entt::entity enemy = f.ships.spawn(combatDef->id, 1, QVector2D(5.0f, 5.0f), true);
|
||||
const entt::entity player = f.ships.spawn(combatDef->id, 1, QVector2D(4.0f, 5.0f), false);
|
||||
const entt::entity enemy = f.ships.spawn(combatDef->id, QVector2D(5.0f, 5.0f), true);
|
||||
const entt::entity player = f.ships.spawn(combatDef->id, QVector2D(4.0f, 5.0f), false);
|
||||
f.wireEnemyTarget(enemy, player);
|
||||
|
||||
const float hpBefore = f.admin.get<HealthComponent>(player).hp;
|
||||
@@ -345,8 +345,8 @@ TEST_CASE("CombatSystem: damage silently dropped if target already dead", "[comb
|
||||
const ShipDef* combatDef = findCombatShip(f.cfg);
|
||||
REQUIRE(combatDef != nullptr);
|
||||
|
||||
const entt::entity enemy = f.ships.spawn(combatDef->id, 1, QVector2D(5.0f, 5.0f), true);
|
||||
const entt::entity player = f.ships.spawn(combatDef->id, 1, QVector2D(4.0f, 5.0f), false);
|
||||
const entt::entity enemy = f.ships.spawn(combatDef->id, QVector2D(5.0f, 5.0f), true);
|
||||
const entt::entity player = f.ships.spawn(combatDef->id, QVector2D(4.0f, 5.0f), false);
|
||||
f.wireEnemyTarget(enemy, player);
|
||||
|
||||
std::vector<BeamFiredEvent> events;
|
||||
@@ -366,8 +366,8 @@ TEST_CASE("CombatSystem: damage still applied if shooter already dead", "[combat
|
||||
const ShipDef* combatDef = findCombatShip(f.cfg);
|
||||
REQUIRE(combatDef != nullptr);
|
||||
|
||||
const entt::entity enemy = f.ships.spawn(combatDef->id, 1, QVector2D(5.0f, 5.0f), true);
|
||||
const entt::entity player = f.ships.spawn(combatDef->id, 1, QVector2D(4.0f, 5.0f), false);
|
||||
const entt::entity enemy = f.ships.spawn(combatDef->id, QVector2D(5.0f, 5.0f), true);
|
||||
const entt::entity player = f.ships.spawn(combatDef->id, QVector2D(4.0f, 5.0f), false);
|
||||
f.wireEnemyTarget(enemy, player);
|
||||
|
||||
const float hpBefore = f.admin.get<HealthComponent>(player).hp;
|
||||
@@ -393,7 +393,7 @@ TEST_CASE("CombatSystem: dead ship is removed after tick step 9", "[combat]")
|
||||
const ShipDef* combatDef = findCombatShip(sim.config());
|
||||
REQUIRE(combatDef != nullptr);
|
||||
|
||||
const entt::entity ship = sim.ships().spawn(combatDef->id, 1,
|
||||
const entt::entity ship = sim.ships().spawn(combatDef->id,
|
||||
QVector2D(10.0f, 10.0f));
|
||||
|
||||
sim.admin().get<HealthComponent>(ship).hp = -1.0f;
|
||||
@@ -411,7 +411,7 @@ TEST_CASE("CombatSystem: scrap is spawned on ship death", "[combat]")
|
||||
// (REQ-RES-SCRAP-DROP): round(threat * scrap_per_threat). The interceptor's
|
||||
// threat is 59.0 and the test config sets scrap_per_threat = 1.0, so it drops
|
||||
// round(59.0 * 1.0) = 59 scrap.
|
||||
const entt::entity ship = sim.ships().spawn("interceptor", 1,
|
||||
const entt::entity ship = sim.ships().spawn("interceptor",
|
||||
QVector2D(10.0f, 10.0f));
|
||||
sim.admin().get<HealthComponent>(ship).hp = -1.0f;
|
||||
|
||||
|
||||
@@ -38,14 +38,13 @@ TEST_CASE("ConfigLoader: loadModules parses modules.toml", "[config][modules]")
|
||||
CHECK(armor.materials.size() == 1);
|
||||
CHECK(armor.materials[0].item == "iron_ingot");
|
||||
CHECK(armor.materials[0].amount == 2);
|
||||
CHECK(armor.playerProductionLevel == 1);
|
||||
CHECK(armor.productionTimeSeconds == Approx(3.0));
|
||||
CHECK(armor.fillColor == "#808080");
|
||||
CHECK(armor.glyph == "A");
|
||||
REQUIRE(armor.statModifiers.size() == 1);
|
||||
CHECK(armor.statModifiers[0].stat == "hp");
|
||||
CHECK(armor.statModifiers[0].modifierType == "multiplicative");
|
||||
CHECK(armor.statModifiers[0].formula.evaluate(1.0) == Approx(1.5));
|
||||
CHECK(armor.statModifiers[0].value == Approx(1.5));
|
||||
}
|
||||
|
||||
TEST_CASE("ConfigLoader: loadModules parses additive modifiers", "[config][modules]")
|
||||
@@ -58,7 +57,7 @@ TEST_CASE("ConfigLoader: loadModules parses additive modifiers", "[config][modul
|
||||
REQUIRE(sensor.statModifiers.size() == 1);
|
||||
CHECK(sensor.statModifiers[0].stat == "sensor_range");
|
||||
CHECK(sensor.statModifiers[0].modifierType == "additive");
|
||||
CHECK(sensor.statModifiers[0].formula.evaluate(1.0) == Approx(100.0));
|
||||
CHECK(sensor.statModifiers[0].value == Approx(100.0));
|
||||
}
|
||||
|
||||
TEST_CASE("ConfigLoader: multiplicative modifier with unit suffix is parsed (weapon_primer)", "[config][modules]")
|
||||
@@ -70,7 +69,7 @@ TEST_CASE("ConfigLoader: multiplicative modifier with unit suffix is parsed (wea
|
||||
const ModuleStatModifier* sm = findModifier(*primer, "attack_rate");
|
||||
REQUIRE(sm != nullptr);
|
||||
CHECK(sm->modifierType == "multiplicative");
|
||||
CHECK(sm->formula.evaluate(1.0) == Approx(1.2));
|
||||
CHECK(sm->value == Approx(1.2));
|
||||
}
|
||||
|
||||
TEST_CASE("ConfigLoader: weapon_stabilizer parses two multiplicative weapon modifiers", "[config][modules]")
|
||||
@@ -83,12 +82,12 @@ TEST_CASE("ConfigLoader: weapon_stabilizer parses two multiplicative weapon modi
|
||||
const ModuleStatModifier* rangeMod = findModifier(*stab, "attack_range");
|
||||
REQUIRE(rangeMod != nullptr);
|
||||
CHECK(rangeMod->modifierType == "multiplicative");
|
||||
CHECK(rangeMod->formula.evaluate(1.0) == Approx(1.5));
|
||||
CHECK(rangeMod->value == Approx(1.5));
|
||||
|
||||
const ModuleStatModifier* rateMod = findModifier(*stab, "attack_rate");
|
||||
REQUIRE(rateMod != nullptr);
|
||||
CHECK(rateMod->modifierType == "multiplicative");
|
||||
CHECK(rateMod->formula.evaluate(1.0) == Approx(0.8));
|
||||
CHECK(rateMod->value == Approx(0.8));
|
||||
}
|
||||
|
||||
TEST_CASE("ConfigLoader: afterburner parses multiplicative speed and additive main_acceleration", "[config][modules]")
|
||||
@@ -101,12 +100,12 @@ TEST_CASE("ConfigLoader: afterburner parses multiplicative speed and additive ma
|
||||
const ModuleStatModifier* speedMod = findModifier(*ab, "speed");
|
||||
REQUIRE(speedMod != nullptr);
|
||||
CHECK(speedMod->modifierType == "multiplicative");
|
||||
CHECK(speedMod->formula.evaluate(1.0) == Approx(1.6));
|
||||
CHECK(speedMod->value == Approx(1.6));
|
||||
|
||||
const ModuleStatModifier* accelMod = findModifier(*ab, "main_acceleration");
|
||||
REQUIRE(accelMod != nullptr);
|
||||
CHECK(accelMod->modifierType == "additive");
|
||||
CHECK(accelMod->formula.evaluate(1.0) == Approx(60.0));
|
||||
CHECK(accelMod->value == Approx(60.0));
|
||||
}
|
||||
|
||||
TEST_CASE("ConfigLoader: maneuvering_thrusters parses multiplicative speed and additive maneuvering_acceleration", "[config][modules]")
|
||||
@@ -119,12 +118,12 @@ TEST_CASE("ConfigLoader: maneuvering_thrusters parses multiplicative speed and a
|
||||
const ModuleStatModifier* speedMod = findModifier(*mt, "speed");
|
||||
REQUIRE(speedMod != nullptr);
|
||||
CHECK(speedMod->modifierType == "multiplicative");
|
||||
CHECK(speedMod->formula.evaluate(1.0) == Approx(1.2));
|
||||
CHECK(speedMod->value == Approx(1.2));
|
||||
|
||||
const ModuleStatModifier* accelMod = findModifier(*mt, "maneuvering_acceleration");
|
||||
REQUIRE(accelMod != nullptr);
|
||||
CHECK(accelMod->modifierType == "additive");
|
||||
CHECK(accelMod->formula.evaluate(1.0) == Approx(10.0));
|
||||
CHECK(accelMod->value == Approx(10.0));
|
||||
}
|
||||
|
||||
TEST_CASE("ConfigLoader: loadShips parses layout field", "[config][ships]")
|
||||
|
||||
@@ -300,13 +300,6 @@ TEST_CASE("RecipeSchematic: newlyUnlockedItemNames is sorted, deduplicated, and
|
||||
{
|
||||
CHECK(opt.newlyUnlockedItemNames[j - 1] < opt.newlyUnlockedItemNames[j]);
|
||||
}
|
||||
|
||||
// A level-up doesn't change the explicit unlock state, so the
|
||||
// implicit unlock set - and thus this preview - must be empty.
|
||||
if (!opt.isNewUnlock)
|
||||
{
|
||||
CHECK(opt.newlyUnlockedItemNames.empty());
|
||||
}
|
||||
}
|
||||
|
||||
SimulationTestAccess::applySchematicChoice(sim, 0);
|
||||
@@ -361,3 +354,54 @@ TEST_CASE("RecipeSchematic: newlyUnlockedItemNames matches recipes that actually
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Owned ship/module schematics leave the drop pool (REQ-DEF-SCHEMATIC-DROP)
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
TEST_CASE("SchematicDrop: an owned ship schematic is never offered again",
|
||||
"[recipe_schematic]")
|
||||
{
|
||||
// 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());
|
||||
REQUIRE_FALSE(sim.isSchematicUnlocked("repair_ship"));
|
||||
|
||||
bool wasUnlocked = false;
|
||||
for (int i = 0; i < 200; ++i)
|
||||
{
|
||||
killEnemyStations(sim);
|
||||
if (!sim.hasSchematicChoicesPending()) { continue; }
|
||||
|
||||
const std::vector<SchematicChoiceOption>& choices =
|
||||
sim.getPendingSchematicChoices();
|
||||
|
||||
// Locate the repair_ship option, if present in this drop.
|
||||
int repairShipIndex = -1;
|
||||
for (int j = 0; j < static_cast<int>(choices.size()); ++j)
|
||||
{
|
||||
if (choices[j].schematicId == "repair_ship") { repairShipIndex = j; }
|
||||
}
|
||||
|
||||
// Once owned, it must never appear in the pool again.
|
||||
if (sim.isSchematicUnlocked("repair_ship"))
|
||||
{
|
||||
CHECK(repairShipIndex == -1);
|
||||
}
|
||||
|
||||
// Prefer picking repair_ship the first time it shows up so we exercise
|
||||
// the transition from unowned to owned; otherwise just take choice 0.
|
||||
int pick = 0;
|
||||
if (repairShipIndex >= 0 && !sim.isSchematicUnlocked("repair_ship"))
|
||||
{
|
||||
pick = repairShipIndex;
|
||||
wasUnlocked = true;
|
||||
}
|
||||
SimulationTestAccess::applySchematicChoice(sim, pick);
|
||||
}
|
||||
|
||||
// Sanity: we actually unlocked it at some point, so the exclusion CHECK above
|
||||
// was meaningfully exercised.
|
||||
CHECK(wasUnlocked);
|
||||
CHECK(sim.isSchematicUnlocked("repair_ship"));
|
||||
}
|
||||
|
||||
|
||||
@@ -110,11 +110,9 @@ TEST_CASE("Ship spawn: no modules leaves base stats unchanged", "[modules]")
|
||||
const ShipDef* def = findSchematic(sim.config(), "interceptor");
|
||||
REQUIRE(def != nullptr);
|
||||
|
||||
const double x = static_cast<double>(def->schematic.playerProductionLevel);
|
||||
const float expectedHp = static_cast<float>(def->health.hpFormula.evaluate(x));
|
||||
const float expectedHp = static_cast<float>(def->health.hp);
|
||||
|
||||
const entt::entity e = sim.ships().spawn("interceptor",
|
||||
def->schematic.playerProductionLevel,
|
||||
QVector2D(5.0f, 5.0f), false, std::nullopt);
|
||||
|
||||
REQUIRE(sim.admin().isValid(e));
|
||||
@@ -127,8 +125,7 @@ TEST_CASE("Ship spawn: multiplicative HP module applies correctly", "[modules]")
|
||||
const ShipDef* def = findSchematic(sim.config(), "interceptor");
|
||||
REQUIRE(def != nullptr);
|
||||
|
||||
const double x = static_cast<double>(def->schematic.playerProductionLevel);
|
||||
const float baseHp = static_cast<float>(def->health.hpFormula.evaluate(x));
|
||||
const float baseHp = static_cast<float>(def->health.hp);
|
||||
|
||||
ShipLayoutConfig layout;
|
||||
PlacedModule pm;
|
||||
@@ -138,7 +135,6 @@ TEST_CASE("Ship spawn: multiplicative HP module applies correctly", "[modules]")
|
||||
layout.placedModules.push_back(pm);
|
||||
|
||||
const entt::entity e = sim.ships().spawn("interceptor",
|
||||
def->schematic.playerProductionLevel,
|
||||
QVector2D(5.0f, 5.0f), false, layout);
|
||||
|
||||
REQUIRE(sim.admin().isValid(e));
|
||||
@@ -154,9 +150,8 @@ TEST_CASE("Ship spawn: additive sensor module applies correctly", "[modules]")
|
||||
const ShipDef* def = findSchematic(sim.config(), "interceptor");
|
||||
REQUIRE(def != nullptr);
|
||||
|
||||
const double x = static_cast<double>(def->schematic.playerProductionLevel);
|
||||
const float tileSize = static_cast<float>(sim.config().world.tileSize_m);
|
||||
const float baseRange_tiles = static_cast<float>(def->sensor.sensorRangeFormula.evaluate(x)) / tileSize;
|
||||
const float baseRange_tiles = static_cast<float>(def->sensor.sensorRange_m) / tileSize;
|
||||
|
||||
ShipLayoutConfig layout;
|
||||
PlacedModule pm;
|
||||
@@ -166,7 +161,6 @@ TEST_CASE("Ship spawn: additive sensor module applies correctly", "[modules]")
|
||||
layout.placedModules.push_back(pm);
|
||||
|
||||
const entt::entity e = sim.ships().spawn("interceptor",
|
||||
def->schematic.playerProductionLevel,
|
||||
QVector2D(5.0f, 5.0f), false, layout);
|
||||
|
||||
REQUIRE(sim.admin().isValid(e));
|
||||
@@ -181,8 +175,7 @@ TEST_CASE("Ship spawn: multiple modules stack correctly", "[modules]")
|
||||
const ShipDef* def = findSchematic(sim.config(), "interceptor");
|
||||
REQUIRE(def != nullptr);
|
||||
|
||||
const double x = static_cast<double>(def->schematic.playerProductionLevel);
|
||||
const float baseHp = static_cast<float>(def->health.hpFormula.evaluate(x));
|
||||
const float baseHp = static_cast<float>(def->health.hp);
|
||||
|
||||
ShipLayoutConfig layout;
|
||||
for (int i = 0; i < 2; ++i)
|
||||
@@ -195,7 +188,6 @@ TEST_CASE("Ship spawn: multiple modules stack correctly", "[modules]")
|
||||
}
|
||||
|
||||
const entt::entity e = sim.ships().spawn("interceptor",
|
||||
def->schematic.playerProductionLevel,
|
||||
QVector2D(5.0f, 5.0f), false, layout);
|
||||
|
||||
REQUIRE(sim.admin().isValid(e));
|
||||
@@ -321,7 +313,6 @@ TEST_CASE("Ship spawn: weapon_primer multiplies attack rate in simulation", "[mo
|
||||
}
|
||||
|
||||
const entt::entity ship = sim.ships().spawn("interceptor",
|
||||
def->schematic.playerProductionLevel,
|
||||
QVector2D(5.0f, 5.0f), false, layout);
|
||||
|
||||
const entt::entity weapon = findFirstWeaponChild(sim.admin(), ship);
|
||||
@@ -349,7 +340,6 @@ TEST_CASE("Ship spawn: weapon_stabilizer multiplies attack range in simulation",
|
||||
}
|
||||
|
||||
const entt::entity ship = sim.ships().spawn("interceptor",
|
||||
def->schematic.playerProductionLevel,
|
||||
QVector2D(5.0f, 5.0f), false, layout);
|
||||
|
||||
const entt::entity weapon = findFirstWeaponChild(sim.admin(), ship);
|
||||
@@ -364,10 +354,9 @@ TEST_CASE("Ship spawn: afterburner additive main_acceleration is converted m/s²
|
||||
const ShipDef* def = findSchematic(sim.config(), "interceptor");
|
||||
REQUIRE(def != nullptr);
|
||||
|
||||
const double x = static_cast<double>(def->schematic.playerProductionLevel);
|
||||
const float tileSize = static_cast<float>(sim.config().world.tileSize_m);
|
||||
const float tickRate = static_cast<float>(kTickRateHz);
|
||||
const float base_mpss = static_cast<float>(def->movement.mainAccelerationFormula.evaluate(x));
|
||||
const float base_mpss = static_cast<float>(def->movement.mainAcceleration_mpss);
|
||||
|
||||
ShipLayoutConfig layout;
|
||||
PlacedModule pm;
|
||||
@@ -377,7 +366,6 @@ TEST_CASE("Ship spawn: afterburner additive main_acceleration is converted m/s²
|
||||
layout.placedModules.push_back(pm);
|
||||
|
||||
const entt::entity e = sim.ships().spawn("interceptor",
|
||||
def->schematic.playerProductionLevel,
|
||||
QVector2D(5.0f, 5.0f), false, layout);
|
||||
|
||||
// added_main_acceleration_mpss = 60; same conversion as base: / tileSize / tickRate
|
||||
@@ -391,10 +379,9 @@ TEST_CASE("Ship spawn: maneuvering_thrusters additive maneuvering_acceleration i
|
||||
const ShipDef* def = findSchematic(sim.config(), "interceptor");
|
||||
REQUIRE(def != nullptr);
|
||||
|
||||
const double x = static_cast<double>(def->schematic.playerProductionLevel);
|
||||
const float tileSize = static_cast<float>(sim.config().world.tileSize_m);
|
||||
const float tickRate = static_cast<float>(kTickRateHz);
|
||||
const float base_mpss = static_cast<float>(def->movement.maneuveringAccelerationFormula.evaluate(x));
|
||||
const float base_mpss = static_cast<float>(def->movement.maneuveringAcceleration_mpss);
|
||||
|
||||
ShipLayoutConfig layout;
|
||||
PlacedModule pm;
|
||||
@@ -404,7 +391,6 @@ TEST_CASE("Ship spawn: maneuvering_thrusters additive maneuvering_acceleration i
|
||||
layout.placedModules.push_back(pm);
|
||||
|
||||
const entt::entity e = sim.ships().spawn("interceptor",
|
||||
def->schematic.playerProductionLevel,
|
||||
QVector2D(5.0f, 5.0f), false, layout);
|
||||
|
||||
// added_maneuvering_acceleration_mpss = 10; same conversion as base: / tileSize / tickRate
|
||||
@@ -433,7 +419,7 @@ TEST_CASE("calculateShipStats: weapon_primer multiplies attack rate in stats vie
|
||||
}
|
||||
|
||||
const ShipStats stats = calculateShipStats(cfg, "interceptor",
|
||||
def->schematic.playerProductionLevel, modules);
|
||||
modules);
|
||||
|
||||
REQUIRE(stats.weapons.has_value());
|
||||
// base: damage = 2, rate = 2.0 hz; weapon_primer multiplies rate by 1.2 → DPS = 2 * 2.4 = 4.8
|
||||
@@ -459,7 +445,7 @@ TEST_CASE("calculateShipStats: weapon_stabilizer multiplies attack range in stat
|
||||
}
|
||||
|
||||
const ShipStats stats = calculateShipStats(cfg, "interceptor",
|
||||
def->schematic.playerProductionLevel, modules);
|
||||
modules);
|
||||
|
||||
REQUIRE(stats.weapons.has_value());
|
||||
// base range = 50 m / tileSize; weapon_stabilizer multiplier = 1.5
|
||||
@@ -472,9 +458,8 @@ TEST_CASE("calculateShipStats: afterburner additive main_acceleration is convert
|
||||
const ShipDef* def = findSchematic(cfg, "interceptor");
|
||||
REQUIRE(def != nullptr);
|
||||
|
||||
const double x = static_cast<double>(def->schematic.playerProductionLevel);
|
||||
const float tileSize = static_cast<float>(cfg.world.tileSize_m);
|
||||
const float base_mpss = static_cast<float>(def->movement.mainAccelerationFormula.evaluate(x));
|
||||
const float base_mpss = static_cast<float>(def->movement.mainAcceleration_mpss);
|
||||
|
||||
std::vector<PlacedModule> modules;
|
||||
PlacedModule pm;
|
||||
@@ -484,7 +469,7 @@ TEST_CASE("calculateShipStats: afterburner additive main_acceleration is convert
|
||||
modules.push_back(pm);
|
||||
|
||||
const ShipStats stats = calculateShipStats(cfg, "interceptor",
|
||||
def->schematic.playerProductionLevel, modules);
|
||||
modules);
|
||||
|
||||
// added_main_acceleration_mpss = 60; converted to tiles/s²: / tileSize
|
||||
const float expected = (base_mpss + 60.0f) / tileSize;
|
||||
@@ -497,9 +482,8 @@ TEST_CASE("calculateShipStats: maneuvering_thrusters additive maneuvering_accele
|
||||
const ShipDef* def = findSchematic(cfg, "interceptor");
|
||||
REQUIRE(def != nullptr);
|
||||
|
||||
const double x = static_cast<double>(def->schematic.playerProductionLevel);
|
||||
const float tileSize = static_cast<float>(cfg.world.tileSize_m);
|
||||
const float base_mpss = static_cast<float>(def->movement.maneuveringAccelerationFormula.evaluate(x));
|
||||
const float base_mpss = static_cast<float>(def->movement.maneuveringAcceleration_mpss);
|
||||
|
||||
std::vector<PlacedModule> modules;
|
||||
PlacedModule pm;
|
||||
@@ -509,7 +493,7 @@ TEST_CASE("calculateShipStats: maneuvering_thrusters additive maneuvering_accele
|
||||
modules.push_back(pm);
|
||||
|
||||
const ShipStats stats = calculateShipStats(cfg, "interceptor",
|
||||
def->schematic.playerProductionLevel, modules);
|
||||
modules);
|
||||
|
||||
// added_maneuvering_acceleration_mpss = 10; converted to tiles/s²: / tileSize
|
||||
const float expected = (base_mpss + 10.0f) / tileSize;
|
||||
|
||||
@@ -94,7 +94,7 @@ TEST_CASE("ShipSystem: interceptor spawn has weapon child and attack behavior, n
|
||||
const GameConfig cfg = loadConfig();
|
||||
ShipSystem ss(cfg, admin);
|
||||
|
||||
const entt::entity e = ss.spawn("interceptor", 1, QVector2D(0.0f, 0.0f));
|
||||
const entt::entity e = ss.spawn("interceptor", QVector2D(0.0f, 0.0f));
|
||||
|
||||
REQUIRE(admin.isValid(e));
|
||||
REQUIRE(admin.isValid(firstWeaponChild(admin, e)));
|
||||
@@ -118,7 +118,7 @@ TEST_CASE("ShipSystem: enemy combat ship has no rally or retreat behavior", "[sh
|
||||
const GameConfig cfg = loadConfig();
|
||||
ShipSystem ss(cfg, admin);
|
||||
|
||||
const entt::entity e = ss.spawn("interceptor", 1, QVector2D(0.0f, 0.0f), /*isEnemy=*/true);
|
||||
const entt::entity e = ss.spawn("interceptor", QVector2D(0.0f, 0.0f), /*isEnemy=*/true);
|
||||
|
||||
REQUIRE(admin.hasAll<AttackBehavior>(e));
|
||||
REQUIRE(admin.hasAll<AdvanceBehavior>(e));
|
||||
@@ -133,7 +133,7 @@ TEST_CASE("ShipSystem: setRetreatEnabled(false) suppresses player retreat behavi
|
||||
ShipSystem ss(cfg, admin);
|
||||
ss.setRetreatEnabled(false);
|
||||
|
||||
const entt::entity e = ss.spawn("interceptor", 1, QVector2D(0.0f, 0.0f));
|
||||
const entt::entity e = ss.spawn("interceptor", QVector2D(0.0f, 0.0f));
|
||||
|
||||
// Other player behaviors are unaffected; only retreat is suppressed.
|
||||
REQUIRE(admin.hasAll<AttackBehavior>(e));
|
||||
@@ -147,7 +147,7 @@ TEST_CASE("ShipSystem: interceptor level 1 stats match config formulas", "[ship]
|
||||
const GameConfig cfg = loadConfig();
|
||||
ShipSystem ss(cfg, admin);
|
||||
|
||||
const entt::entity e = ss.spawn("interceptor", 1, QVector2D(0.0f, 0.0f));
|
||||
const entt::entity e = ss.spawn("interceptor", QVector2D(0.0f, 0.0f));
|
||||
|
||||
// hp_formula = "40 + 5*x" at x=1 → 45
|
||||
REQUIRE(admin.get<HealthComponent>(e).maxHp == Approx(45.0f));
|
||||
@@ -163,28 +163,28 @@ TEST_CASE("ShipSystem: interceptor level 1 stats match config formulas", "[ship]
|
||||
REQUIRE(admin.get<WeaponComponent>(wc).cooldownTicks == Approx(0.0f));
|
||||
}
|
||||
|
||||
TEST_CASE("ShipSystem: interceptor level 5 hp matches formula", "[ship]")
|
||||
TEST_CASE("ShipSystem: interceptor hp matches config value", "[ship]")
|
||||
{
|
||||
EntityAdmin admin;
|
||||
const GameConfig cfg = loadConfig();
|
||||
ShipSystem ss(cfg, admin);
|
||||
|
||||
const entt::entity e = ss.spawn("interceptor", 5, QVector2D(0.0f, 0.0f));
|
||||
const entt::entity e = ss.spawn("interceptor", QVector2D(0.0f, 0.0f));
|
||||
|
||||
// hp_formula = "40 + 5*x" at x=5 → 65
|
||||
REQUIRE(admin.get<HealthComponent>(e).maxHp == Approx(65.0f));
|
||||
// interceptor hp = 45 (plain value) in the test config
|
||||
REQUIRE(admin.get<HealthComponent>(e).maxHp == Approx(45.0f));
|
||||
}
|
||||
|
||||
TEST_CASE("ShipSystem: interceptor level 0 maxSpeed_tpt matches formula / tileSize / kTickRateHz", "[ship]")
|
||||
TEST_CASE("ShipSystem: interceptor maxSpeed_tpt matches config value / tileSize / kTickRateHz", "[ship]")
|
||||
{
|
||||
EntityAdmin admin;
|
||||
const GameConfig cfg = loadConfig();
|
||||
ShipSystem ss(cfg, admin);
|
||||
|
||||
const entt::entity e = ss.spawn("interceptor", 0, QVector2D(0.0f, 0.0f));
|
||||
const entt::entity e = ss.spawn("interceptor", QVector2D(0.0f, 0.0f));
|
||||
|
||||
// speed_mps_formula = "2000 + 50*x" m/s at x=0 → 2000 m/s; maxSpeed_tpt = 2000/(10*30)
|
||||
const float expected = 2000.0f / 10.0f / static_cast<float>(kTickRateHz);
|
||||
// speed_mps = 2050 m/s in the test config; maxSpeed_tpt = 2050/(10*30)
|
||||
const float expected = 2050.0f / 10.0f / static_cast<float>(kTickRateHz);
|
||||
REQUIRE(admin.get<DynamicBodyComponent>(e).maxSpeed_tpt == Approx(expected));
|
||||
}
|
||||
|
||||
@@ -200,7 +200,7 @@ TEST_CASE("ShipSystem: salvage_ship spawn with salvage module has cargo child an
|
||||
ShipSystem ss(cfg, admin);
|
||||
|
||||
const ShipLayoutConfig layout = makeSingleModuleLayout("salvager");
|
||||
const entt::entity e = ss.spawn("salvage_ship", 1, QVector2D(0.0f, 0.0f), false, layout);
|
||||
const entt::entity e = ss.spawn("salvage_ship", QVector2D(0.0f, 0.0f), false, layout);
|
||||
|
||||
REQUIRE(admin.isValid(firstSalvageChild(admin, e)));
|
||||
REQUIRE(admin.hasAll<SalvageScrapBehavior>(e));
|
||||
@@ -216,7 +216,7 @@ TEST_CASE("ShipSystem: salvage_ship cargo capacity matches config", "[ship]")
|
||||
ShipSystem ss(cfg, admin);
|
||||
|
||||
const ShipLayoutConfig layout = makeSingleModuleLayout("salvager");
|
||||
const entt::entity e = ss.spawn("salvage_ship", 1, QVector2D(0.0f, 0.0f), false, layout);
|
||||
const entt::entity e = ss.spawn("salvage_ship", QVector2D(0.0f, 0.0f), false, layout);
|
||||
|
||||
// salvager: cargo_capacity_formula = "10", collection_range_m_formula = "500" m → 500/10 = 50 tiles
|
||||
const entt::entity sc = firstSalvageChild(admin, e);
|
||||
@@ -241,7 +241,7 @@ TEST_CASE("ShipSystem: repair_ship spawn with repair module has repair child and
|
||||
ShipSystem ss(cfg, admin);
|
||||
|
||||
const ShipLayoutConfig layout = makeSingleModuleLayout("repair_tool");
|
||||
const entt::entity e = ss.spawn("repair_ship", 1, QVector2D(0.0f, 0.0f), false, layout);
|
||||
const entt::entity e = ss.spawn("repair_ship", QVector2D(0.0f, 0.0f), false, layout);
|
||||
|
||||
REQUIRE(admin.isValid(firstRepairChild(admin, e)));
|
||||
REQUIRE(admin.hasAll<RepairBehavior>(e));
|
||||
@@ -256,7 +256,7 @@ TEST_CASE("ShipSystem: repair_ship level 1 repair stats match config formulas",
|
||||
ShipSystem ss(cfg, admin);
|
||||
|
||||
const ShipLayoutConfig layout = makeSingleModuleLayout("repair_tool");
|
||||
const entt::entity e = ss.spawn("repair_ship", 1, QVector2D(0.0f, 0.0f), false, layout);
|
||||
const entt::entity e = ss.spawn("repair_ship", QVector2D(0.0f, 0.0f), false, layout);
|
||||
|
||||
const entt::entity rc = firstRepairChild(admin, e);
|
||||
REQUIRE(admin.isValid(rc));
|
||||
@@ -280,9 +280,9 @@ TEST_CASE("ShipSystem: spawned ships are valid entities", "[ship]")
|
||||
const GameConfig cfg = loadConfig();
|
||||
ShipSystem ss(cfg, admin);
|
||||
|
||||
const entt::entity e1 = ss.spawn("interceptor", 1, QVector2D(0.0f, 0.0f));
|
||||
const entt::entity e1 = ss.spawn("interceptor", QVector2D(0.0f, 0.0f));
|
||||
const ShipLayoutConfig salvageLayout = makeSingleModuleLayout("salvager");
|
||||
const entt::entity e2 = ss.spawn("salvage_ship", 1, QVector2D(1.0f, 0.0f), false, salvageLayout);
|
||||
const entt::entity e2 = ss.spawn("salvage_ship", QVector2D(1.0f, 0.0f), false, salvageLayout);
|
||||
|
||||
REQUIRE(admin.isValid(e1));
|
||||
REQUIRE(admin.isValid(e2));
|
||||
@@ -295,7 +295,7 @@ TEST_CASE("ShipSystem: despawn removes the ship and its weapon children", "[ship
|
||||
const GameConfig cfg = loadConfig();
|
||||
ShipSystem ss(cfg, admin);
|
||||
|
||||
const entt::entity e = ss.spawn("interceptor", 1, QVector2D(0.0f, 0.0f));
|
||||
const entt::entity e = ss.spawn("interceptor", QVector2D(0.0f, 0.0f));
|
||||
const entt::entity wc = firstWeaponChild(admin, e);
|
||||
REQUIRE(admin.isValid(e));
|
||||
REQUIRE(admin.isValid(wc));
|
||||
|
||||
@@ -234,20 +234,17 @@ void MainWindow::handleEvent(std::shared_ptr<const LayoutDialogRequestedEvent> e
|
||||
}
|
||||
|
||||
std::set<std::string> unlockedModuleIds;
|
||||
std::map<std::string, int> moduleLevels;
|
||||
for (const ModuleDef& def : m_sim->config().modules.modules)
|
||||
{
|
||||
if (m_sim->isModuleSchematicUnlocked(def.id))
|
||||
{
|
||||
unlockedModuleIds.insert(def.id);
|
||||
}
|
||||
moduleLevels[def.id] = m_sim->moduleSchematicLevel(def.id);
|
||||
}
|
||||
|
||||
ShipLayoutDialog dialog(&m_sim->config(), schematicId, currentLayout,
|
||||
m_layoutBlueprints,
|
||||
std::move(unlockedModuleIds),
|
||||
std::move(moduleLevels),
|
||||
m_gameWorldView->isDebugDrawEnabled(),
|
||||
this);
|
||||
if (dialog.exec() == QDialog::Accepted && dialog.result().has_value())
|
||||
|
||||
@@ -70,19 +70,6 @@ SchematicChoiceDialog::SchematicChoiceDialog(
|
||||
typeLabel->setAlignment(Qt::AlignCenter);
|
||||
cardLayout->addWidget(typeLabel);
|
||||
|
||||
QString statusText;
|
||||
if (option.isNewUnlock)
|
||||
{
|
||||
statusText = tr("New unlock");
|
||||
}
|
||||
else
|
||||
{
|
||||
statusText = tr("Level up -> %1").arg(option.targetLevel);
|
||||
}
|
||||
QLabel* statusLabel = new QLabel(statusText, card);
|
||||
statusLabel->setAlignment(Qt::AlignCenter);
|
||||
cardLayout->addWidget(statusLabel);
|
||||
|
||||
QLabel* unlocksHeaderLabel = new QLabel(tr("Unlocks recipes for:"), card);
|
||||
QFont unlocksHeaderFont = unlocksHeaderLabel->font();
|
||||
unlocksHeaderFont.setBold(true);
|
||||
|
||||
@@ -821,9 +821,8 @@ void SelectedBuildingPanel::buildEntityShip(entt::entity entity)
|
||||
const ShipIdentityComponent& identity = admin.get<ShipIdentityComponent>(entity);
|
||||
const HealthComponent& health = admin.get<HealthComponent>(entity);
|
||||
|
||||
m_entityTitleLabel->setText(tr("Ship: %1 (Lv %2)")
|
||||
.arg(QString::fromStdString(identity.schematicId))
|
||||
.arg(identity.level));
|
||||
m_entityTitleLabel->setText(tr("Ship: %1")
|
||||
.arg(QString::fromStdString(identity.schematicId)));
|
||||
m_entityTitleLabel->show();
|
||||
|
||||
const ShipStats stats = buildShipStatsFromEntity(admin, entity);
|
||||
|
||||
@@ -365,14 +365,12 @@ ShipLayoutDialog::ShipLayoutDialog(const GameConfig* config,
|
||||
const ShipLayoutConfig& currentLayout,
|
||||
std::vector<ShipLayoutBlueprint>& allBlueprints,
|
||||
std::set<std::string> unlockedModuleIds,
|
||||
std::map<std::string, int> moduleLevels,
|
||||
bool debugDraw,
|
||||
QWidget* parent)
|
||||
: QDialog(parent)
|
||||
, m_config(config)
|
||||
, m_shipId(shipId)
|
||||
, m_unlockedModuleIds(std::move(unlockedModuleIds))
|
||||
, m_moduleLevels(std::move(moduleLevels))
|
||||
, m_rows(0)
|
||||
, m_cols(0)
|
||||
, m_placedModules(currentLayout.placedModules)
|
||||
@@ -699,16 +697,7 @@ void ShipLayoutDialog::updateGridWidget()
|
||||
|
||||
void ShipLayoutDialog::updateStats()
|
||||
{
|
||||
int level = 1;
|
||||
for (const ShipDef& def : m_config->ships.ships)
|
||||
{
|
||||
if (def.id == m_shipId)
|
||||
{
|
||||
level = def.schematic.playerProductionLevel;
|
||||
break;
|
||||
}
|
||||
}
|
||||
m_statsPanel->refresh(m_shipId, level, m_placedModules, m_moduleLevels);
|
||||
m_statsPanel->refresh(m_shipId, m_placedModules);
|
||||
}
|
||||
|
||||
bool ShipLayoutDialog::canPlaceModule(const ModuleDef& def, QPoint position,
|
||||
|
||||
@@ -27,7 +27,6 @@ public:
|
||||
const ShipLayoutConfig& currentLayout,
|
||||
std::vector<ShipLayoutBlueprint>& allBlueprints,
|
||||
std::set<std::string> unlockedModuleIds,
|
||||
std::map<std::string, int> moduleLevels,
|
||||
bool debugDraw,
|
||||
QWidget* parent = nullptr);
|
||||
|
||||
@@ -63,7 +62,6 @@ private:
|
||||
const GameConfig* m_config;
|
||||
std::string m_shipId;
|
||||
std::set<std::string> m_unlockedModuleIds;
|
||||
std::map<std::string, int> m_moduleLevels;
|
||||
std::vector<std::string> m_shipLayout;
|
||||
int m_rows;
|
||||
int m_cols;
|
||||
|
||||
@@ -116,12 +116,9 @@ ShipStatsPanel::ShipStatsPanel(const GameConfig* config, QWidget* parent)
|
||||
}
|
||||
|
||||
void ShipStatsPanel::refresh(const std::string& shipId,
|
||||
int level,
|
||||
const std::vector<PlacedModule>& modules,
|
||||
const std::map<std::string, int>& moduleLevelOverrides)
|
||||
const std::vector<PlacedModule>& modules)
|
||||
{
|
||||
const ShipStats stats = calculateShipStats(*m_config, shipId, level, modules,
|
||||
moduleLevelOverrides);
|
||||
const ShipStats stats = calculateShipStats(*m_config, shipId, modules);
|
||||
const QString hpText = tr("HP: %1").arg(static_cast<int>(stats.hp + 0.5f));
|
||||
applyStats(stats, hpText);
|
||||
|
||||
|
||||
@@ -20,9 +20,7 @@ public:
|
||||
explicit ShipStatsPanel(const GameConfig* config, QWidget* parent = nullptr);
|
||||
|
||||
void refresh(const std::string& shipId,
|
||||
int level,
|
||||
const std::vector<PlacedModule>& modules,
|
||||
const std::map<std::string, int>& moduleLevelOverrides = {});
|
||||
const std::vector<PlacedModule>& modules);
|
||||
|
||||
void refreshFromLive(const ShipStats& stats, float currentHp);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user