Wiki: player-facing rewrite of every layer + add a scenario FAQ, linked from Home

This commit is contained in:
flan
2026-07-16 02:18:25 +00:00
parent ff703271ba
commit 8250dcb369
32 changed files with 1654 additions and 2510 deletions
+97 -114
View File
@@ -4,177 +4,161 @@
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.**
Searching a prisoner is not one of them, and no such job exists anywhere in the base game. Contraband
adds it as a **new** warden job, not a change to an existing 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:
The design splits one job into two halves:
| Class | Question it answers | Notes |
| Half | 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 search **assignment** | **Who** may be searched? | it is ordinary warden work — a warden already has the run of the prison, so no new authority is invented |
| The search **act** | **What** is a search? | walk to the pawn, turn them over, resolve |
> The WorkGiver is the authority. This driver is just the act.
> The assignment is the authority. The act 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.
Police** module adds a *second* authority — a cop who may stop and frisk a **free colonist** — reusing
the same act with a **narrower reach** (a street frisk gets what is on the body, 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
## Who gets searched
`Contraband_WardenSearch` is a `Warden` work-type giver:
The search is warden work, and it sits at a deliberate spot in the warden's priorities:
| Field | Value | Why |
| Property | 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 |
| Work type | Warden | it is warden work |
| Priority within warden work | **below feeding** | a starving prisoner matters more than a hidden shiv |
| Capacities needed | Manipulation, Sight | you search with your hands and eyes |
| Ranked by suspicion | **yes** | pick the right prisoner, not the nearest |
`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.
That last one — ranking by suspicion rather than distance — is what lets a gnawed bed and a thick
record send the warden to the *right* prisoner first.
`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.
A search is offered only if all of these hold: the warden should be looking after this prisoner, the
per-prisoner cooldown is up, the prisoner is **awake, not downed, and not in a mental state**, and the
warden can reach them.
Note what is **not** checked — and the source is emphatic about it:
Note what is **not** checked:
> 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
> 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.
The same prisoner cannot be turned over more than **once per day** (60,000 ticks). 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:
Everyone is worth a routine toss; the tells push a prisoner up the queue. Priority is built up like
this:
```
priority = 4 // baseline — everyone gets frisked
+ 12 × bedDamageFraction // chewed furniture = probable cause
+ min(8, timesCaught×2 + contrabandMade×0.5 + escapeAttempts×3) // the record
```
- **Base 4** — a routine toss for every prisoner.
- **Up to +12 for a gnawed bed** — scaled by how damaged their owned bed is. A badly damaged bed
shoots to the top.
- **Up to +8 for their record** — prior catches (×2 each), contraband made in the past (×0.5 each),
and foiled escape attempts (×3 each, **the heaviest term**), capped at +8 so the record cannot
completely swamp fresh probable cause.
| 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 |
| Gnawed bed | up to +12 | how far below full HP their owned bed sits |
| Times caught | ×2 | already caught with contraband before |
| Contraband made | ×0.5 | has finished contraband in the past |
| Escape attempts | ×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.
The record is only read here, never created. 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:
**Worked example.** A prisoner whose bed sits at 40% HP (so 60% missing), 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
```
> 4 + (12 × 0.6) + min(8, 2×2 + 1×3) = 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.
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:
The searcher walks to the subject, spends a **900-tick** turn 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."*
1. Starts the daily cooldown on that prisoner.
2. Ticks up how many times this pawn has been searched — *"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.
Then it goes through everything the subject is hiding that this search can **reach** (a full warden
search reaches both the body and the cell; a narrower future street-frisk reaches only the body). 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:
For each reachable secret, the chance to find it is built like this:
```
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)
```
- Start from a **base of 0.35**, plus **0.03 for each level of the warden's Social skill**.
- Scale by how concealable the item is: the factor pivots at concealability 0.5. A drug at 0.5 is
average (×1.0), a shiv at 0.75 is halved (×0.5), a heavy weapon at 0.35 is boosted (×1.3).
- If the item is a **tunnel in progress**, add its extra find chance (up to +0.40 for a nearly
finished shaft), and the ceiling rises.
- Multiply the whole thing by the warden's **Diligence** — a lax, kind, or miserable guard scales it
down.
- Before the find roll, a **bent** warden gets a flat chance to look the other way (their Corruption ×
0.6) even on an item they would otherwise have found.
- The chance is clamped to between 0.02 and 0.95 (0.98 for a tunnel).
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.
- **Skill raises the ceiling; concealability lowers it.** Each level of the warden's Social skill adds
a little; a well-hidden item takes a lot of it back.
- **A good warden still misses a well-hidden shiv more often than not.** A level-8 Social warden
searching for a 0.75-concealability shiv is already well under a coin-flip *before* Diligence scales
it down further.
- **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
- **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
```
- The item is confiscated — it **never becomes a real object**, nothing drops on the floor.
- The prisoner's times-caught tally ticks up (raising future suspicion, feeding classification).
- If it was a tunnel, their escape-attempt tally ticks up too — a foiled dig counts the 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.
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
Every outcome updates Core's criminal record, which is what makes the search a *loop* rather than a
one-off dice roll:
| Field | Written when | Read by |
| Tally | Updated 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 |
| Times searched | every search, hit or miss | priority, suspicion, classification |
| Times caught | a find | priority (×2), classification |
| Escape attempts | a foiled tunnel | priority (×3, heaviest), classification |
If you run **Institution: Justice**, those same fields drive a prisoner's security classification and
If you run **Institution: Justice**, those same tallies 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
troublemakers correctly and deters the next attempt. If you run Contraband alone, the tallies still
route the warden. See [Compatibility](Compatibility.md).
## Quick reference
@@ -184,15 +168,14 @@ route the warden. See [Compatibility](Compatibility.md).
| 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 |
| Search duration | 900 ticks | one turn, hit or miss |
| Search cooldown | 60,000 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 |
| Gnawed-bed weight | up to +12 | how far below full HP their bed sits |
| Record weight | up to +8 | caught ×2 + made ×0.5 + escapes ×3 |
| Default reach | body + cell | narrowed by a future street-frisk |
---
*Part of the **Institution** suite — Core · Contraband · Justice · Gangs, alongside Foul Play and
Ward. Each stands alone; together they interlock.*
**