Editor/Mcp/BridgeNpcTools.cs

Editor-side MCP toolset wrapper for NPC utilities. It defines static methods that call McpGate.Run to invoke editor tools for creating NPC brains, schedules, spawners, utility AI, placing patrol routes, assigning routes, and simulating perception.

File AccessNetworking
// AUTO-GENERATED by scripts/emit-mcp-wrappers.mjs — DO NOT EDIT.
// Regenerate: node scripts/extract-manifest.mjs && node scripts/emit-mcp-wrappers.mjs
// Source of truth: sbox-mcp-server/src/tools/ (zod schemas) → scripts/tools-manifest.json

using System.Text.Json.Nodes;
using System.Threading.Tasks;
using Editor.Mcp;

/// <summary>
/// NPC brains (state machines), spawners, patrol routes, and perception simulation.
/// </summary>
[McpToolset( "bridge_npc", "NPC brains (state machines), spawners, patrol routes, and perception simulation." )]
public static class BridgeNpcTools
{
	/// <summary>
	/// Wire a placed route (or an arbitrary ordered GUID list) into an NpcBrain's Waypoints list on a
	/// target NPC. This is the list-of-GameObject-references case that plain set_property can't
	/// express. Pass either waypointIds (explicit order) or routeId (a route parent whose children
	/// become the waypoints in hierarchy order). The list count is returned; List&lt;GameObject&gt;
	/// refs may read back as handles/GUIDs via get_property, so trust the count or confirm patrol in
	/// play mode.
	/// </summary>
	/// <param name="npcId">GUID of the GameObject holding the NpcBrain (or any component with a List&lt;GameObject&gt; waypoint property).</param>
	/// <param name="waypointIds">Ordered waypoint GameObject GUIDs (e.g. from place_patrol_route). Takes precedence over routeId.</param>
	/// <param name="routeId">A route parent GUID whose children (in hierarchy order) become the waypoints.</param>
	/// <param name="property">The List&lt;GameObject&gt; property name to set. Defaults to 'Waypoints'. (Use 'SpawnPoints' to wire spawn points on a spawner.).</param>
	[McpTool( "assign_patrol_route" )]
	public static Task<object> AssignPatrolRoute( string npcId, string[] waypointIds = null, string routeId = null, string property = null )
		=> McpGate.Run( "assign_patrol_route", McpGate.Args( ( "npcId", npcId ), ( "waypointIds", waypointIds ), ( "routeId", routeId ), ( "property", property ) ) );

