Decorator-first AI layer for OpenCore NPCs.
@open-core/frameworkowns entity lifecycle (NPC,Npcs).@open-core/npc-intelligenceadds controller-driven behavior, skills, planners, and optional LLM decisions.
pnpm add @open-core/npc-intelligenceDefault server entrypoint exports only the core surface:
npcIntelligencePluginIntelligentNpcAPINpcIntelligentController,NpcSkill,OnNpcHook,OnNpcEventskillKey(SkillClass)helper- core types:
NpcContext,SkillResult,NpcIntelligentControllerDefinition - built-in skills:
IdleSkill,MoveToSkill,MoveRelativeSkill,SetHeadingSkill,WaitSkill,LookAtEntitySkill,GoToCarDriveParkSkill
Advanced APIs are in @open-core/npc-intelligence/server/advanced.
import { Server } from '@open-core/framework/server'
import {
npcIntelligencePlugin,
IntelligentNpcAPI,
NpcIntelligentController,
GoToCarDriveParkSkill,
} from '@open-core/npc-intelligence/server'
await Server.init({
mode: 'CORE',
plugins: [npcIntelligencePlugin()],
})
@NpcIntelligentController({
id: 'rule-driver',
planner: 'rule',
skills: [GoToCarDriveParkSkill],
})
class RuleDriverController {
constructor(private readonly npcInt: IntelligentNpcAPI) {}
async assignDriveTask(vehicleNetId: number) {
const npc = await this.npcInt.spawn({
model: 'mp_m_freemode_01',
position: { x: 0, y: 0, z: 72 },
networked: true,
})
await this.npcInt
.rule('rule-driver')
.for(npc)
.name('Valentine Driver')
.npcType('driver')
.do(GoToCarDriveParkSkill, {
vehicleNetId,
dest: { x: 120, y: -760, z: 26 },
})
.run()
}
}Set API key via environment variable:
export OPENROUTER_API_KEY=...import { Server } from '@open-core/framework/server'
import {
npcIntelligencePlugin,
NpcIntelligentController,
GoToCarDriveParkSkill,
MoveToSkill,
} from '@open-core/npc-intelligence/server'
await Server.init({
mode: 'CORE',
plugins: [
npcIntelligencePlugin({
openRouter: {
model: 'openai/gpt-4o-mini',
temperature: 0.2,
maxTokens: 220,
},
debug: {
enabled: process.env.NPC_AI_DEBUG === '1',
runtime: true,
llm: process.env.NPC_AI_DEBUG_LLM === '1',
},
}),
],
})
@NpcIntelligentController({
id: 'ai-driver',
planner: 'ai',
skills: [GoToCarDriveParkSkill, MoveToSkill],
ai: {
model: 'openai/gpt-4o-mini',
temperature: 0.25,
perSkill: {
goToCarDrivePark: {
model: 'openai/gpt-4o',
temperature: 0.1,
},
},
},
})
class AiDriverController {}Use AI builder context (instead of raw setObservation) for readability:
await npcInt
.ai('ai-driver')
.for(npc)
.name('Courier #4')
.npcType('courier')
.goal('deliver-order', 'arrive quickly but safely')
.instruction('Take vehicle 120 and park near the player')
.playerPos(player)
.deny(WaitSkill)
.run()- Skills are class-based and must use
@NpcSkill(). - Key is automatic from class name:
MoveToSkill->moveToGoToCarDriveParkSkill->goToCarDrivePark
- Controllers use class references directly:
skills: [MoveToSkill, WaitSkill]. - Rule runner infers args from skill class:
do(MoveToSkill, { x, y, z }).
import { NpcSkill } from '@open-core/npc-intelligence/server'
import type {
NpcContext,
SkillResult,
NpcSkill as NpcSkillContract,
} from '@open-core/npc-intelligence/server'
type Args = { x: number; y: number; z: number }
@NpcSkill()
class MoveToPointSkill implements NpcSkillContract<Args> {
execute(ctx: NpcContext, args: Args): SkillResult {
ctx.npc.setPosition(args)
return { ok: true }
}
}LookAtEntitySkill requires:
entity: BaseEntity & Spatial
This means the target must be a BaseEntity that also implements Spatial (getPosition).
Enable runtime/LLM logs:
NPC_AI_DEBUG=1enables runtime logs (attach, observe, decisions, skill execution).NPC_AI_DEBUG_LLM=1prints LLM request/response traces.
Use when you need custom planner/provider wiring:
import {
NpcAiPlanner,
NpcRulePlanner,
createOpenRouterProvider,
} from '@open-core/npc-intelligence/server/advanced'rule(...): deterministic. Programmer chooses exact skill(s), sequence, and arguments.ai(...): planner chooses the next skill from the controller skill list using current context.- Both return explicit
RunResult(ok,done,skill,waitMs,error).
OPENROUTER_API_KEYis env-only. No Convar fallback.- If AI planner is configured but no provider/API key is available, controller falls back to rule planner.