Files
institution/Wiki/contraband/Warden-Search.md
T
flan 3530100ed5 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.
2026-07-15 20:30:38 +00:00

199 lines
9.5 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.
# Warden Search
*The sixteenth warden job vanilla never wrote. A guard turns the cell over, and might find nothing —
which is exactly why it has to cost.*
The warden can chat, convert, feed, execute, enslave, release, suppress, and eleven other things.
Searching a prisoner is not one of them, and no such verb exists anywhere in the base game.
Contraband adds it as a **new** `WorkGiver`, not a patch on a vanilla one, and builds it around a
single hard rule: **the search must be able to come up empty.**
## Authority vs. act
The design splits one job into two classes:
| Class | Question it answers | Notes |
|---|---|---|
| `WorkGiver_Warden_Search` | **Who** may be searched? | subclasses `WorkGiver_Warden` — a warden already has the run of the prison, so no new authority is invented |
| `JobDriver_SearchPawn` | **What** is a search? | takes any pawn, searches any pawn |
> The WorkGiver is the authority. This driver is just the act.
Why bother splitting them? Because the enforcement layer is meant to grow. A future **Institution:
Police** module adds a *second* authority — a cop who may stop and frisk a **free colonist** — as a
new WorkGiver that reuses the same driver with a **narrower `Reach`** (a street frisk gets what is
`OnBody`, not what is under the floorboards). That module is an *addition*, not a rewrite, precisely
because "who may search" and "what a search reaches" were never welded together.
## The WorkGiver: who gets searched
`Contraband_WardenSearch` is a `Warden` work-type giver:
| Field | Value | Why |
|---|---|---|
| `workType` | Warden | it is warden work |
| `priorityInType` | 40 | **below feeding** — a starving prisoner matters more than a hidden shiv |
| `requiredCapacities` | Manipulation, Sight | you search with your hands and eyes |
| `Prioritized` | **true** (in C#) | rank candidates by suspicion, not by distance |
`Prioritized` has to be set in code: in 1.6 it is a `WorkGiver_Scanner` property, **not** a
`WorkGiverDef` field, and an XML `<prioritized>` silently errors on load. That flag is what lets a
gnawed bed and a thick record send the warden to the *right* prisoner first instead of the nearest.
`JobOnThing` will offer a search only if all of these hold: the warden should take care of this
prisoner, the target is a `Pawn`, the per-prisoner cooldown is up (`CanSearchNow`), the prisoner is
**awake, not downed, and not in a mental state**, and the warden can reserve them.
Note what is **not** checked — and the source is emphatic about it:
> Whether the prisoner is actually hiding anything. **The warden does not know. Nobody knows until
> the cell is turned over.** If the job were only offered when there was something to find, the mere
> appearance of the job would BE the discovery, and searching would stop being a gamble the player
> pays for in warden-hours. An empty search has to be possible, and has to cost.
### The cooldown
`MapComponent_Contraband` won't let the same prisoner be turned over more than **once per day**:
```
SearchCooldownTicks = 60000 // one in-game day
```
You cannot brute-force a well-hidden shiv by re-searching the same prisoner ten times in an hour.
Every search is one shot, and then that prisoner is off the list for a day.
## Priority: the search reads the tells
`GetPriority` is where suspicion becomes routing. Everyone is worth a routine toss; the tells push a
prisoner up the queue:
```
priority = 4 // baseline — everyone gets frisked
+ 12 × bedDamageFraction // chewed furniture = probable cause
+ min(8, timesCaught×2 + contrabandMade×0.5 + escapeAttempts×3) // the record
```
| Term | Weight | Meaning |
|---|---|---|
| Base | 4 | a routine toss for every prisoner |
| Gnawed bed | up to +12 | `1 − HP/maxHP` of their owned bed — a badly damaged bed shoots to the top |
| `timesCaught` | ×2 | already caught with contraband before |
| `contrabandMade` | ×0.5 | has finished contraband in the past |
| `escapeAttempts` | ×3 | **the heaviest term** — a known tunneller is checked first, every time |
| (record cap) | +8 max | the record cannot completely swamp fresh probable cause |
The record is read with `PeekFor` (a read-only lookup that never creates a record). The base of 4 for
everyone is not an accident — **innocent prisoners still get frisked**, because the search must be
able to come up empty or its mere offer would be the discovery.
**Worked example.** A prisoner whose bed sits at 40% HP (`missing = 0.6`), with 2 prior catches and
1 foiled tunnel:
```
priority = 4 + 12×0.6 + min(8, 2×2 + 0 + 1×3)
= 4 + 7.2 + min(8, 7)
= 4 + 7.2 + 7 = 18.2
```
versus **4** for a clean prisoner with an intact bed. The warden walks past the quiet one and goes
straight for the one who has been busy. This closes the loop [Improvised Weapons](Improvised-Weapons.md)
opens: whittling a shiv chews the bed, the damage is probable cause, and the warden's *suspicion* —
not the player's eye — sends them to search.
## The act: what a search does
`JobDriver_SearchPawn` walks the searcher to the subject, runs a **900-tick** wait toil with a
progress bar, then resolves. Resolution always does two things, hit or miss:
1. `MarkSearched(subject)` — starts the daily cooldown.
2. `timesSearched++` on the record — *"classification and the guard's future suspicion read how often
this pawn has been turned over."*
Then it gathers everything the subject is hiding that this search can **reach** (`Reach` defaults to
`OnBody | InCell`; a subclass can narrow it). If nothing is reachable, the job ends here — the time
was still spent, and the player is told nothing.
### Find math, per hidden item
For each reachable secret:
```
conceal = item.concealability // 0 = always found, 1 = never
chance = clamp( (0.35 + 0.03 × socialSkill) × (1 − conceal) × 2 , 0.02 , 0.95 )
if item is a tunnel in progress:
chance = clamp( chance + tunnelExtra , 0.02 , 0.98 ) // tunnelExtra up to +0.40
chance ×= WardenDisposition.Diligence(searcher) // the guard's thoroughness
if Rand.Chance( WardenDisposition.Corruption(searcher) × 0.6 ):
continue // the warden saw it and said nothing — looked away
found = Rand.Chance(chance)
```
Reading it in plain terms:
- **Skill raises the ceiling; concealability lowers it.** `BaseFindChance` is 0.35; each level of the
warden's **Social** skill adds 0.03. The `(1 − conceal) × 2` factor pivots at concealability 0.5
(neutral, ×1.0): a drug at 0.5 is average, a shiv at 0.75 is halved (×0.5), a heavy weapon at 0.35
is boosted (×1.3).
- **A good warden still misses a well-hidden shiv more often than not.** Worked: a level-8 Social
warden searching for a 0.75-concealability shiv gets `(0.35 + 0.24) × 0.5 = 0.295` *before*
Diligence scales it down further. Well under a coin-flip.
- **A tunnel is a different story.** The pick hides at 0.5, but a nearly-finished shaft adds up to
+0.40 and the cap rises to 0.98 — the deeper the dig, the harder it is to miss. A routine cell toss
catches a dig in progress.
- **The warden's own thoroughness and price** enter last: `Diligence` multiplies the whole chance
down for a lax, kind, or miserable guard, and `Corruption` gives a bent guard a flat chance to look
the other way even on an item they would otherwise have found. Both are on the
[Corruption](Corruption.md) page.
### On a find
```
Confiscate(subject, item.def) // it never becomes a Thing — nothing drops on the floor
timesCaught++ // raises future suspicion, feeds classification
if it was a tunnel: escapeAttempts++ // a foiled dig is a logged attempt, same as one that ran
```
and the player gets a message — *"{warden} searched {prisoner} and found hidden {item}"*, or the
tunnel variant. If nothing is found the player is told **nothing**: they do not learn there *was*
something, only that the warden's time was spent.
## Writing back to the record
Every outcome updates Core's `CriminalRecord`, which is what makes the search a *loop* rather than a
one-off dice roll:
| Field | Written when | Read by |
|---|---|---|
| `timesSearched` | every search, hit or miss | priority, suspicion, classification |
| `timesCaught` | a find | priority (×2), classification |
| `escapeAttempts` | a foiled tunnel | priority (×3, heaviest), classification |
If you run **Institution: Justice**, those same fields drive a prisoner's security classification and
feed the deterrence signal — a prison that catches contraband becomes a prison that classifies its
troublemakers correctly and deters the next attempt. If you run Contraband alone, the fields still
route the warden. See [Compatibility](Compatibility.md).
## Quick reference
| Constant | Value | Meaning |
|---|---|---|
| Base find chance | 0.35 | before skill and concealability |
| Per Social level | +0.03 | skill raises the ceiling |
| Find chance clamp | 0.02 .. 0.95 | (0.98 for a tunnel) |
| Search duration | 900 ticks | one wait toil, hit or miss |
| Search cooldown | 60000 ticks (1 day) | per prisoner |
| Priority base | 4 | every prisoner is worth a routine toss |
| Gnawed-bed weight | up to +12 | `1 − HP/maxHP` |
| Record weight | up to +8 | `timesCaught×2 + contrabandMade×0.5 + escapeAttempts×3` |
| Default `Reach` | `OnBody \| InCell` | narrowed by a future street-frisk subclass |
---
*Part of the **Institution** suite — Core · Contraband · Justice · Gangs, alongside Foul Play and
Ward. Each stands alone; together they interlock.*
*Developed with substantial assistance from Claude (Anthropic).*