feat: API 5, bosses, passive NPCs and the example profiles (stage 6) #10
@@ -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++)
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user