# Perception, Crowd & the Morality System

V5 is seven games wearing one trench coat. A 1947 LA detective's witness on a
rain-slick street, a Hong Kong triad lookout, an 1899 frontier shopkeep sizing
up a rider, a Witcher-cell village watchman, a sci-fi station marine — all of
them read the world with the **same semantics**: the same notion of a stimulus,
the same Idle → Curious → Alert → Hostile escalation, the same idea of "a crime
was witnessed, and someone is now walking toward an authority to report it." And
every one of those acts lands on a single **moral compass** that the cross-cell
Mind-Palace and the Bureau XP economy can read, even though each cell measures
right-and-wrong on its own local axis (Cop/Triad here, Family/Outsider there,
Honor/Bounty out west, Paragon/Renegade in orbit).

V5 keeps that honest across cells by pushing all of it into four small,
deterministic C++ modules that share one design rule: **the logic is pure
functions over plain data, and the caller holds the state.** `V5Perception`
turns geometry into suspicion; `V5Crowd` turns a Mass-Entity density profile
into a panicking, fleeing, bark-shouting population; `V5Honor` is the
morality/reputation core — honor bands, bounty, witness reports, and the
cross-cell bridge; and `V5Wanted` is the star-rated, era-scaled law-enforcement
response the bounty system escalates into. None of it is a TypeScript contract
package — the source of truth is the Unreal C++ under
`V5/ue/Source/V5Perception`, `…/V5Crowd`, `…/V5Honor`, `…/V5Wanted` and each
module's `Private/Tests`. This page is the architecture-side companion for V5's
shared AI and morality systems, part of the **Shared Gameplay Foundations**
group; the orientation hub is [../V5_ARCHITECTURE.md](../V5_ARCHITECTURE.md).

## What ships, honestly

Five claims, plainly, so the rest reads at face value.

- **Real, deterministic, value-tested logic.** All four modules compile — the
  linker has produced `Binaries/Linux/libUnrealEditor-V5Perception.so`,
  `…-V5Crowd.so`, `…-V5Honor.so`, and `…-V5Wanted.so` on this box. The
  algorithms are domain-specific, not renamed CRUD, and the automation specs
  assert _computed_ values rather than shapes: that assault-then-loot-then-help
  on a fresh honor meter lands at exactly `-8 → -11 → -7`, that a 1.5×
  train-robbery charge pushes a bounty to `$240`, that an all-light-side bridge
  input produces a moral compass `> 0.9` and an all-dark one `< -0.9`, that an
  NPC on a panic radius boundary at 50 m is still "affected" but one metre
  further out is not. Those fail against placeholders.

- **The crowd vocabulary is a real authored corpus, not a generator.** `V5Crowd`
  loads `V5/ue/Content/V5Crowd/Data/crowd_catalog.json` — a **1.5 MB,
  6,050-line** file of ambient barks plus 8 density profiles and 6 body-language
  era bundles — and `ValidateCrowdCatalogJson` is a real linter that rejects the
  set unless it has ≥ 5000 bark lines, unique ids, all six era bundles, and
  coverage of the five playable crowd cells. The bytes are authored on disk, not
  faked by a handful of templates pretending to be thousands.

- **Stateless-library design is the architecture, not a shortcut.** Every system
  class in these four modules is a `UBlueprintFunctionLibrary` of `static`
  `BlueprintPure` functions — 33 of them — operating on `USTRUCT` value types.
  There is **no `UV5PerceptionComponent`, no actor, no `UWorldSubsystem`, and no
  Behavior Tree, Blackboard, EQS, `UAISense`, or StateTree** anywhere in
  `V5Perception/V5Crowd/V5Honor/V5Wanted` (a `grep` returns nothing). The pawn
  or Blueprint that owns the NPC holds the `CurrentSuspicion`/`FV5WantedState`
  and calls these functions each tick. That is a deliberate data-oriented choice
  — reproducible, server-friendly, trivially testable — but it is also the
  honest caveat: these are **library-grade building blocks with no in-tree
  runtime loop wiring perception → crowd → honor → wanted together.** No
  production caller of `UV5_Perception_StateMachine::Evaluate` exists outside
  the specs.

