Skip to content

NPCKit

NPCKit is helpers, not an AI runtime. The kernel ships syscalls, and chase/cover/flank policy is game rules — so the kit deliberately owns no decision loop. What it owns is a thin spawn/despawn lifecycle and per-npc state (the skill model, senses memory, cooldowns), plus a vocabulary of combat helpers your own scheduled tick composes: acquisition, cone-gated sight, skill-shaped aim, movesets, defensive reactions, pathfinding movement, squad intel, and cover scoring. There was once a built-in behavior runtime with priority lists and behavior factories; it was deleted on purpose. If you want a strategic layer above raw loop logic, that is BehaviorTree — still ticked by you.

The kit splits into three layers:

  1. The kit instance — archetype registry, the active-npc roster, squads, sound emission, world queries (hasLineOfSight, raycast), and cover scoring. kit:update() is the only kit-side stepping function, and you call it: it runs the squad directors and lands due melee windups. It is clock-driven with no task.delay, so specs stay deterministic.
  2. The npc handle — one table per spawned npc carrying Id, Archetype, Difficulty, Target, Alive, an Rng (seeded per npc when the kit has a Seed), and State: the scratch that makes an npc feel like it has a memory. State.Cooldowns, State.HeardPosition/HeardAt/HeardSource, State.LastSeenPosition/LastSeenAt, State.NoticedAt, State.SuppressedUntil, State.Detection, State.Ammo/Reloads/Windups, State.Facing, State.Follower, State.AimDrift.
  3. Your loop. Nothing ticks unless you schedule it. The canonical shape: kernel.Scheduler:every(cadence, think), where think calls kit:update(), walks kit:all(), and composes handle helpers per npc.

Movement and perception delegate to shared systems rather than reimplementing them: hearing runs through Senses (the same ear model hand-rolled agents use), routing runs through a Pathfinding follower per npc, Kind = "Projectile" actions fire the same kernel Projectiles definitions players shoot, and spawned models auto-track in Zones when attached.

local ServerScriptService = game:GetService("ServerScriptService")
local Root = ServerScriptService.ChloeKernelServer
local NPCKit = require(Root.Kits.NPCKit)
local Pathfinding = require(Root.Pathfinding)
return function(kernel: any)
local Paths = Pathfinding.attach(kernel)
local Arena = Pathfinding.grid({
Origin = Vector3.new(-200, 0, -200),
Width = 100,
Height = 100,
CellSize = 4,
})
local Npcs = NPCKit.attach(kernel, {
Pathfinding = Paths,
Grid = Arena,
Sounds = {
Occlusion = "Path",
Topics = { ["Weapon.Fired"] = 60 },
},
})
end
Option Description
Pathfinding A Pathfinding.attach() instance. Required (with Grid) for routed movement and "Path" sound occlusion; without it moveTowards walks straight.
Grid The Pathfinding.grid() the npcs walk on. Also backs grid sightLine vision checks and grid-cell cover scans.
PathMethod Overrides the skill-tuned "ThetaStar"/"AStar" routing choice for every npc.
Projectiles The kernel Projectiles service. Required for Kind = "Projectile" actions — act errors without it.
Zones A Zones instance. Spawned models auto-track for Zone.EntityEntered/Left.
Container Parent for spawned models. Default workspace.
Seed Deterministic Rng derivation: npc n gets Random.new(Seed + n). Omit for nondeterministic spawns.
GetTargets Replaces the whole candidate roster. Default: session players with characters, plus every alive npc whose archetype sets Targetable = true.
Clock Injectable time source. Default os.clock. Everything time-based (cooldowns, windups, memory, detection) reads it.
Sounds.Occlusion "Through" (default) ignores walls, "Blocked" stops sound at walls, "Path" measures distance around obstacles on the grid.
Sounds.Topics { [busTopic] = baseRange } — listed topics auto-emit a sound at the acting entity’s position (first published arg with a resolvable position marks the origin). Npc.* topics work too: { ["Npc.Hitscan"] = 60 } makes npc gunfire alert other npcs.
HasLineOfSight (from, to) -> boolean injectable — replaces both the grid sight line and the world raycast (specs, custom worlds).
Raycast (origin, displacement, excludeModel?) -> RaycastResult? injectable hitscan/sweep query. Default excludes the shooter’s own model.

