#include "game/effects/tech_effects.h" #include #include namespace sots::effects { namespace { using sots::sim::Species; constexpr int F(PlayerFlag f) { return static_cast(f); } constexpr int Sp(Species s) { return static_cast(s); } // Plague-cure bits: one per plague type in vaccine order; the universal antidote covers // the first four. constexpr unsigned kCurePlague = 0x01; constexpr unsigned kCureRetroPlague = 0x02; constexpr unsigned kCureBeastPlague = 0x04; constexpr unsigned kCureAssimilationPlague = 0x08; constexpr unsigned kCureNaniteVirus = 0x10; constexpr unsigned kCureUniversal = 0x0f; // Zuul node-bore parameter rows, lowest to highest drive. constexpr int kNodeBoreRows[3][3] = {{45, 15, 3}, {65, 35, 4}, {95, 60, 5}}; struct TableEntry { TechId id; std::vector effects; }; const std::vector& Table() { using K = EffectKind; static const std::vector table = { {TechId::CCC_AdvSens, {{K::SetFlag, F(PlayerFlag::AdvancedSensors)}}}, {TechId::IND_Waldo, {{K::AddConstructionMod, 0, -0.10}, {K::AddOutputMod, 0, 0.15}}}, {TechId::IND_CyberInt, {{K::AddConstructionMod, 0, -0.05}, {K::AddOutputMod, 0, 0.20}}}, {TechId::IND_ExpSys, {{K::AddConstructionMod, 0, -0.10}, {K::AddOutputMod, 0, 0.15}}}, {TechId::IND_OrbFound, {{K::AddSavingsMod, 0, -0.05}}}, {TechId::IND_OrbDry, {{K::AddConstructionModClass, 1, -0.05}, {K::AddConstructionModClass, 2, -0.05}}}, {TechId::IND_GravCon, {{K::AddOutputMod, 0, 0.30}}}, {TechId::IND_HvyPlat, {{K::AddOutputMod, 0, 0.10}}}, {TechId::IND_AstMine, {{K::SetFlag, F(PlayerFlag::AsteroidMining)}}}, {TechId::IND_MsMine, {{K::RaiseMaxOverharvest, 0, 0.1}, {K::AddMiningRate, 0, 1.0}}}, {TechId::BIO_GnMod, {{K::AddPopulationMod, 0, 0.10}}}, {TechId::BIO_AtmoAd, {{K::AddSuitTolerance, 0, 0.75}, {K::AddPopulationMod, 0, 0.06}, {K::AddTerraformMod, 0, 0.35}}}, {TechId::BIO_EnvTail, {{K::AddPopulationMod, 0, 0.20}, {K::AddTerraformMod, 0, 0.45}}}, {TechId::BIO_GrvAdpt, {{K::AddSuitTolerance, 0, 1.50}, {K::AddPopulationMod, 0, 0.10}, {K::AddTerraformMod, 0, 0.35}}}, {TechId::IND_ArcCon, {{K::SetFlag, F(PlayerFlag::Arcology)}, {K::AddPopulationMod, 0, 0.15}, {K::ReevaluateCivilianCaps}}}, {TechId::IND_EleNans, {{K::AddTerraformMod, 0, 0.60}}}, {TechId::BIO_TerBac, {{K::AddTerraformMod, 0, 0.45}}}, {TechId::IND_AtProc, {{K::AddTerraformMod, 0, 0.50}}}, {TechId::DRV_TpGate, {{K::RaiseGateTraffic, 0}}}, {TechId::DRV_GatAmp, {{K::RaiseGateTraffic, 1}}}, {TechId::DRV_FarCast, {{K::SetFarCasting}}}, {TechId::CCC_AI, {{K::AiTechBonus, static_cast(AiBonusSlot::Research)}}}, {TechId::CCC_AIAdmin, {{K::AiTechBonus, static_cast(AiBonusSlot::Admin)}}}, {TechId::CCC_AIFac, {{K::AiTechBonus, static_cast(AiBonusSlot::Factory)}}}, {TechId::CCC_AIVrus, {{K::FlagSystemsAI}}}, {TechId::CCC_AISlv, {{K::FlagSystemsAI}, {K::EnableAiBenefit}}}, {TechId::CCC_FtlEcon, {{K::SetFlagUnlessRebel, F(PlayerFlag::TradeAllowed)}}}, {TechId::CCC_ComRaid, {{K::SetFlag, F(PlayerFlag::CommerceRaiding)}}}, {TechId::DRV_GrvSyn, {{K::SetFlag, F(PlayerFlag::GravSynth)}}}, {TechId::CCC_DatCor, {{K::SetFlag, F(PlayerFlag::ViewIntel)}}}, {TechId::IND_HrdStrct, {{K::MulDefenceDamageMod, 0, 0.25}, {K::MulOutputMod, 0, 0.90}}}, {TechId::DRN_AdvRob, {{K::AddConstructionMod, 0, -0.05}}}, {TechId::IND_CruisCon, {{K::GrantTechIfSpecies, Sp(Species::Zuul), static_cast(static_cast(TechId::IND_BrdPod))}}}, {TechId::CCC_SpyBm, {{K::CaptureDesignsWith, static_cast(TechId::IND_SlvgTech)}}}, {TechId::IND_SlvgTech, {{K::CaptureDesignsWith, static_cast(TechId::CCC_SpyBm)}}}, {TechId::BIO_PLGVAC, {{K::CurePlague, static_cast(kCurePlague)}}}, {TechId::BIO_RTPLGVAC, {{K::CurePlague, static_cast(kCureRetroPlague)}}}, {TechId::BIO_BSTVAC, {{K::CurePlague, static_cast(kCureBeastPlague)}}}, {TechId::BIO_ASPLGVAC, {{K::CurePlague, static_cast(kCureAssimilationPlague)}}}, {TechId::BIO_CONNAN, {{K::CurePlague, static_cast(kCureNaniteVirus)}}}, {TechId::BIO_UNIANTI, {{K::CurePlague, static_cast(kCureUniversal)}}}, {TechId::DRV_RIP, {{K::NodeBoreParams, 0}}}, {TechId::DRV_REND, {{K::NodeBoreParams, 1}}}, {TechId::DRV_RAD, {{K::NodeBoreParams, 2}}}, }; return table; } const std::vector& NoEffects() { static const std::vector none; return none; } double AiBonusFor(AiBonusSlot slot, const AiBonusValues& v) { switch (slot) { case AiBonusSlot::Research: return v.research; case AiBonusSlot::Admin: return v.admin; case AiBonusSlot::Factory: return v.factory; } return 0.0; } void AddAiBonus(PlayerEconomyState& s, AiBonusSlot slot, double amount) { switch (slot) { case AiBonusSlot::Research: s.resMod += amount; break; case AiBonusSlot::Admin: s.incMod += amount; break; case AiBonusSlot::Factory: s.outMod += amount; break; } } void ApplyOne(PlayerEconomyState& s, const TechEffect& e, const ApplyContext& ctx, TechApplyOutcome& out) { using K = EffectKind; switch (e.kind) { case K::AddConstructionMod: for (double& c : s.conMod) c += e.value; break; case K::AddConstructionModClass: if (e.index >= 0 && e.index < 3) s.conMod[e.index] += e.value; break; case K::AddSavingsMod: for (double& m : s.savMod) m += e.value; break; case K::AddOutputMod: s.outMod += e.value; break; case K::MulOutputMod: s.outMod *= e.value; break; case K::AddPopulationMod: s.popMod += e.value; break; case K::AddTerraformMod: s.terraMod += e.value; break; case K::AddSuitTolerance: s.suitTol += e.value; break; case K::RaiseMaxOverharvest: s.maxOverharvest = std::max(s.maxOverharvest, e.value); break; case K::AddMiningRate: s.miningRate += e.value; break; case K::MulDefenceDamageMod: s.defenceDamageMod *= e.value; break; case K::SetFlag: if (e.index >= 0 && e.index < kPlayerFlagCount) s.flags[e.index] = true; break; case K::SetFlagUnlessRebel: if (!s.rebelAI && e.index >= 0 && e.index < kPlayerFlagCount) s.flags[e.index] = true; break; case K::RaiseGateTraffic: { double v = 0.0; if (ctx.tuning) v = e.index == 0 ? ctx.tuning->PERGATETRAFFIC_DRV_TpGate : ctx.tuning->PERGATETRAFFIC_DRV_GatAmp; s.perGateTraffic = std::max(s.perGateTraffic, v); break; } case K::SetFarCasting: s.castRange = 10.0; s.castEfficiency = 2.0; s.castThreshold = 1.0; break; case K::AiTechBonus: if (s.aiBenefit) AddAiBonus(s, static_cast(e.index), AiBonusFor(static_cast(e.index), ctx.aiBonus)); break; case K::FlagSystemsAI: out.flagSystemsAI = true; break; case K::EnableAiBenefit: SetAiBenefit(s, true, ctx); break; case K::CurePlague: { const unsigned mask = static_cast(e.index); s.hasVaccine |= mask; s.hasImmunity |= mask; out.plagueCuredMask |= mask; break; } case K::GrantTechIfSpecies: if (Sp(s.species) == e.index) out.grantedTech = static_cast(static_cast(e.value)); break; case K::CaptureDesignsWith: if (s.HasResearched(static_cast(e.index))) s.flags[F(PlayerFlag::CaptureDesigns)] = true; break; case K::NodeBoreParams: if (e.index >= 0 && e.index < 3 && kNodeBoreRows[e.index][0] > s.nodeBoreParams[0]) { for (int i = 0; i < 3; ++i) s.nodeBoreParams[i] = kNodeBoreRows[e.index][i]; out.nodeBoreParamsChanged = true; } break; case K::ReevaluateCivilianCaps: out.reevaluateCivilianCaps = true; break; } } } // namespace const std::vector& EffectsOf(TechId id) { for (const TableEntry& t : Table()) { if (t.id == id) return t.effects; } return NoEffects(); } unsigned PlagueCureMask(TechId id) { for (const TechEffect& e : EffectsOf(id)) { if (e.kind == EffectKind::CurePlague) return static_cast(e.index); } return 0; } void RebuildSpeciesTechFlags(PlayerEconomyState& s) { for (int sp = 0; sp < sots::sim::kSpeciesCount; ++sp) { unsigned bits = 0; for (int k = 0; k < kXenoLevelCount; ++k) { const TechId id = XenoTechId(static_cast(k), static_cast(sp)); if (s.HasResearched(id)) bits |= 1u << k; } s.speciesFlags[sp] = bits; } } bool AiRebellionPossible(const PlayerEconomyState& s) { return !s.HasResearched(TechId::CCC_AISlv) && !sots::sim::IsNpcSpecies(s.species); } void SetAiBenefit(PlayerEconomyState& s, bool on, const ApplyContext& ctx) { if (s.aiBenefit == on) return; s.aiBenefit = on; const double sign = on ? 1.0 : -1.0; const TechId aiTechs[3] = {TechId::CCC_AI, TechId::CCC_AIAdmin, TechId::CCC_AIFac}; for (TechId id : aiTechs) { if (!s.HasResearched(id)) continue; for (const TechEffect& e : EffectsOf(id)) { if (e.kind != EffectKind::AiTechBonus) continue; const AiBonusSlot slot = static_cast(e.index); AddAiBonus(s, slot, sign * AiBonusFor(slot, ctx.aiBonus)); } } } TechApplyOutcome ApplyTechEffect(PlayerEconomyState& s, TechId id, const ApplyContext& ctx) { TechApplyOutcome out; if (!IsValidTechId(id) || s.HasResearched(id)) return out; s.researched.set(static_cast(TechIdIndex(id))); out.applied = true; for (const TechEffect& e : EffectsOf(id)) ApplyOne(s, e, ctx, out); // Every completion: re-derive the per-species xenotech bits and report the species // whose temperance is held (their addiction is cured on owned systems). RebuildSpeciesTechFlags(s); for (int sp = 0; sp < sots::sim::kSpeciesCount; ++sp) { if (s.speciesFlags[sp] & (1u << static_cast(XenoLevel::Temperance))) out.temperanceSpeciesMask |= 1u << sp; } return out; } TechApplyOutcome ApplyTechEffectByName(PlayerEconomyState& s, std::string_view name, const ApplyContext& ctx) { // Node-track techs are keyed by name in the species table, not by id. for (int sp = 0; sp < sots::sim::kSpeciesCount; ++sp) { const char* track = NodeTrackTechName(static_cast(sp)); if (track == nullptr) continue; std::string_view t(track); if (t.size() != name.size()) continue; bool same = true; for (std::size_t i = 0; i < t.size() && same; ++i) { same = std::tolower(static_cast(t[i])) == std::tolower(static_cast(name[i])); } if (same) { s.nodeTrackMask |= 1u << sp; TechApplyOutcome out; out.applied = true; return out; } } const TechId id = TechIdFromName(name); if (id == TechId::None) return TechApplyOutcome{}; return ApplyTechEffect(s, id, ctx); } const char* const kDesignOptionNamesA[kDesignOptionCountA] = { "IND_REFCOAT", "IND_IMPRFCT", "IND_PLYALLOY", "IND_MAGLAT", "IND_QRKRES", "IND_ADMALY", "IND_PREDGUN", "SLD_DEF", "SLD_ERGAB", "SLD_MKONE", "SLD_MKTWO", "SLD_MKTHREE", "SLD_MKFOUR", "SLD_CLK", "SLD_IMPCLK", "SLD_INTANG", "WEP_VRFTECH", "WEP_NUKEWHD", "WEP_GMAWHD", "WEP_FUSWHD", "WEP_AMWHD", "WEP_NEUTRND", "CCC_INTSENS", "CCC_SNSJAM", "CCC_QNTCHAF", "CCC_CMBTALG", "CCC_HOLOTAC", "CCC_ADVCNC", "CCC_AdvSens", "DRV_MCROFUS", "DRV_INCTHRST", "DRV_SMLFUS", }; const char* const kDesignOptionNamesB[kDesignOptionCountB] = { "DRV_NODE", "DRV_NODFOC", "DRV_NODPATH", "DRV_STRWRP", "DRV_IMPSTWRP", "DRV_FLICKER", "DRV_HYPER", "DRV_HYPRFLD", "DRV_WARP", "SLD_MESSHLD", "SLD_GRVSHLD", "CCC_AIFRCON", "DRV_RIP", "DRV_REND", "DRV_RAD", "BIO_CONNAN", "SLD_DISR", "SLD_MAGNI", "DRV_QNTCAP", "CCC_FCCOM", "IND_HRDELEC", "IND_TRKSTL", "DRV_VDCTR", "DRV_VDCRV", "DRV_VDMSTR", "BIO_SMRTNAN", "WEP_ACCAMP", "WEP_MwMsl", "WEP_HvyPmsl", }; DesignOptionMasks ComputeDesignOptionMasks(const std::function& hasResearchedByName) { DesignOptionMasks m; for (int i = 0; i < kDesignOptionCountA; ++i) { if (hasResearchedByName(kDesignOptionNamesA[i])) m.a |= 1u << i; } for (int i = 0; i < kDesignOptionCountB; ++i) { if (hasResearchedByName(kDesignOptionNamesB[i])) m.b |= 1u << i; } return m; } } // namespace sots::effects