- **Mass-linked, not Mass-driven.** `V5Crowd.Build.cs` links `MassEntity`, and
  `BuildPopulationBatch` emits a real `FV5CrowdMassEntityBatch` — a lightweight
  entity count, a spawn radius, a derived `EntitySpacingMeters`, and a template
  id like `mass.crowd.urban.civilian`. But the density/panic/promotion math runs
  on plain structs, not a live `FMassEntityManager` processor graph — Mass-aware
  authoring metadata, mirroring V4's posture.

- **One documented approximation in the bridge.** The monolith's moral-compass
  formula lists two separate period weights (`w_period_made`, `w_period_vice`);
  the shipped `ComputeMoralCompass` collapses them into a single `PeriodWeight`
  applied to both the Family-rep and case-rating terms, with the normalizer
  using `PeriodWeight * 2`. The compass is real and tested; this is the one
  place the code simplifies the design doc, called out here so it isn't mistaken
  for drift.

## Perception: one stimulus model for every NPC

`V5Perception` is the cross-cell sensory core. It has no per-NPC object — it is
a set of pure functions in `V5Perception/Private/V5PerceptionSystems.cpp` keyed
by a `FV5PerceptionTuning` the caller supplies.

### Cell tuning — 7 cells × 6 NPC classes

`BuildCellTuningDatasets` authors one dataset per V5 cell (`Urban`, `Period`,
`Frontier`, `Hunter`, `SciFi`, `Steampunk`, `MindPalace` — the spec asserts the
count equals `GetAllV5Cells().Num()`), each multiplying a base table of six NPC
classes by a per-cell `SightScalar`/`HearingScalar`. The scalars encode the
fiction: `Frontier` sees and hears keener (1.12 / 1.18), `Hunter` keener still
(1.18 / 1.25), the `MindPalace` dream-space is deliberately dull (0.75 / 0.75),
and `SciFi` has sharp eyes but muffled ears (1.25 / 0.90). Within a dataset, an
`EliteGuard` carries a 55 m sight range and a low 42-point alert threshold while
a `Civilian` gets 28 m and a sluggish 60 — so the same stimulus escalates a
trained guard faster than a bystander, by data, not by branching code.

### The suspicion state machine

`UV5_Perception_StateMachine::Evaluate` is the integration step. Each stimulus
is converted to a suspicion increment by `StimulusSuspicion`, and the table is
domain-specific: a `Sight` stimulus is worth `34 × Strength`, but **`52` if the
target is a known suspect** (gated on line-of-sight _and_ being inside
`SightRangeMeters`, else zero); `Sound` is `24 × Strength` inside
`HearingRadiusMeters`; a `WitnessReport` is a flat `48 × Strength`; and `Damage`
is `65`, or **`100` — instant Hostile — when `bHostileAction` is set.**
De-escalation (`DeescalationPerSecond`, default 4/s) is applied _only_ on a tick
where the stimulus added nothing, so a guard with a live stimulus this frame
never bleeds suspicion at the same time — the same "perceive or decay, never
both" invariant V4 enforces. `StateForSuspicion` then maps the clamped `[0,100]`
value through the tuning's `Curious/Alert/Hostile` thresholds, and `bEscalated`
is a true state-rank comparison against the previous state. (One honest
difference from V4: there is **no hysteresis margin** here — it is a plain
threshold comparison, with no guard against strobing on a boundary.) The spec
walks a guard `Idle → Curious` on a sound, `→ Alert` on a known-suspect
sighting, and `→ Hostile` on a hostile-action damage event, checking
`bShouldInvestigate` flips off once Hostile.

### Sight, hearing, and the sound-absorption model

