Police NPCs¶
Server-side AI for foot-patrol police officers. Lives in src/server/services/AI/PoliceNPC/.
Architecture¶
The NPC logic uses a Hierarchical State Machine (HSM). There are two levels:
- State Groups (
G_*modules) — high-level operational modes. Each group has acanEnter, an optionalEnter/Exit, and aBeginStatethat names which child state to start in. - Child States — concrete behaviors that run every tick. Each has
canEnter,tick, and optionalEnter/Exit.
The core loop in Core.lua fires every ~100 ms per NPC (staggered by NPC id to spread load). On each tick it:
- Checks if the NPC should be cleaned up (dead, despawning, etc.)
- Finds a target via
Target.findand updatesEntity.DistanceandEntity.CanSeeTarget - Scans all possible next states (child states listed in
nextStates+ all groups except the current one) and transitions to the first whosecanEnterreturns true - Calls
State.tick(Entity)
State Groups and Their Children¶
G_Patrol — no target¶
The NPC wanders the map when there is no wanted player nearby.
| State | What it does |
|---|---|
Wander |
Follows path nodes around the map at walk speed |
EnterPatrolCar |
Drives a patrol vehicle along road nodes (up to 2 vehicles patrol simultaneously) |
The group starts despawning the NPC after 25 seconds if it stays in Wander with no patrol car, preventing orphaned NPCs.
1_G_Drive — target is in a vehicle and far away¶
Entered when the target is identified and either in a moving vehicle or more than 300 studs away (or unidentified and the last-known position is more than 500 studs away). The NPC runs to their claimed patrol car and drives it.
| State | What it does |
|---|---|
RunToCar |
Pathfinds to the patrol car door then sits in the seat |
2_G_Investigate — target is unidentified and nearby¶
The NPC knows a crime occurred but hasn't identified the player yet. Entered when target exists but CrimeRecord.Identified is false and within 400 studs.
| State | What it does |
|---|---|
PatrolArea |
Patrols around the last-known crime position looking for the suspect |
InvestigateClue |
Moves to a specific clue position |
3_G_Search — target identified, search timer active¶
Entered when the player is identified and ClientSearchTick > 0. The NPC sweeps the area.
| State | What it does |
|---|---|
SearchArea |
Moves to likely player positions, calling out |
4_G_Combat — target identified, armed, not searching, within range¶
Entered when the player has a HAS_GUN advisory, is identified, not in a search phase, within ~175 studs (300 if in a car), and a cover position exists nearby. The NPC prioritises taking cover and shooting from behind it.
| State | What it does |
|---|---|
TakeCover |
Finds nearest cover node, moves behind it, fires when line-of-sight is clear |
G_Chase — target identified, not armed/in search, within range¶
The default close-range engagement group when the player doesn't have a gun (or the armed-player conditions for Combat aren't met).
| State | What it does |
|---|---|
RunAfterTarget |
Sprints directly toward the suspect |
TazeTarget |
Fires taser when close enough |
CircleAroundTarget |
Strafes around the suspect to avoid being an easy target |
EjectTargetFromVehicle |
Reaches into the car and removes the player from the seat |
ArrestTarget |
Handcuffs and arrests the suspect once they stop resisting |
State Transition Priority¶
Groups are evaluated in priority order. The first group whose canEnter returns true wins and its BeginState becomes the new active state. Child states within a group use nextStates to declare legal transitions — only those listed can be entered from within the group.
When a group changes, groupState.Exit fires on the old group and groupState.Enter fires on the new one. When only a child state changes within the same group, only the child Enter/Exit run.
Target Detection¶
Target.find on each tick looks at the dispatch crime records to find a wanted player. It also cross-checks whether the NPC has line-of-sight to the target's head using PoliceNPCService:CanSeeTarget, which does a dot-product facing check (>0.3) before raycasting.
If the NPC can see a player carrying a weapon (via tool tag check), it advises HAS_GUN to CentralService, which attaches the advisory to the player's runtime crime record.
Entity Setup¶
Each NPC entity is constructed via the EntityService component system (PoliceNPCs tag). Construct sets up:
- Pathfinding
Pathobject - Weapon table (
Pistol,Taser) with ammo counts and cooldowns - Animations loaded from
ServerStorage.Animations - A billboard health bar that appears when the NPC takes damage
- Ragdoll rig via
RagdollModule - Network ownership locked to the server
HeartbeatUpdate runs every frame but is throttled per-entity to ~100 ms. It also handles the arm/neck aiming rotation toward the target when a weapon is equipped.
Decision Logging¶
DecisionLogger records every state transition and notable detection event with a reason string. The debug UI (NPCDebugController + PoliceNPCService.Client:GetDecisionLog) can pull the last 50 log entries for a specific NPC instance. This is gated behind the Cmdr debug permission system.