#include "game/effects/tech_effects.h" #include "game/effects/tech_id.h" #include #include #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(TechId::CCC_AdvSens), 10000); CHECK_EQ(static_cast(TechId::IND_HrdStrct), 10030); CHECK_EQ(static_cast(TechId::WEP_ACCAMP), 10192); CHECK_EQ(static_cast(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(XenoTechId(XenoLevel::Translation2, Species::Morrigi)), 10125); CHECK_EQ(static_cast(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(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(Species::Liir)], 0x001u); CHECK_EQ(s.speciesFlags[static_cast(Species::Human)], 0u); CHECK_EQ(o.temperanceSpeciesMask, 0u); ApplyTechEffect(s, TechId::XNC_Translation3_Liir, ctx); CHECK_EQ(s.speciesFlags[static_cast(Species::Liir)], 0x005u); o = ApplyTechEffect(s, TechId::XNC_Temperance_Hiver, ctx); CHECK_EQ(s.speciesFlags[static_cast(Species::Hiver)], 0x020u); CHECK_EQ(o.temperanceSpeciesMask, 1u << static_cast(Species::Hiver)); o = ApplyTechEffect(s, TechId::XNC_Accommodate_Morrigi, ctx); // every completion reports temperance CHECK_EQ(s.speciesFlags[static_cast(Species::Morrigi)], 0x080u); CHECK_EQ(o.temperanceSpeciesMask, 1u << static_cast(Species::Hiver)); ApplyTechEffect(s, TechId::XNC_Proliferate_Zuul, ctx); CHECK_EQ(s.speciesFlags[static_cast(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(Species::Human)); o = ApplyTechEffectByName(s, "ccc_ndtrkzul", ctx); CHECK_EQ(s.nodeTrackMask, (1u << static_cast(Species::Human)) | (1u << static_cast(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 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"); }