`EvaluateSight` is a real cone test: normalize observer-forward and to-target,
take the dot product, convert to a half-angle, and require
`Distance ≤ EffectiveRange` and `Angle ≤ SightConeDegrees × 0.5` _and_ a caller
line-of-sight bool. Hearing is the more interesting half.
`ComputeTransmittedStrength` runs the loudness through a stack of
`FV5PerceptionAbsorptionLayer` walls, each attenuating by a material coefficient
× thickness — `Foliage 0.18`, `Drywall 0.35`, `Brick 0.58`, `Stone 0.72`,
`Bulkhead 0.82` per metre — clamped so a single wall can never block more than
95 %. `EvaluateHearing` then declares a sound heard only if the transmitted
strength clears `0.12` _and_ the listener is inside the strength-scaled radius.
The spec proves a brick-plus-bulkhead double wall drops a full-strength sound
below audibility at a distance that was clearly audible in open air — sound that
genuinely routes around geometry, not a flat radius.

### Witness propagation and "investigate last-known"

`UV5_Perception_WitnessReport::Propagate` is the NPC-to-NPC alert spread: every
recipient inside `PropagationRadiusMeters` (default 80 m) joins the alerted set,
the propagated suspicion is `Urgency × 65`, and recipients flag alerted only if
that clears 50. `BuildMoveRequest` closes the loop — for a Curious or Alert
state it emits an `FV5PerceptionInvestigateMoveRequest` aimed at the stimulus's
`LastKnownLocation`, with a tighter 120 m acceptance radius when Alert versus
220 m when Curious. That last-known plumbing turns a suspicion spike into an
actual search behavior the pawn can drive.

## Crowd simulation — Mass-batched density, panic, and barks

`V5Crowd` (`V5Crowd/Private/V5CrowdSystems.cpp`) turns a density profile into a
living street.

**Density batching and pawn promotion.** `BuildPopulationBatch` takes a
`FV5CrowdDensityProfile` (Urban targets 250 NPCs within 100 m, per the catalog),
scales the count by a console-throttle percentage and a streaming-churn
dampener, then derives `EntitySpacingMeters` as `sqrt(πr² / count)` so the crowd
is laid out by area, not stacked — the spec confirms desktop keeps the full 250
while a console build throttles below it. `EvaluatePromotion` is the
lightweight-entity-to-pawn rule: an NPC promotes to a full pawn inside its role
radius — **Named 18 m, Generic 30 m, Ambient 60 m** — or unconditionally if it
is mission-critical, mid-interaction, or already promoted, each path stamping a
distinct `ReasonTag`.

**Panic propagation.** `EvaluatePanicAtLocation` is the heart of the crowd
response. The effective radius is the base radius scaled by a stimulus
multiplier (`Gunshot 1.0`, `VehicleCrash 1.15`, `Alarm 1.25`,
`MonsterAttack 1.5`, `Explosion 1.7`), a line-of-sight factor (1.0 vs 0.55 when
occluded), an indoor factor (0.8), and an **age decay that fades the stimulus
over 45 s down to a 0.25 floor** — so a gunshot's panic footprint shrinks the
longer ago it fired. Inside that radius the panic intensity is a clamped
distance falloff, the NPC flees along the vector _away_ from the stimulus, and
`bShouldFlee` trips on intensity ≥ 0.25 or unconditionally for explosions and
monster attacks. The spec pins the edges precisely: at 25 m from a 50 m gunshot
the NPC has ≥ 0.5 intensity and flees outward (+X), exactly on the 50 m boundary
it is affected but only startles, at 51 m it is untouched, an explosion still
reaches 80 m, and the same gunshot _occluded_ no longer reaches an NPC at 30 m.

**Witness reporting into Honor.** `UV5_Crowd_WitnessReport::BuildWitnessReport`
is the seam where the crowd hands a crime to the morality system. It fails loud
on each precondition — missing witness/authority, no line of sight, crime beyond
85 m, or zero bounty at stake each return a tagged reason and submit nothing —
and only then calls into `V5Honor`'s `BuildWitnessReport`, optionally shaving
the report delay 20 % when panic-accelerated. The spec confirms a witness with a
bounty in view does submit to Honor and that panic acceleration keeps the delay
under baseline.

