Tactical Action · Features

The Stealth & Real-Time-Tactics Cells

A focused page within the Tactical Action Features documentation. The full map and every sibling page live in the Features hub.

5sections11 minread3diagrams

On this page

Two of V4's cells are built on the same insight: the most dangerous thing in the room is not a guard's gun, it is a guard's attention. The Stealth cell (V4Stealth) is the Splinter Cell / Hitman surface — one operator, first- or third-person, who survives by managing light, sound, and a stolen uniform. The Real-Time-Tactics cell (V4Tactics, which V4 markets as Real-Time Stealth Tactics) is the Commandos / Desperados surface — a top-down squad of six specialists who pause time, queue precise orders, and trigger them in unison so that six kills land before a single patrol can shout. They look nothing alike on screen, but they read the world through one perception model, hide bodies with sibling components, and weaponise the same vision cones. This page covers what each cell plays like and the signature systems that give it its feel, grounded in the in-tree Unreal C++ that actually ships. It is part of The Cells group; the catalogue hub is ../V4_features.md.

What ships, honestly#

Both cell modules are real, compiled, and value-tested — and both lean on referenced art rather than baked binaries. The line between the two is drawn explicitly below so the rest of the page reads at face value.

  • V4Stealth is eight .cpp / eight .h of real stealth logic. The light gauge, sound footprint, disguise economy, body-drag, gadget, and mark-and-execute components all carry domain formulas, not CRUD. Two automation specs assert computed values: V4.Stealth.LightGauge.Sampling walks a lit light through a light-out into darkness and checks the 150 cm close-detect boundary; V4.Stealth.Disguise.Enforcer proves an enforcer detection time of exactly 4.0 × 0.5 = 2.0 s and that a sub-two-second disguise swap is refused.
  • V4Tactics is eight .cpp / eight .h of real squad-tactics logic. The command queue, party roster, vision cone, body-disposal, and the Showdown subsystem are all in-tree; V4.Tactics.Showdown.DependencyResolver proves the topological order resolver and its cycle rejection, and V4.Mode.RTST.Desperados drives a full six-specialist mission set through it.
  • Three honest caveats. First, "cell" means the engine module, not a one-to-one game: the Splinter Cell and Hitman plugins compose V4Stealth with V4Tactical and V4Perception, and the Commandos plugin pulls V4Tactical + V4Stealth + V4Tactics together. Second, the logic is code but the art is JSON — meshes, montages, and levels are *.v4asset.json sidecars and DT_* data tables (e.g. the Hitman world under V4/stealth/hitman/), not cooked .uasset. Third, two seams are narrower than they look and are flagged in place: CanSplinterCellSightDetect() currently delegates to the generic detect path, and the RTT vision cone's low-light term is a flat designer scalar, not a live lux probe. The architecture-side treatment of the shared perception model is ../architecture/ai-perception-stealth.md.

The Stealth cell — V4Stealth#

One operator against a populated building. You are not stronger than the guards; you are quieter, and the cell turns quietness into mechanics you can read on the HUD. V4Stealth.Build.cs depends on V4Perception, V4Gameplay, and GameplayAbilities, so every exposure the player generates feeds the shared suspicion gauge that the NPCs read — the player's side of the equation lives here, the guard's side lives in V4Perception.

Reading the dark — the light/shadow gauge#

The Splinter Cell fantasy is "the shadows are a tool," and UV4LightGaugeComponent (V4Stealth/Public/V4LightGaugeComponent.h) makes the shadows a real number. CalculateLuxAtCapsule() sums every enabled FV4StealthLuxSample against the player capsule with a squared distance falloff, measured to the capsule's vertical axis rather than a single point — so a crouched silhouette under a ceiling lamp reads as lit, not as a dot that the lamp happens to miss. The lux total classifies through DarkLuxThreshold = 0.3 and LitLuxThreshold = 0.65 over an AmbientLux = 0.02 floor into EV4LightExposureState { Dark, Dim, Lit }.

