fix(kit): kit extras found by item definition; stage 5 pairs in sight (#20, #22) #24

Merged
whitlocktech merged 2 commits from fix/kit-extras-belt-only into edge 2026-11-02 05:46:21 -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 })
{