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.
199 lines
9.5 KiB
Markdown
199 lines
9.5 KiB
Markdown
# 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).*
|