Files
institution/Wiki/core/Treatment-Engine.md
T
flan e96ac23949 Move AI attribution to a single NOTICE file
Per the updated project policy, AI/Claude attribution lives in exactly one NOTICE file per repo and
nowhere else -- removed it from the About description, README, wiki, and roadmap.
2026-07-16 01:30:09 +00:00

160 lines
6.9 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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.*