feat: API 5, bosses, passive NPCs and the example profiles (stage 6) #10

Merged
whitlocktech merged 4 commits from feat/stage-6 into edge 2026-10-06 00:45:58 +00:00
Showing only changes of commit cf7f9cd38d - Show all commits

View File

@@ -393,6 +393,74 @@ namespace Oxide.Plugins
arg.ReplyWith("rnt: kit probe started");
}
/// <summary>
/// A probe after the 2026-10-05 Rust update: a stock scientist and one of ours (t_sentry), each with a
/// stand-in 10 m off, every 2 s for 20 s; and how far the map's own scientists move in that time.
/// </summary>
[ConsoleCommand("rnt.stockprobe")]
private void CmdStockProbe(ConsoleSystem.Arg arg)
{
if (arg.Connection != null && !arg.IsAdmin) return;
Vector3 f;
if (!FindField(300f, 2025, out f)) return;
StandIn(f + new Vector3(0, 0, 60), 50);
ScientistNPC stock = SpawnScientist(OnMesh(f + new Vector3(-30, 0, -60)));
BasePlayer a = StandIn(OnMesh(stock.transform.position + new Vector3(10, 0, 0)), 71);
BasePlayer ours = Spawn(OnMesh(f + new Vector3(30, 0, -60)), "t_sentry");
BasePlayer b = ours != null ? StandIn(OnMesh(ours.transform.position + new Vector3(10, 0, 0)), 72) : null;
List<ScientistNPC> world = BaseNetworkable.serverEntities.OfType<ScientistNPC>().Where(s => s.GetType().Name != "RunicNpcPlayer" && s != stock).Take(20).ToList();
Dictionary<ScientistNPC, Vector3> from = world.ToDictionary(s => s, s => s.transform.position);
_hitsOnStandIns = 0;
int n = 0;
timer.Repeat(2f, 10, () =>
{
n++;
string Line(ScientistNPC s, BasePlayer t)
{
if (Gone(s)) return "gone";
var senses = s.Brain?.Senses;
return $"state={s.Brain?.CurrentState?.StateType} players={senses?.Memory?.Players.Count} targets={senses?.Memory?.Targets.Count} knows={Knows(s, t)} best={((IAIAttack)s).GetBestTarget()?.ShortPrefabName ?? "none"}";
}
Puts($"[stock {n}] stock: {Line(stock, a)} | ours: {Line(ours as ScientistNPC, b)} | hits on stand-ins {_hitsOnStandIns}");
if (n == 10)
{
var moved = world.Where(s => !Gone(s)).Select(s => Vector3.Distance(s.transform.position, from[s])).ToList();
Puts($"[stock] the map's own scientists: {moved.Count}, moved >1 m in 20 s: {moved.Count(m => m > 1f)}, furthest {(moved.Count > 0 ? moved.Max() : 0):0.0} m; ai.think={ConVar.AI.think} ai.move={ConVar.AI.move} ai.ignoreplayers={ConVar.AI.ignoreplayers}");
Api<int>("RunicNpc_DespawnOwner", Owner);
ClearWorld();
}
});
arg.ReplyWith("rnt: stock probe started");
}
/// <summary>A probe: our monument roamer at the test's monument, and that monument's own scientists, every 4 s for 40 s.</summary>
[ConsoleCommand("rnt.monprobe")]
private void CmdMonProbe(ConsoleSystem.Arg arg)
{
if (arg.Connection != null && !arg.IsAdmin) return;
Vector3 at;
string name;
if (!FindMonument(out at, out name)) { arg.ReplyWith("no monument"); return; }
StandIn(at + new Vector3(0, 0, 100), 2);
BasePlayer mon = Spawn(at, "t_mon");
AIInformationZone zone = AIInformationZone.GetForPoint(at, false);
List<ScientistNPC> locals = BaseNetworkable.serverEntities.OfType<ScientistNPC>()
.Where(s => s.GetType().Name != "RunicNpcPlayer" && Vector3.Distance(s.transform.position, at) < 150f).ToList();
Dictionary<ScientistNPC, Vector3> from = locals.ToDictionary(s => s, s => s.transform.position);
Vector3 monFrom = mon != null ? mon.transform.position : Vector3.zero;
Puts($"[mon] {name} at {at}: zone {(zone != null ? zone.name : "none")} movePoints={(zone != null ? zone.paths?.Count ?? -1 : -1)}, ours {(mon != null ? "spawned" : "REFUSED")}, its own scientists within 150 m: {locals.Count}");
int n = 0;
timer.Repeat(4f, 10, () =>
{
n++;
ScientistNPC s = mon as ScientistNPC;
var moved = locals.Where(x => !Gone(x)).Select(x => Vector3.Distance(x.transform.position, from[x])).ToList();
Puts($"[mon {n}] ours: state={s?.Brain?.CurrentState?.StateType} moved={(Gone(mon) ? -1 : Vector3.Distance(mon.transform.position, monFrom)):0.0} dest={(s?.Brain?.Navigator != null ? s.Brain.Navigator.Destination.ToString() : "-")} moving={s?.Brain?.Navigator?.Moving} | locals moved >1 m: {moved.Count(m => m > 1f)}/{moved.Count}, furthest {(moved.Count > 0 ? moved.Max() : 0):0.0}");
if (n == 10) { Api<int>("RunicNpc_DespawnOwner", Owner); ClearWorld(); }
});
arg.ReplyWith("rnt: monument probe started");
}
/// <summary>Every live RunicNPC NPC of an owner (or all), as RunicNpc_List describes it: for walks.</summary>
[ConsoleCommand("rnt.list")]
private void CmdList(ConsoleSystem.Arg arg)
@@ -615,10 +683,16 @@ namespace Oxide.Plugins
Vector3 monument;
string monumentName;
BasePlayer mon = null;
List<ScientistNPC> locals = new List<ScientistNPC>();
var localsFrom = new Dictionary<ScientistNPC, Vector3>();
if (FindMonument(out monument, out monumentName))
{
StandIn(monument + new Vector3(0, 0, 100), 2);
mon = Spawn(monument, "t_mon");
// The monument's own scientists: a monument roamer is meant to roam as they do.
locals = BaseNetworkable.serverEntities.OfType<ScientistNPC>()
.Where(s => s.GetType().Name != "RunicNpcPlayer" && Vector3.Distance(s.transform.position, monument) < 150f).ToList();
foreach (ScientistNPC s in locals) localsFrom[s] = s.transform.position;
}
Vector3 home = wander.transform.position;
@@ -658,7 +732,13 @@ namespace Oxide.Plugins
if (order[i] != (order[i - 1] + 1) % loop.points.Count) inOrder = false;
Check("move.route.inOrder", order.Count >= 3 && inOrder, string.Join(">", order.Select(x => x.ToString()).ToArray()));
if (mon != null)
Check("move.monument.roams", monMax > 5f, $"{monumentName}: furthest {monMax:0.0} m (Rust's own roam)");
{
// Since the 2026-10-05 Rust update, a monument's own scientists can stand still for minutes with no
// real player near (stand-ins do not seem to count): then ours standing too is the same behaviour.
int localsMoved = locals.Count(s => !Gone(s) && Vector3.Distance(s.transform.position, localsFrom[s]) > 5f);
Check("move.monument.roams", monMax > 5f || (locals.Count > 0 && localsMoved == 0),
$"{monumentName}: ours furthest {monMax:0.0} m (Rust's own roam); the monument's own scientists moved >5 m: {localsMoved}/{locals.Count}");
}
else
Check("move.monument.roams", false, "no monument with an AI zone");
@@ -689,11 +769,20 @@ namespace Oxide.Plugins
private IEnumerator Sentry(Vector3 field)
{
BasePlayer sentry = Spawn(field, "t_sentry");
// On a clear line (no rock between): after the 2026-10-05 Rust update's navmesh rebuild the field's
// first spot had one, and the sentry never saw its stand-in. A probe 10 m apart in the open hit 13 times.
Vector3 sentryAt, standInAt;
if (!ClearLine(field, 12f, out sentryAt, out standInAt))
{
sentryAt = field;
standInAt = field + new Vector3(12, 0, 0);
}
BasePlayer sentry = Spawn(sentryAt, "t_sentry");
yield return new WaitForSeconds(1f);
Vector3 spot = sentry.transform.position;
_hitsOnStandIns = 0;
StandIn(spot + new Vector3(12, 0, 0), 4);
StandIn(standInAt, 4);
float moved = 0f;
for (int t = 0; t < 40; t++)
{