kit:define(name, config) registers a template; defining the same name twice errors. Per-action and per-reaction Difficulty overrides resolve once at define time, and reaction names sort alphabetically at define time (see Defense).

Field Description
Model Template Model cloned per spawn. Omit for headless rigs and specs — everything except a physical body still works.
Health Sets Humanoid.MaxHealth and Health on the clone.
WalkSpeed Sets Humanoid.WalkSpeed on the clone.
Difficulty Preset name or a custom table. Default "Medium".
Moveset { [actionName] = Action } — see Movesets.
DespawnSeconds Corpse time between Npc.Died and destroy(). Default 3.
Targetable Joins the default target roster so other npcs can hunt this kind. NPC-vs-NPC is opt-in per archetype; self-targeting is always excluded.
Faction Same-faction npcs never target each other. Also read by TeamKit as the npc’s team.
Squad Auto-join this named squad on spawn (shared blackboard + director roles).
Reactions { [reactionName] = Reaction } defensive rolls — see Defense.
GetPosition (npc) -> Vector3 injectable for custom rigs and specs. Default: Model.PrimaryPart.Position, pivot fallback, Home when modelless.
Move (npc, point) injectable movement application. Default Humanoid:MoveTo(point).

Difficulty is nine knobs, not a vibe. Every knob is orthogonal: one body can carry Perfect defense and Novice offense through per-action and per-reaction overrides.

Knob Meaning
AimErrorDegrees Max per-axis deviation from the true firing direction.
ReactionSeconds Delay between noticing a target and acting (canReact).
LeadSkill 0..1 — fraction of the ideal moving-target intercept applied.
CooldownMultiplier Multiplies action cooldowns and reload times.
HearingMultiplier Scales every sound’s range for this npc. Custom-table default 1.
HearingBlurStuds Heard-position error at the edge of range (0 = pinpoint). Custom-table default 8.
PathSkill 0..1 routing quality — high picks any-angle Theta* and repaths sooner, low picks plain A* and keeps stale paths longer. Custom-table default 1.
DefenseSkill 0..1 chance a defensive Reaction succeeds. Custom-table default 0.45.
FieldOfViewDegrees Horizontal vision cone centered on facing; canSee fails outside it. Hearing stays omnidirectional. Custom-table default 120.

The seven presets in NPCKit.Difficulties:

Knob Perfect HumanPeak ReallyGood Good Medium Novice Bad
AimErrorDegrees 0 0.8 2 4 7 12 20
ReactionSeconds 0 0.12 0.18 0.3 0.5 0.8 1.2
LeadSkill 1 0.95 0.85 0.7 0.5 0.25 0
CooldownMultiplier 1 1 1.1 1.25 1.5 2 3
HearingMultiplier 1.5 1.25 1.1 1 0.9 0.75 0.6
HearingBlurStuds 0 2 4 7 12 20 32
PathSkill 1 0.95 0.85 0.7 0.55 0.35 0.2
DefenseSkill 0.95 0.85 0.75 0.6 0.45 0.25 0.1
FieldOfViewDegrees 360 270 240 200 160 130 100

Perfect is an aimbot with all-round vision; Bad can’t hit a barn and sees 100 degrees in front. Custom tables slot anywhere presets are accepted — archetype Difficulty, per-spawn overrides, per-action and per-reaction Difficulty, and npc:setDifficulty(nameOrTable) for live mid-fight swaps. An unknown preset name errors with the valid list.

Npcs:define("Guard", {
Model = game:GetService("ServerStorage").NPC.Guard,
Health = 120,
WalkSpeed = 14,
Difficulty = "Good",
Moveset = {
Rifle = { Kind = "Hitscan", Range = 180, Damage = 12, Cooldown = 0.6, Magazine = 6, ReloadSeconds = 2.2 },
},
})
local Guard = Npcs:spawn("Guard", CFrame.new(10, 5, -40))
local Veteran = Npcs:spawn("Guard", CFrame.new(14, 5, -40), { Difficulty = "HumanPeak" })

spawn(archetypeName, at, overrides?) takes a CFrame or Vector3 and per-spawn overrides for Difficulty and Squad. With a Model, the clone is named {archetype}{id}, pivoted to at, gets WalkSpeed/Health applied, and is parented to Container. Two Humanoid connections wire automatically:

  • Humanoid.Died → the kit publishes Npc.Died and schedules npc:destroy() after DespawnSeconds (default 3).
  • Humanoid.HealthChanged → any health drop calls npc:notifyDamage(nil), so external damage that bypassed the kit still suppresses and alerts the npc (the attacker is unknown at this layer; kit actions pass it directly).

