176 lines
9.8 KiB
Markdown
176 lines
9.8 KiB
Markdown
# 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 |= bit 0 / 1 / 2 / 3 / 4; cure that plague type on owned systems and ships (outcome) |
|
||
| 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.
|