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

219 lines
12 KiB
C++

// Code-defined tech effects: what researching a tech does to a player's strategic
// modifiers, as a data table plus a small apply layer.
//
// Effects are additive per research event and permanent (there is no un-research).
// The state below carries the player fields the effects touch, named after the save
// tags where one exists; the apply function only ever reads and writes this struct and
// reports the things it cannot do itself (touch systems, grant another tech) in an
// outcome the caller acts on.
#pragma once
#include <bitset>
#include <cstdint>
#include <functional>
#include <string_view>
#include <vector>
#include "game/effects/tech_id.h"
#include "game/sim/species.h"
#include "game/sim/tuning.h"
namespace sots::effects {
// Boolean abilities a tech switches on.
enum class PlayerFlag : int {
AdvancedSensors = 0, // hadvs: sensor ranges x ADVSENS_SENSORS_MOD; partial contacts count as known
AsteroidMining, // AMine: asteroid resources counted in output
Arcology, // harcc: +1e8 imperial / +2e8 civilian carrying capacity
GravSynth, // hgs: synced to clients; the drive modifier is applied design-side
TradeAllowed, // CnTrd: trade routes may be registered (not for the rebel AI)
CommerceRaiding, // CnRad
ViewIntel, // CnVItl: intel view on other empires (client side)
CaptureDesigns, // cdp: observed/captured designs (needs both spy beam and salvage)
Count
};
constexpr int kPlayerFlagCount = static_cast<int>(PlayerFlag::Count);
// Slots of the AI-benefit tech bonus.
enum class AiBonusSlot : int { Research = 0, Admin = 1, Factory = 2 };
enum class EffectKind : int {
AddConstructionMod, // ConMod[0..2] += value
AddConstructionModClass, // ConMod[index] += value
AddSavingsMod, // SavMod[0..2] += value
AddOutputMod, // OutMod += value
MulOutputMod, // OutMod *= value
AddPopulationMod, // PopMod += value
AddTerraformMod, // TerraMod += value
AddSuitTolerance, // SuitTol += value
RaiseMaxOverharvest, // MaxOH = max(MaxOH, value)
AddMiningRate, // MinRate += value
MulDefenceDamageMod, // pddm *= value
SetFlag, // flag(index) = true
SetFlagUnlessRebel, // flag(index) = true unless the player is the rebel AI
RaiseGateTraffic, // PrGtTrf = max(PrGtTrf, tuning key: index 0 = TpGate, 1 = GatAmp)
SetFarCasting, // CstR = 10, CstE = 2, CstT = 1
AiTechBonus, // slot(index) += supplied bonus while the AI benefit is on
FlagSystemsAI, // every owned system gets its AI flag (reported to the caller)
EnableAiBenefit, // the AI benefit is switched (back) on
CurePlague, // HasVac |= mask, HasImm |= mask (index = mask); cured on systems/ships
GrantTechIfSpecies, // index = species: the tech in `value` is granted for free
CaptureDesignsWith, // CaptureDesigns flag when this and tech(index) are both researched
NodeBoreParams, // index = parameter row; the highest row wins
ReevaluateCivilianCaps, // systems whose civilians sit at the cap are re-evaluated
};
struct TechEffect {
EffectKind kind;
int index = 0;
double value = 0;
};
// The effects of one tech (empty for techs without a strategic effect).
const std::vector<TechEffect>& EffectsOf(TechId id);
// Everything a tech touches on the player. Defaults are the "no tech yet" values; the
// caller seeds species-dependent starts (SuitTol) from its own tables.
// Every modifier below is a 4-byte float in the player object and every effect is applied
// as `field = (float)((double)field OP constant)` -- the x87 loads the float, combines it
// with a double literal and stores back through a float. Modelling them as `double` and
// rounding once at the end drifts from the original after a few techs, so they are floats
// here and `ApplyTechEffect` rounds at every step. See docs/B2.md.
struct PlayerEconomyState {
sots::sim::Species species = sots::sim::Species::Human;
bool rebelAI = false;
float conMod[3] = {1.f, 1.f, 1.f}; // ConMod: construction cost per hull class
float savMod[3] = {1.f, 1.f, 1.f}; // SavMod
float outMod = 1.f; // OutMod
float popMod = 1.f; // PopMod
float terraMod = 1.f; // TerraMod
float suitTol = 0.f; // SuitTol: species start value + adaptation techs
float maxOverharvest = 0.f; // MaxOH
float miningRate = 0.f; // MinRate
float defenceDamageMod = 1.f; // pddm
float resMod = 1.f; // ResMod (AI research bonus lands here)
float incMod = 1.f; // IncMod
float castRange = 0.f; // CstR
float castEfficiency = 0.f; // CstE
float castThreshold = 0.f; // CstT
int perGateTraffic = 0; // PrGtTrf -- an int, raised by an integer max
// Node-bore parameters. In the player object these live in a separately allocated
// 3-word block whose pointer is null while no bore drive is researched, so "absent"
// is a state of its own rather than three zeroes.
bool hasNodeBoreParams = false;
int nodeBoreParams[3] = {0, 0, 0};
bool flags[kPlayerFlagCount] = {};
bool aiBenefit = true; // AIBn: false after an AI rebellion
unsigned nodeTrackMask = 0; // NPTrk: bit per Species whose traffic is visible
unsigned hasVaccine = 0; // HasVac
unsigned hasImmunity = 0; // HasImm
unsigned speciesFlags[sots::sim::kSpeciesCount] = {}; // xenotech bits per target species
// Sticky "level-1 translation for this species has been researched" mask, one bit per
// Species (never the NPC race, never cleared). Re-derived alongside speciesFlags.
unsigned translationKnownMask = 0;
std::bitset<kTechIdCount> researched;
bool Flag(PlayerFlag f) const { return flags[static_cast<int>(f)]; }
bool HasResearched(TechId id) const { return IsValidTechId(id) && researched.test(static_cast<std::size_t>(TechIdIndex(id))); }
};
// Values of the three AI-benefit bonuses. The game reads them from a 6-row table in the
// executable ({tech id, rebellion odds, bonus value}); all three bonus values are 0.5
// (B2, table dumped). The caller may still override them.
struct AiBonusValues {
double research = 0.5; // added to ResMod (CCC_AI)
double admin = 0.5; // added to IncMod (CCC_AIAdmin)
double factory = 0.5; // added to OutMod (CCC_AIFac)
};
struct ApplyContext {
const sots::sim::TuningTable* tuning = nullptr; // gate traffic keys
AiBonusValues aiBonus;
};
// What the caller has to do after an apply because it needs game state this layer has
// no access to.
struct TechApplyOutcome {
bool applied = false; // false when the id is invalid or already researched
TechId grantedTech = TechId::None; // research this one too (Zuul boarding pods)
unsigned plagueCuredMask = 0; // clear these plague types on owned systems and ships
bool flagSystemsAI = false; // mark every owned system's AI flag
bool reevaluateCivilianCaps = false; // re-evaluate systems whose civilians are at the cap
unsigned temperanceSpeciesMask = 0; // bit per Species: cure addiction to it on owned systems
bool nodeBoreParamsChanged = false;
};
// Mark `id` researched and apply its effects. Every completion also rebuilds the
// per-species xenotech flags and reports the temperance species. Applying an already
// researched or invalid id does nothing. CONFIDENCE: high on the tabled effects (their
// constants were read directly); medium on the AI-benefit re-application and the
// node-bore "highest wins" rule.
TechApplyOutcome ApplyTechEffect(PlayerEconomyState& s, TechId id, const ApplyContext& ctx);
// The completion callback itself, with no already-researched guard: exactly what the game
// runs every time a tech is marked researched, in the game's order -- the one matching
// branch of the id chain, then the tail (plague mask, design-option masks are the caller's,
// node-bore parameters, species flags, the Zuul grant, the capture-designs pair test, the
// temperance sweep). `ApplyTechEffect` is this plus the guard; a differential hook must use
// this one, because by the time the callback runs the tech is already marked researched.
TechApplyOutcome ApplyTechCompletion(PlayerEconomyState& s, TechId id, const ApplyContext& ctx);
// Just the tail of a completion: the node-bore selection, the species flag words and their
// sticky translation mask, the capture-designs pair test and the temperance report. The
// game runs it for *every* tech marked researched, including the ~half of the data files
// that are not in the 196-name key space at all and so have no branch of their own.
TechApplyOutcome RunCompletionTail(PlayerEconomyState& s);
// Apply a completion by data-file name: resolves the id (case-insensitively), and also
// handles the node-track techs, which are keyed by name in the species table rather than
// by id. Returns the outcome; `applied` is false for names without a code effect.
TechApplyOutcome ApplyTechEffectByName(PlayerEconomyState& s, std::string_view name, const ApplyContext& ctx);
// Switch the AI benefit on or off, adding or removing the bonus of every researched AI
// tech accordingly (off = AI rebellion; on again = the AI slave tech). CONFIDENCE: medium.
void SetAiBenefit(PlayerEconomyState& s, bool on, const ApplyContext& ctx);
// Recompute speciesFlags[sp] from the researched set: bit k is set when the level-k
// xenotech aimed at species sp is researched. Also run on load. CONFIDENCE: high.
void RebuildSpeciesTechFlags(PlayerEconomyState& s);
// Whether an AI rebellion can still happen to this player: only while the AI slave tech
// is not researched (and the player is not the NPC race). CONFIDENCE: high.
bool AiRebellionPossible(const PlayerEconomyState& s);
// Plague-cure bit of a vaccine tech (0 for anything else); the universal antidote
// covers the first four plague types. CONFIDENCE: high.
unsigned PlagueCureMask(TechId id);
// Design-option availability masks: two words whose bits mean "the named tech is
// researched". The names are game vocabulary; their ids are not all recovered, so the
// masks are computed from a by-name predicate. Bit i of A is kDesignOptionNamesA[i], of
// B kDesignOptionNamesB[i]. Consumers are design-side and not modelled. CONFIDENCE: high
// on the tables.
constexpr int kDesignOptionCountA = 32;
constexpr int kDesignOptionCountB = 29;
extern const char* const kDesignOptionNamesA[kDesignOptionCountA];
extern const char* const kDesignOptionNamesB[kDesignOptionCountB];
// The same two tables keyed the way the game keys them: bit i is "tech id kDesignOptionIdsX[i]
// is researched". Recovered in B2 from the two {tech id, bit} tables in the executable.
extern const TechId kDesignOptionIdsA[kDesignOptionCountA];
extern const TechId kDesignOptionIdsB[kDesignOptionCountB];
struct DesignOptionMasks {
std::uint32_t a = 0;
std::uint32_t b = 0;
};
DesignOptionMasks ComputeDesignOptionMasks(const std::function<bool(std::string_view)>& hasResearchedByName);
// By id -- what the executable actually does, and what a differential hook needs.
DesignOptionMasks ComputeDesignOptionMasks(const std::function<bool(TechId)>& hasResearched);
// Chance of an AI rebellion contributed by researching `id`, from the same 6-row table as
// the bonus values: 0.1 for the three AI techs, 0.2 for CCC_AIFRCON, 0 for everything else.
// The risk applies only while AiRebellionPossible(). CONFIDENCE: high (table dumped);
// how the odds are consumed is not modelled here.
double AiRebellionOdds(TechId id);
} // namespace sots::effects