# Interrogation, Dialogue & Heist Systems

V5 is one open-world narrative game wearing seven costumes, and combat is only
half of what each costume promises. The other half is the set of **non-combat
signature verbs** — the things a player does that no shooting model can stand in
for: leaning across a 1947 interrogation table reading a witness's face for the
lie, spinning a six-spoke conversation wheel to charm or threaten a sci-fi
quartermaster, and chalking a bank job onto a planning board before a four-crew
South Bay heist. Three Unreal C++ modules own those verbs: **`V5Interrogation`**
(the MotionScan-style facial-tell parser that anchors the Period noir Vice
Squad), **`V5Dialogue`** (the paragon/renegade dialogue wheel, sci-fi-forward
but shared across every cell), and **`V5Heist`** (the pre-mission planner that
is the urban-crime cell's defining loop). Each is the marquee interaction of a
different cell, and together they are the proof that V5 means "narrative
universe" and not "shooter with skins."

What unites them mechanically is a shared shape: an authored, JSON/YAML-driven
**catalog** of content; a small set of `UBlueprintFunctionLibrary` statics (plus
one `AActor` and one `UWorldSubsystem`) holding the **deterministic decision
logic**; and a **classification step** that turns the player's choices into a
graded outcome — interrogation rating, paragon meter, heist ending — which other
systems consume. None of the three rolls dice. A Truth/Doubt/Lie call is right
or wrong against an authored truth-state; a wheel spoke is unlocked or gated by
a threshold; a heist ends Smooth, Hard, or Catastrophic by a fixed rule over
measured metrics. That determinism is what makes them testable, and they are
tested. This page is part of the **Signature Subsystems** group; the section hub
is [../V5_ARCHITECTURE.md](../V5_ARCHITECTURE.md).

## What ships, honestly

All three modules are **real, building Unreal C++ with domain-specific logic and
genuine automation tests** that spawn worlds, run sessions, and assert
_computed_ values — not skeletons, and not tests that only check truthiness. The
`.so` binaries (`libUnrealEditor-V5Interrogation.so`, `-V5Dialogue.so`,
`-V5Heist.so`) exist. But each sits at an honest distance from the _captured
content_ and the _in-world execution_ the design promises, and naming those
distances up front lets the rest of the page read at face value:

1. **Interrogation logic is real; the faces are a content pipeline it points
   at.** `V5Interrogation` validates a tell library, runs Truth/Doubt/Lie
   scoring, plays back authored blendshape keys, and rates a session — all in
   tested C++. The 12-camera MotionScan capture, the per-actor 16-hour FACS
   calibration, and the 60 fps `.usd` blendshape stacks (arch §Capture Pipeline)
   are a _production process_, not code in this module: the validator only
   asserts that each tell **references** a `/Game/V5FaceCapture/USD/` path and
   carries ≥3 keyframes (`V5InterrogationTellLibrary.cpp:198`), and the player
   samples those keys deterministically. It does not perform face capture.

2. **The Vice Squad scene catalog is procedurally expanded, with placeholder
   prose.** `ValidateSceneCatalogJson` expands authored _group templates_ into
   54 scenes (`V5InterrogationTellLibrary.cpp:306`), each with three questions
   whose truth-states cycle and whose evidence requirements are derived. The
   _logic_ is real; the dramatic _lines_
   (`"The contradiction is pinned to collected evidence."`) are templated, not
   54 hand-written interrogations.

3. **The 12,000-node dialogue corpus is a validated manifest, not 12,000
   authored lines.** The wheel, gating, interrupt, and VO-bank lookup are real
   and tested against real `sampleTrees`. The headline node count is a _declared
   distribution_ the validator enforces (`V5DialogueAuthoring.cpp:181`) and the
   `dialogue.py` compiler expands into node _records_ (ids/speaker/cell) — the
   prose for the full corpus is not in the repo.

4. **The heist module is the planning + classification brain; the mission that
   spawns AI crew lives elsewhere.** `V5Heist` genuinely links `V5Combat` and
   `V5Vehicles` (`V5Heist.Build.cs`) and ships real plan validation, conflict
   geometry, rehearsal, a replay hash, and a three-ending resolver. But
   `ResolveEnding` classifies an `FV5HeistExecutionMetrics` struct that the
   _in-world_ mission (the `V5UrbanHeistCity` mode plugin) must populate during
   play; the AI-crew spawn-and-drive runtime is not in this module.

## The MotionScan interrogation system

`V5Interrogation` is the Period cell's marquee verb: you watch a witness answer,
read the involuntary facial _tell_, and call their statement as **Truth**,
**Doubt**, or **Lie** — and a Lie is only a valid challenge if you are holding
the evidence that pins it.

### Twelve tells and the Truth/Doubt/Lie call

The vocabulary is a closed enum of twelve micro-expressions —
`EV5InterrogationTellType` (`V5InterrogationTypes.h:8`): EyeDart, BrowTwitch,
LipPress, JawClench, NostrilFlare, GazeBreak, MicroSmile, Swallow,
ShoulderTension, FingerTap, BreathHitch, VoiceCrack. The tell library is loaded
from JSON and validated hard: exactly **12 distinct tells, each a distinct
type**, each with a USD-backed blendshape path and at least three keyframes; the
loader rejects anything else (`V5InterrogationTellLibrary.cpp:233`). A witness's
answer carries one of three hidden truth-states — `Truthful`, `Withholding`,
`Lying` (`V5InterrogationTypes.h:24`) — and the player picks one of three
responses, `Truth`, `Doubt`, `Lie` (`:32`).

The match is resolved by `UV5_Interrogation_TruthDoubtLie::ResolveInput`
(`V5InterrogationTruthDoubtLie.cpp:3`). The three correct pairings are explicit:
Truth↔Truthful, Doubt↔Withholding, and Lie↔Lying **only when the required
evidence is in hand** (`:11`). The scoring is graded by difficulty: a correct
evidence-backed Lie is worth **+3**, a correct Truth or Doubt **+2**, a Lie
attempted without the demanded evidence **−2**, and any other miss **−1**
(`:16`). That `−2` is the design's teeth — bluffing a "Lie" call with no proof
is strictly worse than honestly doubting.

### Evidence-backed lies and scoring

The evidence gate is its own unit so it can be queried for UI affordances before
the player commits. `UV5_Interrogation_EvidenceBackedLie::CanChallengeWithLie`
returns true only when the statement is genuinely a lie, names a required
evidence id, and that id is in the player's inventory
(`V5InterrogationEvidenceBackedLie.cpp:8`) — the "press X to refute" prompt
lights up exactly when a refutation is actually available. A whole session's
calls are graded by `UV5_Interrogation_RatingSystem::RateSession`
(`V5InterrogationRatingSystem.cpp:3`): a **Brilliant** requires _every_ call
correct **and** a score of at least `2 ×` the question count (so a flawless run
that never earned the +3 evidence bonus still clears it); ≥75% correct is
**Good**, ≥50% **Average**, else **Poor** (`:23`). The
`V5.Interrogation.Session AndRating` automation test drives a real
three-question scene — Doubt on a withholding answer, an evidence-backed Lie on
a lying answer, Truth on a truthful answer — and asserts the result is
`Brilliant` with `bPerfect` set (`V5InterrogationTests.cpp:157`). A Brilliant
rating is the gate the arch promises for downstream dialogue options and
companion reactions (arch §Interrogation Scoring).

```mermaid
flowchart TD
  Q["Question (hidden TruthState:<br/>Truthful / Withholding / Lying)"] --> TELL
  TELL["TellPlayer samples blendshape keys<br/>in sync with VO + visemes"] --> CALL
  CALL{"Player call:<br/>Truth / Doubt / Lie"}
  CALL -->|Lie| EV{"Required evidence<br/>in inventory?"}
  EV -->|no| PEN["−2 (bluff penalty)"]
  EV -->|yes & state=Lying| HIT["+3 evidence-backed refute"]
  CALL -->|Truth = Truthful| OK2["+2"]
  CALL -->|Doubt = Withholding| OK2
  CALL -->|mismatch| MISS["−1"]
  PEN --> RATE
  HIT --> RATE
  OK2 --> RATE
  MISS --> RATE
  RATE["RateSession → Brilliant / Good /<br/>Average / Poor"] --> PASS{"Brilliant?"}
  PASS -->|no, < 3 passes| FOLLOW["Follow-up pass unlocked"]
  PASS -->|yes| GATE["Gates dialogue + companion reactions"]
```

### Facial-tell playback, VO sync, and multi-pass

The tell is not a flipbook —
`UV5_Interrogation_FacialTellPlayer::SampleBlend shapeWeights`
(`V5InterrogationFacialTellPlayer.cpp:34`) computes each blendshape weight as a
function of playback time, applying a linear fade from each keyframe's onset
scaled by the tell's `IntensityScalar` and taking the max where shapes overlap
(`:42`). It is a pure function of `(tell, time)`, which is why two independent
players produce bit-identical weights — the test asserts exactly that
determinism on `eyeLookRight_R` (`V5InterrogationTests.cpp:96`).
`UV5_Interrogation_VOSyncSubsystem::BuildSyncState`
(`V5InterrogationVOSyncSubsystem.cpp:3`) layers lip-sync on top: it phase-drives
three visemes (A/E/M) across the line's duration and flags the facial tell
active while playback is within the tell window (`:9`), so the tell and the
mouth move together. Finally, `AV5_Interrogation_Session`
(`V5InterrogationSession.cpp:7`) is the per-scene actor that walks questions and
accumulates results, and `UV5_Interrogation_MultiplePass` grants a **follow-up
pass** whenever a session rates below Brilliant and fewer than three passes are
spent (`V5InterrogationMultiplePass.cpp:18`) — you can re-interrogate to claw
back a botched read, but a Brilliant closes the witness out.

**Edge cases.** Submitting a choice with no current question returns a default
zero-score result rather than crashing (`V5InterrogationSession.cpp:36`); a
non-lying question never demands evidence, so an honest Doubt is always
available; a malformed tell library surfaces a populated error array instead of
a partial load (`V5InterrogationTests.cpp:116`); and `BestRating` only improves
across passes, so a worse retry can never erase a prior better grade
(`V5InterrogationMultiplePass.cpp:14`).

## The dialogue wheel

`V5Dialogue` is the shared conversation system — primarily the sci-fi cell's
voice but reusable everywhere (arch §Module Inventory). It is a runtime state
machine plus an authoring/compile path, and its defining shape is the six-spoke
wheel with paragon/renegade morality baked into option gating.

### Six spokes, fixed geometry, per-cell flavor

`UV5_Dialogue_Wheel::BuildWheelState` always emits exactly six spokes at fixed
screen angles — Right at 0°, UpperRight 45°, UpperLeft 135°, Left 180°,
LowerLeft 225°, LowerRight 315° (`V5DialogueWheel.cpp:5`) — and tints each by
the option's _tone_ (`EV5DialogueTone`: Neutral, Paragon, Renegade, Charm,
Intimidate, Continue), pulling Paragon/Charm to the cell's paragon tint and
Renegade/Intimidate to its renegade tint (`:19`). The tints and labels are
per-cell: Urban reads "Cop / Triad", Period "Family / Outsider"
(`dialogue_cell_flavors.json`), so the same wheel geometry speaks in each cell's
register. The state struct flags whether a Charm or Intimidate spoke is present
(`:63`) so the HUD can surface the persuasion affordance, and the
`V5.Dialogue.BranchingWheel` test asserts the wheel emits six spokes and marks
charm/intimidate availability (`V5DialogueTests.cpp:65`).