**Body language and the bark corpus.** `SelectBodyLanguageLibrary` picks the
era-correct animation set, and `SelectAmbientBark` is a graceful-degradation
matcher: it filters the 6,050-line corpus by era + cell + mood + zone, falls
back to era+cell+mood, then era+mood, and picks within the match set by
`abs(Seed) % count`, so the same seed deterministically yields the same line.

## The morality system — Honor, bounty, and the moral compass

`V5Honor` is the signature open-world-narrative system: a persistent
reputation/morality core whose effects ripple through greetings, prices,
encounters, and law enforcement.

**Honor meter and bands.** `HonorScore` lives in `[-100, 100]`.
`HonorDeltaForAction` is the authored consequence table — `AssaultCivilian -8`,
`LootBodyInTown -3`, `HelpStranger +4`, `StoryMercy +12`, `StoryCruelty -12` —
and `GetHonorBand` buckets the score into `VeryLow (≤ -60)`, `Low (< -20)`,
`Neutral (≤ 20)`, `High (< 60)`, `VeryHigh (≥ 60)`. The score round-trips
through the GAS attribute set `UV5_AttributeSet_Honor` (which also carries the
`ParagonScore`, `RenegadeScore`, `CopRep`, `TriadRep`, `FamilyRep`,
`OutsiderRep`, and `WitcherHumanity` sub-axes that feed the bridge below).

**Bounty and witness reports.** `GetBaseBounty` prices crimes — `Trespass $5`,
`Theft $10`, `Assault $15`, `Murder $60`, `TrainRobbery $120` — and
`ApplyBountyCharge` **adds nothing for an unwitnessed crime**, otherwise scales
by a state multiplier (the spec proves a witnessed murder posts `$60` and a 1.5×
train robbery adds `$180` to reach `$240`). `BuildWitnessReport` is the tension
mechanic: a witness will report only if there is both a witness id, an authority
id, and a bounty at stake, and the time-to-report is
`clamp(90 + distance×0.6 − low-honor-acceleration + high-honor-delay, 15, 180)`
— so an infamous (very-low-honor) player is reported _faster_, a respected one
gets a grace delay, and the spec confirms the low-honor report fires sooner and
that advancing past the timer transitions the report `Fleeing → Reported`.

**NPC reactions.** The honor band drives lived-in reactions: `BuildGreeting`
swaps lines and tone tags (`hostile` at VeryLow, `admiring` at VeryHigh),
`BuildDiscount` gives shopkeepers a `10 %` discount at High and `20 %` at
VeryHigh, and `BuildEncounterFlavor` reweights the encounter director — high
honor biases toward help-needed events, low honor toward ambushes, and a
standing bounty adds bounty-hunter weight proportional to dollars owed.

### The cross-cell bridge and the Wanted escalation

`ComputeMoralCompass` is the meta-axis: it folds every cell's local morality
into a `LightScore` and a `DarkScore` (`max(0, CopRep)` and case-rating on the
light side, `TriadRep`/`OutsiderRep` and `RenegadeScore` on the dark), then
normalizes the difference into a `MoralCompass` in `[-1, 1]`.
`GetBureauXPMultiplier` rewards conviction in either direction — `1.5×` at
magnitude ≥ 0.7, `1.25×` at ≥ 0.4, `1.0×` near zero. On enforcement,
`V5Wanted`'s `EvaluateStarLevel` maps heat to a `0–5` star rating (thresholds
`10/25/45/70/100`), `ApplyCrimeEvent` adds heat (`×1.35` if violent, `+6` for a
vehicle escape) only when witnessed, `TickEscalation` ramps heat faster at
higher stars while police hold contact, and `BuildSpawnPlan` assembles an
**era-scaled** response — modern LAPD escalating to attack-helo at 4★ and
tank/VTOL at 5★, an 1899 `Marshal` + `PosseRider`, a Hunter `VillageWatch` +
`LordsGuard`, a sci-fi `FactionTrooper` + `PatrolCruiser`. Players shed it via
`TickOffTheRadar` (90 s clear once ≥ 35 m from last-known with no police
visual/audio), `TickSafehouseHide` (3× faster in a garage), or `TryPayoff`
(`baseCostPerStar × stars² + bounty`, then a 900 s cooldown).

