Aggregate the whole suite wiki onto the Institution page
Wiki/Home.md is the suite's front door; every layer's wiki now mirrors here under Wiki/<layer>/ (core, contraband, justice, gangs, ward), so the whole suite reads in one place with the index linking to the local pages. The layer repos stay canonical -- Tools/sync-wiki.sh refreshes the aggregated copy on demand. The README's main page points to it.
This commit is contained in:
@@ -0,0 +1,165 @@
|
||||
# Criminal Record — the history of fact
|
||||
|
||||
If [Propensity](Propensity.md) is *suspicion* — "this pawn seems dangerous" — the criminal record is
|
||||
*fact*: "this pawn **has** escaped twice and shanked a guard." The two are deliberately different
|
||||
kinds of thing. Propensity is computed fresh from who a pawn is and how they are treated; a record is
|
||||
a durable log of what they have actually done, written once when it happens and read forever after.
|
||||
|
||||
`CriminalRecord` is the single per-pawn history the **whole suite** writes to and reads from.
|
||||
Classification grades on it, parole gates on it, the deterrence loop reacts to it, and reintegration
|
||||
remembers it. Because there is exactly one record per pawn and everyone shares it, no two modules can
|
||||
disagree about a pawn's past.
|
||||
|
||||
Everything here is verified against `Source/Core/CriminalRecord.cs`.
|
||||
|
||||
---
|
||||
|
||||
## The fields
|
||||
|
||||
`CriminalRecord` is a plain `IExposable` bag of counters plus two special values. Core defines the
|
||||
fields and the storage; it does **not** write most of them — the modules that own each event do.
|
||||
|
||||
| Field | Type | Default | What it records | Who writes it | Who reads it |
|
||||
|---|---|---:|---|---|---|
|
||||
| `crimesCommitted` | `int` | `0` | count of committed crimes | Justice `RecordCrime` (also on gang fights) | classification risk score, deterrence |
|
||||
| `escapeAttempts` | `int` | `0` | breakout attempts | Contraband (escape/tunnel logic) | classification risk score |
|
||||
| `contrabandMade` | `int` | `0` | items brewed / whittled | Contraband (shivs, vessels) | classification risk score |
|
||||
| `timesSearched` | `int` | `0` | how often searched | Contraband (warden search) | search prioritisation, audit |
|
||||
| `timesCaught` | `int` | `0` | searches that found something | Contraband (warden search) | classification risk score |
|
||||
| `lastCrimeTick` | `int` | `−1` | game tick of the most recent crime | Justice `RecordCrime` | recency / cooldown checks |
|
||||
| `reform` | `float` | `0` | rehabilitation score (see below) | Justice `Discipline`, `Parole` | **Propensity.Nurture**, parole gate, reintegration |
|
||||
| `pardoned` | `bool` | `false` | granted a clean slate after genuine reform | Justice `Parole` (on release) | parole gate, reintegration |
|
||||
|
||||
`lastCrimeTick` defaults to `−1` — the sentinel for "never," distinct from tick `0`. Everything else
|
||||
defaults to zero/false.
|
||||
|
||||
> **Note on ownership.** Core is the *vault*, not the *clerk*. It hands out records and persists them;
|
||||
> the write logic ("a crime just happened, bump the counter") lives in the module where the event
|
||||
> occurs, above Core. The columns above name where that logic lives in the suite — Core itself only
|
||||
> defines the fields.
|
||||
|
||||
## `reform` — the pivot of the suite
|
||||
|
||||
Most fields are inert tallies. `reform` is the one that feeds back into behaviour, and it is worth
|
||||
its own section.
|
||||
|
||||
```
|
||||
reform : float, nominally 0..1 but can go negative
|
||||
raised by good treatment and good conduct
|
||||
decays without either
|
||||
read by Propensity.Nurture, the parole gate, and reintegration
|
||||
```
|
||||
|
||||
The source is blunt about its role: *"it is what makes reform MEAN something."* A record is where a
|
||||
sentence's *outcome* is stored. Three systems lean on that one float:
|
||||
|
||||
- **[Propensity](Propensity.md)'s Nurture** multiplies disposition by `Clamp(1 − reform×0.5, 0.4, 2)`
|
||||
— so a genuinely rehabilitated pawn (`reform > 0`) is calmer *for good*, and a prisonized one
|
||||
(`reform < 0`) is inflamed *for good*. This is how institutionalization and recidivism enter the
|
||||
model without a separate mechanic bolted on.
|
||||
- **Parole** (Justice) gates release on it: a pawn is only releasable once `reform` clears a
|
||||
threshold. It is the number that says "this one is ready."
|
||||
- **Reintegration** reads it to decide when a clean slate is earned — which is what `pardoned` marks.
|
||||
|
||||
Although the field is documented as `0..1`, Justice's `Discipline` can drive it negative (harsh
|
||||
punishment subtracts from it). That negative range is not a bug — it is the "hardened" end of the
|
||||
spectrum, and `Nurture`'s clamp is written to expect it.
|
||||
|
||||
## `pardoned` and `IsBlank`
|
||||
|
||||
`pardoned` flips to `true` exactly once, after genuine reform, so reintegration can grant a clean
|
||||
slate without erasing the history that earned it — the counters stay, but the pawn is marked
|
||||
forgiven.
|
||||
|
||||
`IsBlank` is a computed convenience:
|
||||
|
||||
```csharp
|
||||
public bool IsBlank => crimesCommitted == 0 && escapeAttempts == 0 && contrabandMade == 0
|
||||
&& timesCaught == 0 && reform == 0f && !pardoned;
|
||||
```
|
||||
|
||||
A blank record is one nothing has ever been written to. Note what `IsBlank` **omits**: `timesSearched`
|
||||
and `lastCrimeTick` are not in the test. A pawn who was searched and found clean — searched but never
|
||||
caught, never a crime — is still "blank" and will be pruned. That is intentional: being *checked* is
|
||||
not a mark against you, only being *found* is.
|
||||
|
||||
---
|
||||
|
||||
## `For` vs `PeekFor` — the distinction that matters
|
||||
|
||||
Records are handed out by `GameComponent_CriminalRecords`, and there are two ways to ask for one. The
|
||||
difference is not cosmetic.
|
||||
|
||||
```csharp
|
||||
// Creates a blank record on first ask. Use on a WRITE path.
|
||||
public static CriminalRecord For(Pawn p);
|
||||
|
||||
// Returns null if no record exists yet. Use on a READ path that must not create.
|
||||
public static CriminalRecord PeekFor(Pawn p);
|
||||
```
|
||||
|
||||
- **`For(pawn)`** guarantees a record — if the pawn has none, it makes a blank one and stores it. Call
|
||||
this when you are about to *write* something ("record a crime"), because you need a record to write
|
||||
to.
|
||||
- **`PeekFor(pawn)`** returns the existing record or `null`. Call this on a *read* path that should
|
||||
not leave a trail. The archetypal caller is `Propensity.Nurture`: it wants to *read* `reform` if a
|
||||
record exists, but merely asking about a pawn's disposition must not conjure a criminal file for an
|
||||
innocent. If `Nurture` used `For`, every mood check would stamp a record onto every pawn on the map.
|
||||
|
||||
Both are `static` and both are null-safe — a null pawn (or a game with no component yet) returns
|
||||
`null` rather than throwing.
|
||||
|
||||
> The rule of thumb: **write with `For`, read with `PeekFor`.** The only reason `Nurture` can be
|
||||
> called on all pawns constantly without littering the save with empty records is that it peeks.
|
||||
|
||||
---
|
||||
|
||||
## Persistence
|
||||
|
||||
`GameComponent_CriminalRecords` is the single source of truth for the whole game. The design note is
|
||||
explicit: *"modules never keep their own per-pawn crime state, they read and write here, which is what
|
||||
keeps them agreeing with each other."*
|
||||
|
||||
It stores records in a `Dictionary<Pawn, CriminalRecord>` and serialises them with RimWorld's Scribe:
|
||||
|
||||
```csharp
|
||||
Scribe_Collections.Look(ref records, "records",
|
||||
LookMode.Reference, LookMode.Deep, ref tmpPawns, ref tmpRecords);
|
||||
```
|
||||
|
||||
The keys save as **references** (a pawn is saved elsewhere; the record just points at them) and the
|
||||
values save **deep** (the record's fields are written inline). Each `CriminalRecord.ExposeData`
|
||||
scribes its own eight fields with their defaults, so a save omits any field still at its default.
|
||||
|
||||
### Post-load cleanup
|
||||
|
||||
On `PostLoadInit`, the component prunes the dictionary:
|
||||
|
||||
```csharp
|
||||
records.RemoveAll(kv => kv.Key == null || kv.Value == null || kv.Value.IsBlank);
|
||||
```
|
||||
|
||||
Three things get dropped:
|
||||
|
||||
1. **Null keys** — a pawn who has been removed or garbage-collected. *"Dead/removed pawns take their
|
||||
records with them; a null key would throw later."* This is defensive: a stale key would crash a
|
||||
later lookup.
|
||||
2. **Null values** — corruption guard.
|
||||
3. **Blank records** — anything `IsBlank` is true for. There is no reason to persist a file that
|
||||
records nothing, and pruning them keeps the save from accumulating one empty record per pawn ever
|
||||
examined.
|
||||
|
||||
The upshot: **records are cheap and self-cleaning.** You can `For(pawn)` freely on a write path
|
||||
without worrying about bloat, because anything that never got a real mark written to it evaporates on
|
||||
the next load.
|
||||
|
||||
### The singleton
|
||||
|
||||
The component keeps a private `static instance` set both in its constructor and re-set on
|
||||
`PostLoadInit`, which is what lets `For` and `PeekFor` be static entry points reachable from anywhere
|
||||
without threading a reference through every caller. If no game is loaded (no component), both return
|
||||
`null` safely.
|
||||
|
||||
---
|
||||
|
||||
*Part of the **Institution** suite. AI disclosure: developed with substantial assistance from Claude (Anthropic).*
|
||||
@@ -0,0 +1,88 @@
|
||||
# Institution: Core — Wiki
|
||||
|
||||
*The shared foundation of the **Institution** suite: a set of RimWorld 1.6 mods about what an
|
||||
institution does to the people inside it, and the crime, policing, and justice that put them there.*
|
||||
|
||||
Core is the engine every other Institution mod runs on. **On its own it changes nothing you can
|
||||
see** — no items, no jobs, no UI, no XML. It ships one small assembly and is completely inert until
|
||||
another Institution mod is installed on top of it. You install Core only because something else asks
|
||||
for it.
|
||||
|
||||
This wiki documents what that engine actually computes, down to the exact numbers.
|
||||
|
||||
---
|
||||
|
||||
## The one idea
|
||||
|
||||
> **Every pawn has a criminal propensity on a spectrum — nature (traits) × nurture (circumstance,
|
||||
> mood, treatment) — and the colony can police it.**
|
||||
|
||||
That single sentence is the whole suite. Colonists, prisoners, and slaves drift toward crime on a
|
||||
continuous spectrum; a policing and justice layer discovers, catches, punishes, reforms, or paroles
|
||||
them. Core is where the *spectrum* lives as code — before anyone is caught, before there is a record
|
||||
to read, before there is a warden to search them.
|
||||
|
||||
## Why a shared engine
|
||||
|
||||
The propensity idea did not start abstract. It was written twice, concretely, in two different mods:
|
||||
the piss spree's `WouldFoulPeople` (which drunk, miserable pawn starts a mess) and the breakout's
|
||||
`WouldArmSelf` (which prisoner whittles a shiv when the door opens). Both asked the same real
|
||||
question — *would **this** pawn do it?* — and both answered it their own way.
|
||||
|
||||
Core exists so they stop disagreeing. When the contraband system, the justice system, and the gang
|
||||
system all read one disposition engine, they agree about who is dangerous instead of each computing
|
||||
it from scratch and drifting apart. One pawn who is "the scary one" is the scary one to every module
|
||||
at once. That coherence — not any single number — is the point of a shared substrate.
|
||||
|
||||
Core carries exactly the three things every Institution mod reads and none should own alone, and
|
||||
nothing else:
|
||||
|
||||
| Pillar | Class | Answers | Page |
|
||||
|---|---|---|---|
|
||||
| **Propensity** | `Propensity` | *Would this pawn do it?* (nature × nurture, seeded) | [Propensity](Propensity.md) |
|
||||
| **Criminal record** | `CriminalRecord` | *What has this pawn actually done?* (one per-pawn history) | [Criminal Record](Criminal-Record.md) |
|
||||
| **Secured context** | `SecuredContexts` | *What kind of hold is this pawn under, and who may search them?* | [Secured Context](Secured-Context.md) |
|
||||
|
||||
The distinction between the first two is the spine of the whole design: **propensity is suspicion, a
|
||||
record is fact.** Propensity says "this pawn *seems* dangerous"; a record says "this pawn *has*
|
||||
escaped twice and shanked a guard." Keeping them separate is what lets the colony act on evidence
|
||||
instead of on vibes.
|
||||
|
||||
## The pages of this wiki
|
||||
|
||||
- **[Propensity](Propensity.md)** — nature × nurture in full: the trait-weight table, the nurture
|
||||
multiplier ladder, the `Would()` formula with worked examples, and why the roll is seeded per pawn.
|
||||
- **[Criminal Record](Criminal-Record.md)** — every field, who writes and reads each, the
|
||||
`For` vs `PeekFor` distinction, how it persists, and why `reform` is the pivot of the suite.
|
||||
- **[Secured Context](Secured-Context.md)** — the Free / Prisoner / Slave kinds, `Of()` and
|
||||
`OnMap()`, `IsHeld` vs `CanConceal`, and why the suite keys off this instead of "prisoner."
|
||||
- **[Treatment Engine](Treatment-Engine.md)** — the shared rehabilitation maths: mark a condition
|
||||
treatable, reduce it by skill × facility quality per session, and build a recovery track toward
|
||||
discharge. Ward's psychiatric care and Justice's reform share this one engine.
|
||||
- **[Modder API](Modder-API.md)** — the public surface any mod can call, and the one seam
|
||||
(`Propensity.DeterrenceFactor`) that lets a justice layer feed back into disposition without Core
|
||||
ever depending on it.
|
||||
|
||||
## The suite
|
||||
|
||||
Each mod stands alone as an install; together they form one system. Core is the leaf everything else
|
||||
depends on.
|
||||
|
||||
| Mod | What it adds | Needs |
|
||||
|---|---|---|
|
||||
| **Institution: Core** (this) | the propensity / record / context engine | base game only |
|
||||
| **Institution: Contraband** | concealment, improvised shivs, tunnels, warden search, warden corruption | Core |
|
||||
| **Institution: Justice** | classification, deterrence, discipline, parole, regime, and **policing** (colony crime → weighed arrest) | Core, Harmony |
|
||||
| **Institution: Gangs** | gangs as contraband economies: joining, smuggling, rivalry, fights-as-crime | Core, Contraband, Justice |
|
||||
| **Institution: Ward** | psychiatric care — a prisoner treated, not punished, on Core's treatment engine | Core, Harmony |
|
||||
| **Foul Play** | the vessel + substance + throw framework, home of the "Piss Nuke" | standalone; bridges to Core + Contraband |
|
||||
|
||||
## Requirements & load order
|
||||
|
||||
- **RimWorld 1.6.** Core references only the base game — no Harmony, no DLC, no other mod.
|
||||
- Load Core **before** any other Institution mod (`loadAfter` Ludeon.RimWorld only).
|
||||
- `packageId`: `flan.institution.core`.
|
||||
|
||||
---
|
||||
|
||||
*Part of the **Institution** suite. AI disclosure: developed with substantial assistance from Claude (Anthropic).*
|
||||
@@ -0,0 +1,233 @@
|
||||
# Modder API — building on Core
|
||||
|
||||
Core is a library. It has no XML, no defs, no UI — its entire purpose is to be *called*, by the other
|
||||
Institution mods and by yours. This page is the public surface, with the one seam that matters most
|
||||
(`Propensity.DeterrenceFactor`) explained in full.
|
||||
|
||||
Everything here is verified against the three source files in `Source/Core/`.
|
||||
|
||||
---
|
||||
|
||||
## The namespace is `Contraband`
|
||||
|
||||
Before anything else: **all of Core's types live in `namespace Contraband`**, not `Core` and not
|
||||
`Institution`. This is historical — the engine was extracted from Institution: Contraband — and it is
|
||||
not going to change under you, but it does mean your `using` looks slightly surprising:
|
||||
|
||||
```csharp
|
||||
using Contraband; // Propensity, CriminalRecord, GameComponent_CriminalRecords,
|
||||
// SecuredKind, SecuredContext, SecuredContexts,
|
||||
// TreatmentProgram, TreatableConditionExtension
|
||||
```
|
||||
|
||||
Reference the assembly `InstitutionCore.dll` (packageId `flan.institution.core`) and load your mod
|
||||
after Core.
|
||||
|
||||
## The public surface at a glance
|
||||
|
||||
| Call | Returns | Creates state? | Use for |
|
||||
|---|---|---|---|
|
||||
| `Propensity.Nature(pawn)` | `float 0..1` | no | fixed trait-based disposition |
|
||||
| `Propensity.Nurture(pawn)` | `float` (multiplier) | no | live circumstance multiplier |
|
||||
| `Propensity.Would(pawn, baseChance, salt, cap=0.85)` | `bool` | no | the seeded yes/no for a behaviour |
|
||||
| `Propensity.DeterrenceFactor` | `Func<Pawn,float>` field | — | the seam a justice layer fills |
|
||||
| `GameComponent_CriminalRecords.For(pawn)` | `CriminalRecord` | **yes** (blank on first ask) | write paths |
|
||||
| `GameComponent_CriminalRecords.PeekFor(pawn)` | `CriminalRecord?` | no | read paths |
|
||||
| `SecuredContexts.Of(pawn)` | `SecuredContext?` | no | one pawn's hold kind |
|
||||
| `SecuredContexts.OnMap(map)` | `IEnumerable<SecuredContext>` | no | every held/free pawn on a map |
|
||||
| `TreatmentProgram.HasTreatableCondition(pawn)` | `bool` | no | is there a marked condition to work through? |
|
||||
| `TreatmentProgram.ReduceConditions(pawn, strength)` | `void` | mutates hediffs | run one session's worth of treatment |
|
||||
| `TreatmentProgram.FacilityQuality(pawn)` | `float 0.5..1.5` | no | scale a session by the room's quality |
|
||||
| `TreatmentProgram.AdvanceRecovery(pawn, def, amount)` | `float` | **yes** (recovery hediff) | build a discharge / parole track |
|
||||
|
||||
All of these are `static` and null-safe. None throws on a null pawn; they return `0.05`/`1f`/`null`/
|
||||
`false` as appropriate. See the [Propensity](Propensity.md), [Criminal Record](Criminal-Record.md),
|
||||
and [Secured Context](Secured-Context.md) pages for the mechanics behind each.
|
||||
|
||||
---
|
||||
|
||||
## Asking "would this pawn?"
|
||||
|
||||
The idiomatic pattern for a new behaviour is: pick a **base rate**, pick a **stable salt** unique to
|
||||
your question, and call `Would`. Do not re-implement the nature/nurture math — that is the whole point
|
||||
of the shared engine.
|
||||
|
||||
```csharp
|
||||
using Contraband;
|
||||
|
||||
// A unique, STABLE salt for this specific question. Any constant int works; keep it
|
||||
// distinct from other questions so the rolls stay independent per behaviour.
|
||||
private const int Salt_WouldStealMeds = 0x5EED01;
|
||||
|
||||
if (Propensity.Would(pawn, baseChance: 0.08f, salt: Salt_WouldStealMeds))
|
||||
{
|
||||
// this pawn, under these conditions, is the kind who does this
|
||||
}
|
||||
```
|
||||
|
||||
Because the roll is seeded on `pawn.thingIDNumber ^ salt`, the answer is a stable fact per pawn per
|
||||
question and survives save/reload — so you can ask it repeatedly and get a consistent character, and
|
||||
different salts give you independent facts about the same pawn. Pass a custom `cap` if `0.85` is the
|
||||
wrong ceiling for your behaviour (e.g. a lower cap for something that should always keep a large
|
||||
chance of not happening).
|
||||
|
||||
If you only need the ingredients — say, to display a disposition readout or gate a threshold — read
|
||||
`Nature` and `Nurture` directly. Institution: Gangs, for instance, joins a pawn to a gang when
|
||||
`Nature(pawn) * Nurture(pawn)` clears its own threshold, computed from these two calls rather than
|
||||
from `Would`.
|
||||
|
||||
## Reading and writing records
|
||||
|
||||
Follow the one rule from the [Criminal Record](Criminal-Record.md) page — **write with `For`, read
|
||||
with `PeekFor`** — so read paths never litter the save with blank records.
|
||||
|
||||
```csharp
|
||||
using Contraband;
|
||||
|
||||
// WRITE: an event happened, so we need a record to write to.
|
||||
CriminalRecord rec = GameComponent_CriminalRecords.For(pawn);
|
||||
rec.contrabandMade++;
|
||||
|
||||
// READ: peek, and tolerate null (the pawn may have no record yet).
|
||||
CriminalRecord existing = GameComponent_CriminalRecords.PeekFor(pawn);
|
||||
if (existing != null && existing.reform < 0f)
|
||||
{
|
||||
// this pawn has been hardened
|
||||
}
|
||||
```
|
||||
|
||||
Core owns the storage and persistence; you own the meaning of your own events. Do not keep your own
|
||||
per-pawn crime dictionary — that is exactly the fragmentation Core exists to prevent.
|
||||
|
||||
## Working across hold kinds
|
||||
|
||||
Reduce a pawn to its context and branch on the *predicate*, not the label:
|
||||
|
||||
```csharp
|
||||
using Contraband;
|
||||
|
||||
SecuredContext? maybe = SecuredContexts.Of(pawn);
|
||||
if (maybe is SecuredContext ctx && ctx.IsHeld)
|
||||
{
|
||||
// prisoner OR slave OR ward patient -- all covered, no special cases
|
||||
}
|
||||
|
||||
// Sweep everyone the suite cares about on a map:
|
||||
foreach (SecuredContext c in SecuredContexts.OnMap(map))
|
||||
{
|
||||
if (c.CanConceal) { /* consider hoarders too, not just the held */ }
|
||||
}
|
||||
```
|
||||
|
||||
`Of` returns `null` for anything the suite does not model (animals, mechs, the dead), so a `null`
|
||||
check is your "not our business" branch.
|
||||
|
||||
## Running a treatment programme
|
||||
|
||||
Core carries the maths of "sustained attention in a secured facility reduces a marked condition and
|
||||
builds a recovery track toward discharge" — the shared engine behind Ward's psychiatric care and
|
||||
Justice's rehabilitation. Mark a hediff treatable with `TreatableConditionExtension`, then drive it
|
||||
from your own job or interaction:
|
||||
|
||||
```csharp
|
||||
using Contraband;
|
||||
|
||||
float strength = skill * TreatmentProgram.FacilityQuality(pawn) * share; // your own blend
|
||||
TreatmentProgram.ReduceConditions(pawn, strength); // lower the condition
|
||||
if (!TreatmentProgram.HasTreatableCondition(pawn))
|
||||
{
|
||||
TreatmentProgram.AdvanceRecovery(pawn, MyDefOf.Recovery, 0.15f * strength); // build discharge
|
||||
}
|
||||
```
|
||||
|
||||
You own the interaction mode, the job, the recovery hediff (give it a negative `severityPerDay` so it
|
||||
decays without attention), and the discharge alert; Core owns only the numbers. Full formulas and the
|
||||
consumer contract are on the **[Treatment Engine](Treatment-Engine.md)** page.
|
||||
|
||||
---
|
||||
|
||||
## The `DeterrenceFactor` seam
|
||||
|
||||
This is the most important extension point in Core, and the reason Core can remain a **dependency-free
|
||||
leaf** while a colony-wide deterrence feedback loop runs *through* it.
|
||||
|
||||
### What it is
|
||||
|
||||
```csharp
|
||||
// In Propensity:
|
||||
public static System.Func<Pawn, float> DeterrenceFactor = _ => 1f;
|
||||
```
|
||||
|
||||
A single static delegate. `Nurture` multiplies its result by `DeterrenceFactor(pawn)` as its last
|
||||
step. Out of the box the delegate returns `1f` for every pawn — perfectly neutral — so **Core alone
|
||||
behaves as if deterrence did not exist.**
|
||||
|
||||
### What it is *for*
|
||||
|
||||
Deterrence is a property of the whole colony's climate of order: a well-policed colony should deter
|
||||
everyone a little; a lawless one should embolden them. That is a feedback loop — catching and
|
||||
punishing *one* pawn changes the disposition of *the rest*. But deterrence itself is a justice-layer
|
||||
concept, and Core must not know the justice layer exists.
|
||||
|
||||
The seam resolves the tension. Core defines *where* the colony's climate multiplies into disposition
|
||||
(the last line of `Nurture`) without defining *what* that climate is. From the source:
|
||||
|
||||
> *"Deterrence lives in the Justice layer, so Core reads it through the seam above — neutral until
|
||||
> Justice fills it. This one seam is what keeps Core a dependency-free leaf despite the deterrence
|
||||
> feedback loop running through it."*
|
||||
|
||||
### How Institution: Justice fills it
|
||||
|
||||
When Institution: Justice loads, it assigns the delegate, pointing it at its own live deterrence
|
||||
level. The shape it installs maps a `0..1` deterrence reading to a multiplier that straddles `1.0`:
|
||||
|
||||
```csharp
|
||||
// Illustrative — this lives in Institution: Justice, not Core.
|
||||
Propensity.DeterrenceFactor = pawn =>
|
||||
{
|
||||
float level = DeterrenceLevelFor(pawn.Map); // 0 = lawless, 1 = tightly policed
|
||||
return Mathf.Lerp(1.3f, 0.7f, level); // lawless emboldens, order deters; neutral at 0.5
|
||||
};
|
||||
```
|
||||
|
||||
| Deterrence level | Factor | Effect on disposition |
|
||||
|---|---:|---|
|
||||
| `0.0` (lawless) | `1.3` | everyone ~30% more inclined |
|
||||
| `0.5` (baseline) | `1.0` | neutral — a default colony sits exactly here |
|
||||
| `1.0` (tightly policed) | `0.7` | everyone ~30% less inclined |
|
||||
|
||||
*(The `1.3 → 0.7` range, neutral at the `0.5` baseline, are Justice's numbers, documented here for context;
|
||||
Core only guarantees the neutral `1f` default. Confirm against Institution: Justice's own source for
|
||||
its current values.)*
|
||||
|
||||
### How to fill it yourself
|
||||
|
||||
If you are writing your own policing or morale layer and want it to feed disposition the same way,
|
||||
just assign the delegate — ideally at game load, and ideally composing with whatever is already there
|
||||
rather than clobbering it:
|
||||
|
||||
```csharp
|
||||
using Contraband;
|
||||
using UnityEngine;
|
||||
|
||||
// Compose: fold your factor into any existing one (e.g. Justice's) instead of overwriting.
|
||||
var previous = Propensity.DeterrenceFactor;
|
||||
Propensity.DeterrenceFactor = pawn => previous(pawn) * MyClimateFactorFor(pawn);
|
||||
```
|
||||
|
||||
Two cautions. First, it is a single static field — if two mods both *assign* it (rather than compose),
|
||||
the last one wins, so compose when you can. Second, keep your factor bounded and centred near `1.0`:
|
||||
`Nurture` has no clamp on this term, so an extreme factor multiplies straight through into every
|
||||
pawn's `Would` roll. Return `1f` when you have nothing to say, exactly as the default does.
|
||||
|
||||
### Why a delegate and not a dependency
|
||||
|
||||
The alternative — Core calling into Justice — would make Core depend on Justice, inverting the whole
|
||||
topology and breaking Core's promise that it references *only the base game*. A single assignable
|
||||
`Func<Pawn,float>`, neutral by default, lets the feedback loop run through Core while the arrow of
|
||||
dependency still points only *at* Core. It is the smallest possible seam that buys the largest possible
|
||||
decoupling, and it is why Core can sit at the bottom of the suite with zero dependencies of its own.
|
||||
|
||||
---
|
||||
|
||||
*Part of the **Institution** suite. AI disclosure: developed with substantial assistance from Claude (Anthropic).*
|
||||
@@ -0,0 +1,265 @@
|
||||
# Propensity — nature × nurture
|
||||
|
||||
`Propensity` is the suite's disposition engine. It answers one question and only one: *"would
|
||||
**this** pawn do it?"* — never *"would a prisoner,"* never *"would a colonist."* Every behaviour in
|
||||
the suite that hinges on a pawn's character routes through here, so the crime system, contraband
|
||||
brewing, escape arming, and gang recruitment all read the same answer instead of each guessing.
|
||||
|
||||
The model is deliberately old-fashioned: **nature × nurture.**
|
||||
|
||||
- **Nature** is a fixed property of the person — their traits. Almost nobody is strongly disposed.
|
||||
This does not change over a pawn's life.
|
||||
- **Nurture** is what the colony has done *to* them — mood, mistreatment, unmet needs, the shadow a
|
||||
sentence leaves. This is the half **you** control, and the half no other mod models.
|
||||
|
||||
Good conditions pull nurture down; neglect and cruelty push it up. A saint left to rot can cross the
|
||||
line; a monster kept content and deterred may never act. The engine is built so both of those stories
|
||||
are possible.
|
||||
|
||||
Everything on this page is verified against `Source/Core/Propensity.cs`.
|
||||
|
||||
---
|
||||
|
||||
## Nature — who they are
|
||||
|
||||
```
|
||||
Nature(pawn) : float in [0, 1]
|
||||
n = 0.05 // baseline — everyone has a little
|
||||
for each listed trait the pawn has:
|
||||
n += weight
|
||||
return Clamp01(n) // never below 0, never above 1
|
||||
```
|
||||
|
||||
The baseline is `0.05`. That is the floor of the human condition in this model: everyone is capable
|
||||
of *something*, most people barely. From there, traits add or subtract.
|
||||
|
||||
### Trait weights
|
||||
|
||||
Traits are read **reflectively by defName** via `DefDatabase<TraitDef>.GetNamedSilentFail`. If a
|
||||
trait's mod is not installed, that entry is silently skipped rather than throwing a hard reference —
|
||||
which is how Core consumes Vanilla Traits Expanded's dark traits without *depending* on VTE.
|
||||
|
||||
| Trait | Weight | Source | Note |
|
||||
|---|---:|---|---|
|
||||
| Psychopath | **+0.45** | Vanilla | no empathy; the heaviest single input |
|
||||
| Kleptomaniac | **+0.40** | Vanilla Traits Expanded | consumed, not rebuilt |
|
||||
| Bloodlust | **+0.35** | Vanilla | enjoys violence |
|
||||
| Pyromaniac | **+0.30** | Vanilla Traits Expanded | consumed, not rebuilt |
|
||||
| Greedy | **+0.25** | Vanilla | wants more than their share |
|
||||
| Abrasive | **+0.15** | Vanilla | friction with everyone |
|
||||
| Ascetic | **−0.15** | Vanilla | wants little, takes little |
|
||||
| Kind | **−0.30** | Vanilla | the strongest pull *down* |
|
||||
|
||||
The design note in the source is explicit: *"We CONSUME Vanilla Traits Expanded's
|
||||
kleptomaniac/pyromaniac as inputs here rather than rebuild them; vanilla's own dark traits count
|
||||
too."* Core does not add traits of its own — it reads the ones the ecosystem already has.
|
||||
|
||||
### Worked Nature values
|
||||
|
||||
Because weights simply add and then clamp, Nature is easy to read by hand:
|
||||
|
||||
| Pawn | Arithmetic | Nature |
|
||||
|---|---|---:|
|
||||
| Ordinary pawn (none of the above) | `0.05` | **0.05** |
|
||||
| Kind pawn | `0.05 − 0.30 = −0.25` → clamp | **0.00** |
|
||||
| A single Greedy trait | `0.05 + 0.25` | **0.30** |
|
||||
| Psychopath | `0.05 + 0.45` | **0.50** |
|
||||
| Psychopath **and** Kind | `0.05 + 0.45 − 0.30` | **0.20** |
|
||||
| Greedy + Abrasive | `0.05 + 0.25 + 0.15` | **0.45** |
|
||||
| Psychopath + Bloodlust + Kleptomaniac | `0.05 + 0.45 + 0.35 + 0.40 = 1.25` → clamp | **1.00** |
|
||||
|
||||
Two things fall out of this. First, the clamp is not decorative: a Kind pawn floors at exactly `0`
|
||||
(nature can never make them *disposed*, only never disposed), and a stacked monster ceilings at `1`.
|
||||
Second, traits genuinely net against each other — a Kind Psychopath is a real, middling `0.20`, not a
|
||||
contradiction the engine has to resolve by fiat.
|
||||
|
||||
Most rolled pawns sit at or near the `0.05` floor. That is intended: *"Most pawns sit near the floor;
|
||||
a rare few are strongly inclined."* A colony full of ordinary people is supposed to be mostly safe on
|
||||
nature alone. What makes them dangerous is nurture.
|
||||
|
||||
---
|
||||
|
||||
## Nurture — what you have done to them
|
||||
|
||||
```
|
||||
Nurture(pawn) : float (a multiplier, >= ~0.5 in normal play)
|
||||
m = 1.0 // ordinary circumstance
|
||||
|
||||
mood = pawn.needs.mood.CurLevelPercentage (or 1.0 if none)
|
||||
if mood < 0.20: m *= 2.5 // at the floor of despair
|
||||
else if mood < 0.35: m *= 1.6 // badly kept
|
||||
|
||||
if IsHeld(pawn) and mood < 0.40: m *= 1.4 // held AND unhappy compounds
|
||||
|
||||
rec = criminal record (peek — does not create one)
|
||||
if rec != null and rec.reform != 0:
|
||||
m *= Clamp(1 - rec.reform * 0.5, 0.4, 2.0) // prisonization
|
||||
|
||||
m *= DeterrenceFactor(pawn) // colony climate of order (1.0 in Core alone)
|
||||
|
||||
return m
|
||||
```
|
||||
|
||||
`1.0` is an ordinary pawn under ordinary conditions. The number rises as things go wrong and falls as
|
||||
they go right. Each clause below is one lever.
|
||||
|
||||
### The multiplier ladder
|
||||
|
||||
| Clause | Factor | Applies when | Stacks? |
|
||||
|---|---|---|---|
|
||||
| **Despair** | ×2.5 | `mood < 0.20` | mutually exclusive with "badly kept" |
|
||||
| **Badly kept** | ×1.6 | `0.20 ≤ mood < 0.35` | mutually exclusive with "despair" |
|
||||
| **Held & unhappy** | ×1.4 | `IsHeld` **and** `mood < 0.40` | on top of the mood clause |
|
||||
| **Prisonization** | ×`Clamp(1 − reform×0.5, 0.4, 2.0)` | `reform ≠ 0` | on top |
|
||||
| **Deterrence** | ×`DeterrenceFactor(pawn)` | always (neutral `1.0` in Core alone) | on top |
|
||||
|
||||
The two mood clauses are an `if / else-if`: a pawn is either in despair *or* badly kept, never both.
|
||||
The "held & unhappy" clause is separate and multiplies again — so a mistreated prisoner in despair
|
||||
compounds `2.5 × 1.4 = 3.5` before anything else. The source calls that exactly what it is: *"a badly
|
||||
run cell."*
|
||||
|
||||
### Prisonization — the reform lever
|
||||
|
||||
The `reform` clause deserves its own look, because it is where a sentence leaves a permanent mark.
|
||||
|
||||
```
|
||||
reform factor = Clamp(1 - reform * 0.5, 0.4, 2.0)
|
||||
```
|
||||
|
||||
| `reform` | Meaning | Factor |
|
||||
|---:|---|---:|
|
||||
| **+1.0** | fully rehabilitated | 0.50 |
|
||||
| +0.5 | improving | 0.75 |
|
||||
| 0 | untouched (clause skipped) | *1.00* |
|
||||
| −0.5 | hardening | 1.25 |
|
||||
| **−1.0** | prisonized | 1.50 |
|
||||
| ≥ +1.2 | (over-reformed) | clamp floor **0.40** |
|
||||
| ≤ −2.0 | (utterly broken) | clamp ceiling **2.00** |
|
||||
|
||||
`reform` is nominally documented as `0..1`, but the punishment machinery can and does drive it
|
||||
**negative** — the source discusses `reform < 0` ("hardened, prisonized") in as many words. Within the
|
||||
realistic range `[−1, +1]` the factor spans `0.5 … 1.5`; the `0.4 / 2.0` clamps only bite at extremes
|
||||
outside that, catching a pawn who has been endlessly punished or endlessly rehabilitated.
|
||||
|
||||
The important design property: Core only ever **reads** `reform` here. Nothing in Core moves it. The
|
||||
Justice layer's `Discipline` and `Parole` are what nudge it up or down — which means
|
||||
*institutionalization and recidivism become the suite's without a parallel mechanic.* A pawn who was
|
||||
broken by a brutal prison stays broken (nurture ×1.5) after release; a pawn genuinely reformed stays
|
||||
calmer (×0.5) for good. The scar is carried by one float.
|
||||
|
||||
### Deterrence — the climate of order
|
||||
|
||||
The final `× DeterrenceFactor(pawn)` is the seam that lets the whole colony's climate feed back into
|
||||
each pawn's disposition. **In Core alone it is neutral — a flat `1.0`** — because Core does not know
|
||||
what deterrence is. When Institution: Justice is loaded, it fills this in: a well-policed colony pulls
|
||||
the factor below `1` and deters everyone a little; a lawless one pushes it above `1` and emboldens
|
||||
them. That is what makes catching and punishing *one* pawn matter to the disposition of *the rest*.
|
||||
|
||||
The full mechanics of the seam — and how to fill it from your own mod — are on the
|
||||
[Modder API](Modder-API.md) page. For now: with Core installed by itself, this clause does nothing,
|
||||
and that is correct.
|
||||
|
||||
---
|
||||
|
||||
## Would — the full roll
|
||||
|
||||
`Nature` and `Nurture` are ingredients. `Would` is the meal: the actual yes/no for a specific
|
||||
behaviour at a specific base rate.
|
||||
|
||||
```
|
||||
Would(pawn, baseChance, salt, cap = 0.85) : bool
|
||||
chance = min(cap, baseChance * (0.1 + Nature(pawn) * 2) * Nurture(pawn))
|
||||
return Rand.ChanceSeeded( Clamp01(chance), pawn.thingIDNumber ^ salt )
|
||||
```
|
||||
|
||||
Three things are happening in that one line of arithmetic:
|
||||
|
||||
1. **`baseChance`** is the caller's dial — the base rate of *this* behaviour for an average pawn
|
||||
(a rare act passes a small number, a common one a larger). Core does not decide it; the module
|
||||
asking the question does.
|
||||
2. **`(0.1 + Nature × 2)`** is the nature amplifier. It spans **×0.1** (a saint, Nature 0) to
|
||||
**×2.1** (Nature 1). At the common `0.05` floor it is `×0.2`. So a floor pawn is one-fifth as
|
||||
likely as the base rate; a maxed monster over twice as likely — *before* nurture.
|
||||
3. **`× Nurture`** applies circumstance on top, and `min(cap, …)` caps the whole thing at `0.85` by
|
||||
default. Nobody is ever a dead certainty; there is always slack. A caller who wants a harder or
|
||||
softer ceiling passes their own `cap`.
|
||||
|
||||
### Worked example 1 — an ordinary colonist, content
|
||||
|
||||
Base rate `0.10`, an ordinary pawn (Nature `0.05`), good mood so Nurture `1.0`:
|
||||
|
||||
```
|
||||
nature term = 0.1 + 0.05 * 2 = 0.2
|
||||
chance = min(0.85, 0.10 * 0.2 * 1.0) = 0.02 → 2%
|
||||
```
|
||||
|
||||
Two percent, and — critically — **seeded**. For this pawn and this question it is a fixed 2% coin
|
||||
that either comes up or does not; it is not re-flipped every tick.
|
||||
|
||||
### Worked example 2 — a mistreated psychopath prisoner
|
||||
|
||||
A Psychopath + Bloodlust prisoner (Nature `0.85`), mood `0.15` (despair ×2.5), held and unhappy
|
||||
(×1.4), `reform = −0.5` (hardening → ×1.25), Core-only so deterrence `1.0`. Base rate `0.10`:
|
||||
|
||||
```
|
||||
nature term = 0.1 + 0.85 * 2 = 1.8
|
||||
nurture = 2.5 * 1.4 * 1.25 = 4.375
|
||||
chance = min(0.85, 0.10 * 1.8 * 4.375) = min(0.85, 0.7875) = 0.7875 → ~79%
|
||||
```
|
||||
|
||||
Same base rate as example 1, same engine — but nature and a badly-run cell have turned a 2% pawn into
|
||||
a near-certainty. The player did that, clause by clause.
|
||||
|
||||
### Worked example 3 — hitting the cap
|
||||
|
||||
A fully stacked monster (Nature clamps to `1.0`), in despair (×2.5) and held & unhappy (×1.4), in a
|
||||
lawless colony where Justice has set deterrence to `1.4`. Base rate `0.10`:
|
||||
|
||||
```
|
||||
nature term = 0.1 + 1.0 * 2 = 2.1
|
||||
nurture = 2.5 * 1.4 * 1.4 = 4.9
|
||||
raw chance = 0.10 * 2.1 * 4.9 = 1.029
|
||||
chance = min(0.85, 1.029) = 0.85 → capped at 85%
|
||||
```
|
||||
|
||||
The raw product blew past `1.0`; the cap reins it to `0.85`. Even here, a `0.15` sliver of "not
|
||||
today" survives. That sliver is deliberate — it keeps the worst pawn a character with a bad day
|
||||
coming, not a scripted event.
|
||||
|
||||
---
|
||||
|
||||
## The seeded roll — characters, not a dice cup
|
||||
|
||||
The last argument of `Would` is a **salt**, and the seed is:
|
||||
|
||||
```
|
||||
seed = pawn.thingIDNumber ^ salt
|
||||
```
|
||||
|
||||
`Rand.ChanceSeeded` turns that seed into a *deterministic* pass/fail. This is the single most
|
||||
important design decision on this page, and it is worth being precise about why.
|
||||
|
||||
**Save/reload stability.** Because the seed is derived from the pawn's stable `thingIDNumber` and a
|
||||
fixed salt — not from the live RNG stream — asking "would they?" gives the same answer before and
|
||||
after a save/reload, as long as the inputs (traits, mood, reform) are the same. You cannot scum a
|
||||
reload to re-roll a pawn into a different person. *"WHO they are does not shift under them on a
|
||||
save/reload."*
|
||||
|
||||
**A character, not a per-tick lottery.** A naive implementation would flip a fresh coin every tick,
|
||||
so any pawn eventually does anything if you wait long enough. Seeding instead makes "would this pawn
|
||||
pocket a shiv?" a *fixed fact* about that pawn under those conditions — *"what makes them a character,
|
||||
not a dice cup."*
|
||||
|
||||
**Live circumstance still bites.** Seeding freezes the *identity*, not the *situation*. Nurture is
|
||||
recomputed live, so as mood collapses or reform hardens, the same pawn's `chance` climbs and the same
|
||||
seeded coin can flip from "no" to "yes." The pawn who would not have acted last month acts now — not
|
||||
because the dice changed, but because you let their world get worse.
|
||||
|
||||
**Different questions, different salts.** Each behaviour passes its own salt, so the rolls are
|
||||
independent. The source puts it plainly: *"a man who would pocket a shiv is not therefore a man who
|
||||
would inform on his cellmate."* One pawn can be reliably one kind of trouble and reliably not another,
|
||||
and that pattern is stable across the whole game.
|
||||
|
||||
---
|
||||
|
||||
*Part of the **Institution** suite. AI disclosure: developed with substantial assistance from Claude (Anthropic).*
|
||||
@@ -0,0 +1,153 @@
|
||||
# Secured Context — the kind of hold a pawn is under
|
||||
|
||||
The suite has a rule that looks small and is load-bearing: **nothing keys off "is this a prisoner."**
|
||||
It keys off `SecuredContext` — the *kind of hold* a pawn is under. That indirection is why a ward
|
||||
patient, a slave, and (later) anyone inside a secure zone get the same concealment, search, schedule,
|
||||
and needs machinery for free, without any module hard-coding a pawn status.
|
||||
|
||||
`SecuredContext` reduces a pawn to one of three kinds and the question "who, if anyone, may search
|
||||
them." Everything downstream reads that, so a new module works across every context the day it is
|
||||
written, and a new *context* works across every existing module the day it is added.
|
||||
|
||||
Verified against `Source/Core/SecuredContext.cs`.
|
||||
|
||||
---
|
||||
|
||||
## The three kinds
|
||||
|
||||
```csharp
|
||||
public enum SecuredKind
|
||||
{
|
||||
Free, // a free colonist
|
||||
Prisoner, // a prisoner of the colony
|
||||
Slave, // a slave (Ideology)
|
||||
}
|
||||
```
|
||||
|
||||
| Kind | Who it is | May search them | Can conceal? | Held against their will? |
|
||||
|---|---|---|---|---|
|
||||
| **Free** | a free colonist | *no one* — until a secure-area or policing layer grants standing | **yes** — they can hoard | no |
|
||||
| **Prisoner** | a prisoner of the colony | the **warden** | yes | **yes** |
|
||||
| **Slave** | a slave (Ideology DLC) | the **overseer** | yes | **yes** |
|
||||
|
||||
Two subtleties fall out of this table:
|
||||
|
||||
- **Free colonists can still conceal.** A free colonist "can hoard contraband, but no one has
|
||||
standing to search them until a secure-area or policing layer grants it." Freedom is not innocence —
|
||||
it is only the absence of an authority permitted to check. That is why `CanConceal` (below) includes
|
||||
the free.
|
||||
- **Prisoner and Slave differ only in who holds the keys.** Both are "held"; the search authority is
|
||||
the warden for one and the overseer for the other. Downstream modules that only care "is someone
|
||||
entitled to search this pawn" read `IsHeld` and never branch on which.
|
||||
|
||||
## The struct
|
||||
|
||||
`SecuredContext` is a small `readonly struct` — the kind, the pawn, and two computed predicates.
|
||||
|
||||
```csharp
|
||||
public readonly struct SecuredContext
|
||||
{
|
||||
public readonly SecuredKind Kind;
|
||||
public readonly Pawn Pawn;
|
||||
|
||||
// Held against their will -- prisoner or slave. Excludes a free colonist.
|
||||
public bool IsHeld => Kind == SecuredKind.Prisoner || Kind == SecuredKind.Slave;
|
||||
|
||||
// Anyone with something to hide -- includes free colonists (who may hoard).
|
||||
public bool CanConceal => Pawn != null;
|
||||
}
|
||||
```
|
||||
|
||||
The two predicates split the world along two different axes, and picking the right one is the whole
|
||||
skill of using this type:
|
||||
|
||||
| Predicate | True for | Ask it when you care about… |
|
||||
|---|---|---|
|
||||
| `IsHeld` | Prisoner, Slave | **custody** — who is under the colony's control, who can be disciplined, whose cell can be searched |
|
||||
| `CanConceal` | Free, Prisoner, Slave (any real pawn) | **contraband** — who could be *hiding* something, regardless of status |
|
||||
|
||||
For example, [Propensity](Propensity.md)'s Nurture uses `IsHeld` in its "held & unhappy compounds"
|
||||
clause — it only wants to pile the ×1.4 penalty on a pawn the colony actually holds and mistreats, not
|
||||
on a grumpy free colonist. A warden-search feature, by contrast, would gate on `CanConceal` to decide
|
||||
who is even worth checking, and then on standing to decide whether it is *allowed* to.
|
||||
|
||||
---
|
||||
|
||||
## `Of` — one pawn to a context
|
||||
|
||||
```csharp
|
||||
public static SecuredContext? Of(Pawn p)
|
||||
```
|
||||
|
||||
`Of` maps a pawn to a context, or **`null`** if the pawn has none. The order of the checks matters and
|
||||
is worth reading literally:
|
||||
|
||||
```
|
||||
if p is not humanlike, or p is dead -> null (not one of ours)
|
||||
if p.IsPrisonerOfColony -> Prisoner
|
||||
if p.IsSlaveOfColony -> Slave
|
||||
if p.IsFreeColonist -> Free
|
||||
otherwise -> null
|
||||
```
|
||||
|
||||
- **The guard comes first.** Animals, mechs, and the dead are simply not the suite's business — they
|
||||
return `null`, and every `OnMap` loop and downstream check skips them without a special case.
|
||||
- **Prisoner and Slave are tested before Free.** A pawn who is somehow both a colonist and imprisoned
|
||||
is classified by their *hold* first. The nullable return is the clean signal for "this pawn is
|
||||
outside the model," so callers pattern-match `HasValue` rather than defaulting to some "Free-ish"
|
||||
status.
|
||||
|
||||
Returning `SecuredContext?` (nullable) rather than a `Free` fallback is deliberate: there is a real
|
||||
difference between "a free pawn we track" and "a pawn we do not model at all," and collapsing them
|
||||
would let non-colony pawns leak into colony machinery.
|
||||
|
||||
## `OnMap` — every context on a map
|
||||
|
||||
```csharp
|
||||
public static IEnumerable<SecuredContext> OnMap(Map map)
|
||||
```
|
||||
|
||||
`OnMap` yields a context for every spawned pawn that has one — prisoners, slaves, and colonists,
|
||||
skipping everything `Of` returns `null` for. It is a lazy `yield` iterator, so a caller can enumerate
|
||||
"everyone the suite cares about on this map" in one loop:
|
||||
|
||||
```csharp
|
||||
foreach (SecuredContext ctx in SecuredContexts.OnMap(map))
|
||||
{
|
||||
if (ctx.IsHeld) { /* consider only the held */ }
|
||||
}
|
||||
```
|
||||
|
||||
A `null` map yields nothing (an empty sequence), so callers do not need to null-check before looping.
|
||||
|
||||
---
|
||||
|
||||
## Why not just "prisoner"?
|
||||
|
||||
This is the design decision the whole page exists to justify, so it is worth stating plainly. If every
|
||||
module checked `pawn.IsPrisonerOfColony` directly, then:
|
||||
|
||||
- adding **slaves** to a feature would mean editing every module;
|
||||
- adding a **ward patient** mode would mean editing every module;
|
||||
- adding a future **secure-zone** concept would mean editing every module.
|
||||
|
||||
By routing all of them through one predicate, a new hold-kind is added *once*, in Core, and every
|
||||
existing module inherits it. The concealment, search, and needs machinery never has to learn what a
|
||||
slave is — it only ever asked `IsHeld` and `CanConceal`.
|
||||
|
||||
### The deliberately-absent Ward kind
|
||||
|
||||
There is no `SecuredKind.WardPatient`, and its absence is intentional. From the source:
|
||||
|
||||
> *"a committed patient IS a prisoner in vanilla's sense (Ward rides the prisoner rail), so it is
|
||||
> `SecuredKind.Prisoner` here and needs no special case. Contraband does not depend on Ward."*
|
||||
|
||||
Because Institution: Ward implements a committed patient *on top of* vanilla's prisoner rail, such a
|
||||
pawn already returns `Prisoner` from `Of` — no new kind, no dependency from Core (or Contraband) onto
|
||||
Ward, and every module treats a ward patient exactly as it treats any prisoner. The right amount of
|
||||
new code to support Ward's hold semantics was zero, and that is the payoff of keying off the context
|
||||
instead of the label.
|
||||
|
||||
---
|
||||
|
||||
*Part of the **Institution** suite. AI disclosure: developed with substantial assistance from Claude (Anthropic).*
|
||||
@@ -0,0 +1,159 @@
|
||||
# The Treatment Engine — shared rehabilitation
|
||||
|
||||
*`TreatmentProgram` + `TreatableConditionExtension`, in `Source/Core/TreatmentProgram.cs`.*
|
||||
|
||||
Core carries one more thing every "fix a held pawn over time" system needs and none should own alone:
|
||||
the maths of a **treatment programme**. Sustained attention in a secured facility reduces a marked
|
||||
condition, the room's quality helps or hurts, and once there is nothing left to reduce a **recovery
|
||||
track** builds toward a dischargeable state.
|
||||
|
||||
It exists for the same reason the propensity engine does. Two systems want the identical loop pointed
|
||||
at different states — **Ward** treats the mental illness that got a pawn committed; **Justice** wants
|
||||
to rehabilitate the disposition that got one imprisoned — and without a shared engine each would grow
|
||||
its own copy and drift. Core owns **only the maths**. Which interaction mode enrols a pawn, which
|
||||
hediffs and thoughts carry the flavour, which jobs run the sessions, and which alert fires on
|
||||
discharge all stay in the consuming mod.
|
||||
|
||||
> The engine does something concrete when there is a real condition to work through, and is a
|
||||
> **no-op** otherwise. A colony that marks nothing treatable never notices it exists.
|
||||
|
||||
---
|
||||
|
||||
## The two pieces
|
||||
|
||||
### `TreatableConditionExtension` — marking a condition
|
||||
|
||||
A `DefModExtension` you attach to a `HediffDef` to declare it something a programme can reduce.
|
||||
|
||||
```xml
|
||||
<HediffDef Name="SomeDisorder">
|
||||
<modExtensions>
|
||||
<li Class="Contraband.TreatableConditionExtension">
|
||||
<reductionPerSession>0.12</reductionPerSession>
|
||||
</li>
|
||||
</modExtensions>
|
||||
</HediffDef>
|
||||
```
|
||||
|
||||
| Field | Default | Meaning |
|
||||
|---|---:|---|
|
||||
| `reductionPerSession` | `0.12` | Severity removed per **completed** session, *before* the caller's skill / facility / share scale it. |
|
||||
|
||||
A full course is several sessions by design — the programme has to be **sustained**, not run once. The
|
||||
extension carries no flavour; it says only *how fast this condition yields*. Ward attaches it to
|
||||
mental illness (via a soft, conditional patch on Rim Disorders' depression, anxiety, PTSD, OCD);
|
||||
anything a mod or a player marks is treated identically, so opting a hediff in is a one-line patch.
|
||||
|
||||
### `TreatmentProgram` — the maths
|
||||
|
||||
A static, null-safe class. It holds no state of its own — it reads and mutates hediffs on the pawn.
|
||||
|
||||
| Call | Returns | What it does |
|
||||
|---|---|---|
|
||||
| `TreatableConditions(pawn)` | `List<Hediff>` | every marked hediff on the pawn that still has severity |
|
||||
| `HasTreatableCondition(pawn)` | `bool` | is there any condition left to work through? |
|
||||
| `ReduceConditions(pawn, strength)` | `void` | lower every marked condition by `reductionPerSession × strength`; remove any that reach ~0 |
|
||||
| `FacilityQuality(pawn)` | `float 0.5..1.5` | how much the pawn's room helps or hurts |
|
||||
| `AdvanceRecovery(pawn, recoveryDef, amount)` | `float` | advance (creating if absent) a recovery hediff, clamped to `1.0`; returns the new severity |
|
||||
| `RecoveryLevel(pawn, recoveryDef)` | `float 0..1` | current level of that recovery track, or `0` |
|
||||
|
||||
---
|
||||
|
||||
## The numbers
|
||||
|
||||
### Facility quality
|
||||
|
||||
```
|
||||
room == null OR psychologically outdoors → 0.75 (a poor makeshift facility, not zero)
|
||||
otherwise → Clamp(0.6 + impressiveness / 120, 0.5, 1.5)
|
||||
```
|
||||
|
||||
| Room impressiveness | Factor |
|
||||
|---|---:|
|
||||
| 0 (bare) | `0.60` |
|
||||
| ~48 (decent) | `1.00` |
|
||||
| ≥108 (impressive) | `1.50` (capped) |
|
||||
| outdoors / none | `0.75` |
|
||||
|
||||
The clamp is deliberate: a grim, filthy, cramped facility heals worse, a calm clean one better, but a
|
||||
palace can't trivialise the labour the programme costs (1.5× ceiling), and even nowhere is a poor
|
||||
makeshift room, not a hard zero.
|
||||
|
||||
### Reducing a condition
|
||||
|
||||
```
|
||||
h.Severity -= reductionPerSession × strength // per marked hediff
|
||||
if h.Severity <= 0.001 → remove it
|
||||
```
|
||||
|
||||
`strength` is the **caller's** combined factor — skill × facility quality × this pawn's share of the
|
||||
session. The engine does not define it; the consumer does, so a one-on-one session at full skill in a
|
||||
good room reduces far more than a distracted share in a squalid one.
|
||||
|
||||
### Advancing recovery
|
||||
|
||||
```
|
||||
recovery = pawn's hediff of recoveryDef (created at 0.001 if absent)
|
||||
recovery.Severity = min(1.0, recovery.Severity + amount)
|
||||
```
|
||||
|
||||
The recovery track is a plain `0..1` severity the consumer supplies the def for (Ward's
|
||||
`Ward_Recovery`). Core only advances it; the consumer decides what its top stage *unlocks* (Ward: a
|
||||
"ready for discharge" alert) and — crucially — gives the hediff a **negative `severityPerDay`** so it
|
||||
**decays without sustained attention**. Recovery you stop maintaining slips back; that decay is what
|
||||
makes the programme a loop rather than a one-time unlock.
|
||||
|
||||
---
|
||||
|
||||
## The consumer contract
|
||||
|
||||
Core draws a hard line at "only the maths". A consumer owns everything with flavour:
|
||||
|
||||
| Core owns (the maths) | The consumer owns (the flavour) |
|
||||
|---|---|
|
||||
| `TreatableConditions` / `HasTreatableCondition` | which hediffs are marked treatable |
|
||||
| `ReduceConditions` | the job/interaction that runs a session |
|
||||
| `FacilityQuality` | the room role that makes a "facility" |
|
||||
| `AdvanceRecovery` / `RecoveryLevel` | the recovery hediff def + what its stable stage unlocks |
|
||||
| — | the discharge alert, the thoughts, the decay rate |
|
||||
|
||||
The idiomatic session, drawn from Ward's `JobDriver_PsychiatricCare` (the reference consumer):
|
||||
|
||||
```csharp
|
||||
using Contraband;
|
||||
|
||||
float skill = 0.7f * social + 0.3f * medicine; // the consumer's own blend
|
||||
float quality = TreatmentProgram.FacilityQuality(patient);
|
||||
float strength = (0.5f + skill / 20f * 0.5f) * quality * share; // 1.0 share for the primary patient
|
||||
|
||||
// 1. Reduce the actual condition first.
|
||||
TreatmentProgram.ReduceConditions(patient, strength);
|
||||
|
||||
// 2. Only once nothing is left to treat does recovery advance -- fix the illness, then stabilise.
|
||||
if (!TreatmentProgram.HasTreatableCondition(patient))
|
||||
{
|
||||
TreatmentProgram.AdvanceRecovery(patient, MyDefOf.RecoveryHediff, 0.15f * quality * share);
|
||||
}
|
||||
```
|
||||
|
||||
"Reduce the condition, *then* build recovery" is the shape Ward uses, but it is a convention, not a
|
||||
rule Core enforces — a rehabilitation consumer with no clinical condition to clear (Justice's reform)
|
||||
can advance a recovery track from the first session and read `RecoveryLevel` to gate a parole.
|
||||
|
||||
---
|
||||
|
||||
## The two consumers
|
||||
|
||||
- **[Institution: Ward](https://git.onetick.ninja/flan/rimworld-ward)** — psychiatric care. Marks
|
||||
mental-illness hediffs treatable, runs warden counselling sessions, builds `Ward_Recovery` toward a
|
||||
"ready for discharge" alert. The reference implementation.
|
||||
- **Institution: Justice** — rehabilitation. Its `reform` score is the disposition axis punishment
|
||||
moves; the recovery track is the natural home for a *sustained rehabilitation programme* that gates
|
||||
parole on the same shared engine, so the two systems agree on "getting better" instead of each
|
||||
inventing it.
|
||||
|
||||
The point, exactly as with propensity: one engine, two states, no drift.
|
||||
|
||||
---
|
||||
|
||||
*Part of the **Institution** suite. AI disclosure: developed with substantial assistance from Claude (Anthropic).*
|
||||
Reference in New Issue
Block a user