### Paragon/Renegade gating and Charm/Intimidate

The morality model lives in the runtime state — `FV5DialogueRuntimeState`
carries `Paragon`, `Renegade`, `Reputation`, and a set of state flags
(`V5DialogueTypes.h:121`). Option eligibility is decided by
`UV5_Dialogue_CharmIntimidate::IsOptionUnlocked`
(`V5DialogueCharmIntimidate.cpp:3`): an option is shown only if the player meets
its `RequiredParagon`, `RequiredRenegade`, and `RequiredReputation` thresholds
**and** holds any `RequiredStateFlag`. This is the arch's "Charm option needs
Paragon ≥ 70" rule made literal.
`UV5_Dialogue_RuntimeStateMachine::ChooseOption` applies the choice
(`V5DialogueRuntimeStateMachine.cpp:40`): it clamps the paragon/renegade meters
into `[0,100]` after each delta (`:50`), grants the option's state flag,
advances to the next node, and detects a terminal node (no options) to end the
conversation. The test seeds Paragon 80 / Reputation high, confirms all six root
spokes unlock including `opt.charm` and `opt.threaten`, then verifies a
low-reputation state _rejects_ the same charm option (`V5DialogueTests.cpp:79`)
and that taking the paragon branch ticks the meter 80 → 85 and grants
`flag.evac.mercy` (`:85`) — gating that would be invisible against a stub.