```mermaid
flowchart TD
    Act[Player action / crime] --> Honor[V5Honor: HonorDeltaForAction<br/>HonorScore in -100..100]
    Act --> Bounty[ApplyBountyCharge<br/>gated on bWitnessed]
    Honor --> Band[GetHonorBand: VeryLow..VeryHigh]
    Band --> React[Greetings / shop discount /<br/>encounter flavor]
    Bounty --> Witness[Witness BuildWitnessReport<br/>time = 90 + dist*0.6 -/+ honor]
    Witness --> Wanted[V5Wanted: EvaluateStarLevel 0-5]
    Wanted --> Spawn[BuildSpawnPlan<br/>era-scaled: Marshal / LAPD / Watch / Faction]
    subgraph Bridge [Cross-cell meta-axis]
        Axes[Cop/Triad - Family/Outsider -<br/>Honor - Humanity - Paragon/Renegade] --> Compass[ComputeMoralCompass<br/>Light - Dark, clamp -1..1]
        Compass --> XP[Bureau XP x1.0 / x1.25 / x1.5]
        Compass --> Gate[Mind-Palace deduction gating]
    end
    Honor --> Axes
    Spawn --> Clear[Off-radar 90s / safehouse 3x / payoff]
```

## Edge cases and failure modes

- **Hostile action skips the curve.** A `bHostileAction` stimulus snaps
  suspicion straight to 100 — no slow climb to Hostile when someone is actively
  attacking.
- **Sound routes around walls.** A double brick/bulkhead wall can drop an
  audible sound below the `0.12` audibility floor at a distance that was clearly
  heard in the open — occlusion is multiplicative, not a flat cutoff.
- **Unwitnessed crimes are free.** Both `ApplyBountyCharge` and
  `ApplyCrimeEvent` no-op on an unwitnessed flag — no bounty, no heat — so a
  clean kill genuinely leaves no trail.
- **Infamy is read faster.** Witness `TimeToReportSeconds` accelerates with
  negative honor and delays with positive — a notorious player is reported
  sooner than a respected one at the same distance.
- **Panic decays with age, not just distance.** The 45 s age scalar shrinks a
  stimulus's panic radius over time; a stale gunshot stops scaring people.
- **The crowd-to-honor seam fails loud.** A witness with no line of sight, a
  crime past 85 m, or zero bounty submits nothing and returns a tagged reason
  rather than half-reporting.
- **The un-orchestrated seam.** Each function is exercised by its spec and
  nothing else in-tree; the modules agree by construction but no runtime loop
  yet chains perception → crowd witness → Honor → Wanted. This is the honest gap
  between the proven blocks and an assembled loop.

## How it connects

This perception/morality core is the hub the rest of V5's NPC behavior reads.
The honor attribute set and the animation tells that present a suspicion state
mount on the shared Ability System and animation/input stack in
[./gas-animation-input.md](./gas-animation-input.md). The crowd density profiles
ride the same World-Partition grid, and the Wanted/encounter directors wire to
the GameFeatures mode plugins, in
[./modes-streaming-procgen.md](./modes-streaming-procgen.md). The
server-authoritative replication of suspicion, honor, bounty, and wanted state —
distinct from the spaceship cell's rollback model — is in
[./netcode-authority-and-determinism.md](./netcode-authority-and-determinism.md).
The orientation hub is [../V5_ARCHITECTURE.md](../V5_ARCHITECTURE.md).

## Related

- [GAS, Animation & Input](./gas-animation-input.md) — the attribute sets
  (`UV5_AttributeSet_Honor`) and ability families the morality verbs mount on
- [Modes, Streaming & Procgen](./modes-streaming-procgen.md) — the
  World-Partition grid the crowd streams on and the mode plugins the Wanted
  director feeds
- [Netcode, Authority & Determinism](./netcode-authority-and-determinism.md) —
  replicating suspicion, honor, bounty, and wanted state across the cells
- The orientation hub: [../V5_ARCHITECTURE.md](../V5_ARCHITECTURE.md)