	/// <summary>
	/// Generate an NpcBrain Component: a behavior state machine
	/// (Idle/Patrol/Wander/Chase/Search/Flee/Ambush) driven by occlusion-aware perception — FOV cone +
	/// sight range + a line-of-sight trace (respects walls/trees) + proximity hearing — with
	/// last-known-position memory (lose-LOS -&gt; search -&gt; give up -&gt; resume). This is the
	/// decision layer on top of bake_navmesh / NavMeshAgent movement. Pick a behavior preset, then tune
	/// via the generated [Property] fields with set_property. After generating: trigger_hotload +
	/// get_compile_errors, place a route with place_patrol_route + assign_patrol_route, bake_navmesh,
	/// and verify perception in EDIT mode with simulate_npc_perception (chase/search behavior needs
	/// play mode). The component is added to a GameObject like any other; it auto-adds a NavMeshAgent
	/// in OnStart.
	/// </summary>
	/// <param name="name">Class/file name. Defaults to 'NpcBrain'. Sanitized to a valid C# identifier.</param>
	/// <param name="directory">Subdirectory under the project root for the .cs file. Defaults to 'Code'.</param>
	/// <param name="behavior">Preset (sets StartState + flee toggle): 'patrol' (walk waypoints), 'guard' (Ambush near spawn until a target enters range), 'hunter' (patrol-&gt;chase-&gt;search, the Sasquatch), 'swarm' (wander/idle-&gt;chase nearest, RUN mobs), 'skittish' (chase but flee on low health). The generated file is the same shape; the preset just changes defaults. Defaults to 'hunter'. One of: patrol | guard | hunter | swarm | skittish.</param>
	/// <param name="targetTag">Tag the NPC hunts (its candidates are GameObjects with this tag). Defaults to 'player'.</param>
	/// <param name="moveSpeed">Patrol/wander speed (NavMeshAgent MaxSpeed). Default 130.</param>
	/// <param name="chaseSpeed">Chase/flee speed. Default 200.</param>
	/// <param name="sightRange">Max sight distance. Default 1500.</param>
	/// <param name="fovDegrees">Full field-of-view cone angle in degrees. Default 110. (Baked into a cosine threshold for cheap, trig-free checks.).</param>
	/// <param name="eyeHeight">Trace origin height above the NPC's feet. Default 64.</param>
	/// <param name="hearingRadius">Proximity-hearing radius — a target within it is investigated (sets last-known-pos) but NOT instantly aggroed. Default 600.</param>
	/// <param name="giveUpTime">Seconds to search after losing line-of-sight before giving up and resuming the start state. Default 6.</param>
	/// <param name="searchRadius">Wander radius around the last-known position while searching. Default 400.</param>
	/// <param name="waypointStopDistance">How close the NPC must get to a waypoint/target before it counts as reached. Default 80.</param>
	/// <param name="canFlee">Enable the Flee state (else the NPC never flees). Defaults from the preset.</param>
	/// <param name="fleeHealthFrac">Flee when CurrentHealthFrac drops to/below this (the game sets CurrentHealthFrac 0..1). Default 0.25.</param>
	/// <param name="networked">When true (default), emit a host-authoritative brain: 'if (IsProxy) return;' + [Sync] CurrentState. NOTE: a no-session solo playtest makes everything a proxy, so a networked brain won't think until a host session exists — pass false to iterate solo in the edit scene.</param>
	[McpTool( "create_npc_brain" )]
	public static Task<object> CreateNpcBrain( string name = null, string directory = null, string behavior = null, string targetTag = null, double? moveSpeed = null, double? chaseSpeed = null, double? sightRange = null, double? fovDegrees = null, double? eyeHeight = null, double? hearingRadius = null, double? giveUpTime = null, double? searchRadius = null, double? waypointStopDistance = null, bool? canFlee = null, double? fleeHealthFrac = null, bool? networked = null )
		=> McpGate.Run( "create_npc_brain", McpGate.Args( ( "name", name ), ( "directory", directory ), ( "behavior", behavior ), ( "targetTag", targetTag ), ( "moveSpeed", moveSpeed ), ( "chaseSpeed", chaseSpeed ), ( "sightRange", sightRange ), ( "fovDegrees", fovDegrees ), ( "eyeHeight", eyeHeight ), ( "hearingRadius", hearingRadius ), ( "giveUpTime", giveUpTime ), ( "searchRadius", searchRadius ), ( "waypointStopDistance", waypointStopDistance ), ( "canFlee", canFlee ), ( "fleeHealthFrac", fleeHealthFrac ), ( "networked", networked ) ) );

