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.
160 lines
6.9 KiB
Markdown
160 lines
6.9 KiB
Markdown
# 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.*
|