2 Commits

Author SHA1 Message Date
df2a258f64 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
2026-11-02 04:24:15 -06:00
5c3a360b70 fix(kit): find a kit's extra by its item definition when it has no held entity (#20)
Rust's November 2026 build (staging 26344826) adds the server variable
ItemModEntity.playerOnlyEntityBeltOnly, on by default: an item Rust marks
player-only, the medical syringe among them, has a held entity only while it
is in the belt. A kit puts its syringes in the main inventory, so the heal
(D260) found no item whose held entity is a MedicalTool and never healed
(s5.kit.heal on the staging drill: heals 0, health 75).

FindHeld now asks the item definition's entity prefab when an item has no
held entity, for every extra (heal, melee, flamethrower, grenade, rocket),
and HasFuel reads the flamethrower's fuel type the same way. Holding the item
moves it to the belt, where Rust makes its entity. A heal that cannot hold the
item (a full belt) is now no heal, not a heal of an item never held (D331).

On rust-staging: s5.kit.heal PASS, heals 2, syringes left 0, health 145.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01E14m6SuuY6i1vASFeGDBeY
2026-11-02 04:24:14 -06:00
2 changed files with 66 additions and 11 deletions

View File

@@ -2784,13 +2784,29 @@ namespace Oxide.Plugins
if (container == null)
continue;
foreach (Item item in container.itemList)
if (item != null && item.GetHeldEntity() is T)
if (item != null && HeldAs<T>(item) != null)
return item;
}
return null;
}
/// <summary>
/// The item's held entity as a <typeparamref name="T"/>, or, when it has none, its definition's
/// entity prefab's. Since Rust's November 2026 build a "player-only" item (a syringe) has a held
/// entity only in the belt (<c>ItemModEntity.playerOnlyEntityBeltOnly</c>), and an extra waits in
/// the main inventory until <see cref="Hold"/> moves it (issue #20).
/// </summary>
private static T HeldAs<T>(Item item) where T : HeldEntity
{
BaseEntity held = item.GetHeldEntity();
if (held != null)
return held as T;
ItemModEntity mod = item.info.GetComponent<ItemModEntity>();
GameObject prefab = mod != null && mod.entityPrefab != null && mod.entityPrefab.isValid ? mod.entityPrefab.Get() : null;
return prefab != null ? prefab.GetComponent<T>() : null;
}
/// <summary>Puts an item in its hands; only the belt can be held, so it is moved there first.</summary>
private static bool Hold(RunicNpcPlayer npc, Item item)
{
@@ -2914,7 +2930,8 @@ namespace Oxide.Plugins
if (amount <= 0f)
return false;
Hold(npc, item);
if (!Hold(npc, item))
return false; // a full belt: nowhere to hold it.
npc.SignalBroadcast(BaseEntity.Signal.Attack);
item.UseItem(1);
npc.Heal(amount);
@@ -3011,8 +3028,10 @@ namespace Oxide.Plugins
private static bool HasFuel(RunicNpcPlayer npc, Item flameItem)
{
FlameThrower f = flameItem.GetHeldEntity() as FlameThrower;
return f != null && (f.ammo > 0 || (f.fuelType != null && npc.inventory.FindItemByItemName(f.fuelType.shortname) != null));
// Off the belt it may have no held entity, so no tank (HeldAs): only the kit's fuel counts then.
FlameThrower held = flameItem.GetHeldEntity() as FlameThrower;
FlameThrower f = HeldAs<FlameThrower>(flameItem);
return f != null && ((held != null && held.ammo > 0) || (f.fuelType != null && npc.inventory.FindItemByItemName(f.fuelType.shortname) != null));
}
/// <summary>

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 })
{