377 lines
15 KiB
C++
377 lines
15 KiB
C++
#include "game/effects/tech_effects.h"
|
|
#include "game/effects/tech_id.h"
|
|
|
|
#include <set>
|
|
#include <string>
|
|
|
|
#include "check.h" // shared with tests/game_sim
|
|
|
|
using namespace sots::effects;
|
|
using sots::sim::Species;
|
|
|
|
static ApplyContext ctx_with_tuning(sots::sim::TuningTable& t) {
|
|
ApplyContext c;
|
|
c.tuning = &t;
|
|
return c;
|
|
}
|
|
|
|
static void test_ids() {
|
|
CHECK_EQ(static_cast<int>(TechId::CCC_AdvSens), 10000);
|
|
CHECK_EQ(static_cast<int>(TechId::IND_HrdStrct), 10030);
|
|
CHECK_EQ(static_cast<int>(TechId::WEP_ACCAMP), 10192);
|
|
CHECK_EQ(static_cast<int>(TechId::None), 10197);
|
|
CHECK(IsValidTechId(10000) && IsValidTechId(10195));
|
|
CHECK(!IsValidTechId(9999) && !IsValidTechId(10196) && !IsValidTechId(TechId::None));
|
|
CHECK_EQ(TechIdIndex(TechId::DRV_RAD), 66);
|
|
CHECK(TechIdFromIndex(64) == TechId::DRV_RIP);
|
|
|
|
CHECK(std::string(TechIdName(TechId::IND_Waldo)) == "IND_Waldo");
|
|
CHECK(TechIdName(TechId::Unresolved_052) == nullptr);
|
|
CHECK(TechIdName(TechId::None) == nullptr);
|
|
CHECK(TechIdFromName("IND_Waldo") == TechId::IND_Waldo);
|
|
CHECK(TechIdFromName("ind_waldo") == TechId::IND_Waldo); // case-insensitive like the game
|
|
CHECK(TechIdFromName("CCC_TRNSLIR") == TechId::XNC_Translation1_Liir);
|
|
CHECK(TechIdFromName("not_a_tech") == TechId::None);
|
|
CHECK(TechIdFromName("") == TechId::None);
|
|
|
|
// every resolved name round-trips, and the table has exactly 196 slots
|
|
int named = 0;
|
|
for (int i = 0; i < kTechIdCount; ++i) {
|
|
const char* n = TechIdName(TechIdFromIndex(i));
|
|
if (n == nullptr) continue;
|
|
++named;
|
|
CHECK(TechIdFromName(n) == TechIdFromIndex(i));
|
|
}
|
|
CHECK_EQ(named, 89); // recovered data-file names so far; bump when more are resolved
|
|
|
|
// xenotech blocks
|
|
CHECK(XenoTechId(XenoLevel::Translation1, Species::Human) == TechId::XNC_Translation1_Human);
|
|
CHECK(XenoTechId(XenoLevel::Translation1, Species::Morrigi) == TechId::XNC_Translation1_Morrigi);
|
|
CHECK_EQ(static_cast<int>(XenoTechId(XenoLevel::Translation2, Species::Morrigi)), 10125);
|
|
CHECK_EQ(static_cast<int>(XenoTechId(XenoLevel::Incorporate, Species::Morrigi)), 10136);
|
|
CHECK(XenoTechId(XenoLevel::Incorporate, Species::Zuul) == TechId::None);
|
|
CHECK(XenoTechId(XenoLevel::Subjugate, Species::Zuul) == TechId::XNC_Subjugate_Zuul);
|
|
CHECK_EQ(static_cast<int>(XenoTechId(XenoLevel::Proliferate, Species::Morrigi)), 10163);
|
|
CHECK(XenoTechId(XenoLevel::Temperance, Species::NPC) == TechId::None);
|
|
CHECK(XenoTechId(XenoLevel::Translation3, Species::NPC) == TechId::None);
|
|
|
|
CHECK(std::string(NodeTrackTechName(Species::Human)) == "CCC_NDTRKHUM");
|
|
CHECK(std::string(NodeTrackTechName(Species::Zuul)) == "CCC_NDTRKZUL");
|
|
CHECK(NodeTrackTechName(Species::Hiver) == nullptr);
|
|
}
|
|
|
|
static void test_industrial() {
|
|
PlayerEconomyState s;
|
|
ApplyContext ctx;
|
|
TechApplyOutcome o = ApplyTechEffect(s, TechId::IND_Waldo, ctx);
|
|
CHECK(o.applied);
|
|
CHECK_NEAR(s.conMod[0], 0.90, 1e-12);
|
|
CHECK_NEAR(s.conMod[1], 0.90, 1e-12);
|
|
CHECK_NEAR(s.conMod[2], 0.90, 1e-12);
|
|
CHECK_NEAR(s.outMod, 1.15, 1e-12);
|
|
CHECK(s.HasResearched(TechId::IND_Waldo));
|
|
|
|
o = ApplyTechEffect(s, TechId::IND_Waldo, ctx); // no double application
|
|
CHECK(!o.applied);
|
|
CHECK_NEAR(s.outMod, 1.15, 1e-12);
|
|
|
|
ApplyTechEffect(s, TechId::IND_CyberInt, ctx); // -0.05 / +0.20
|
|
CHECK_NEAR(s.conMod[0], 0.85, 1e-12);
|
|
CHECK_NEAR(s.outMod, 1.35, 1e-12);
|
|
ApplyTechEffect(s, TechId::IND_ExpSys, ctx); // -0.10 / +0.15
|
|
CHECK_NEAR(s.conMod[2], 0.75, 1e-12);
|
|
CHECK_NEAR(s.outMod, 1.50, 1e-12);
|
|
ApplyTechEffect(s, TechId::IND_OrbDry, ctx); // classes 1 and 2 only
|
|
CHECK_NEAR(s.conMod[0], 0.75, 1e-12);
|
|
CHECK_NEAR(s.conMod[1], 0.70, 1e-12);
|
|
CHECK_NEAR(s.conMod[2], 0.70, 1e-12);
|
|
ApplyTechEffect(s, TechId::DRN_AdvRob, ctx);
|
|
CHECK_NEAR(s.conMod[0], 0.70, 1e-12);
|
|
CHECK_NEAR(s.conMod[1], 0.65, 1e-12);
|
|
|
|
ApplyTechEffect(s, TechId::IND_OrbFound, ctx);
|
|
CHECK_NEAR(s.savMod[0], 0.95, 1e-12);
|
|
CHECK_NEAR(s.savMod[2], 0.95, 1e-12);
|
|
|
|
ApplyTechEffect(s, TechId::IND_GravCon, ctx); // 1.80
|
|
ApplyTechEffect(s, TechId::IND_HvyPlat, ctx); // 1.90
|
|
CHECK_NEAR(s.outMod, 1.90, 1e-12);
|
|
ApplyTechEffect(s, TechId::IND_HrdStrct, ctx); // multiplicative
|
|
CHECK_NEAR(s.outMod, 1.71, 1e-12);
|
|
CHECK_NEAR(s.defenceDamageMod, 0.25, 1e-12);
|
|
|
|
// the order of additive and multiplicative effects matters
|
|
PlayerEconomyState r;
|
|
ApplyTechEffect(r, TechId::IND_HrdStrct, ctx);
|
|
ApplyTechEffect(r, TechId::IND_GravCon, ctx);
|
|
CHECK_NEAR(r.outMod, 1.20, 1e-12); // 0.9 + 0.3, not 1.3 x 0.9
|
|
|
|
ApplyTechEffect(s, TechId::IND_AstMine, ctx);
|
|
CHECK(s.Flag(PlayerFlag::AsteroidMining));
|
|
ApplyTechEffect(s, TechId::IND_MsMine, ctx);
|
|
CHECK_NEAR(s.maxOverharvest, 0.1, 0.0);
|
|
CHECK_NEAR(s.miningRate, 1.0, 0.0);
|
|
PlayerEconomyState q;
|
|
q.maxOverharvest = 0.3;
|
|
ApplyTechEffect(q, TechId::IND_MsMine, ctx);
|
|
CHECK_NEAR(q.maxOverharvest, 0.3, 0.0); // max(), not assignment
|
|
|
|
ApplyTechEffect(s, TechId::CCC_AdvSens, ctx);
|
|
CHECK(s.Flag(PlayerFlag::AdvancedSensors));
|
|
}
|
|
|
|
static void test_biology() {
|
|
PlayerEconomyState s;
|
|
s.suitTol = 0.07;
|
|
ApplyContext ctx;
|
|
ApplyTechEffect(s, TechId::BIO_GnMod, ctx);
|
|
CHECK_NEAR(s.popMod, 1.10, 1e-12);
|
|
ApplyTechEffect(s, TechId::BIO_AtmoAd, ctx);
|
|
CHECK_NEAR(s.suitTol, 0.82, 1e-12);
|
|
CHECK_NEAR(s.popMod, 1.16, 1e-12);
|
|
CHECK_NEAR(s.terraMod, 1.35, 1e-12);
|
|
ApplyTechEffect(s, TechId::BIO_GrvAdpt, ctx);
|
|
CHECK_NEAR(s.suitTol, 2.32, 1e-12);
|
|
CHECK_NEAR(s.popMod, 1.26, 1e-12);
|
|
CHECK_NEAR(s.terraMod, 1.70, 1e-12);
|
|
ApplyTechEffect(s, TechId::BIO_EnvTail, ctx);
|
|
CHECK_NEAR(s.popMod, 1.46, 1e-12);
|
|
CHECK_NEAR(s.terraMod, 2.15, 1e-12);
|
|
ApplyTechEffect(s, TechId::IND_EleNans, ctx); // +0.60
|
|
ApplyTechEffect(s, TechId::BIO_TerBac, ctx); // +0.45
|
|
ApplyTechEffect(s, TechId::IND_AtProc, ctx); // +0.50
|
|
CHECK_NEAR(s.terraMod, 3.70, 1e-12);
|
|
|
|
TechApplyOutcome o = ApplyTechEffect(s, TechId::IND_ArcCon, ctx);
|
|
CHECK(s.Flag(PlayerFlag::Arcology));
|
|
CHECK_NEAR(s.popMod, 1.61, 1e-12);
|
|
CHECK(o.reevaluateCivilianCaps);
|
|
}
|
|
|
|
static void test_gates_and_casting() {
|
|
sots::sim::TuningTable t;
|
|
t.PERGATETRAFFIC_DRV_TpGate = 5;
|
|
t.PERGATETRAFFIC_DRV_GatAmp = 12;
|
|
ApplyContext ctx = ctx_with_tuning(t);
|
|
|
|
PlayerEconomyState s;
|
|
ApplyTechEffect(s, TechId::DRV_TpGate, ctx);
|
|
CHECK_NEAR(s.perGateTraffic, 5.0, 0.0);
|
|
ApplyTechEffect(s, TechId::DRV_GatAmp, ctx);
|
|
CHECK_NEAR(s.perGateTraffic, 12.0, 0.0);
|
|
|
|
PlayerEconomyState r; // reverse order: max wins
|
|
ApplyTechEffect(r, TechId::DRV_GatAmp, ctx);
|
|
ApplyTechEffect(r, TechId::DRV_TpGate, ctx);
|
|
CHECK_NEAR(r.perGateTraffic, 12.0, 0.0);
|
|
|
|
PlayerEconomyState n; // no tuning: nothing to raise
|
|
ApplyTechEffect(n, TechId::DRV_TpGate, ApplyContext{});
|
|
CHECK_NEAR(n.perGateTraffic, 0.0, 0.0);
|
|
|
|
ApplyTechEffect(s, TechId::DRV_FarCast, ctx);
|
|
CHECK_NEAR(s.castRange, 10.0, 0.0);
|
|
CHECK_NEAR(s.castEfficiency, 2.0, 0.0);
|
|
CHECK_NEAR(s.castThreshold, 1.0, 0.0);
|
|
ApplyTechEffect(s, TechId::DRV_GrvSyn, ctx);
|
|
CHECK(s.Flag(PlayerFlag::GravSynth));
|
|
}
|
|
|
|
static void test_ai() {
|
|
ApplyContext ctx;
|
|
ctx.aiBonus = {0.2, 0.3, 0.4};
|
|
|
|
PlayerEconomyState s;
|
|
CHECK(s.aiBenefit);
|
|
ApplyTechEffect(s, TechId::CCC_AI, ctx);
|
|
CHECK_NEAR(s.resMod, 1.2, 1e-12);
|
|
ApplyTechEffect(s, TechId::CCC_AIAdmin, ctx);
|
|
CHECK_NEAR(s.incMod, 1.3, 1e-12);
|
|
ApplyTechEffect(s, TechId::CCC_AIFac, ctx);
|
|
CHECK_NEAR(s.outMod, 1.4, 1e-12);
|
|
|
|
SetAiBenefit(s, false, ctx); // rebellion: bonuses withdrawn
|
|
CHECK_NEAR(s.resMod, 1.0, 1e-12);
|
|
CHECK_NEAR(s.incMod, 1.0, 1e-12);
|
|
CHECK_NEAR(s.outMod, 1.0, 1e-12);
|
|
SetAiBenefit(s, false, ctx); // idempotent
|
|
CHECK_NEAR(s.outMod, 1.0, 1e-12);
|
|
SetAiBenefit(s, true, ctx);
|
|
CHECK_NEAR(s.resMod, 1.2, 1e-12);
|
|
CHECK_NEAR(s.outMod, 1.4, 1e-12);
|
|
|
|
PlayerEconomyState off; // researched while off: nothing
|
|
off.aiBenefit = false;
|
|
ApplyTechEffect(off, TechId::CCC_AIFac, ctx);
|
|
CHECK_NEAR(off.outMod, 1.0, 1e-12);
|
|
TechApplyOutcome o = ApplyTechEffect(off, TechId::CCC_AISlv, ctx); // slave AI: benefit back on
|
|
CHECK(off.aiBenefit);
|
|
CHECK_NEAR(off.outMod, 1.4, 1e-12);
|
|
CHECK(o.flagSystemsAI);
|
|
CHECK(!AiRebellionPossible(off));
|
|
|
|
PlayerEconomyState v;
|
|
CHECK(AiRebellionPossible(v));
|
|
o = ApplyTechEffect(v, TechId::CCC_AIVrus, ctx);
|
|
CHECK(o.flagSystemsAI);
|
|
CHECK(AiRebellionPossible(v));
|
|
v.species = Species::NPC;
|
|
CHECK(!AiRebellionPossible(v));
|
|
|
|
PlayerEconomyState none; // unknown table values: 0
|
|
ApplyTechEffect(none, TechId::CCC_AI, ApplyContext{});
|
|
CHECK_NEAR(none.resMod, 1.0, 0.0);
|
|
}
|
|
|
|
static void test_flags_and_species() {
|
|
ApplyContext ctx;
|
|
PlayerEconomyState s;
|
|
ApplyTechEffect(s, TechId::CCC_FtlEcon, ctx);
|
|
CHECK(s.Flag(PlayerFlag::TradeAllowed));
|
|
PlayerEconomyState rebel;
|
|
rebel.rebelAI = true;
|
|
ApplyTechEffect(rebel, TechId::CCC_FtlEcon, ctx);
|
|
CHECK(!rebel.Flag(PlayerFlag::TradeAllowed));
|
|
ApplyTechEffect(s, TechId::CCC_ComRaid, ctx);
|
|
CHECK(s.Flag(PlayerFlag::CommerceRaiding));
|
|
ApplyTechEffect(s, TechId::CCC_DatCor, ctx);
|
|
CHECK(s.Flag(PlayerFlag::ViewIntel));
|
|
|
|
// design capture needs both techs, in either order
|
|
ApplyTechEffect(s, TechId::CCC_SpyBm, ctx);
|
|
CHECK(!s.Flag(PlayerFlag::CaptureDesigns));
|
|
ApplyTechEffect(s, TechId::IND_SlvgTech, ctx);
|
|
CHECK(s.Flag(PlayerFlag::CaptureDesigns));
|
|
PlayerEconomyState r;
|
|
ApplyTechEffect(r, TechId::IND_SlvgTech, ctx);
|
|
CHECK(!r.Flag(PlayerFlag::CaptureDesigns));
|
|
ApplyTechEffect(r, TechId::CCC_SpyBm, ctx);
|
|
CHECK(r.Flag(PlayerFlag::CaptureDesigns));
|
|
|
|
// Zuul get boarding pods with cruiser construction; nobody else does
|
|
TechApplyOutcome o = ApplyTechEffect(s, TechId::IND_CruisCon, ctx);
|
|
CHECK(o.grantedTech == TechId::None);
|
|
PlayerEconomyState z;
|
|
z.species = Species::Zuul;
|
|
o = ApplyTechEffect(z, TechId::IND_CruisCon, ctx);
|
|
CHECK(o.grantedTech == TechId::IND_BrdPod);
|
|
CHECK(!z.HasResearched(TechId::IND_BrdPod)); // the caller researches it
|
|
o = ApplyTechEffect(z, o.grantedTech, ctx);
|
|
CHECK(o.applied && z.HasResearched(TechId::IND_BrdPod));
|
|
|
|
// node-bore parameters: highest drive wins regardless of order
|
|
ApplyTechEffect(z, TechId::DRV_RIP, ctx);
|
|
CHECK_EQ(z.nodeBoreParams[0], 45);
|
|
CHECK_EQ(z.nodeBoreParams[2], 3);
|
|
o = ApplyTechEffect(z, TechId::DRV_RAD, ctx);
|
|
CHECK(o.nodeBoreParamsChanged);
|
|
CHECK_EQ(z.nodeBoreParams[0], 95);
|
|
CHECK_EQ(z.nodeBoreParams[1], 60);
|
|
o = ApplyTechEffect(z, TechId::DRV_REND, ctx);
|
|
CHECK(!o.nodeBoreParamsChanged);
|
|
CHECK_EQ(z.nodeBoreParams[0], 95);
|
|
}
|
|
|
|
static void test_plague() {
|
|
ApplyContext ctx;
|
|
PlayerEconomyState s;
|
|
CHECK_EQ(PlagueCureMask(TechId::BIO_PLGVAC), 1u);
|
|
CHECK_EQ(PlagueCureMask(TechId::BIO_CONNAN), 16u);
|
|
CHECK_EQ(PlagueCureMask(TechId::BIO_UNIANTI), 15u);
|
|
CHECK_EQ(PlagueCureMask(TechId::IND_Waldo), 0u);
|
|
|
|
TechApplyOutcome o = ApplyTechEffect(s, TechId::BIO_RTPLGVAC, ctx);
|
|
CHECK_EQ(s.hasVaccine, 2u);
|
|
CHECK_EQ(s.hasImmunity, 2u);
|
|
CHECK_EQ(o.plagueCuredMask, 2u);
|
|
o = ApplyTechEffect(s, TechId::BIO_UNIANTI, ctx);
|
|
CHECK_EQ(s.hasVaccine, 15u);
|
|
CHECK_EQ(o.plagueCuredMask, 15u);
|
|
ApplyTechEffect(s, TechId::BIO_CONNAN, ctx);
|
|
CHECK_EQ(s.hasImmunity, 31u);
|
|
}
|
|
|
|
static void test_xenotech() {
|
|
ApplyContext ctx;
|
|
PlayerEconomyState s;
|
|
TechApplyOutcome o = ApplyTechEffect(s, TechId::XNC_Translation1_Liir, ctx);
|
|
CHECK_EQ(s.speciesFlags[static_cast<int>(Species::Liir)], 0x001u);
|
|
CHECK_EQ(s.speciesFlags[static_cast<int>(Species::Human)], 0u);
|
|
CHECK_EQ(o.temperanceSpeciesMask, 0u);
|
|
ApplyTechEffect(s, TechId::XNC_Translation3_Liir, ctx);
|
|
CHECK_EQ(s.speciesFlags[static_cast<int>(Species::Liir)], 0x005u);
|
|
o = ApplyTechEffect(s, TechId::XNC_Temperance_Hiver, ctx);
|
|
CHECK_EQ(s.speciesFlags[static_cast<int>(Species::Hiver)], 0x020u);
|
|
CHECK_EQ(o.temperanceSpeciesMask, 1u << static_cast<int>(Species::Hiver));
|
|
o = ApplyTechEffect(s, TechId::XNC_Accommodate_Morrigi, ctx); // every completion reports temperance
|
|
CHECK_EQ(s.speciesFlags[static_cast<int>(Species::Morrigi)], 0x080u);
|
|
CHECK_EQ(o.temperanceSpeciesMask, 1u << static_cast<int>(Species::Hiver));
|
|
ApplyTechEffect(s, TechId::XNC_Proliferate_Zuul, ctx);
|
|
CHECK_EQ(s.speciesFlags[static_cast<int>(Species::Zuul)], 0x100u);
|
|
|
|
// rebuild from the researched set alone (load path)
|
|
PlayerEconomyState l;
|
|
l.researched = s.researched;
|
|
RebuildSpeciesTechFlags(l);
|
|
for (int sp = 0; sp < sots::sim::kSpeciesCount; ++sp) CHECK_EQ(l.speciesFlags[sp], s.speciesFlags[sp]);
|
|
}
|
|
|
|
static void test_by_name() {
|
|
ApplyContext ctx;
|
|
PlayerEconomyState s;
|
|
TechApplyOutcome o = ApplyTechEffectByName(s, "ind_gravcon", ctx);
|
|
CHECK(o.applied);
|
|
CHECK_NEAR(s.outMod, 1.30, 1e-12);
|
|
o = ApplyTechEffectByName(s, "CCC_NDTRKHUM", ctx);
|
|
CHECK(o.applied);
|
|
CHECK_EQ(s.nodeTrackMask, 1u << static_cast<int>(Species::Human));
|
|
o = ApplyTechEffectByName(s, "ccc_ndtrkzul", ctx);
|
|
CHECK_EQ(s.nodeTrackMask, (1u << static_cast<int>(Species::Human)) | (1u << static_cast<int>(Species::Zuul)));
|
|
o = ApplyTechEffectByName(s, "WEP_SOMETHING_DATA_ONLY", ctx);
|
|
CHECK(!o.applied);
|
|
CHECK_NEAR(s.outMod, 1.30, 1e-12);
|
|
o = ApplyTechEffectByName(s, "IND_SPNLMNT", ctx); // in the table, no effect
|
|
CHECK(o.applied);
|
|
CHECK(s.HasResearched(TechId::IND_SPNLMNT));
|
|
CHECK_NEAR(s.outMod, 1.30, 1e-12);
|
|
}
|
|
|
|
static void test_table_shape() {
|
|
// Techs with a strategic effect have entries; unresolved and gate-only ids do not.
|
|
CHECK(!EffectsOf(TechId::IND_Waldo).empty());
|
|
CHECK_EQ(EffectsOf(TechId::IND_Waldo).size(), std::size_t{2});
|
|
CHECK(EffectsOf(TechId::Unresolved_052).empty());
|
|
CHECK(EffectsOf(TechId::CCC_HYPCOM).empty());
|
|
CHECK(EffectsOf(TechId::None).empty());
|
|
int withEffects = 0;
|
|
for (int i = 0; i < kTechIdCount; ++i) {
|
|
if (!EffectsOf(TechIdFromIndex(i)).empty()) ++withEffects;
|
|
}
|
|
CHECK_EQ(withEffects, 44);
|
|
|
|
// design-option masks
|
|
std::set<std::string> have = {"IND_REFCOAT", "SLD_INTANG", "DRV_NODE", "WEP_HvyPmsl"};
|
|
DesignOptionMasks m = ComputeDesignOptionMasks([&](std::string_view n) { return have.count(std::string(n)) > 0; });
|
|
CHECK_EQ(m.a, (1u << 0) | (1u << 15));
|
|
CHECK_EQ(m.b, (1u << 0) | (1u << 28));
|
|
m = ComputeDesignOptionMasks([](std::string_view) { return false; });
|
|
CHECK_EQ(m.a, 0u);
|
|
CHECK_EQ(m.b, 0u);
|
|
m = ComputeDesignOptionMasks([](std::string_view) { return true; });
|
|
CHECK_EQ(m.a, 0xffffffffu);
|
|
CHECK_EQ(m.b, 0x1fffffffu);
|
|
}
|
|
|
|
int main() {
|
|
test_ids();
|
|
test_industrial();
|
|
test_biology();
|
|
test_gates_and_casting();
|
|
test_ai();
|
|
test_flags_and_species();
|
|
test_plague();
|
|
test_xenotech();
|
|
test_by_name();
|
|
test_table_shape();
|
|
return simtest::finish("test_effects");
|
|
}
|