Open-World Narrative · Architecture

Interrogation, Dialogue & Heist Systems

A focused page within the Open-World Narrative Architecture documentation. The full map and every sibling page live in the Architecture hub.

5sections12 minread2diagrams

On this page

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.

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).

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).

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 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.
  • 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.
  • For these sections in prose and the full subsystem glossary, return to the hub: ../V5_ARCHITECTURE.md.