	/// <summary>
	/// Generate a daily-routine NPC brain: a [Property] list of schedule entries (startHour/endHour
	/// 0..24, taskName, target = named scene GameObject or fixed position), the hour read from any
	/// create_day_night_clock component (capability match: a float TimeOfDay property, same GameObject
	/// first then scene-wide) with an HONEST fallback to its own internal clock when none exists (check
	/// the generated UsingClockComponent bool), walking the NPC to the active entry's target and idling
	/// outside the schedule, plus a static OnTaskChanged(brain, taskName) event and [Sync(FromHost)]
	/// CurrentTask. Entries with endHour &lt; startHour wrap past midnight. Returns {created, path,
	/// className, tasks[], propertyNames[], note}. Next: trigger_hotload + get_compile_errors, attach
	/// (targetId or add_component_with_properties), create the named target GameObjects (e.g.
	/// 'WorkSpot'), pair with create_day_night_clock for shared time, verify via get_runtime_property
	/// CurrentTask in play mode. Limits: default movement is a direct transform walk (walks through
	/// walls) — pass useNavMeshAgent:true for pathfinding (then bake_navmesh is REQUIRED); a clock with
	/// a different shape (e.g. 0..1 DayProgress) will NOT bind; networked default true won't tick in a
	/// no-session solo playtest (networked:false to iterate). Refused during play mode; refuses to
	/// overwrite an existing file.
	/// </summary>
	/// <param name="name">Class/file name. Defaults to 'NpcScheduleBrain'. Sanitized to a valid C# identifier.</param>
	/// <param name="directory">Subdirectory under the project root for the .cs file. Defaults to 'Code'.</param>
	/// <param name="schedule">Schedule entries baked as inspector-editable defaults. Defaults to Work 8-17 @ 'WorkSpot', Relax 17-22 @ 'HomeSpot' (idles/sleeps otherwise). JSON array.</param>
	/// <param name="moveSpeed">Walk speed in world units/s. Defaults to 100.</param>
	/// <param name="arriveDistance">Distance at which the NPC counts as arrived and idles at the spot. Defaults to 32.</param>
	/// <param name="useNavMeshAgent">true: move via NavMeshAgent.MoveTo (real pathfinding — REQUIRES bake_navmesh or the NPC won't move). Defaults to false (direct transform walk, no navmesh needed, walks through walls).</param>
	/// <param name="fallbackDayLengthSeconds">Internal fallback clock only: real seconds per 24 in-game hours when NO TimeOfDay clock component exists. Defaults to 600.</param>
	/// <param name="fallbackStartHour">Internal fallback clock only: starting hour 0..24. Defaults to 8.</param>
	/// <param name="networked">true (default): host-authoritative (IsProxy guard) + [Sync(FromHost)] CurrentTask. false: local build for solo iteration.</param>
	/// <param name="targetId">GUID of the NPC GameObject to attach to (only attaches if the type is already in the TypeLibrary — hotload first).</param>
	[McpTool( "create_npc_schedule_brain" )]
	public static Task<object> CreateNpcScheduleBrain( string name = null, string directory = null, JsonNode schedule = null, double? moveSpeed = null, double? arriveDistance = null, bool? useNavMeshAgent = null, double? fallbackDayLengthSeconds = null, double? fallbackStartHour = null, bool? networked = null, string targetId = null )
		=> McpGate.Run( "create_npc_schedule_brain", McpGate.Args( ( "name", name ), ( "directory", directory ), ( "schedule", schedule ), ( "moveSpeed", moveSpeed ), ( "arriveDistance", arriveDistance ), ( "useNavMeshAgent", useNavMeshAgent ), ( "fallbackDayLengthSeconds", fallbackDayLengthSeconds ), ( "fallbackStartHour", fallbackStartHour ), ( "networked", networked ), ( "targetId", targetId ) ) );

