sots-engine/docs/game-effects.md

9.8 KiB
Raw Permalink Blame History

game/effects — code-defined tech effects

Module: src/game/effects/. Two data tables and a small apply layer over a plain PlayerEconomyState; no game state, no I/O. Depends on game/sim for Species and TuningTable.

Build/test: tests/game_effects/build_and_run.sh (plain g++, -Wall -Wextra -Werror), or -DSOTS_GAME_EFFECTS_TESTS=ON once src/game/effects is added to the root CMake after src/game/sim.

Confidence legend as in game-sim.md.

The key space (tech_id.h)

The game does not key effects on the .tech files. It carries a fixed list of 196 tech names; when the master tree is built, every name is matched case-insensitively against the loaded techs and its position becomes the tech's id, 10000 + index (10197 is the "none" sentinel). All hard-coded effects, runtime gates and design-option bitmasks read those ids. A tech absent from the list has no code effect beyond what its data file says (prerequisites, section/weapon availability).

TechId reproduces that list as an enum with the same numeric values, generated from one X-macro so the enum and the name table cannot drift. Since B2 it is the whole table, read out of the executable: all 196 names, so TechIdName never returns nullptr for a valid id and TechIdFromName resolves every tech the code can key on. Enum identifiers are the data-file names except in the xenotech block, where the role-based names are kept (XNC_Temperance_Hiver = "XNC_TEMPHVR") because XenoTechId is built on them.

Xenotech block

Ids 10114–10163 are the per-species xenotechs, nine families in flag-bit order, each a block of one tech per target species (XenoTechId(level, species)):

