sots-engine/src/game/effects/tech_effects.cpp

283 lines
13 KiB
C++

#include "game/effects/tech_effects.h"
#include <algorithm>
#include <array>
namespace sots::effects {
namespace {
using sots::sim::Species;
constexpr int F(PlayerFlag f) { return static_cast<int>(f); }
constexpr int Sp(Species s) { return static_cast<int>(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<TechEffect> effects;
};
const std::vector<TableEntry>& Table() {
using K = EffectKind;
static const std::vector<TableEntry> 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<int>(AiBonusSlot::Research)}}},
{TechId::CCC_AIAdmin, {{K::AiTechBonus, static_cast<int>(AiBonusSlot::Admin)}}},
{TechId::CCC_AIFac, {{K::AiTechBonus, static_cast<int>(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<double>(static_cast<int>(TechId::IND_BrdPod))}}},
{TechId::CCC_SpyBm, {{K::CaptureDesignsWith, static_cast<int>(TechId::IND_SlvgTech)}}},
{TechId::IND_SlvgTech, {{K::CaptureDesignsWith, static_cast<int>(TechId::CCC_SpyBm)}}},
{TechId::BIO_PLGVAC, {{K::CurePlague, static_cast<int>(kCurePlague)}}},
{TechId::BIO_RTPLGVAC, {{K::CurePlague, static_cast<int>(kCureRetroPlague)}}},
{TechId::BIO_BSTVAC, {{K::CurePlague, static_cast<int>(kCureBeastPlague)}}},
{TechId::BIO_ASPLGVAC, {{K::CurePlague, static_cast<int>(kCureAssimilationPlague)}}},
{TechId::BIO_CONNAN, {{K::CurePlague, static_cast<int>(kCureNaniteVirus)}}},
{TechId::BIO_UNIANTI, {{K::CurePlague, static_cast<int>(kCureUniversal)}}},
{TechId::DRV_RIP, {{K::NodeBoreParams, 0}}},
{TechId::DRV_REND, {{K::NodeBoreParams, 1}}},
{TechId::DRV_RAD, {{K::NodeBoreParams, 2}}},
};
return table;
}
const std::vector<TechEffect>& NoEffects() {
static const std::vector<TechEffect> 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<AiBonusSlot>(e.index), AiBonusFor(static_cast<AiBonusSlot>(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<unsigned>(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<TechId>(static_cast<int>(e.value));
break;
case K::CaptureDesignsWith:
if (s.HasResearched(static_cast<TechId>(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<TechEffect>& 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<unsigned>(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<XenoLevel>(k), static_cast<Species>(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<AiBonusSlot>(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<std::size_t>(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<int>(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<Species>(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<unsigned char>(t[i])) == std::tolower(static_cast<unsigned char>(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<bool(std::string_view)>& 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