Problem: the only agent brain is a five-state enum (packages/core/src/ai/mobBrain.ts:4), behaviors are four descriptors (packages/core/src/scene/behaviors.ts), perception is a cone test (packages/core/src/sensor/visionCone.ts), navigation is an XZ grid A* (packages/core/src/nav/navGrid.ts). Verified on main.
Tasks, one PR each, in order. Everything here is pure core with tests; the probe at the end needs a clip.
-
Perception service. New packages/core/src/sensor/perception.ts: createPerception({ sightRange, sightConeDeg, hearingRange, memorySeconds, occluded?: (from, to) => boolean }) with pushStimulus({ kind: "sight" | "sound" | "damage"; sourceId; position; loudness?; at }), observe(observer: { id; position; yaw }, candidates: { id; position }[], nowMs) that updates memory, memory(observerId) -> { targetId; lastSeenAt; lastKnownPos; confidence }[], retune, snapshot, restore. Confidence decays linearly over memorySeconds. Tests: seen target remembered after leaving the cone, sound stimulus creates memory without sight, occluder blocks sight. bun run gen. CHANGELOG Added.
-
Decision graph. New packages/core/src/ai/decisionGraph.ts: DecisionNode = { kind: "selector"; children } | { kind: "sequence"; children } | { kind: "condition"; key; op; value } | { kind: "action"; action: string; params? } | { kind: "utility"; options: { score: { key; weight }[]; node }[] }; createDecisionGraphRuntime(graph, actions: Record<string, (ctx, params, blackboard) => "running" | "done" | "failed">) with tick(ctx, blackboard, dt), snapshot, restore. Blackboard is a Record<string, number | boolean | string>. Tests: selector falls through, sequence stops on failure, utility picks the highest score, running action resumes next tick. Then re-express mobBrain as one graph in packages/core/src/ai/mobBrainGraph.ts and add a test proving the same decisions as mobBrain.test.ts on the same inputs.
-
Behavior descriptor. Add { kind: "decisionGraph"; graph: DecisionGraph; actions: string } (actions registered by name through a small registry registerBehaviorActions(id, actions)) to the union in packages/core/src/scene/behaviors.ts and advance it in packages/core/src/scene/behaviorRuntime.ts at interestScheduler cadence. Test: an entity with the descriptor moves toward a remembered position.
-
Polygon navmesh. New packages/core/src/nav/navMesh.ts: NavMeshData { verts: number[]; polys: number[][]; links: { from; to; cost? }[]; areas?: number[] }, buildNavAdjacency, findPath(mesh, from, to) -> { points: [x,y,z][]; polys: number[] } (A* over polygon centers then funnel/string-pull), closestPoint, raycastNav. Tests on a hand-built L-shaped mesh and a mesh with an off-mesh link. Keep navGrid untouched.
-
Navmesh bake. New package packages/navbake/ (layout like packages/sql/), depending on recast-navigation (npm recast-navigation, pin latest): bakeNavMesh({ positions, indices, agentRadius, agentHeight, maxSlope, maxClimb }) -> NavMeshData. Add bakes?: { kind: "nav" | "minimap"; id; data }[] to the editor document in packages/core/src/editor/types.ts (migrate the existing minimap bake to it in a compatible way), an editor command bakeNavMesh in packages/editor/src/handlers/, and an MCP tool. Register the package everywhere (build chain, export manifest, README table, tarball test). Test: baking a floor with a wall produces a path that goes around the wall.
-
Tactical queries. New packages/core/src/ai/tacticalQueries.ts: coverPoints(mesh, threatPos, losBlocked: (a, b) => boolean) (navmesh boundary edge midpoints with blocked line of sight to the threat), scorePositions(points, { distanceTo, flankOf, retreatFrom }, weights) bounded by a maxCandidates. Tests.
-
Probe and clip. A guard in apps/dev or examples/: patrols, hears the player (sound stimulus from movement), investigates the last known position, loses the player, returns. bun run pr-video clip. Closes #1684.
Rules: wait for bun run agent:bootstrap --check; branch claude/<slug> off origin/main; claim comment first; one task per PR with Refs #1684; bun run gen after export changes (JSDoc on every export; create* factories need snapshot/restore); CHANGELOG bullet; check-types and test on touched packages plus check-stateful-ratchet, check-doc-symbols, check-orphan-ratchet; use ctx.rng, never Math.random; merge origin/main before pushing; squash auto-merge; fix CI on the same branch.
Problem: the only agent brain is a five-state enum (
packages/core/src/ai/mobBrain.ts:4), behaviors are four descriptors (packages/core/src/scene/behaviors.ts), perception is a cone test (packages/core/src/sensor/visionCone.ts), navigation is an XZ grid A* (packages/core/src/nav/navGrid.ts). Verified on main.Tasks, one PR each, in order. Everything here is pure core with tests; the probe at the end needs a clip.
Perception service. New
packages/core/src/sensor/perception.ts:createPerception({ sightRange, sightConeDeg, hearingRange, memorySeconds, occluded?: (from, to) => boolean })withpushStimulus({ kind: "sight" | "sound" | "damage"; sourceId; position; loudness?; at }),observe(observer: { id; position; yaw }, candidates: { id; position }[], nowMs)that updates memory,memory(observerId) -> { targetId; lastSeenAt; lastKnownPos; confidence }[],retune,snapshot,restore. Confidence decays linearly overmemorySeconds. Tests: seen target remembered after leaving the cone, sound stimulus creates memory without sight, occluder blocks sight.bun run gen. CHANGELOG Added.Decision graph. New
packages/core/src/ai/decisionGraph.ts:DecisionNode = { kind: "selector"; children } | { kind: "sequence"; children } | { kind: "condition"; key; op; value } | { kind: "action"; action: string; params? } | { kind: "utility"; options: { score: { key; weight }[]; node }[] };createDecisionGraphRuntime(graph, actions: Record<string, (ctx, params, blackboard) => "running" | "done" | "failed">)withtick(ctx, blackboard, dt),snapshot,restore. Blackboard is aRecord<string, number | boolean | string>. Tests: selector falls through, sequence stops on failure, utility picks the highest score, running action resumes next tick. Then re-expressmobBrainas one graph inpackages/core/src/ai/mobBrainGraph.tsand add a test proving the same decisions asmobBrain.test.tson the same inputs.Behavior descriptor. Add
{ kind: "decisionGraph"; graph: DecisionGraph; actions: string }(actions registered by name through a small registryregisterBehaviorActions(id, actions)) to the union inpackages/core/src/scene/behaviors.tsand advance it inpackages/core/src/scene/behaviorRuntime.tsatinterestSchedulercadence. Test: an entity with the descriptor moves toward a remembered position.Polygon navmesh. New
packages/core/src/nav/navMesh.ts:NavMeshData { verts: number[]; polys: number[][]; links: { from; to; cost? }[]; areas?: number[] },buildNavAdjacency,findPath(mesh, from, to) -> { points: [x,y,z][]; polys: number[] }(A* over polygon centers then funnel/string-pull),closestPoint,raycastNav. Tests on a hand-built L-shaped mesh and a mesh with an off-mesh link. KeepnavGriduntouched.Navmesh bake. New package
packages/navbake/(layout likepackages/sql/), depending onrecast-navigation(npmrecast-navigation, pin latest):bakeNavMesh({ positions, indices, agentRadius, agentHeight, maxSlope, maxClimb }) -> NavMeshData. Addbakes?: { kind: "nav" | "minimap"; id; data }[]to the editor document inpackages/core/src/editor/types.ts(migrate the existing minimap bake to it in a compatible way), an editor commandbakeNavMeshinpackages/editor/src/handlers/, and an MCP tool. Register the package everywhere (build chain, export manifest, README table, tarball test). Test: baking a floor with a wall produces a path that goes around the wall.Tactical queries. New
packages/core/src/ai/tacticalQueries.ts:coverPoints(mesh, threatPos, losBlocked: (a, b) => boolean)(navmesh boundary edge midpoints with blocked line of sight to the threat),scorePositions(points, { distanceTo, flankOf, retreatFrom }, weights)bounded by amaxCandidates. Tests.Probe and clip. A guard in
apps/devorexamples/: patrols, hears the player (sound stimulus from movement), investigates the last known position, loses the player, returns.bun run pr-videoclip.Closes #1684.Rules: wait for
bun run agent:bootstrap --check; branchclaude/<slug>offorigin/main; claim comment first; one task per PR withRefs #1684;bun run genafter export changes (JSDoc on every export;create*factories need snapshot/restore); CHANGELOG bullet;check-typesandteston touched packages pluscheck-stateful-ratchet,check-doc-symbols,check-orphan-ratchet; usectx.rng, neverMath.random; mergeorigin/mainbefore pushing; squash auto-merge; fix CI on the same branch.