bit family block base targets
0 Translation 1 10114 Human, Hiver, Tarkas, Liir, Zuul, Morrigi
1 Translation 2 10120 same six
2 Translation 3 10126 same six (the Zuul slot's data name is XNC_DOMZUUL)
3 Incorporate 10132 five: no Zuul
4 Addict 10137 five
5 Temperance 10142 five
6 Subjugate 10147 six
7 Accommodate 10153 five
8 Proliferate 10158 five

Ids 10163 / 10164 — the two slots after the proliferate block — are the node-track techs CCC_NDTRKHUM / CCC_NDTRKZUL. The NPC race is never a target. Confidence: high throughout since B2 (the family order, the block bases, the species order inside a block and which families omit the Zuul are all read off the name table). Before B2 the proliferate block was modelled as six entries, which made XenoTechId(Proliferate, Morrigi) return the Human node-track tech.

Node-track techs

Seeing a species' node-space traffic is granted by a tech keyed by name in the species table rather than by the effect chain: CCC_NDTRKHUM (10163) for Human traffic, CCC_NDTRKZUL (10164) for Zuul traffic, none for the others. NodeTrackTechName(species) exposes the name and both now resolve through TechIdFromName. Confidence: high on the names and ids, medium on the reader semantics.

The effects table (tech_effects.h)

EffectsOf(id) returns the list of typed effects ({kind, index, value}) applied when the tech completes. Effects are additive per research event and permanent. 44 ids carry an entry; every other id returns an empty list.

Everything below is float32. The modifiers are 4-byte floats in the player object and each step is field = (float)((double)field OP k), where k is the widened float32 literal the executable carries -- 0.05 is (double)0.05f, not 0.05. PlayerEconomyState therefore holds floats and the table's constants are written with an f suffix; see docs/B2.md. PrGtTrf is the exception: it is an int, raised with an integer max.

tech effects
CCC_AdvSens flag AdvancedSensors
IND_Waldo, IND_ExpSys ConMod[0..2] −0.10; OutMod +0.15
IND_CyberInt ConMod[0..2] −0.05; OutMod +0.20
IND_OrbFound SavMod[0..2] −0.05
IND_OrbDry ConMod[1] −0.05; ConMod[2] −0.05 (index 0 untouched — ConMod is per hull class, medium)
IND_GravCon OutMod +0.30
IND_HvyPlat OutMod +0.10
IND_AstMine flag AsteroidMining
IND_MsMine MaxOH = max(MaxOH, 0.1); MinRate +1.0
BIO_GnMod PopMod +0.10
BIO_AtmoAd SuitTol +0.75; PopMod +0.06; TerraMod +0.35
BIO_EnvTail PopMod +0.20; TerraMod +0.45
BIO_GrvAdpt SuitTol +1.50; PopMod +0.10; TerraMod +0.35
IND_ArcCon flag Arcology; PopMod +0.15; re-evaluate systems whose civilians sit at the cap (outcome)
IND_EleNans TerraMod +0.60
BIO_TerBac TerraMod +0.45
IND_AtProc TerraMod +0.50
DRV_TpGate / DRV_GatAmp PrGtTrf = max(PrGtTrf, PERGATETRAFFIC_DRV_TpGate / _GatAmp) from the tuning table
DRV_FarCast CstR 10, CstE 2, CstT 1
CCC_AI / CCC_AIAdmin / CCC_AIFac ResMod / IncMod / OutMod += the AI-bonus value for that slot, only while the AI benefit is on
CCC_AIVrus every owned system gets its AI flag (outcome)
CCC_AISlv same, plus the AI benefit is switched back on; AiRebellionPossible becomes false
CCC_FtlEcon flag TradeAllowed, unless the player is the rebel AI
CCC_ComRaid flag CommerceRaiding
DRV_GrvSyn flag GravSynth (client sync only; the drive modifier is design-side)
CCC_DatCor flag ViewIntel
IND_HrdStrct pddm ×0.25; OutMod ×0.90 — multiplicative, so order against the additive OutMod techs matters
DRN_AdvRob ConMod[0..2] −0.05
IND_CruisCon Zuul only: IND_BrdPod is granted (outcome; the caller researches it)
CCC_SpyBm, IND_SlvgTech flag CaptureDesigns once both are researched
BIO_PLGVAC / RTPLGVAC / BSTVAC / ASPLGVAC / CONNAN HasVac, HasImm
BIO_UNIANTI same with mask 0x0f
DRV_RIP / REND / RAD node-bore parameters {45,15,3} / {65,35,4} / {95,60,5}, highest wins (re-derived in the tail, and the block is absent when none is researched)

Confidence: high on every constant above (each was read with its literal) and, since B2, high on the node-bore rule (the selector tests RAD, then REND, then RIP, and the first hit wins) and on the AI-benefit re-application. The three AI-bonus values are now recovered as well: 0.5 each, and AiRebellionOdds carries the same table's odds column (0.1 for the three AI techs, 0.2 for CCC_AIFRCON).

Three of the table's entries are not applied by the completing tech's own branch -- the game does them in the tail of every completion, so ApplyTechCompletion does too:

  • the two design-option masks (ComputeDesignOptionMasks, now available keyed by id);
  • the node-bore parameters, re-derived from the whole researched set;
  • the capture-designs pair test, which fires on whichever completion first sees both CCC_SpyBm and IND_SlvgTech researched -- not only on those two techs' own.

Every completion also rebuilds speciesFlags[] (bit k of species sp = the level-k xenotech for sp is researched), ORs a sticky translationKnownMask bit for every non-NPC species whose level-1 translation is researched, and reports, in the outcome, every species whose temperance bit is held so the caller can cure addiction to it on owned systems.

Where the modifiers are consumed

OutMod → TotalSystemOutput (game/sim); PopMod → PopulationGrowthDelta; TerraMod → TerraformDelta; SuitTol → HazardModifier and SuitabilityCostMod; ConMod[i] → per-hull-class construction cost (medium); SavMod[i] → maintenance side (low); pddm → planetary-defence damage (low); MaxOH / MinRate → mining sliders; PrGtTrf → gate traffic capacity; the flags → the gates listed in the RE catalog. hazardMod is 1 (skipped) when speciesFlags[sp] has the accommodate bit or the player is the rebel AI.

Design-option masks

Two 32-bit words of "named tech researched" bits feed the design side; kDesignOptionNamesA (32 names) and kDesignOptionNamesB (29 names) are the tables and ComputeDesignOptionMasks(hasResearchedByName) builds the words from a by-name predicate, because not every name's id is recovered. Consumers are not modelled. Confidence: high on the tables.

Apply layer

PlayerEconomyState s;              // seeded by the caller (species, SuitTol start, ...)
ApplyContext ctx{&tuning, aiBonus};
TechApplyOutcome o = ApplyTechEffect(s, TechId::IND_HrdStrct, ctx);

ApplyTechEffect marks the id researched, applies its effects, runs the tail, and returns what the caller must do with real game state: grantedTech, plagueCuredMask, flagSystemsAI, reevaluateCivilianCaps, temperanceSpeciesMask, nodeBoreParamsChanged. Applying an invalid or already-researched id is a no-op (applied == false). ApplyTechCompletion is the same thing without the already-researched guard: that is what the game's callback is, because by the time it runs the node is already marked researched. A differential hook must use it. ApplyTechEffectByName resolves a data-file name first and also handles the node-track names. SetAiBenefit(s, on, ctx) adds or withdraws every researched AI tech's bonus (AI rebellion / AI slave tech). RebuildSpeciesTechFlags is also the load path.

Not modelled / open

  • Nothing is left open in the key space: all 196 names are in the table since B2.
  • The plague-cure test in the game matches a vaccine tech or any descendant of it; PlagueCureMask is exact-id only, because the descendant relation needs the tech tree.
  • The events raised on completion (research complete / under budget), the plague-cure roll, the Zuul starting immunity/temperance flags, and the home-system bonus initialisation that reads the *_HOME tuning keys.
  • Runtime gates that read ids at use sites (advanced-sensor contact rules, spy-beam intel, tunnel sensors, hyper-com retargeting, hull regeneration, missile/beam/cannon variants, plague-type maps): documented in the RE catalog, to be modelled where those systems are built.