	/// <summary>
	/// Generate a spawner Component that instantiates an NPC prefab over time / in escalating waves at
	/// spawn points, capped by maxAlive. RUN's swarm backbone and Sasquatched's round-start spawn.
	/// After generating: set NpcPrefab via set_prefab_ref, set SpawnPoints (reuse place_patrol_route to
	/// make a set of empties, then assign_patrol_route with property='SpawnPoints'), trigger_hotload +
	/// get_compile_errors. Verify by watching the GameObject count over time in play mode. Networked
	/// spawns use NetworkSpawn() and are host-only.
	/// </summary>
	/// <param name="name">Class/file name. Defaults to 'NpcSpawner'.</param>
	/// <param name="directory">Subdirectory under the project root. Defaults to 'Code'.</param>
	/// <param name="mode">'continuous' (one every interval), 'waves' (a batch every interval, waveCount times), 'burst' (one batch then stop). Default 'waves'. One of: continuous | waves | burst.</param>
	/// <param name="count">NPCs per wave (waves) or per batch (burst/continuous batch). Default 5.</param>
	/// <param name="interval">Seconds between spawns (continuous) or between waves (waves). Default 8.</param>
	/// <param name="waveCount">Number of waves (waves mode). Default 3.</param>
	/// <param name="waveGrowth">Multiply count each wave (&gt;1 = escalating). Default 1.0.</param>
	/// <param name="radius">Random scatter radius around a spawn point. Default 200.</param>
	/// <param name="maxAlive">Cap on concurrent live NPCs (important so swarms don't melt the frame rate). Default 12.</param>
	/// <param name="networked">When true (default), spawn via NetworkSpawn() (host-only, try/catch solo-safe) so clients see the NPCs; false = a plain local Clone for solo/edit testing.</param>
	[McpTool( "create_npc_spawner" )]
	public static Task<object> CreateNpcSpawner( string name = null, string directory = null, string mode = null, double? count = null, double? interval = null, double? waveCount = null, double? waveGrowth = null, double? radius = null, double? maxAlive = null, bool? networked = null )
		=> McpGate.Run( "create_npc_spawner", McpGate.Args( ( "name", name ), ( "directory", directory ), ( "mode", mode ), ( "count", count ), ( "interval", interval ), ( "waveCount", waveCount ), ( "waveGrowth", waveGrowth ), ( "radius", radius ), ( "maxAlive", maxAlive ), ( "networked", networked ) ) );

	/// <summary>
	/// Generate a utility-AI (scored-action) brain: one file with an abstract {name}Action : Component
	/// base (Score() 0..1 + Begin/Tick/End lifecycle), a sealed {name}Brain that every EvaluateInterval
	/// picks the highest-scoring sibling action (score × ScoreWeight, current action gets
	/// +HysteresisBonus so near-ties don't flip-flop), and two example actions — {name}IdleAction
	/// (constant fallback score) and {name}WanderAction (desire builds while idle, walks to random
	/// points by direct transform movement, no navmesh). How it differs from create_npc_brain: the FSM
	/// has a FIXED transition table; here behavior EMERGES from per-frame scores — add behaviors by
	/// subclassing the base on the same GameObject, no transition wiring. Returns {created, path,
	/// classNames[4], propertyNames[], note}. Next: trigger_hotload + get_compile_errors, attach the
	/// brain AND example actions to one GameObject (targetId attaches only the brain), verify in play
	/// mode via get_runtime_property CurrentActionName. Limits: networked default true =
	/// host-authoritative (won't tick in a no-session solo playtest — use networked:false); actions
	/// Tick on the simulating machine only. Refused during play mode; refuses to overwrite an existing
	/// file.
	/// </summary>
	/// <param name="name">System prefix — generates {name}Action / {name}Brain / {name}IdleAction / {name}WanderAction in {name}Ai.cs. Defaults to 'Utility'. Sanitized to a valid C# identifier.</param>
	/// <param name="directory">Subdirectory under the project root for the .cs file. Defaults to 'Code'.</param>
	/// <param name="evaluateInterval">Seconds between score evaluations (the active action still Ticks every frame). Defaults to 0.25.</param>
	/// <param name="hysteresisBonus">Score bonus the current action gets during evaluation — stickiness that prevents flip-flopping between near-tied actions. Defaults to 0.15.</param>
	/// <param name="moveSpeed">Example WanderAction walk speed in world units/s. Defaults to 80.</param>
	/// <param name="wanderRadius">Example WanderAction roam radius around its start position. Defaults to 300.</param>
	/// <param name="networked">true (default): host-authoritative brain (IsProxy guard) + [Sync(FromHost)] CurrentActionName. false: local build for solo iteration.</param>
	/// <param name="targetId">GUID of a GameObject to attach the BRAIN to (actions must be added separately; only attaches if the type is already in the TypeLibrary — hotload first).</param>
	[McpTool( "create_utility_ai" )]
	public static Task<object> CreateUtilityAi( string name = null, string directory = null, double? evaluateInterval = null, double? hysteresisBonus = null, double? moveSpeed = null, double? wanderRadius = null, bool? networked = null, string targetId = null )
		=> McpGate.Run( "create_utility_ai", McpGate.Args( ( "name", name ), ( "directory", directory ), ( "evaluateInterval", evaluateInterval ), ( "hysteresisBonus", hysteresisBonus ), ( "moveSpeed", moveSpeed ), ( "wanderRadius", wanderRadius ), ( "networked", networked ), ( "targetId", targetId ) ) );

