From 246ad17c60b577748cf324bde6f2cbaf31efcf62 Mon Sep 17 00:00:00 2001 From: wtclaude Date: Fri, 9 Oct 2026 00:11:41 -0500 Subject: [PATCH] feat(commands): rnpc here stands the NPC a metre in front, facing you (D325) At the admin's feet it spawned inside them, looking away (9d player walk). It now stands 1 m ahead on the ground, turned to face the admin. If something is in the way at chest height, there is no ground within 2 m below, or a walker would have no navmesh, it falls back to the admin's own spot and the reply says so. The harness's cmd.chat.here now checks the metre. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01E14m6SuuY6i1vASFeGDBeY --- plugin/RunicNPC.cs | 40 ++++++++++++++++++++++++++++++++++++++-- tools/RunicNpcTest.cs | 4 +++- 2 files changed, 41 insertions(+), 3 deletions(-) diff --git a/plugin/RunicNPC.cs b/plugin/RunicNPC.cs index 25a8737..5b0de53 100644 --- a/plugin/RunicNPC.cs +++ b/plugin/RunicNPC.cs @@ -5179,6 +5179,39 @@ namespace Oxide.Plugins // ---- where an admin stands and looks ---- /// The ground, a floor or a roof the admin is looking at, within . + /// D325: how far in front of the admin rnpc here stands the NPC, centre to centre. + private const float HereAhead = 1f; + + /// + /// D325: the spot in front of the admin, on the ground, facing them. False, with the + /// admin's own spot and facing, when it does not fit: something in the way at chest height, no ground within + /// below their feet, or, for one that walks, no navmesh there. + /// + private static bool HereSpot(BasePlayer player, bool sentry, out Vector3 spot, out float yaw) + { + Vector3 feet = player.transform.position; + Vector3 ahead = player.eyes.HeadForward(); + ahead.y = 0f; + if (ahead.sqrMagnitude < 0.01f) + ahead = new Vector3(player.transform.forward.x, 0f, player.transform.forward.z); + ahead.Normalize(); + + Vector3 chest = feet + Vector3.up; + RaycastHit hit; + if (!Physics.SphereCast(chest, 0.3f, ahead, out hit, HereAhead, GroundMask, QueryTriggerInteraction.Ignore) && + Physics.Raycast(chest + ahead * HereAhead, Vector3.down, out hit, 1f + MaxDrop, GroundMask, QueryTriggerInteraction.Ignore) && + (sentry || OffMeshReason(hit.point) == null)) + { + spot = hit.point; + yaw = Quaternion.LookRotation(-ahead).eulerAngles.y; + return true; + } + + spot = feet; + yaw = player.eyes.rotation.eulerAngles.y; + return false; + } + private static bool LookSpot(BasePlayer player, out Vector3 spot) { RaycastHit hit; @@ -5433,6 +5466,7 @@ namespace Oxide.Plugins Vector3 spot; float yaw; + string hereNote = null; if (o.At.HasValue) { spot = o.At.Value; @@ -5445,8 +5479,8 @@ namespace Oxide.Plugins } else if (here) { - spot = c.Player.transform.position; - yaw = c.Player.eyes.rotation.eulerAngles.y; + if (!HereSpot(c.Player, p.IsSentry, out spot, out yaw)) + hereNote = $"There is no room {HereAhead:0} m in front of you, so it stands where you do."; } else { @@ -5511,6 +5545,8 @@ namespace Oxide.Plugins Movement m = movement ?? p.Movement; c.Say($"Placed {id}: {data.Count} × '{name}' ({p.Role}{(p.IsSentry ? "" : ", " + m.Mode + (kind == "wander" ? $" {m.Radius:0} m" : ""))}), " + $"respawn {data.Respawn:0} s, {data.RespawnMode}{(data.Tether != null ? $", inside zone {data.Tether}" : "")}. Rename it with rnpc rename {id} ."); + if (hereNote != null) + c.Say(hereNote); if (built) c.Say("This spot is on a player-built structure. If it is destroyed, the NPC falls back to the nearest navmesh."); c.Say(CostWarning(PlannedTotal())); diff --git a/tools/RunicNpcTest.cs b/tools/RunicNpcTest.cs index 1a7f52b..ed7b7ba 100644 --- a/tools/RunicNpcTest.cs +++ b/tools/RunicNpcTest.cs @@ -1297,7 +1297,9 @@ namespace Oxide.Plugins permission.GrantUserPermission(uid, "runicnpc.place", null); RunicNPC.Call("CmdChat", admin, "rnpc", new[] { "here", "t_roam", "count=1" }); JObject chatPlaced = PlacementData("t_roam-2"); - Check("cmd.chat.here", chatPlaced != null && Flat(V(chatPlaced["position"].ToObject>()), b) < 1f, "" + chatPlaced?.ToString(Formatting.None)); + // D325: a metre in front of the admin, not at their feet. + float hereAway = chatPlaced == null ? -1f : Flat(V(chatPlaced["position"].ToObject>()), admin.transform.position); + Check("cmd.chat.here", hereAway > 0.8f && hereAway < 1.2f, $"{hereAway:0.00} m from the admin; " + chatPlaced?.ToString(Formatting.None)); RunicNPC.Call("CmdChat", admin, "rnpc", new[] { "respawn", "t_roam-2" }); Check("cmd.chat.adminNeedsAdmin", Row("t_roam-2") != null, "respawn is refused with runicnpc.place alone (no error)"); RunicNPC.Call("CmdChat", admin, "rnpc", new[] { "remove", "t_roam-2" });