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.
Mental model
Section titled “Mental model”The kit splits into three layers:
- 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 notask.delay, so specs stay deterministic. - The npc handle — one table per spawned npc carrying
Id,Archetype,Difficulty,Target,Alive, anRng(seeded per npc when the kit has aSeed), andState: 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. - Your loop. Nothing ticks unless you schedule it. The canonical shape:
kernel.Scheduler:every(cadence, think), wherethinkcallskit:update(), walkskit: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.
Attaching
Section titled “Attaching”local ServerScriptService = game:GetService("ServerScriptService")
local Root = ServerScriptService.ChloeKernelServerlocal 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 }, }, })endAttach options
Section titled “Attach options”| 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. |
Archetypes
Section titled “Archetypes”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). |
The skill model
Section titled “The skill model”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.
Spawning and lifecycle
Section titled “Spawning and lifecycle”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 publishesNpc.Diedand schedulesnpc:destroy()afterDespawnSeconds(default 3).Humanoid.HealthChanged→ any health drop callsnpc: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 game-owned tick
Section titled “The game-owned tick”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 endend, 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.
Aiming
Section titled “Aiming”npc:aimAt(targetPosition, targetVelocity?, projectileSpeed?, gravity?, skill?) returns a unit direction through the full skill model:
- 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 byLeadSkill, soBad(0) never leads andMedium(0.5) trails sprinting targets. - 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. - 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 onState.NoticedAt); suppression blooms it 1.5× whileState.SuppressedUntilholds. Error stays withinAimErrorDegreesper 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.
Movesets
Section titled “Movesets”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. |
The four kinds
Section titled “The four kinds”Projectilefires the shared kernel Projectiles service with the npc handle as the owner session — one server code path, two kinds of trigger finger.Projectile.Hitand the def’sOnHitreceive the handle exactly like a player’s session. RequiresProjectilesat attach.Hitscanresolves instantly throughkit: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.Meleegates onRange, then either lands instantly or arms a windup. Windup landings resolve inkit:update()on the kit clock: reach is re-checked atRange × 1.25(a small grace, so mid-windup dodges escape), the victim’s reactions roll, thenDamage/OnHitapplies.Npc.Windup(npc, name, target, seconds)publishes at swing start — the telegraph feed.Customruns 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.
Magazines, and lane discipline
Section titled “Magazines, and lane discipline”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).
Senses
Section titled “Senses”Hearing
Section titled “Hearing”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: onePathfinding:distanceFieldflood 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 withoutdistanceFieldfalls 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.
Vision
Section titled “Vision”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.
Targeting and memory
Section titled “Targeting and memory”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. WithoutRequireSightthat is the whole check. With it, a seen target refreshesState.LastSeenPosition/LastSeenAtand reports to the squad; a target unseen for more than 0.35s that reappears re-imposes half the reaction time (State.NoticedAtbackdated by half ofReactionSeconds) — 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
acquireRangefromkit:targets(), skipping itself, its own model, and faction-mates;RequireSightgates candidates by the cone-and-raycast check. Acquisition stampsState.NoticedAt(the reaction clock) and reports to the squad. - Detection, when
DetectionSecondsis 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 publishesNpc.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?).
Movement
Section titled “Movement”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:
- Method —
PathSkill >= 0.7picks any-angle"ThetaStar", below it plain"AStar"(staircase routes); the kit-widePathMethodoverrides both. - Repath eagerness —
RepathDistance = CellSize × (1 + (1 − PathSkill) × 4): high skill repaths on a 1-cell goal drift,Badclings 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/Jumphints setHumanoid.Jump— the unstick hop and off-mesh link traversal. - The npc inherits the follower’s robustness for free: failed-search backoff,
Grid.Versioninvalidation 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.
Defense: Reactions and defend()
Section titled “Defense: Reactions and defend()”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)endThe 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
Againstis nil or containscontext.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 rollsDefenseSkill, andNpc.Defense(npc, name, success, context)publishes either way — deflect flashes and fumbles both get VFX hooks. - One attempt per attack. A failed roll returns
falseimmediately; 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.
Squads
Section titled “Squads”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 helpers
Section titled “Cover helpers”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.
Brains: BehaviorTree
Section titled “Brains: BehaviorTree”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.
Teams and factions
Section titled “Teams and factions”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.
API reference
Section titled “API reference”| Member | Description |
|---|---|
NPCKit.attach(kernel, options?) → kit |
Construct and wire sound-topic subscriptions. Starts no loop. |
NPCKit.Difficulties |
The preset table (Perfect … Bad). |
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. |
Npc handle
Section titled “Npc handle”| 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. |
Hooks & bus topics
Section titled “Hooks & bus topics”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.