Detection is then gated by that state. CanBeSightDetected(distance, hasLOS) returns false with no line of sight, true whenever you are not in Dark, and in true darkness only when the observer closes inside DarkCloseDetectionDistanceCentimeters = 150 cm. Darkness hides you at range and fails you at arm's length — the rule the genre lives on. Shooting a bulb is a real state change, not a cosmetic: ApplyLightOut(LightId) flips that sample's bEnabled off (reversible via RestoreLight), dropping the next capsule sample below the dark threshold. LightGaugeSpec.cpp walks the whole loop — lit overhead light, light-out, dark, then the 150 cm gate — and the honest seam shows up right there: CanSplinterCellSightDetect() is its own named entry point, but the body simply returns CanBeSightDetected(...). It is the hook where a stricter Splinter-Cell-only rule would land; today it shares the generic threshold, and the spec tests it at the 150/151 cm boundary as exactly that.

Sound, surfaces, and bodies#

Light is only half of exposure; the other half is the noise you make. UV4SoundFootprintComponent owns the emission side (distinct from the guard's hearing). Base radii fall off with stance — Sprint 1200, Run 900, Walk 500, Crouch 250, Crawl 120 cm — and scale by a surface multiplier resolved from the physical material: Concrete 1.0, Wood 1.15, Metal 1.35, Gravel 1.45, Water 1.6, Carpet 0.55 (registered as defaults in RegisterDefaultSurfaceProfiles). EvaluateHearingRange answers whether a listener is inside that radius and with what falloff strength; the spec asserts the ordering a player must internalise — a metal walkway is louder than concrete, carpet is quieter — rather than just "a number came back."

When a guard does go down, UV4BodyDragComponent is the carry verb: AttachBody/DropBody at a DragMovementSpeedMultiplier = 0.5 (half speed), the tax that makes hiding a corpse a real decision under a patrol clock. Drag it out of sight, or the discovery raises the alarm for the whole sector.

The disguise economy — the Hitman face#

The same module wears the Hitman mask. UV4DisguiseComponent is a genuine disguise economy, not a costume slot. BeginDisguisePickupSwap() fails loud: it returns false unless the target was knocked out and dragged and hidden in a container and the swap takes ≥ 2 s — only then is the uniform worn. Once you are wearing it, ComputeSuspicionMultiplier(zone, suspiciousAction, obviousWeapon) is the live cost knob: an allowed zone scales incoming suspicion by 0.3 × lerp(1.0, 0.5, Stealthiness), a restricted zone by 1.75×, a suspicious action by an extra , an obvious weapon by 1.5×. The enforcer — the NPC who wears your stolen uniform and sees through it — is modelled as detection time, not as a suspicion bump: ComputeEnforcerDetectionTimeSeconds shortens the base detect time by a clamped scale, so the disguise that fools the crowd is your fastest tell near the people whose job it imitates. (An EnforcerSuspicionMultiplier = 2.0 field exists but is not read by the multiplier path — the wired enforcer mechanic is the detection-time one, which DisguiseSpec.cpp pins at 4.0 × 0.5 = 2.0 s.)

That sandbox has data behind it. V4/stealth/hitman/persistent-world.json defines a named-NPC roster (TargetA, TargetB, SecurityChief, Chef, Doctor, VIPGuest, Mechanic) keyed {MapId}.{NpcSuffix}, with story-persistent target deaths, a codex that unlocks each NPC's schedule once observed in mission, and a /crime-rate telemetry dashboard aggregating community kills by season, map, room, and method. The seasonal companion rotates those routines quarterly (schedule-rotation-patch-notes.md) — the same roster, re-timed each season — and the NPC daily schedules themselves are authored FV4ScheduleNode graphs in V4Schedules, which V4Tactics also depends on.

Gadgets and mark-and-execute#

The loadout layer rounds out the kit. UV4StealthGadgetComponent runs a charge-limited loadout over EV4StealthGadgetType { StickyCam, EMP, OpticCable, SleepingGas, StickyShocker }, decrementing charges on activation and tracking active effects. UV4MarkAndExecuteComponent is the Conviction-style tag-then-fire chain as a small honest slot economy: AwardMarkSlot earns capacity (capped at MaxMarkSlots = 4), TryMarkTarget consumes a slot, ExecuteMarkedTargets fires the queue and clears it. All of these verbs surface as real Gameplay Abilities — UGA_BodyDrag, UGA_LightKill (Damage 100), UGA_StealthTakedown (TakedownId = "SneakChoke"), UGA_StickyCam, UGA_MarkAndExecute, UGA_Disguise (default Stealthiness 0.85) — mounted on the shared UV4GameplayAbilityBase, so a stealth operator carries the same attribute spine as every other cell.

flowchart TB Lights["world lights"] --> Lux["UV4LightGaugeComponent<br/><sub>lux → Dark 0.3 / Lit 0.65 · 150cm close-gate</sub>"] Move["stance × surface"] --> Foot["UV4SoundFootprintComponent<br/><sub>120–1200cm × 0.55–1.6</sub>"] Body["downed NPC"] --> Drag["UV4BodyDragComponent<br/><sub>0.5× drag speed</sub>"] Disg["UV4DisguiseComponent<br/><sub>zone 0.3/1.75× · enforcer detect-time</sub>"] Lux --> Susp["V4Perception<br/><sub>shared suspicion gauge</sub>"] Foot --> Susp Disg --> Susp Drag --> Disg

The Real-Time-Tactics cell — V4Tactics#

Now pull the camera up to a top-down isometric, swap the lone operator for a squad of six, and add the verb the genre is named for: pause. The RTST cell is real-time-with-pause, no respawns — lose a specialist and the mission fails. You watch enemy vision cones sweep the ground, queue a precise move-attack-distract plan, and release it. V4Tactics.Build.cs depends on V4Perception and V4Schedules (notably not V4Stealth — that fusion happens one layer up, in the Commandos plugin).

Pause, queue, trigger — the command model#

Two components carry the moment-to-moment loop. UV4PartyComponent holds the roster — MaxPartyMembers = 6, with a focused-specialist selection and add/remove that broadcasts focus changes. UV4CommandQueueComponent is the per-character order buffer, capped at MaxCommandsPerSpecialist = 2 and drained FIFO; CommandSpec.cpp proves the cap (a third order for one specialist is rejected) and the first-in-first-out pop order. Each FV4TacticsCommand is a typed unit of intent — Move / Attack / ItemUse / Distraction / Takedown, a target, a DependencyIds list, a ResolveSeconds, and a bPreFireValid flag — which is exactly the shape the Showdown planner consumes.

Showdown Mode — the simultaneous-turn planner#

UV4ShowdownModeSubsystem is the Desperados signature, and the code is emphatic that the world slows, it does not stop: ShowdownWorldTimeDilation = 0.125f is one-eighth speed, chosen so the scene stays readable while the AI cannot react in time. You queue up to MaxShowdownCommands = 4 cross-specialist actions, then TriggerShowdown() fires them. The heart of it is ResolveCommandOrder(), a real topological sort: it repeatedly emits any command whose dependencies are already resolved, routes invalid or missing-dependency commands into a OutBlockedCommandIds list, and — crucially — returns false when a full pass makes no progress, which is precisely a dependency cycle. ShowdownSpec.cpp queues a shot that depends on a coin throw out of order and asserts the coin resolves first and the shot second; then it makes the two depend on each other and asserts both are blocked. This is a genuine simultaneous-turn solver, not a slow-motion toggle.

The full mode test goes further. V4.Mode.RTST.Desperados builds the six-specialist roster (Gunslinger, Sharpshooter/Medic, Brawler, Saloon Infiltrator, Mystic, Pack-Animal), authors seven distractions and fourteen missions (ten main + four side ops), drives the Showdown planner through a four-action plan that resolves dependencies and executes in under one second of real time, and confirms a failure preview catches a queued action whose dependency is missing before the trigger — the UI tell the genre needs. A registered feel-test case asserts that sub-one-second budget and passes.

stateDiagram-v2 [*] --> RealTime RealTime --> Showdown : SetShowdownActive(true)<br/>world × 0.125 Showdown --> Showdown : QueueShowdownCommand()<br/>(≤ 4, bPreFireValid) Showdown --> Resolve : TriggerShowdown() Resolve --> RealTime : ResolveCommandOrder()<br/>topo-sort · fire ordered · drop blocked GUIDs note right of Resolve cycle or missing dep → false, offending GUIDs reported. end note

Vision cones — the genre's iconic primitive#

UV4VisionConeComponent is both UI and logic. It is a 2D cone — Forward, HalfAngleDegrees = 60 (a 120° field), RangeCentimeters = 1200 (12 m) — with green/yellow/red Calm/Alert/Hostile colours. ContainsPoint() is just GetDetectionWeight() > 0; GetDetectionWeight() blends an angle term and a distance term (angleAlpha × 0.65 + distAlpha × 0.35) so dead-centre, close targets weigh most. BuildGroundConeVertices() fans the cone into a triangle strip for the floor decal you see swept across the ground. The honest seam: that weight is then multiplied by (1 - LowLightFalloff), and LowLightFalloff is a flat per-cone designer scalar (default 0.5), not a live lux query — the lux-driven version of sight lives in the shared UV4PerceptionSenseLibrary. So this component is the RTT-facing cone primitive with a static darkness knob; the real light coupling is a cross-system concern documented in the architecture companion.

Specialists, distractions, body disposal#

UV4SpecialistAbilitiesComponent grants and activates the per-character verbs in EV4SpecialistVerb { Lasso, Coin, Perfume, MindControl, Whistle, StoneThrow, DecoyBoot, BodyDecoy } — the distraction toolkit that lures patrols off their routes. And the cell has its own body-disposal path: UV4BodyDisposalComponent runs BeginDragBody → HideDraggedBodyInContainer, broadcasting OnBodyHidden so a found body can raise the squad's alarm. It is the sibling of V4Stealth's UV4BodyDragComponent — two cells, the same "where do I put this corpse" problem, solved at each cell's altitude.

Where the two cells meet#

Neither cell owns the rule that decides whether you were seen — that lives in V4Perception and V4Crowd, which both cells consume. A guard's suspicion runs through UV4SuspicionStateMachine (Calm → Curious → Alert → Hostile with a hysteresis margin so it can't strobe); a Hitman crowd panics through UV4CrowdSubsystem, which tiers up to 400 agents into Background / Medium / Hero, routes panicked civilians to the nearest exfil node with capacity, and demotes tiers until the estimated tick fits MassTickBudgetMilliseconds = 8 (the perf spec proves 400 agents stay under budget). That crowd layer is Mass-adjacent: each agent carries a real FMassEntityHandle and GetMassIntegrationStatus() honestly reports which Mass modules are loaded, but the panic/flee/tier work runs on a plain TArray that mirrors Mass concepts rather than a live processor graph. The Desperados mode test confirms the wiring from the other direction — it asserts the cell reuses V4Tactics.VisionCone, V4Tactics.ShowdownMode, V4Perception.LineOfSight, and V4Perception.Suspicion. The full perception and crowd treatment is its own page: ../architecture/ai-perception-stealth.md.

flowchart LR Stealth["V4Stealth<br/><sub>light · sound · disguise</sub>"] --> Perc["V4Perception<br/><sub>suspicion state machine</sub>"] Tactics["V4Tactics<br/><sub>cones · Showdown · squad</sub>"] --> Perc Perc --> Crowd["V4Crowd<br/><sub>panic · tiers · exfil (≤ 8ms)</sub>"] Sched["V4Schedules"] --> Tactics Sched --> Stealth