Types
The contracts VluxyAI is built from. You extend the package by passing in your own module that satisfies one of these types; the package never learns your module exists.
The important ones:
- [Types.Brain]: a table of states. The part you write for every AI.
- [Types.Pathfinder]: anything with
FindPath. Plans, never moves. - [Types.Mover]: anything with
FollowandStop. Moves, never plans. - [Types.Sense]: anything with
Tickthat writes to the Types.Blackboard. -
[Types.AgentDefinition]: the composition of the above, which
Build()validates and turns into an Agent.
Types
Connection
interface Connection {Connected: boolean--
false once disconnected.
}What Types.Signal Connect returns. Any cleanup helper that calls Disconnect accepts it.
Signal<T...>
interface Signal<T...> {}
A small signal with the same surface as RBXScriptSignal, implemented in plain Luau so the
core runs outside Roblox. Movers and agents expose these; a Cleaner releases their connections.
NoiseSignal
What a Hearing sense listens to: fired with a world position, an optional loudness (1 by
default), an optional kind ("Footstep", "Door") and an optional source, the Instance that
made the noise (a player's character, a rig), so a brain can tell whose noise it was.
StepAction
type StepAction = "Walk" | "Jump" | "Custom"
What a Types.Mover does at a step. Mirrors Enum.PathWaypointAction. Custom steps come
from PathfindingModifier labels or your own pathfinder; the mover walks to them like any other
step and the Types.Navigator's StepReached tells you when it got there, so a door or a ladder
is your code.
Step
interface Step {Label: string?--
The PathfindingModifier label, or whatever your pathfinder attaches. A Types.StepHandler registered for it runs when the step is reached.
}One point along a Types.Path.
StepHandler
What happens at a labelled step: open the door, climb the ladder, play the vault. Registered
with Builder:OnStep under the step's Label. Runs in its own thread and may yield; the
navigator holds the mover at the step, reports Interacting, and walks the rest of the path
when the handler returns. A new target or Stop while it runs abandons the path; the handler
should check Agent:IsDestroyed after a long yield.
Return false to say the step could not be taken (a locked door). The path is given up as
Failed, and the pathfinder is told to Close that step for the seconds given as the second
return (default 10), so the next plan goes another way. Returning nothing or true means
the step was taken.
StepRegistration
interface StepRegistration {Handler: StepHandlerTag: string?--
When the step arrives without an Instance (a navmesh link or modifier), the nearest instance carrying this CollectionService tag within Radius of the step is put in step.Instance before the handler runs.
Radius: number?--
Studs. Default 8.
}What Builder:OnStep stores for a label: the handler, how to find the door for a step that does not already know it, and how to tell that there is nothing to do. A plain Types.StepHandler is accepted wherever this is.
DoorDriver
interface DoorDriver {IsOpen: (self: any,door: Instance) → boolean--
Open, or opening: an agent reaching it walks on without stopping.
Open: (self: any,agent: Agent) → ()--
Start opening it. Must not yield; the agent waits OpenTime after it.
IsLocked: ((self: any,agent: Agent) → boolean)?--
Optional. A locked door is refused and planned around.
OpenTime: ((self: any,door: Instance) → number)?--
Optional. Seconds the door takes to open, which the agent waits after Open before walking through. Default: Doors.Step's OpenTime.
}How Doors opens a door in your game. The package never moves a door itself: your door script does, and the driver is the few lines that connect the two. Doors.Attributes is a ready-made driver that talks through attributes, for a door script that listens to them.
Path
An ordered list of steps from where the agent was to where it wants to be. The first step is the agent's own position when the pathfinder includes it; movers cope either way.
Pathfinder
interface Pathfinder {Destroy: ((self: any) → ())?--
Optional cleanup. Called when the agent is destroyed.
}
Plans and nothing else. A pathfinder never moves the agent, never creates parts and never keeps
a target. from and to may be at any height: from is the mover's root and to is often a
player's root or a floor node; the path it returns holds floor positions. An empty path counts
as a failure. FindPath runs in its own thread and may yield or error; an error is treated as a
failed plan and warned about once. VluxyAI.Pathfinders ships Navmesh, Straight and
Ladder; your own is any table with FindPath.
Blocked is how a pathfinder that watches the world (Navmesh listens to its Path.Blocked)
tells the navigator that the path being walked is no longer good, so the agent re-plans as soon
as the cadence allows instead of walking into the obstacle and waiting for the mover's stuck
check. A pathfinder that cannot know leaves it out; Ladder and Hybrid forward it from the
rung that produced the current path.
Mover
interface Mover {Stop: (self: any) → ()--
Halts. Fires Arrived(false) if a path was in progress.
GetPosition: (self: any) → Vector3--
Where the agent's root or pivot is right now: the body, not the floor.
GetSpeed: (self: any) → number--
Studs per second.
SetSpeed: (self: any,speed: number) → ()--
Studs per second.
IsMoving: (self: any) → boolean--
true while a path is in progress.
Arrived: Signal<boolean>--
Fired once per Follow: true at the end of the path, false if stopped, replaced or stuck.
Destroy: ((self: any) → ())?--
Optional cleanup. Called when the agent is destroyed.
Face: ((self: any,position: Vector3?) → ())?--
Optional. Turn toward position while standing still; nil stops. Agent:LookAt calls it.
Teleport: ((self: any,position: Vector3) → ())?--
Optional. Put the body at position at once, dropping any path. Agent:Teleport calls it.
SetNudge: ((self: any,offset: Vector3?) → ())?--
Optional. Aim at each step plus offset (never the last step), so a steering sense can push the body sideways; nil clears. Neighbours calls it.
GetPivot: ((self: any) → CFrame)?--
Optional. Position and facing. Required for an agent with no Entity: Agent:GetPivot reads it.
Step: ((self: any,deltaTime: number) → ())?--
Optional. A mover with no clock of its own: Agent:Step calls it first, with the same delta time. Leave it out on a mover that runs on Heartbeat, or it moves twice.
}Executes a Types.Path and nothing else. Rules that keep a mover and the Navigator honest:
-
FollowfiresArrived(false)for the path it replaces before starting the new one, and the new path's outcome is reported on a later frame, never from insideFollow. An empty path isArrived(false)on the next frame. Followis called from the navigator's planning thread, so it must not yield.-
Arrived(true)means the last step was reached. The navigator then checks the target for itself: withinArriveDistanceit reports arrival, otherwise it re-plans or reports failure. Arrived(false)from a stuck detector makes the navigator re-plan on its cadence.
VluxyAI.Movers ships Humanoid (uses MoveTo), CFrame (steps a pivot, no rig needed) and
Record (a pivot in a table, no Instance at all);
your own is any table with this shape. Make the signals with VluxyAI.Signal.new().
Blackboard
type Blackboard = {[string]: any}The only thing states read about the world. Senses write to it every tick; states and utility considerations read it and never query the world themselves. Each sense documents the keys it writes. Declare your own blackboard type per AI and cast once at the boundary.
Sense
interface Sense {Tick: (self: any,deltaTime: number) → ()--
Called once per agent tick. Writes to agent.Blackboard.
Destroy: ((self: any,agent: Agent?) → ())?--
Optional cleanup, called with each agent that is destroyed. A shared sense releases that agent's state and tears down only when none remain; called with no agent it tears down outright.
Configure: ((self: any,patch: {[string]: any}) → ())?--
Optional. Changes options after construction; every built-in has it, so a director can widen a cone or shorten a memory mid-round.
}
Perceives and nothing else. Senses tick in the order they were added, before the navigator and
the brain. A sense keeps no per-agent state of its own: memory (SeenAt, HeardAt) lives on
the blackboard, so one instance may serve many agents and a state may clear a memory early by
setting the key to nil. Distances are math.huge when there is nothing to measure.
VluxyAI.Senses ships Sight, Proximity, Hearing, Sounds, Awareness, Watched,
Surroundings and Neighbours; your own is any table with Tick.
Transition
interface Transition {To: string--
The state to enter. Must exist in the brain; Build() checks.
}
A declarative transition. The first one whose When returns true wins. To may name the
current state, which re-enters it (Exit then Enter); an Update returning the current name
does nothing. If When forces a change through Agent:SetState, that change wins and its own
result is ignored.
Interrupt
interface Interrupt {To: string--
The state to enter. Must exist in the brain; Build() checks.
When: (agent: Agent) → boolean--
Checked every tick, in order, before the current state's own transitions.
Except: {string}?--
States in which this interrupt is not checked, such as the one it leads to or an attack that must finish.
}A Types.Transition that applies from every state: "hunt whoever I see", "freeze when lit", "stop when the director says so". Written once on the brain instead of in every state. Interrupts are checked in order before the current state's transitions; the first true one wins the tick.
State
interface State {Update: ((deltaTime: number) → string?)?--
Runs every tick. Return a state name to change state, or nothing to stay.
}
One state of a Types.Brain. Every field is optional, so a state can be as small as a single
Update. Returning the current state's name from Update does nothing; use Agent:SetState to
re-enter a state on purpose. If Update itself forces a change through SetState, its return
value is ignored so two transitions never stack.
Brain
interface Brain {Initial: string--
The state entered on Start().
Interrupts: {Interrupt}?--
Transitions that apply from every state, checked before the current state's own.
}A flat finite state machine as a table. The whole AI, readable top to bottom.
Curve
type Curve = (x: number) → number
Maps a normalised input in [0, 1] to a score in [0, 1]. Utility.Curves ships the common
shapes; any function works.
Consideration<T>
interface Consideration<T> {Input: (candidate: T) → number--
Reads a raw value about the candidate, usually off the blackboard.
Min: number?--
The raw value that maps to 0. Default 0.
Max: number?--
The raw value that maps to 1. Default 1.
Invert: boolean?--
Flip the normalised input before the curve, so a big distance scores low.
}
One factor in scoring a candidate. A candidate's score is the product of its considerations, so
any one of them can veto by returning 0.
Scored<T>
interface Scored<T> {Candidate: T--
The thing that was scored.
Score: number--
Its score in [0, 1].
}One row of a Utility.ScoreAll result.
PathStatus
type PathStatus = "Idle" | "Planning" | "Moving" | "Interacting" | "Arrived" | "Failed"
Where a Navigator is with its current target. Idle means no target or stopped;
Interacting means a Types.StepHandler is running at a step and the walk resumes after.
NavigationOptions
interface NavigationOptions {RepathInterval: number?--
Seconds between re-plans while the target keeps moving. Must be above 0; the three distances below may be 0. Default 0.5.
RepathDistance: number?--
Studs the target must move before a re-plan happens. Default 2.
MaxPathAge: number?--
Seconds a path is followed before it is planned again even though the target has not moved, so the agent keeps up with a changing level. Must be above 0. Default math.huge (never).
ArriveDistance: number?--
Horizontal studs from the target that count as arrived. Must exceed the two bodies' radii for a chase to ever arrive. Default 2.5.
ArriveHeight: number?--
Height difference allowed for arrival, so a target on the floor above does not count. Default 8.
DirectInterval: number?--
With a direct pathfinder (Builder:UseDirect): seconds between checks that the way to the target is still clear. Must be above 0. Default 0.15.
DirectStuckTime: number?--
Seconds walking direct without moving a stud before the navigator gives up on it and plans. Must be above 0. Default 1.
DirectCooldown: number?--
Seconds after giving up on a direct walk before trying one again. Default 3.
Lead: number?--
Seconds of a moving target's velocity to plan ahead of it, so a chase heads where the player is going. 0 turns it off. Default 0.5.
LeadMax: number?--
Studs the lead is capped at. Default 8.
LeadMinDistance: number?--
Within this many studs of the target there is no lead, so an agent at attack range does not overshoot. Default 10.
FailedRetry: number?--
Seconds before a target that failed twice in a row is tried again, doubling for each further failure up to 4, instead of every RepathInterval. A target that moves is tried again at once. Default 1.
}Tunes how Navigator turns repeated MoveTo calls into plans.
Navigator
interface Navigator {MoveTo: (self: any,target: Vector3) → ()--
Head to target. Safe to call every tick; re-plans on a cadence.
Stop: (self: any) → ()--
Forget the target and halt.
IsDirect: (self: any) → boolean--
Whether the agent is walking straight at its target rather than following a plan.
GetRemainingDistance: (self: any) → number--
Studs left along the current path, math.huge when there is none.
}
Owns the pair of a Types.Pathfinder and a [Types.Mover]: plans with one, follows with the
other, and re-plans when the target moves, the pathfinder reports the path blocked, or the path
is older than MaxPathAge. Every agent with both has one at agent.Navigator.
AgentDefinition
interface AgentDefinition {Entity: Model?--
The model the AI drives. Needs a PrimaryPart. Leave it out for a headless agent, whose Mover has GetPivot (Record).
Data: {[string]: any}?--
Initial agent.Data. Copied per agent. Yours to use.
Tick: number?--
Seconds between ticks. Default 0.1.
Steps: {[string]: StepHandler | StepRegistration}?--
What to do at labelled steps, by label. See Builder:OnStep.
Direct: Pathfinder?--
Checks whether the agent can walk straight at its target; while it can, nothing is planned. Must not yield. See Builder:UseDirect.
StrictBlackboard: boolean?--
Warn once when a state reads a blackboard key that looks like a typo of one a sense wrote (SeenTaget). For development; costs a metatable on every read. Default false.
Name: string?--
Used in messages. Default Entity.Name, or "Agent" with no entity.
}
Everything an Agent is built from. The Builder produces one of these; you can also write it
as a table and hand it to VluxyAI.NewAgent. Validate checks it either way.
Agent
interface Agent {Entity: Model--
The model being driven. nil on a headless agent, though typed as a Model so rig code reads simply; library code goes through GetPivot.
Name: string--
For messages.
Data: {[string]: any}--
Free scratch space for your states.
Destroyed: Signal<()>--
Fired once, at the end of Destroy, so anything built around the agent can go with it.
Events: Signal<(string,any)>--
Everything the agent announces through Agent:Emit; a Broadcaster relays it to clients.
MoveTo: (target: Vector3) → ()--
Shorthand for Navigator:MoveTo. Errors without a navigator; warns and does nothing before Start.
SetSpeed: (speed: number,seconds: number?) → ()--
The base speed; the mover gets it times the speed scale. With seconds, ramps there over the ticks. No-op without a mover.
SetSpeedScale: (scale: number) → ()--
A multiplier layered on the base speed, kept across states. 1 is none.
TimerDone: (name: string) → boolean--
true once the timer ran out, or if it was never started.
TimeInState: (self: Agent) → number--
Seconds since the current state was entered, paused time excluded. 0 before Start.
Pause: (self: Agent) → ()--
Freeze: no ticks, no movement, timers on hold. For cutscenes, jumpscares and a director backing an enemy off.
LookAt: (position: Vector3?) → ()--
Turn toward position while standing still, nil to stop. Needs a mover with Face; otherwise does nothing.
Teleport: (position: Vector3) → ()--
Put the body at position at once, dropping the current path. Uses the mover's Teleport, else the entity's PivotTo.
IsNew: (key: string) → boolean--
true while Blackboard[key] is non-nil and differs from the value last passed to Acknowledge.
Acknowledge: (key: string) → ()--
Marks the current Blackboard[key] as dealt with, so IsNew is false until it changes.
Start: (manual: boolean?) → Agent--
Enters the initial state and starts ticking on Heartbeat. With manual, you drive Step yourself.
Emit: (name: string,payload: any?) → ()--
Announces something for clients: a scream, a kill, a door. Fires Events.
}The running AI. Handed to every Enter, Update, Exit, When and Input function.
ClientState
interface ClientState {Enter: ((replica: any) → ())?--
Runs when the server enters the state of that name.
Update: ((replica: any,deltaTime: number) → ())?--
Runs every frame while it is current.
Exit: ((replica: any) → ())?--
Runs when the server leaves it.
}The client's side of one server state: what a Replica does while the enemy is in it. The same shape as a Types.State without transitions, since the server decides those.
Visuals
Client states keyed by server state name. Only the states a client needs to react to are listed; the rest are tracked without doing anything.
Snapshot
interface Snapshot {State: string?--
The current state name.
Yaw: number--
Facing in radians about Y.
}Everything a client that just joined needs, from Broadcaster.Snapshot to Replica.ApplySnapshot.
KillKind
type KillKind = "Live" | "BlackBox" | "Scare"How a Kill is presented: played out in the world, cut to black, or a scare, which is the live hold without the death.
HealthProvider
interface HealthProvider {}
How Attack and Kill read and change a victim's health when the game does not use the
Humanoid for it. Without one, the Humanoid's Health and TakeDamage are used.
KillEvent
interface KillEvent {Kind: KillKindDuration: number--
Seconds until the victim dies (or, for a scare, is released), for the client to match.
}
The payload of the "Kill" event an agent emits, and of the "Scare" event a scare emits.
A "KillCancelled" (or "ScareCancelled") event with { Kind, Victim } follows if it ends
early.