The model registers in kit.ByModel (how ray victims resolve back to handles for automatic defenses) and, with Zones attached, tracks for Zone.EntityEntered/Left. Npc.Spawned publishes last. npc:destroy() is idempotent: it untracks zones, disconnects everything, leaves the squad, deregisters, and destroys the model.

kit:targets() returns the default candidate roster — players with characters plus Targetable npcs — unless GetTargets replaced it. kit:all() returns alive npcs in id order (the roster your loop walks); kit:count() counts active handles; kit:destroy() tears down every npc and sound subscription.

The complete pattern — acquire on sight, investigate noise, chase, square up, attack:

kernel.Scheduler:every(0.15, function()
Npcs:update() -- squad directors + due melee windups
for _, Npc in Npcs:all() do
Npc:updateTarget(80, 120, { RequireSight = true, MemorySeconds = 3 })
local Target = Npc.Target
if Target == nil then
if Npc.State.HeardPosition then
Npc:moveTowards(Npc.State.HeardPosition) -- investigate the noise
end
continue
end
local Position = Npc:targetPosition(Target)
if Position == nil then
continue
end
if (Position - Npc:position()).Magnitude > 30 then
Npc:moveTowards(Position)
else
Npc:faceTowards(Position)
if Npc:canReact() then
Npc:act("Rifle", Target)
end
end
end
end, kernel.Priority.Low)

Targets are shape-agnostic throughout: Vector3, Player, character or npc Model, BasePart, another npc handle, a session (resolved via .Player), or any table carrying Position. npc:targetPosition(target) and npc:targetVelocity(target) do the resolution.

npc:aimAt(targetPosition, targetVelocity?, projectileSpeed?, gravity?, skill?) returns a unit direction through the full skill model:

  1. Lead — the exact intercept solve (|delta + v*t| = s*t), not a distance/speed estimate; an insolvable intercept (target outrunning the shot) falls back to the estimate. The solved lead time is scaled by LeadSkill, so Bad (0) never leads and Medium (0.5) trails sprinting targets.
  2. Ballistic arc — with gravity > 0, a low-arc launch solution compensates the drop; out-of-range shots lob at the 45-degree max-range angle.
  3. Error — correlated drift plus near-Gaussian jitter (a two-sample average, so it stays bounded), blended 60/40 and scaled by AimErrorDegrees. Drift persists between shots through an exponential moving average, so bursts walk like a hand instead of bracketing the target statistically. Time-on-target tightens the cone (down to 0.6× after two seconds on State.NoticedAt); suppression blooms it 1.5× while State.SuppressedUntil holds. Error stays within AimErrorDegrees per axis except when suppressed.

act calls aimAt for you with the action’s parameters; call it directly when a Custom delivery needs a direction on your own terms.

An action is a named entry in the archetype’s Moveset, executed with npc:act(name, target). act returns false when gated — cooldown, mid-reload, out of range, no target position, or lane discipline — and gating never burns the cooldown. Acceptance consumes the cooldown window, one magazine round, and publishes Npc.Action(npc, name, target). Unknown action names error.