### Authoring: the 12,000-node catalog and the compile path

Dialogue is authored in YAML and validated on two sides. In-engine,
`UV5_Dialogue_Authoring::ValidateDialogueYaml` enforces the launch distribution
— Urban 1,500 / Period 4,000 / Frontier 2,500 / Hunter 2,500 / SciFi 1,500,
summing to **12,000** nodes — and refuses any other shape
(`V5DialogueAuthoring.cpp:181`), then parses the `sampleTrees` into real
`FV5DialogueNodeDefinition` graphs the runtime can walk. Out-of-engine,
`V5/tools/missions/dialogue.py` is the compile-time path the arch names (arch
§Dialogue Authoring): it loads the same YAML, validates the identical
`EXPECTED_COUNTS`, and expands the groups into node _records_ — assigning a
`treeId` every 24 nodes and a speaker every 64 — written out as a
`V5DialogueDataTable` artifact (`compile_dialogue`). The two validators agreeing
on 12,000 is the cross-check; as noted above, the count is a manifest target,
and only the sample trees carry shipped prose.

**Edge cases.** `BeginTree` fails closed if the tree has no id, no root, or no
nodes, and reports an immediately-terminal root rather than starting a dead
conversation (`V5DialogueRuntimeStateMachine.cpp:5`); choosing an ineligible or
unknown option leaves the state untouched and returns false (`:44`); and
`UV5_Dialogue_Interrupt::EvaluateInterrupt` fires a Paragon/Renegade cinematic
interrupt only when the input lands on the _exact_ trigger frame
(`V5DialogueInterrupt.cpp:8`) — the frame-perfect QTE window the arch describes
for interrupt-able cinematic ranges.

