fix(drill): stage 5's pairs stand in each other's sight; say how the leashed NPC came to fight (#20, #22)
All checks were successful
PR Checks / plugin-checks (pull_request) Successful in 31s

s5.fight.shootBack failed on every staging drill run because, on the drill's
3500 map (seed 1234), a cliff rock (cliff_hills_large_e_arid) stands between
the fifth fight's victim and the bully 15 m north of it: the bully never saw
the victim, so never shot it. In the open the victim answers within a second.
Each fight's second NPC now stands where each one's eyes see the other's
centre, both ways, as RunicNPC's own line of sight runs: north first, then the
other seven ways round, with a note when it moves.

s5.fight.leash failed once (run 49) and did not come back in three tries. The
first moment the leashed NPC has a target, the harness now notes the target,
its health, and how far it and the scientist are from its home, so the next
failing run says whether it sensed, was shot, or heard an alarm.

Harness 0.7.5.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01E14m6SuuY6i1vASFeGDBeY
This commit is contained in:
2026-11-02 04:24:15 -06:00
parent 5c3a360b70
commit df2a258f64

View File

@@ -45,7 +45,7 @@ namespace Oxide.Plugins
/// a made-up user id (11400000001 and up), so <c>IsNpc</c> is false. They take no damage.
/// </para>
/// </summary>
[Info("RunicNpcTest", "Runic Gateway", "0.7.4")]
[Info("RunicNpcTest", "Runic Gateway", "0.7.5")]
[Description("RunicNPC stage 2 to 7 tests, the stage 6 spike and stage 9's cost check. A developer tool: never ship it.")]
internal class RunicNpcTest : RustPlugin
{
@@ -1753,6 +1753,31 @@ namespace Oxide.Plugins
return false;
}
/// <summary>
/// Where a stage 5 fight's second NPC stands: <paramref name="dist"/> m from the first, on the navmesh,
/// with a clear line between their eyes. North first, as the fights were laid out; then the other seven
/// ways round. On the drill's 3500 map a cliff rock stood between the victim and the bully north of it,
/// so neither ever saw the other and s5.fight.shootBack failed on every run (issue #20).
/// </summary>
private Vector3 Partner(Vector3 at, float dist)
{
for (int k = 0; k < 8; k++)
{
float angle = k * Mathf.PI / 4f;
Vector3 p = OnMesh(at + new Vector3(Mathf.Sin(angle), 0f, Mathf.Cos(angle)) * dist);
NavMeshHit hit;
if (!SampleNavmesh(p, 1f, out hit)) continue;
// Each one's eyes to the other's centre, both ways, as RunicNPC's own line of sight runs.
if (Physics.Linecast(at + Vector3.up * 1.6f, p + Vector3.up * 0.9f, 1218519041, QueryTriggerInteraction.Ignore) ||
Physics.Linecast(p + Vector3.up * 1.6f, at + Vector3.up * 0.9f, 1218519041, QueryTriggerInteraction.Ignore)) continue;
if (k > 0) Note($"s5: the partner {dist:0} m from {at} stands {k * 45}° round from north (no clear line north)");
return p;
}
Note($"s5: no clear line {dist:0} m round {at}; the partner stands north as before");
return OnMesh(at + new Vector3(0, 0, dist));
}
private int HorrorLeft() => BaseNetworkable.serverEntities.OfType<BaseCombatEntity>().Count(c => !c.IsDestroyed && c.faction == BaseCombatEntity.Faction.Horror);
private IEnumerator Stage5()
@@ -1793,16 +1818,18 @@ namespace Oxide.Plugins
Vector3 d = OnMesh(f + new Vector3(70, 0, 0));
Vector3 e = OnMesh(f + new Vector3(140, 0, 0));
// Each fight's second NPC where the two can see each other (issue #20), found before anyone stands there.
Vector3 aTo = Partner(a, 15f), bTo = Partner(b, 15f), cTo = Partner(c, 15f), dTo = Partner(d, 25f), eTo = Partner(e, 15f);
BasePlayer red = Spawn(a, "t_red");
BasePlayer blue = Spawn(OnMesh(a + new Vector3(0, 0, 15)), "t_blue");
BasePlayer blue = Spawn(aTo, "t_blue");
BasePlayer hunter = Spawn(b, "t_hunter");
ScientistNPC prey = SpawnScientist(b + new Vector3(0, 0, 15));
ScientistNPC prey = SpawnScientist(bTo);
BasePlayer neutral = Spawn(c, "t_roam");
ScientistNPC bystander = SpawnScientist(c + new Vector3(0, 0, 15));
ScientistNPC bystander = SpawnScientist(cTo);
BasePlayer leashed = Spawn(d, "t_leashed");
ScientistNPC far = SpawnScientist(d + new Vector3(0, 0, 25));
ScientistNPC far = SpawnScientist(dTo);
BasePlayer victim = Spawn(e, "t_roam");
BasePlayer bully = Spawn(OnMesh(e + new Vector3(0, 0, 15)), "t_bully");
BasePlayer bully = Spawn(eTo, "t_bully");
var fighters = new Dictionary<string, BaseEntity>
{
{ "red", red }, { "blue", blue }, { "hunter", hunter }, { "prey", prey }, { "neutral", neutral },
@@ -1823,7 +1850,16 @@ namespace Oxide.Plugins
if (factionsAt < 0f && (Gone(red) || Gone(blue))) factionsAt = t;
if (scientistAt < 0f && Gone(prey)) scientistAt = t;
if (!Gone(neutral) && TargetOf(Describe(neutral)) != "none") neutralFought = true;
if (!Gone(leashed) && TargetOf(Describe(leashed)) != "none") leashedFought = true;
if (!leashedFought && !Gone(leashed) && TargetOf(Describe(leashed)) != "none")
{
leashedFought = true;
// Run 49 failed here once and it has not been seen since: say how it came to fight. A target its
// own sense found is within its chase (8 m) of home; a hurt one was shot; otherwise an ally's alarm.
Dictionary<string, object> dl = Describe(leashed);
Vector3 leashHome = V(dl["home"]);
Note($"s5.fight.leash: fighting at {t} s, target {dl["target"]}, health {Convert.ToSingle(dl["health"]):0}/{Convert.ToSingle(dl["maxHealth"]):0}, " +
$"{Flat(V(dl["position"]), leashHome):0.0} m from home; the scientist {(Gone(far) ? "gone" : $"{Flat(far.transform.position, leashHome):0.0} m from its home, {far.health:0} hp")}");
}
if (!Gone(victim) && TargetOf(Describe(victim)) != "none") victimAnswered = true;
foreach (BasePlayer x in new[] { red, blue, hunter })
{