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.
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.soon 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.9and 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.
V5CrowdloadsV5/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 — andValidateCrowdCatalogJsonis 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
UBlueprintFunctionLibraryofstaticBlueprintPurefunctions — 33 of them — operating onUSTRUCTvalue types. There is noUV5PerceptionComponent, no actor, noUWorldSubsystem, and no Behavior Tree, Blackboard, EQS,UAISense, or StateTree anywhere inV5Perception/V5Crowd/V5Honor/V5Wanted(agrepreturns nothing). The pawn or Blueprint that owns the NPC holds theCurrentSuspicion/FV5WantedStateand 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 ofUV5_Perception_StateMachine::Evaluateexists outside the specs. -
Mass-linked, not Mass-driven.
V5Crowd.Build.cslinksMassEntity, andBuildPopulationBatchemits a realFV5CrowdMassEntityBatch— a lightweight entity count, a spawn radius, a derivedEntitySpacingMeters, and a template id likemass.crowd.urban.civilian. But the density/panic/promotion math runs on plain structs, not a liveFMassEntityManagerprocessor 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 shippedComputeMoralCompasscollapses them into a singlePeriodWeightapplied to both the Family-rep and case-rating terms, with the normalizer usingPeriodWeight * 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).
Edge cases and failure modes#
- Hostile action skips the curve. A
bHostileActionstimulus 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.12audibility floor at a distance that was clearly heard in the open — occlusion is multiplicative, not a flat cutoff. - Unwitnessed crimes are free. Both
ApplyBountyChargeandApplyCrimeEventno-op on an unwitnessed flag — no bounty, no heat — so a clean kill genuinely leaves no trail. - Infamy is read faster. Witness
TimeToReportSecondsaccelerates 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. 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. 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. The orientation hub is ../V5_ARCHITECTURE.md.
Related#
- GAS, Animation & Input — the attribute sets
(
UV5_AttributeSet_Honor) and ability families the morality verbs mount on - Modes, Streaming & Procgen — the World-Partition grid the crowd streams on and the mode plugins the Wanted director feeds
- Netcode, Authority & Determinism — replicating suspicion, honor, bounty, and wanted state across the cells
- The orientation hub: ../V5_ARCHITECTURE.md