## The heist planner

`V5Heist` is the urban-crime cell's signature loop: before the job you author
the crew, vehicles, loadouts, and route on a planning board; you rehearse it at
quarter speed to surface conflicts; and after the job a fixed rule grades the
outcome. The module links `V5Combat` and `V5Vehicles` (`V5Heist.Build.cs`) and
exposes its logic as a family of `UBlueprintFunctionLibrary` statics plus one
Slate panel.

### Plan validation

Each authoring stage has a validator that returns a tagged
`FV5HeistValidationResult`. `UV5_Heist_CrewSelection::ValidateCrewSelection`
requires unique crew ids, **1–4 protagonists**, at least one **Driver**, and at
least one **specialist** (Hacker or Demo) (`V5HeistSystems.cpp:105`).
`ValidateVehicleAssignments` demands a **Getaway** vehicle, a real assigned crew
member, and reliability ≥ 0.3 (`:154`). `ValidateWeaponLoadouts` insists every
crew member has a primary, secondary, and special item (`:175`). And
`UV5_Heist_RouteAuthoring::ValidateRoute` requires all six waypoint phases —
Approach, Entry, InsideTarget, Escape, DumpVehicle, Safehouse — to be present
(`:212`), with `BuildOrderedRoute` sorting by sequence index (`:202`). The
20-heist catalog (12 main-campaign + 8 co-op replay templates, enforced at
`V5HeistCatalog.cpp:321`; 3,537 lines of authored intel/crew/route data) is the
content these validators run over.

### Conflict detection is real geometry

The rehearsal's value is its conflict highlighting, and that is genuine
computational geometry, not a flag.
`UV5_Heist_ConflictDetection::DetectConflicts` walks each consecutive route
segment and, for every patrol zone, computes the true **point-to-segment
distance** from the zone center to the path (`DistancePointToSegment`,
`V5HeistSystems.cpp:20` — projecting the center onto the segment with a clamped
dot-product). A segment that passes within a zone's radius raises a
`HighPatrolZone` conflict whose severity is **Red** when the zone's patrol
weight ≥ 0.75 (else Amber) and whose risk score is `weight × 100` (`:250`). The
`V5.Heist` test lays a patrol zone over a route leg and asserts a Red conflict
is flagged (`V5HeistTests.cpp:70`). That risk feeds
`UV5_Heist_Planner::BuildPlannerPanelState`, which sums conflict risk and maps
it to an accent color — green / amber / red at the 25 and 75 thresholds
(`V5HeistSystems.cpp:68`) — and renders through the real `SV5HeistPlannerPanel`
Slate widget (`:83`).