Field Applies to Description
Kind all "Projectile" | "Hitscan" | "Melee" | "Custom". Anything else errors at execution.
Cooldown all Seconds, scaled by CooldownMultiplier. Default 1.
Difficulty all Per-action skill override (preset name or table), resolved at define time. Governs this action’s aim error, lead, arc, and cooldown pace.
DefId Projectile Kernel Projectiles definition id to fire.
Speed Projectile, Custom Projectile speed for target leading.
Gravity Projectile, Custom Enables the ballistic aim solve — match the projectile def’s Gravity.
OriginHeight Projectile, Hitscan Muzzle offset above npc:position(). Default 2.
Range Melee, Hitscan Melee reach (default 6); also caps hitscan distance (default 200).
Damage Melee, Hitscan Humanoid:TakeDamage amount, applied only when no OnHit is set.
OnHit Melee, Hitscan (npc, target) — replaces the default damage. Hitscan passes the victim model, or the raw ray result when nothing (or a deflecting npc) was hit.
Run Custom (npc, target, direction?) — required for Custom. direction arrives pre-aimed through the skill model (nil with no target).
Magazine any Shots before a forced reload pause. nil = no reload.
ReloadSeconds any Reload length, scaled by CooldownMultiplier. Default 2.
WindupSeconds Melee Telegraph: damage lands this long after the swing starts, and only if the victim is still in reach.
FriendlyFire Projectile, Hitscan true opts this ranged action out of squad lane discipline.
  • Projectile fires the shared kernel Projectiles service with the npc handle as the owner session — one server code path, two kinds of trigger finger. Projectile.Hit and the def’s OnHit receive the handle exactly like a player’s session. Requires Projectiles at attach.
  • Hitscan resolves instantly through kit:raycast — aim error only, no lead, no arc. Victims resolve through nested prop models to the Humanoid-bearing ancestor (a hit on a held tool’s model attributes to the character). An npc victim rolls its defensive reactions first; a successful deflect records no victim and deals no damage. Npc.Hitscan(npc, origin, hitPosition, victim?) publishes hit or miss — the tracer feed.
  • Melee gates on Range, then either lands instantly or arms a windup. Windup landings resolve in kit:update() on the kit clock: reach is re-checked at Range × 1.25 (a small grace, so mid-windup dodges escape), the victim’s reactions roll, then Damage/OnHit applies. Npc.Windup(npc, name, target, seconds) publishes at swing start — the telegraph feed.
  • Custom runs your function with a pre-aimed direction — point it at whatever server function already backs the player intent (WeaponKit or SpellKit handlers), and npcs get the same delivery with skill-shaped aim.

With Magazine = n, the n-th accepted shot empties the magazine and arms a reload: Npc.Reload(npc, name, seconds) publishes and act returns false until ReloadSeconds × CooldownMultiplier elapses — the pause players push into.

Ranged actions (Projectile/Hitscan) fired by a squad member hold fire when a squadmate stands inside the firing lane — alive, between origin and target, within 3 studs of the lane axis. The gated attempt returns false without burning cooldown or ammo. FriendlyFire = true opts a single action out (panic fire, explosives you want reckless).

kit:emitSound(position, { Range?, Loudness?, Source? }) broadcasts a sound (default Range 40; Loudness multiplies it) and every alive npc rolls its own hearing — range scaled by HearingMultiplier, heard position blurred by up to HearingBlurStuds at the edge of range. The ear model itself is the standalone Senses module (Senses.hear), so kit npcs and hand-rolled agents share one set of ears. Occlusion resolves kit-side first:

  • Through (default) — straight-line range check, walls ignored.
  • Blocked — no line of sight from listener to origin kills the sound.
  • Path — distance measured around obstacles: one Pathfinding:distanceField flood per sound hands every listener its route length (a gunshot near fifty listeners costs one flood, not fifty searches). No route — or an origin off-grid or inside a wall — means not heard. An injected finder without distanceField falls back to one A* route per listener.

A heard sound writes State.HeardPosition (pre-blurred), State.HeardAt, State.HeardSource, clears the State.SearchUntil/State.SearchGoal scratch fields (yours to use — a fresh sound restarts your hunt at the new spot), reports to the squad when the source is known, and publishes Npc.Heard. An npc always ignores its own sounds, so listing Npc.Hitscan in Sounds.Topics safely makes npc gunfire alert everyone else.

Sounds.Topics turns bus traffic into noise with zero wiring: each listed topic emits at the first published argument that resolves to a position, with the listed value as base range.

npc:canSee(target, maxRange?, fovOverride?) gates in cost order: range, then the FieldOfViewDegrees cone against the rig’s actual facing (a cheap dot compare — the raycast only runs for things in frame), then line of sight. npc:facing() is the PrimaryPart look vector, falling back to State.Facing (written by movement, faceTowards, or directly on headless rigs); an unknown facing leaves the cone open. Line of sight is kit:hasLineOfSight — the injected function, else the grid’s sightLine when both points land on the grid (vision crosses valleys that block feet), else a world raycast that ignores npc bodies (it excludes Container).

npc:sweep(direction, arcDegrees?, rayCount?, range?) slices the pie: a horizontal fan of short raycasts (defaults 90 degrees, 5 rays, 24 studs) returning the first Humanoid model touched. Fan the unseen angle before the body commits past a corner.

npc:updateTarget(acquireRange, giveUpRange, options?) is the acquisition helper. Options:

Option Description
RequireSight Acquisition (and continued tracking) needs canSee. Default false.
MemorySeconds How long a briefly-occluded target stays tracked. Default 3.
DetectionSeconds Enables graded awareness instead of an instant lock.

The mechanics, in order:

  • Keep the current target inside giveUpRange. Without RequireSight that is the whole check. With it, a seen target refreshes State.LastSeenPosition/LastSeenAt and reports to the squad; a target unseen for more than 0.35s that reappears re-imposes half the reaction time (State.NoticedAt backdated by half of ReactionSeconds) — a reappearance is a fresh stimulus, not a free instant re-engage.
  • Track through brief occlusion for MemorySeconds. Past that, the target drops and the last seen position lands in the hearing memory (State.HeardPosition) — your loop hunts the spot like a soldier instead of pathing omnisciently through walls.
  • Acquire the nearest candidate in acquireRange from kit:targets(), skipping itself, its own model, and faction-mates; RequireSight gates candidates by the cone-and-raycast check. Acquisition stamps State.NoticedAt (the reaction clock) and reports to the squad.
  • Detection, when DetectionSeconds is set: sight charges an accumulator instead of locking. Charge rate scales with proximity (0.5×–1.5× across the acquire range) and target movement (1.5× against anything moving faster than 8 studs/s), and decays at the base rate while nothing is visible. Crossing half charge publishes Npc.Suspicious(npc, target, glimpsedPosition) and points the hearing memory at the glimpse; full charge acquires.

npc:canReact() answers the reaction gate: true once ReactionSeconds have elapsed since State.NoticedAt (immediately for zero-reaction skills with no notice stamp).

Pain registers. npc:notifyDamage(attacker?) suppresses aim for 1.5 seconds (State.SuppressedUntil — read it in your loop to duck an exposed peek) and, when the npc has no target, drops the attacker’s position into the hearing memory: shooting a guard in the back turns it around. Kit Hitscan/Melee actions call it automatically on npc victims (even when the hit was deflected or blocked), Humanoid health drops on spawned rigs call it with an unknown attacker, and your own damage code should call it too. Publishes Npc.Damaged(npc, attacker?).

npc:moveTowards(goal) is one movement step on a per-npc Pathfinding follower — call it every tick you want the npc moving; it is not fire-and-forget. With Pathfinding and Grid attached:

  • MethodPathSkill >= 0.7 picks any-angle "ThetaStar", below it plain "AStar" (staircase routes); the kit-wide PathMethod overrides both.
  • Repath eagernessRepathDistance = CellSize × (1 + (1 − PathSkill) × 4): high skill repaths on a 1-cell goal drift, Bad clings to a stale path until the goal has drifted 4+ cells.
  • Fresh routes publish Npc.Path(npc, waypoints) — the full route with the npc’s own position first, for visualization subscribers.
  • Follower Stuck/Jump hints set Humanoid.Jump — the unstick hop and off-mesh link traversal.
  • The npc inherits the follower’s robustness for free: failed-search backoff, Grid.Version invalidation when the map refreshes, and the 1-second stuck watchdog.

Without a grid the npc walks straight at the goal. The horizontal step direction writes State.Facing, keeping the vision cone honest on headless rigs. The final point applies through the archetype’s Move injectable or Humanoid:MoveTo.

Reactions are the archetype’s defensive vocabulary — parries, deflection wards, counter-spells:

Npcs:define("Duelist", {
Difficulty = "Good",
Reactions = {
Deflect = {
Against = { "Hitscan", "Spell" }, -- attack Types this answers; nil = anything
Cooldown = 0.8, -- seconds between attempts, success or failure. Default 1
Difficulty = "HumanPeak", -- per-reaction skill; falls back to the npc's
Run = function(Npc, Context) -- runs on a successful defense only
-- counter VFX, riposte, reflected bolt
end,
},
},
})

npc:defend(context) rolls the gauntlet and returns true when the attack is blocked. Call it before your own damage lands:

if not TargetNpc:defend({ Type = "Spell", Attacker = CasterSession }) then
ApplyDamage(TargetNpc)
end