	/// <summary>
	/// Place a set of waypoint GameObjects (tagged empties) for a patrol route and group them under a
	/// parent route object — authorable in one call. Optionally snaps each point to the ground (raycast
	/// down) so waypoints sit on the navmesh, not floating. Returns the route parent GUID + ordered
	/// waypoint GUIDs to feed into assign_patrol_route. Validate connectivity afterward with
	/// get_navmesh_path between consecutive waypoints (catches a 'point in a wall').
	/// </summary>
	/// <param name="points">Ordered world positions for the route (at least 2). JSON array.</param>
	/// <param name="name">Route name. Defaults to 'PatrolRoute'. Waypoints are named &lt;route&gt;_WP0, _WP1, ...</param>
	/// <param name="tag">Tag applied to each waypoint. Defaults to 'waypoint'.</param>
	/// <param name="snapToGround">Drop each point onto the surface below via a downward raycast. Default true.</param>
	/// <param name="parentId">Existing parent GameObject GUID to nest the waypoints under; otherwise a new route empty is created at the points' centroid.</param>
	[McpTool( "place_patrol_route" )]
	public static Task<object> PlacePatrolRoute( JsonNode points, string name = null, string tag = null, bool? snapToGround = null, string parentId = null )
		=> McpGate.Run( "place_patrol_route", McpGate.Args( ( "points", points ), ( "name", name ), ( "tag", tag ), ( "snapToGround", snapToGround ), ( "parentId", parentId ) ) );

	/// <summary>
	/// READ-ONLY edit-mode verifier: evaluate the NPC's perception math RIGHT NOW without entering play
	/// mode. Given an NPC (reads its NpcBrain SightRange/FovDegrees/EyeHeight/TargetTag + transform)
	/// and either a targetId or a point, it runs the SAME line-of-sight check the brain uses — FOV cone
	/// (dot vs the baked cosine), sight-range gate, and an occlusion trace from the eye to the target —
	/// and reports the result AND why. This is the keystone verifier: it makes the perception layer
	/// checkable in edit mode (no flaky screenshot timing) — e.g. place the Sasquatch, place a camper
	/// behind a tree, and confirm the tree blocks LOS. Call params override the brain's values, so it
	/// also works before/without an NpcBrain (uses defaults). Safe in play mode too (read-only, like
	/// raycast).
	/// </summary>
	/// <param name="npcId">GUID of the NPC GameObject (ideally with an NpcBrain; its perception [Property] values are read).</param>
	/// <param name="targetId">GUID of the target GameObject to test visibility to (e.g. a player). Provide this OR point.</param>
	/// <param name="point">A raw world point to test visibility to. Provide this OR targetId. As "x,y,z" (or JSON {x,y,z}).</param>
	/// <param name="sightRange">Override the sight range for this check (else read from the NpcBrain / default 1500).</param>
	/// <param name="fovDegrees">Override the FOV cone angle for this check (else read from the NpcBrain / default 110).</param>
	/// <param name="eyeHeight">Override the eye height for this check (else read from the NpcBrain / default 64).</param>
	/// <param name="targetTag">Override the target tag (canSee also requires the target to carry this tag; else read from the NpcBrain / default 'player').</param>
	[McpTool( "simulate_npc_perception" )]
	public static Task<object> SimulateNpcPerception( string npcId, string targetId = null, string point = null, double? sightRange = null, double? fovDegrees = null, double? eyeHeight = null, string targetTag = null )
		=> McpGate.Run( "simulate_npc_perception", McpGate.Args( ( "npcId", npcId ), ( "targetId", targetId ), ( "point", point ), ( "sightRange", sightRange ), ( "fovDegrees", fovDegrees ), ( "eyeHeight", eyeHeight ), ( "targetTag", targetTag ) ) );
}