fix: example NPC outfits at full durability; /rnpc here a metre in front (D323–D325) #16
@@ -4126,8 +4126,9 @@ namespace Oxide.Plugins
|
||||
/// <summary>Placements wait while Kits reloads with the example kits (D283 reloads RunicNPC with it).</summary>
|
||||
private bool _holdForKits;
|
||||
|
||||
private static JObject ExItem(string shortname, int amount, int position, float condition = 0f) =>
|
||||
JObject.FromObject(new { Shortname = shortname, Skin = 0UL, Amount = amount, Condition = condition, MaxCondition = condition, Ammo = 0, Position = position, Frequency = -1 });
|
||||
/// <summary>Condition is filled in when the kit is written, from this Rust's item definition (D324).</summary>
|
||||
private static JObject ExItem(string shortname, int amount, int position) =>
|
||||
JObject.FromObject(new { Shortname = shortname, Skin = 0UL, Amount = amount, Condition = 0f, MaxCondition = 0f, Ammo = 0, Position = position, Frequency = -1 });
|
||||
|
||||
private static JObject ExKit(string name, JObject[] main, JObject[] wear, JObject[] belt) => new JObject
|
||||
{
|
||||
@@ -4139,17 +4140,24 @@ namespace Oxide.Plugins
|
||||
|
||||
private static JObject[] Wear(params string[] items) => items.Select((s, i) => ExItem(s, 1, i)).ToArray();
|
||||
|
||||
/// <summary>D289: the four kits. A short name this Rust does not know is left out with a warning.</summary>
|
||||
/// <summary>
|
||||
/// D289: the four kits, each in its own outfit (D323): the raider in scrap armour, the camp guard in metal,
|
||||
/// the sniper in hide, the Juggernaut in heavy plate. A short name this Rust does not know is left out with
|
||||
/// a warning.
|
||||
/// </summary>
|
||||
private static Dictionary<string, JObject> ExampleKits() => new Dictionary<string, JObject>
|
||||
{
|
||||
["rnpc_raider"] = ExKit("rnpc_raider", new[] { ExItem("ammo.rifle", 120, 0) },
|
||||
Wear("hoodie", "pants", "shoes.boots", "mask.bandana"), new[] { ExItem("rifle.semiauto", 1, 0, 100f) }),
|
||||
Wear("coffeecan.helmet", "roadsign.jacket", "roadsign.kilt", "hoodie", "pants", "shoes.boots", "burlap.gloves"),
|
||||
new[] { ExItem("rifle.semiauto", 1, 0) }),
|
||||
["rnpc_campguard"] = ExKit("rnpc_campguard", new[] { ExItem("ammo.pistol", 200, 0) },
|
||||
Wear("metal.facemask", "metal.plate.torso", "hoodie", "pants", "shoes.boots"), new[] { ExItem("smg.thompson", 1, 0, 100f) }),
|
||||
Wear("metal.facemask", "metal.plate.torso", "tshirt.long", "pants", "shoes.boots", "roadsign.gloves"),
|
||||
new[] { ExItem("smg.thompson", 1, 0) }),
|
||||
["rnpc_sniper"] = ExKit("rnpc_sniper", new[] { ExItem("ammo.rifle", 60, 0) },
|
||||
Wear("hat.boonie", "hoodie", "pants", "shoes.boots"), new[] { ExItem("rifle.bolt", 1, 0, 100f) }),
|
||||
Wear("hat.boonie", "mask.balaclava", "attire.hide.poncho", "attire.hide.pants", "attire.hide.boots"),
|
||||
new[] { ExItem("rifle.bolt", 1, 0) }),
|
||||
["rnpc_juggernaut"] = ExKit("rnpc_juggernaut", new[] { ExItem("ammo.rifle", 300, 0) },
|
||||
Wear("heavy.plate.helmet", "heavy.plate.jacket", "heavy.plate.pants", "shoes.boots"), new[] { ExItem("lmg.m249", 1, 0, 100f) })
|
||||
Wear("heavy.plate.helmet", "heavy.plate.jacket", "heavy.plate.pants", "shoes.boots"), new[] { ExItem("lmg.m249", 1, 0) })
|
||||
};
|
||||
|
||||
private static JObject ExRanges(float sense, float lose, float chase, float attack) =>
|
||||
@@ -4225,10 +4233,19 @@ namespace Oxide.Plugins
|
||||
var known = new JArray();
|
||||
foreach (JToken item in (JArray)kv.Value[list])
|
||||
{
|
||||
if (ItemManager.FindItemDefinition((string)item["Shortname"]) != null)
|
||||
known.Add(item);
|
||||
else
|
||||
ItemDefinition def = ItemManager.FindItemDefinition((string)item["Shortname"]);
|
||||
if (def == null)
|
||||
{
|
||||
PrintWarning($"Example kit {kv.Key}: this Rust has no item '{item["Shortname"]}', so it is left out.");
|
||||
continue;
|
||||
}
|
||||
|
||||
// D324: Kits sets an item's condition and maximum straight from the file, so each is
|
||||
// written at this Rust's full durability for it (0 where the item has none).
|
||||
float max = def.condition.enabled ? def.condition.max : 0f;
|
||||
item["Condition"] = max;
|
||||
item["MaxCondition"] = max;
|
||||
known.Add(item);
|
||||
}
|
||||
|
||||
kv.Value[list] = known;
|
||||
@@ -5162,6 +5179,39 @@ namespace Oxide.Plugins
|
||||
// ---- where an admin stands and looks ----
|
||||
|
||||
/// <summary>The ground, a floor or a roof the admin is looking at, within <see cref="LookReach"/>.</summary>
|
||||
/// <summary>D325: how far in front of the admin <c>rnpc here</c> stands the NPC, centre to centre.</summary>
|
||||
private const float HereAhead = 1f;
|
||||
|
||||
/// <summary>
|
||||
/// D325: the spot <see cref="HereAhead" /> 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
|
||||
/// <see cref="MaxDrop" /> below their feet, or, for one that walks, no navmesh there.
|
||||
/// </summary>
|
||||
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;
|
||||
@@ -5416,6 +5466,7 @@ namespace Oxide.Plugins
|
||||
|
||||
Vector3 spot;
|
||||
float yaw;
|
||||
string hereNote = null;
|
||||
if (o.At.HasValue)
|
||||
{
|
||||
spot = o.At.Value;
|
||||
@@ -5428,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
|
||||
{
|
||||
@@ -5494,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} <name>.");
|
||||
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()));
|
||||
|
||||
@@ -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<Dictionary<string, object>>()), 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<Dictionary<string, object>>()), 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" });
|
||||
@@ -2423,6 +2425,18 @@ namespace Oxide.Plugins
|
||||
JObject items = kitFile?["_kits"]?["rnpc_juggernaut"] as JObject;
|
||||
int itemCount = items == null ? 0 : ((JArray)items["MainItems"]).Count + ((JArray)items["WearItems"]).Count + ((JArray)items["BeltItems"]).Count;
|
||||
Check("s6.examples.items", itemCount == 6, $"rnpc_juggernaut carries {itemCount} of its 6 items (a short name this Rust lacks would be left out)");
|
||||
// D324: every item is written at this Rust's full durability for it, 0 where it has none.
|
||||
var worn = new List<string>();
|
||||
foreach (string k in exampleKits)
|
||||
foreach (string list in new[] { "MainItems", "WearItems", "BeltItems" })
|
||||
foreach (JToken it in (kitFile?["_kits"]?[k]?[list] as JArray) ?? new JArray())
|
||||
{
|
||||
ItemDefinition def = ItemManager.FindItemDefinition((string)it["Shortname"]);
|
||||
float want = def != null && def.condition.enabled ? def.condition.max : 0f;
|
||||
if ((float)it["Condition"] != want || (float)it["MaxCondition"] != want)
|
||||
worn.Add($"{k}/{it["Shortname"]} {it["Condition"]}/{it["MaxCondition"]} (want {want})");
|
||||
}
|
||||
Check("s6.examples.fullCondition", worn.Count == 0, worn.Count == 0 ? "every item at its full durability" : string.Join("; ", worn.ToArray()));
|
||||
}
|
||||
|
||||
// ---- stage 7: loot (D290–D301) ----
|
||||
|
||||
Reference in New Issue
Block a user