The rules, exactly:

  • Reactions are checked in name-sorted order (resolved at define time). Hash order would make which reaction answers nondeterministic and break seeded replays.
  • A reaction matches when Against is nil or contains context.Type. Non-matching and cooling-down reactions are skipped.
  • The first matching, ready reaction attempts: the attempt consumes its cooldown (Cooldown × CooldownMultiplier) whether it succeeds or fails, success rolls DefenseSkill, and Npc.Defense(npc, name, success, context) publishes either way — deflect flashes and fumbles both get VFX hooks.
  • One attempt per attack. A failed roll returns false immediately; later reactions never pile on.

Kit Hitscan and Melee actions roll the victim’s reactions automatically when the victim is an npc. MeleeKit closes the loop from the other direction: a combatant with a defend function and no armed manual parry rolls defend({ Type = "Melee", Attacker, Action }) when a swing’s hit frame reaches it, and success counts as a full parry (attacker staggered) — difficulty-driven npc parries with zero extra wiring.

Give archetypes Faction (faction-mates never target each other — even a polluted custom roster is filtered) and Squad (auto-join on spawn; or kit:squad(name):add(npc) / squad:remove(npc) manually).

The blackboard is honest. Members report what their senses actually produced — sight fixes exact, heard positions blurred by the listener’s skill — via squad:report(reporter, target, position, velocity?); updateTarget and _hear report automatically. squad:lastKnown(target) returns the newest entry ({ Position, Velocity?, At, Reporter }); npc:lastIntel(target) compares the npc’s own last sighting against squad reports and returns the fresher — never the live position. Blackboard intel expires after 30 seconds, so departed players are not retained and dead men stop being suppressed.

The director fields a real fireteam. Each kit:update(), per engaged target: the closest member holds "Pressure", and the rest alternate "Flank" and "Suppress". Flankers split sides — Squad.FlankSigns[npc] is 1 or -1, alternating, so the pincer closes from both directions. Role changes publish Npc.Tactic(npc, role) for client animation polish; roles clear on disengage. Your loop reads squad:role(npc) and acts it out: pour npc:act at npc:lastIntel(target).Position to pin, curve to an enfilade point off the pressure axis to flank.

Cover is scoring only — your loop decides when to hide, hold, peek, and relocate.

kit:findCover(npc, threat, { SearchRadius?, SpotTag?, BackAway? }) returns the nearest position the threat cannot see, or nil when nothing breaks line of sight. Candidates come from parts tagged SpotTag within SearchRadius (default 40), or from grid cells when no tag is given. Scoring: distance from the npc, +SearchRadius when a squadmate already camps within 6 studs (spread out, don’t stack), +2 × SearchRadius for spots closer to the threat when BackAway is set (fighters that give ground). A tagged node with a Direction attribute — the facing of the wall protecting it — only counts when the threat is on its covered side (dot ≥ 0.2 against the threat axis).

kit:findPeekPoint(spot, threat) finds the corner beside a cover spot: the nearest lateral offset (perpendicular to the threat axis, at 1–2 grid cells or 3-stud steps) that is walkable and has eye-level line of sight to the threat. Peeking means leaning to that, not strolling at the enemy until sight happens.

When ad-hoc loop logic stops being enough (“am I suppressed? is a squadmate distracting them? was that corner cleared?”), compose the same helpers under a BehaviorTree: build one tree per npc, use the npc handle (or a table wrapping it) as the blackboard, and tick from your loop with the kit clock — BehaviorTree.tick(Tree, Npc, Dt, Npcs.Clock) — so Cooldown nodes stay deterministic under spec. Call BehaviorTree.reset(Tree) when the target changes. The kit never ticks trees itself.

TeamKit resolves an npc handle’s team through Archetype.Faction, so a Faction = "Raiders" npc and a Raiders-team player read as teammates in every friendly-fire veto with zero extra wiring. Inside NPCKit, Faction additionally filters target acquisition between npcs. Use the same string for both and the whole combat surface agrees on who is friendly.