```mermaid
flowchart LR
  CAT["heist_catalog.json<br/>(12 campaign + 8 co-op)"] --> PLAN
  subgraph PLAN["Author (validated)"]
    CREW["Crew 1–4 +<br/>driver + specialist"]
    VEH["Vehicles<br/>(getaway req.)"]
    LOAD["Loadouts<br/>(per-crew)"]
    ROUTE["Route<br/>(6 phases)"]
  end
  PLAN --> REH["Rehearsal @ 0.25× →<br/>point-to-segment conflict scan"]
  REH --> EXEC["In-world mission<br/>(V5UrbanHeistCity)"]
  EXEC -->|"Tab-pause live re-route"| EXEC
  EXEC --> MET["FV5HeistExecutionMetrics<br/>(alarm, casualties, escape, gain)"]
  MET --> END{"ResolveEnding"}
  END --> SMOOTH["Smooth ×1.15 + follow-up"]
  END --> HARD["Hard (cut = gain)"]
  END --> CAT2["Catastrophic ×0.25"]
```

### Rehearsal, live re-route, replay, and three endings

`UV5_Heist_RehearsalPreview::BuildRehearsalPreview` fixes the **0.25× time
scale** the arch promises (4× slowed) and stretches the preview duration
accordingly — a 120 s plan rehearses in 480 s (`V5HeistSystems.cpp:261`,
asserted at `V5HeistTests.cpp:75`). Mid-mission,
`UV5_Heist_LiveOverride::ApplyLiveOverride` swaps in a re-ordered route and
marks each waypoint `bRuntimeOverride` **only** while the Tab-pause is active
(`:270`) — the live re-routing the design calls for.
`UV5_Heist_ReplayExport::BuildReplayExport` builds a deterministic CRC32 hash
over the plan id, seed, crew count, and waypoint count (`:284`) so a plan can be
exported and reproduced. The capstone is
`UV5_Heist_EndingResolver::ResolveEnding` (`:298`), a strict classifier over
measured metrics: a failed escape, any crew loss, or near-zero gain is
**Catastrophic** (cut ×0.25, no follow-up); a clean run — no alarm, no civilian
casualties, ≥95% gain — is **Smooth** (cut ×1.15, follow-up unlocked);
everything between is **Hard**, with the cut clamped to the gain fraction and
follow-up gated at ≥35% gain. The `V5.Heist.EndingClassification` test pins all
three arms (`V5HeistTests.cpp:107`).

**Edge cases.** Conflict detection skips a degenerate zero-length segment by
falling back to point distance (`V5HeistSystems.cpp:24`); a live override with
an empty route or no active pause returns the current route unchanged (`:272`);
a replay is marked exportable only with a real heist id and non-empty crew and
route (`:294`); and the ending resolver's Catastrophic check runs _first_, so an
escape failure outranks an otherwise-high gain.

## Where this connects

These three verbs are the cells' showcases, and they hand off to the rest of V5
at well-defined seams:

- **Presentation.** The MotionScan tells, viseme lip-sync, dialogue-wheel HUD,
  and heist planner Slate panel are realized by the audio/VFX/UI layer —
  [./audio-vfx-cinematics-ui.md](./audio-vfx-cinematics-ui.md) owns the VO
  ducking, the wheel widget, and the cinematic interrupt cameras these systems
  drive.
- **Creation.** Interrogation scenes, dialogue trees, and heists are all
  authorable content; the in-editor authoring tools (Heist Author, the dialogue
  and case authoring panels) and the cross-cell Mind Palace deduction board live
  in [./creator-suite-and-mind-palace.md](./creator-suite-and-mind-palace.md).
- **Cell spine.** Interrogation is the Period Vice Squad's marquee loop and the
  dialogue wheel threads through the Hunter and Period stories; the cell
  rulesets, the Made Man/Vice Squad period structure, and the Bureau XP ledger
  that consumes interrogation ratings are covered in
  [./hunter-period-systems-and-bureau-spine.md](./hunter-period-systems-and-bureau-spine.md).
- For these sections in prose and the full subsystem glossary, return to the
  hub: [../V5_ARCHITECTURE.md](../V5_ARCHITECTURE.md).