Member Description
NPCKit.attach(kernel, options?) → kit Construct and wire sound-topic subscriptions. Starts no loop.
NPCKit.Difficulties The preset table (PerfectBad).
kit:define(name, config) Register an archetype. Duplicate names error.
kit:spawn(archetypeName, at, overrides?) → npc Spawn at a CFrame/Vector3. Overrides: Difficulty, Squad. Publishes Npc.Spawned.
kit:all() → { npc } Alive npcs in id order.
kit:count() → number Active handle count (includes corpses awaiting despawn).
kit:targets() → { any } The candidate roster (players + Targetable npcs, or GetTargets).
kit:update() Step squad directors and land due melee windups. Call from your tick.
kit:squad(name) → squad Get-or-create a named squad.
kit:emitSound(position, { Range?, Loudness?, Source? }?) Broadcast a sound; every alive npc rolls its own hearing.
kit:hasLineOfSight(from, to) → boolean Injected fn → grid sightLine → world raycast excluding Container.
kit:raycast(origin, displacement, excludeModel?) → RaycastResult? The hitscan/sweep world query.
kit:findCover(npc, threat, options?) → Vector3? Nearest threat-blind position, or nil.
kit:findPeekPoint(spot, threat) → Vector3? The corner beside a cover spot with eyes on the threat.
kit:destroy() Destroy every npc, squad, and sound subscription.
Member Description
npc:position() → Vector3 GetPosition injectable → PrimaryPart/pivot → Home.
npc:targetPosition(target) → Vector3? Resolve any target shape to a position.
npc:targetVelocity(target) → Vector3? AssemblyLinearVelocity for characters, .Velocity for tables.
npc:aimAt(position, velocity?, speed?, gravity?, skill?) → Vector3 Unit direction through the full skill model.
npc:act(name, target) → boolean Execute a moveset action. false = gated, nothing consumed.
npc:defend(context) → boolean Roll defensive reactions; true = blocked.
npc:notifyDamage(attacker?) Register a hit: suppression + hearing memory + Npc.Damaged.
npc:updateTarget(acquireRange, giveUpRange, options?) Acquisition, sight memory, graded detection.
npc:canReact() → boolean Reaction-time gate since State.NoticedAt.
npc:canSee(target, maxRange?, fov?) → boolean Range → vision cone → line of sight.
npc:facing() → Vector3? Rig orientation, else State.Facing; nil leaves the cone open.
npc:faceTowards(point) Body tracking while standing still; pivots the rig and writes State.Facing.
npc:sweep(direction, arc?, rays?, range?) → Model? Raycast fan; first Humanoid model touched. Defaults 90°, 5 rays, 24 studs.
npc:lastIntel(target) → { Position, At }? Freshest of own sighting vs squad report. Never the live position.
npc:moveTowards(goal) One movement step on the pathfinding follower.
npc:setDifficulty(nameOrTable) Live skill swap.
npc:destroy() Idempotent teardown.
npc.Id, npc.Name, npc.Archetype, npc.Difficulty, npc.Target, npc.State, npc.Alive, npc.Model, npc.Humanoid, npc.Squad, npc.Home, npc.Rng Handle fields. State is the per-npc memory scratch.
Member Description
squad:add(npc) / squad:remove(npc) Membership. Archetype Squad auto-joins on spawn.
squad:report(reporter, target, position, velocity?) Write senses-produced intel. Newer overwrites older per target.
squad:lastKnown(target) → entry? { Position, Velocity?, At, Reporter }; entries expire after 30s.
squad:role(npc) → string? "Pressure" | "Flank" | "Suppress", or nil when disengaged.
squad.FlankSigns[npc] 1 or -1 — which side this flanker takes.

NPCKit defines no hook points. Bus topics published:

Topic Args When
Npc.Spawned npc After spawn wiring completes.
Npc.Died npc Humanoid death; precedes the DespawnSeconds delay.
Npc.Action npc, actionName, target A moveset action executed (not gated attempts).
Npc.Heard npc, heardPosition, source? A sound survived range and occlusion; position pre-blurred by skill.
Npc.Suspicious npc, target, glimpsedPosition Graded detection crossed half charge.
Npc.Damaged npc, attacker? notifyDamage — kit hits, Humanoid drops, or your call.
Npc.Defense npc, reactionName, success, context A defensive reaction attempt — deflects and fumbles.
Npc.Reload npc, actionName, seconds A magazine emptied; reload armed.
Npc.Windup npc, actionName, target, seconds A telegraphed melee swing started.
Npc.Path npc, waypoints A fresh route was computed (full list, own position first).
Npc.Hitscan npc, origin, hitPosition, victim? A hitscan action fired, hit or miss — the tracer feed.
Npc.Tactic npc, role Director role flip: Pressure/Flank/Suppress.

Topics consumed: everything listed in Sounds.Topics (as sound sources). With Zones attached, spawned models additionally fire Zone.EntityEntered/Zone.EntityLeft through that system.